2744-invoiceIn-module_rdy_n_summary #602

Merged
joan merged 10 commits from 2744-invoiceIn-module_rdy_n_summary into dev 2021-04-15 10:06:35 +00:00
83 changed files with 801 additions and 235 deletions
Showing only changes of commit d8628379dd - Show all commits

View File

@ -66,15 +66,38 @@ For end-to-end tests run from project's root.
$ gulp e2e $ gulp e2e
``` ```
## Recommended tools ## Visual Studio Code extensions
* Visual Studio Code Open Visual Studio Code, press Ctrl+P and paste the following commands.
In Visual Studio Code we use the ESLint extension. Open Visual Studio Code, press Ctrl+P and paste the following command. In Visual Studio Code we use the ESLint extension.
``` ```
ext install dbaeumer.vscode-eslint ext install dbaeumer.vscode-eslint
``` ```
Gitlens for visualization of code authorship
```
ext install eamodio.gitlens
```
Spanish language pack
```
ext install ms-ceintl.vscode-language-pack-es
```
### Recommended extensions
Material icon Theme
```
ext install pkief.material-icon-theme
```
Material UI Themes
```
ext install equinusocio.vsc-material-theme
```
## Built With ## Built With
* [angularjs](https://angularjs.org/) * [angularjs](https://angularjs.org/)

View File

@ -15,15 +15,24 @@ module.exports = Self => {
Self.notifyIssues = async ctx => { Self.notifyIssues = async ctx => {
const models = Self.app.models; const models = Self.app.models;
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const [urgentIssue] = await Self.rawSql(` const tickets = await models.OsTicket.rawSql(`
SELECT * FROM managedesktop.vn_workOrderInmediata LIMIT 1 SELECT t.ticket_id AS id, t.number, ua.username, td.subject
`); FROM ost_ticket t
JOIN ost_user_account ua ON t.user_id = ua.user_id
JOIN ost_ticket__cdata td ON t.ticket_id = td.ticket_id
JOIN ost_ticket_priority tp ON td.priority = tp.priority_id
LEFT JOIN ost_department dept ON dept.id = t.dept_id
WHERE tp.priority = 'emergency'
AND (t.staff_id = 0 AND t.team_id = 0)
AND dept.code = 'IT'
`);
if (!urgentIssue) return; if (!tickets.length) return;
const message = $t(`There's a new urgent ticket`, { let message = $t(`There's a new urgent ticket:`);
title: urgentIssue.title, const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
issueId: urgentIssue.workOrderId tickets.forEach(ticket => {
message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} (@${ticket.username})](${ostUri + ticket.id})`;
}); });
const department = await models.Department.findOne({ const department = await models.Department.findOne({

View File

@ -6,11 +6,12 @@ describe('Chat notifyIssue()', () => {
return value; return value;
}; };
const chatModel = app.models.Chat; const chatModel = app.models.Chat;
const osTicketModel = app.models.OsTicket;
const departmentId = 31; const departmentId = 31;
it(`should not call to the send() method and neither return a response`, async() => { it(`should not call to the send() method and neither return a response`, async() => {
spyOn(chatModel, 'send').and.callThrough(); spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'rawSql').and.returnValue([]); spyOn(osTicketModel, 'rawSql').and.returnValue([]);
const response = await chatModel.notifyIssues(ctx); const response = await chatModel.notifyIssues(ctx);
@ -20,7 +21,13 @@ describe('Chat notifyIssue()', () => {
it(`should return a response calling the send() method`, async() => { it(`should return a response calling the send() method`, async() => {
spyOn(chatModel, 'send').and.callThrough(); spyOn(chatModel, 'send').and.callThrough();
spyOn(chatModel, 'rawSql').and.returnValue([{title: 'Issue title'}]); spyOn(osTicketModel, 'rawSql').and.returnValue([{
id: 1,
number: '00001',
username: 'batman',
subject: 'Issue title'}
]);
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const department = await app.models.Department.findById(departmentId); const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName; let orgChatName = department.chatName;
@ -30,7 +37,7 @@ describe('Chat notifyIssue()', () => {
expect(response.statusCode).toEqual(200); expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent'); expect(response.message).toEqual('Fake notification sent');
expect(chatModel.send).toHaveBeenCalledWith(ctx, '#IT', `@all ➔ There's a new urgent ticket`); expect(chatModel.send).toHaveBeenCalledWith(ctx, '#IT', expectedMessage);
// restores // restores
await department.updateAttribute('chatName', orgChatName); await department.updateAttribute('chatName', orgChatName);

View File

@ -94,6 +94,9 @@
}, },
"Warehouse": { "Warehouse": {
"dataSource": "vn" "dataSource": "vn"
},
"OsTicket": {
"dataSource": "osticket"
} }
} }

View File

@ -8,25 +8,23 @@
}, },
"properties": { "properties": {
"id": { "id": {
"type": "Number", "type": "number",
"id": true, "id": true,
"description": "The id" "description": "The id"
}, },
"name": { "name": {
"type": "String", "type": "string",
"required": true "required": true
}, },
"collectionFk": { "collectionFk": {
"type": "String", "type": "string",
"required": true "required": true
}, },
"updated": { "updated": {
"type": "Number" "type": "number"
}, },
"nRefs": { "nRefs": {
"type": "Number", "type": "number"
"required": true,
"default": 1
} }
}, },
"relations": { "relations": {

12
back/models/osticket.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "OsTicket",
"base": "VnModel",
"acls": [{
"property": "validations",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -1 +0,0 @@
Delete this

View File

@ -0,0 +1,26 @@
CREATE OR REPLACE DEFINER = root@`%` VIEW `vn`.`saleValue` AS
SELECT `wh`.`name` AS `warehouse`,
`c`.`name` AS `client`,
`c`.`typeFk` AS `clientTypeFk`,
`u`.`name` AS `buyer`,
`it`.`id` AS `itemTypeFk`,
`it`.`name` AS `family`,
`s`.`itemFk` AS `itemFk`,
`s`.`concept` AS `concept`,
`s`.`quantity` AS `quantity`,
`b`.`buyingValue` + `b`.`freightValue` + `b`.`comissionValue` + `b`.`packageValue` AS `cost`,
(`b`.`buyingValue` + `b`.`freightValue` + `b`.`comissionValue` + `b`.`packageValue`) * `s`.`quantity` AS `value`,
`tm`.`year` AS `year`,
`tm`.`week` AS `week`
FROM `vn`.`sale` `s`
JOIN `vn`.`item` `i` ON `i`.`id` = `s`.`itemFk`
JOIN `vn`.`itemType` `it` ON `it`.`id` = `i`.`typeFk`
JOIN `account`.`user` `u` ON `u`.`id` = `it`.`workerFk`
JOIN `vn`.`ticket` `t` ON `t`.`id` = `s`.`ticketFk`
JOIN `vn`.`client` `c` ON `c`.`id` = `t`.`clientFk`
JOIN `vn`.`warehouse` `wh` ON `wh`.`id` = `t`.`warehouseFk`
JOIN `vn`.`time` `tm` ON `tm`.`dated` = CAST(`t`.`shipped` AS DATE)
JOIN `cache`.`last_buy` `lb` ON `lb`.`item_id` = `i`.`id` AND `lb`.`warehouse_id` = `wh`.`id`
JOIN `vn`.`buy` `b` ON `b`.`id` = `lb`.`buy_id`
WHERE `wh`.`isManaged` <> 0;

View File

@ -0,0 +1,24 @@
drop procedure weekWaste;
create definer = root@`%` procedure weekWaste__()
BEGIN
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = DATE_ADD(CURDATE(), INTERVAL -1 WEEK);
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
sum(saleTotal) as total,
sum(saleWaste) as dwindle
FROM bs.waste
WHERE year = vYear and week = vWeek
GROUP BY buyer
) sub
ORDER BY percentage DESC;
END;

View File

@ -0,0 +1,28 @@
drop procedure weekWaste_byWorker;
create definer = root@`%` procedure weekWaste_byWorker__(IN vWorkerFk int)
BEGIN
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = TIMESTAMPADD(WEEK,-1,CURDATE());
SELECT *, 100 * mermas / total as porcentaje
FROM (
SELECT ws.family,
sum(ws.saleTotal) as total,
sum(ws.saleWaste) as mermas
FROM bs.waste ws
JOIN vn.worker w ON w.user = ws.buyer
WHERE year = vYear AND week = vWeek
AND w.id = vWorkerFk
GROUP BY family
) sub
ORDER BY porcentaje DESC;
END;

View File

@ -0,0 +1,25 @@
drop procedure weekWaste_getDetail;
create definer = root@`%` procedure weekWaste_getDetail__()
BEGIN
DECLARE vLastWeek DATE;
DECLARE vWeek INT;
DECLARE vYear INT;
SET vLastWeek = TIMESTAMPADD(WEEK,-1,CURDATE());
SET vYear = YEAR(vLastWeek);
SET vWeek = WEEK(vLastWeek, 1);
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE year = vYear AND week = vWeek
GROUP BY buyer, family
) sub
ORDER BY percentage DESC;
END;

View File

@ -0,0 +1,22 @@
ALTER TABLE `bs`.`waste`
ADD itemFk INT NULL AFTER `family`;
ALTER TABLE `bs`.`waste`
ADD itemTypeFk SMALLINT(5) UNSIGNED NULL AFTER `itemFk`;
ALTER TABLE `bs`.`waste`
ADD CONSTRAINT waste_itemType_id
FOREIGN KEY (itemTypeFk) REFERENCES vn.itemType (id)
ON UPDATE CASCADE;
ALTER TABLE `bs`.`waste`
ADD CONSTRAINT waste_item_id
FOREIGN KEY (itemFk) REFERENCES vn.item (id)
ON UPDATE CASCADE;
ALTER TABLE `bs`.`waste` DROP PRIMARY KEY;
ALTER TABLE `bs`.`waste`
AD PRIMARY KEY (buyer, year, week, family, itemFk);

View File

@ -0,0 +1,38 @@
UPDATE `bs`.nightTask t SET t.`procedure` = 'waste_addSales' WHERE t.id = 54;
DROP PROCEDURE IF EXISTS `bs`.`waste_Add`;
CREATE
DEFINER = root@`%` PROCEDURE `bs`.`waste_addSales`()
BEGIN
DECLARE vWeek INT;
DECLARE vYear INT;
SELECT week, year
INTO vWeek, vYear
FROM vn.time
WHERE dated = CURDATE();
REPLACE bs.waste
SELECT *, 100 * mermas / total as porcentaje
FROM (
SELECT buyer,
year,
week,
family,
itemFk,
itemTypeFk,
floor(sum(value)) as total,
floor(sum(IF(clientTypeFk = 'loses', value, 0))) as mermas
FROM vn.saleValue
where year = vYear and week = vWeek
GROUP BY family, itemFk
) sub
ORDER BY mermas DESC;
END;

View File

@ -1300,18 +1300,23 @@ INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRa
(103, 2000, 0.00, 0.00, 0.02, 1.00), (103, 2000, 0.00, 0.00, 0.02, 1.00),
(104, 2500, 150.00, 0.02, 0.10, 1.00); (104, 2500, 150.00, 0.02, 0.10, 1.00);
INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `saleTotal`, `saleWaste`, `rate`) INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `itemFk`, `itemTypeFk`, `saleTotal`, `saleWaste`, `rate`)
VALUES VALUES
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation', '1062', '51', '4.8'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation', 1, 1, '1062', '51', '4.8'),
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Colombia', '35074', '687', '2.0'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Colombia', 2, 1, '35074', '687', '2.0'),
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Mini', '1777', '13', '0.7'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Mini', 3, 1, '1777', '13', '0.7'),
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Short', '9182', '59', '0.6'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Carnation Short', 4, 1, '3182', '59', '0.6'),
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Containers', '-74', '0', '0.0'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Crisantemo', 5, 1, '1747', '13', '0.7'),
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Packagings', '-7', '0', '0.0'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Lilium Oriental', 6, 1, '7182', '59', '0.6'),
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Freight', '1100', '0', '0.0'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Alstroemeria', 7, 1, '1777', '13', '0.7'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Funeral Accessories', '848', '-187', '-22.1'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Cymbidium', 1, 1, '4181', '59', '0.6'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', '186', '0', '0.0'), ('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Cymbidium', 2, 1, '7268', '59', '0.6'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', '277', '0', '0.0'); ('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Containers', 2, 1, '-74', '0', '0.0'),
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Packagings', 3, 1, '-7', '0', '0.0'),
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Freight', 4, 1, '1100', '0', '0.0'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Funeral Accessories', 5, 1, '848', '-187', '-22.1'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', 6, 1, '186', '0', '0.0'),
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', 7, 1, '277', '0', '0.0');
INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`producer`,`printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`) INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`producer`,`printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`)
VALUES VALUES
@ -1579,9 +1584,9 @@ INSERT INTO `vn`.`claimState`(`id`, `code`, `description`, `roleFk`, `priority`)
( 2, 'managed', 'Gestionado', 1, 5), ( 2, 'managed', 'Gestionado', 1, 5),
( 3, 'resolved', 'Resuelto', 72, 7), ( 3, 'resolved', 'Resuelto', 72, 7),
( 4, 'canceled', 'Anulado', 72, 6), ( 4, 'canceled', 'Anulado', 72, 6),
( 5, 'disputed', 'Cuestionado', 72, 3), ( 5, 'incomplete', 'Incompleta', 72, 3),
( 6, 'mana', 'Mana', 1, 4), ( 6, 'mana', 'Mana', 1, 4),
( 7, 'inProgress', 'En Curso', 1, 2); ( 7, 'lack', 'Faltas', 1, 2);
INSERT INTO `vn`.`claim`(`id`, `ticketCreated`, `claimStateFk`, `observation`, `clientFk`, `workerFk`, `responsibility`, `isChargedToMana`, `created` ) INSERT INTO `vn`.`claim`(`id`, `ticketCreated`, `claimStateFk`, `observation`, `clientFk`, `workerFk`, `responsibility`, `isChargedToMana`, `created` )
VALUES VALUES

View File

@ -31,7 +31,7 @@ describe('Client create path', () => {
await page.write(selectors.createClientView.taxNumber, '74451390E'); await page.write(selectors.createClientView.taxNumber, '74451390E');
await page.write(selectors.createClientView.userName, 'CaptainMarvel'); await page.write(selectors.createClientView.userName, 'CaptainMarvel');
await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es'); await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es');
await page.autocompleteSearch(selectors.createClientView.salesPerson, 'replenisher'); await page.autocompleteSearch(selectors.createClientView.salesPerson, 'salesPerson');
await page.waitToClick(selectors.createClientView.createButton); await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();

View File

@ -96,7 +96,7 @@ describe('Client Edit basicData path', () => {
await page.write(selectors.clientBasicData.phone, '333333333'); await page.write(selectors.clientBasicData.phone, '333333333');
await page.clearInput(selectors.clientBasicData.mobile); await page.clearInput(selectors.clientBasicData.mobile);
await page.write(selectors.clientBasicData.mobile, '444444444'); await page.write(selectors.clientBasicData.mobile, '444444444');
await page.autocompleteSearch(selectors.clientBasicData.salesPerson, 'replenisherNick'); await page.autocompleteSearch(selectors.clientBasicData.salesPerson, 'salesPerson');
await page.autocompleteSearch(selectors.clientBasicData.channel, 'Metropolis newspaper'); await page.autocompleteSearch(selectors.clientBasicData.channel, 'Metropolis newspaper');
await page.waitToClick(selectors.clientBasicData.saveButton); await page.waitToClick(selectors.clientBasicData.saveButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
@ -143,7 +143,7 @@ describe('Client Edit basicData path', () => {
const result = await page const result = await page
.waitToGetProperty(selectors.clientBasicData.salesPerson, 'value'); .waitToGetProperty(selectors.clientBasicData.salesPerson, 'value');
expect(result).toEqual('replenisherNick'); expect(result).toEqual('salesPersonNick');
}); });
it('should now confirm the channel have been selected', async() => { it('should now confirm the channel have been selected', async() => {

View File

@ -83,9 +83,6 @@ export default class Field extends FormInput {
this._required = value; this._required = value;
let required = this.element.querySelector('.required'); let required = this.element.querySelector('.required');
display(required, this._required); display(required, this._required);
this.$.$applyAsync(() =>
this.input.setAttribute('required', value));
} }
get required() { get required() {

View File

@ -2,7 +2,7 @@
.vn-descriptor-popover { .vn-descriptor-popover {
vn-descriptor-content > .descriptor { vn-descriptor-content > .descriptor {
width: 260px; width: 256px;
& > .header > a:first-child { & > .header > a:first-child {
visibility: hidden; visibility: hidden;

View File

@ -11,7 +11,7 @@ vn-descriptor-content {
& > img[ng-src] { & > img[ng-src] {
min-height: 16em; min-height: 16em;
display: block; display: block;
width: 256px; max-width: 100%;
height: 256px; height: 256px;
} }

View File

@ -59,9 +59,10 @@
"Swift / BIC can't be empty": "Swift / BIC can't be empty", "Swift / BIC can't be empty": "Swift / BIC can't be empty",
"Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})", "Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})",
"Claim will be picked": "The product from the claim ({{claimId}})]({{{claimUrl}}}) from the client *{{clientName}}* will be picked", "Claim will be picked": "The product from the claim [({{claimId}})]({{{claimUrl}}}) from the client *{{clientName}}* will be picked",
"Claim state has changed to incomplete": "The state of the claim [({{claimId}})]({{{claimUrl}}}) from client *{{clientName}}* has changed to *incomplete*",
"This ticket is not an stowaway anymore": "The ticket id [{{ticketId}}]({{{ticketUrl}}}) is not an stowaway anymore", "This ticket is not an stowaway anymore": "The ticket id [{{ticketId}}]({{{ticketUrl}}}) is not an stowaway anymore",
"Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member", "Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member",
"Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member", "Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member",

View File

@ -123,9 +123,10 @@
"Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
"Claim will be picked": "Se recogerá el género de la reclamación ({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}*", "Claim will be picked": "Se recogerá el género de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}*",
"Claim state has changed to incomplete": "Se ha cambiado el estado de la reclamación [({{claimId}})]({{{claimUrl}}}) del cliente *{{clientName}}* a *incompleta*",
"This ticket is not an stowaway anymore": "El ticket id [{{ticketId}}]({{{ticketUrl}}}) ha dejado de ser un polizón", "This ticket is not an stowaway anymore": "El ticket id [{{ticketId}}]({{{ticketUrl}}}) ha dejado de ser un polizón",
"Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}", "Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}",
"ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto", "ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto",
@ -172,7 +173,7 @@
"Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío", "Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío",
"This BIC already exist.": "Este BIC ya existe.", "This BIC already exist.": "Este BIC ya existe.",
"That item doesn't exists": "Ese artículo no existe", "That item doesn't exists": "Ese artículo no existe",
"There's a new urgent ticket": "Hay un nuevo ticket urgente: [{{title}}](https://cau.verdnatura.es/WorkOrder.do?woMode=viewWO&woID={{issueId}})", "There's a new urgent ticket:": "Hay un nuevo ticket urgente:",
"Invalid account": "Cuenta inválida", "Invalid account": "Cuenta inválida",
"Compensation account is empty": "La cuenta para compensar está vacia", "Compensation account is empty": "La cuenta para compensar está vacia",
"This genus already exist": "Este genus ya existe", "This genus already exist": "Este genus ya existe",

View File

@ -17,6 +17,15 @@
"connectTimeout": 40000, "connectTimeout": 40000,
"acquireTimeout": 20000 "acquireTimeout": 20000
}, },
"osticket": {
"connector": "vn-mysql",
"database": "vn",
"debug": false,
"host": "localhost",
"port": "3306",
"username": "root",
"password": "root"
},
"tempStorage": { "tempStorage": {
"name": "tempStorage", "name": "tempStorage",
"connector": "loopback-component-storage", "connector": "loopback-component-storage",

View File

@ -43,7 +43,6 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const args = ctx.args; const args = ctx.args;
const $t = ctx.req.__; // $translate
let tx; let tx;
let myOptions = {}; let myOptions = {};
@ -66,10 +65,14 @@ module.exports = Self => {
} }
} }
}, myOptions); }, myOptions);
// Get sales person from claim client
const salesPerson = claim.client().salesPersonUser();
let changedHasToPickUp = false; let changedHasToPickUp = false;
if (args.hasToPickUp) if (args.hasToPickUp)
changedHasToPickUp = true; changedHasToPickUp = true;
// Validate when claimState has been changed
if (args.claimStateFk) { if (args.claimStateFk) {
const canUpdate = await canChangeState(ctx, claim.claimStateFk, myOptions); const canUpdate = await canChangeState(ctx, claim.claimStateFk, myOptions);
const hasRights = await canChangeState(ctx, args.claimStateFk, myOptions); const hasRights = await canChangeState(ctx, args.claimStateFk, myOptions);
@ -78,18 +81,20 @@ module.exports = Self => {
if (!canUpdate || !hasRights || changedHasToPickUp && !isClaimManager) if (!canUpdate || !hasRights || changedHasToPickUp && !isClaimManager)
throw new UserError(`You don't have enough privileges to change that field`); throw new UserError(`You don't have enough privileges to change that field`);
} }
delete args.ctx; delete args.ctx;
const updatedClaim = await claim.updateAttributes(args, myOptions); const updatedClaim = await claim.updateAttributes(args, myOptions);
// Get sales person from claim client
const salesPerson = claim.client().salesPersonUser(); // When hasToPickUp has been changed
if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp) { if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp)
const origin = ctx.req.headers.origin; notifyPickUp(ctx, salesPerson.id, claim);
const message = $t('Claim will be picked', {
claimId: claim.id, // When claimState has been changed
clientName: claim.client().name, if (args.claimStateFk) {
claimUrl: `${origin}/#!/claim/${claim.id}/summary` const newState = await models.ClaimState.findById(args.claimStateFk, null, options);
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); if (newState.code == 'incomplete')
notifyStateChange(ctx, salesPerson.id, claim);
} }
if (tx) await tx.commit(); if (tx) await tx.commit();
@ -115,4 +120,29 @@ module.exports = Self => {
return canUpdate; return canUpdate;
} }
async function notifyStateChange(ctx, workerId, claim) {
const origin = ctx.req.headers.origin;
const models = Self.app.models;
const $t = ctx.req.__; // $translate
const message = $t('Claim state has changed to incomplete', {
claimId: claim.id,
clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary`
});
await models.Chat.sendCheckingPresence(ctx, workerId, message);
}
async function notifyPickUp(ctx, workerId, claim) {
const origin = ctx.req.headers.origin;
const models = Self.app.models;
const $t = ctx.req.__; // $translate
const message = $t('Claim will be picked', {
claimId: claim.id,
clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${claim.id}/summary`
});
await models.Chat.sendCheckingPresence(ctx, workerId, message);
}
}; };

View File

@ -28,10 +28,10 @@
<vn-autocomplete <vn-autocomplete
disabled="false" disabled="false"
ng-model="$ctrl.claim.workerFk" ng-model="$ctrl.claim.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
where="{role: 'employee'}" where="{role: 'salesPerson'}"
label="Attended by"> label="Attended by">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete

View File

@ -68,7 +68,7 @@
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
ng-model="claimDevelopment.workerFk" ng-model="claimDevelopment.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"

View File

@ -25,20 +25,20 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.salesPersonFk" ng-model="filter.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'salesPerson'}"
label="Salesperson"> label="Salesperson">
<tpl-item>{{firstName}} {{name}}</tpl-item> <tpl-item>{{firstName}} {{name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.attenderFk" ng-model="filter.attenderFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'salesPerson'}"
label="Attended by"> label="Attended by">
<tpl-item>{{firstName}} {{name}}</tpl-item> <tpl-item>{{firstName}} {{name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>

View File

@ -1,23 +0,0 @@
const app = require('vn-loopback/server/server');
describe('Client activeWorkersWithRole', () => {
it('should return the sales people as result', async() => {
let filter = {where: {role: 'salesPerson'}};
let result = await app.models.Client.activeWorkersWithRole(filter);
let isSalesPerson = await app.models.Account.hasRole(result[0].id, 'salesPerson');
expect(result.length).toEqual(19);
expect(isSalesPerson).toBeTruthy();
});
it('should return the buyers as result', async() => {
let filter = {where: {role: 'buyer'}};
let result = await app.models.Client.activeWorkersWithRole(filter);
let isBuyer = await app.models.Account.hasRole(result[0].id, 'buyer');
expect(result.length).toEqual(17);
expect(isBuyer).toBeTruthy();
});
});

View File

@ -8,7 +8,6 @@ const LoopBackContext = require('loopback-context');
module.exports = Self => { module.exports = Self => {
// Methods // Methods
require('../methods/client/activeWorkersWithRole')(Self);
require('../methods/client/getCard')(Self); require('../methods/client/getCard')(Self);
require('../methods/client/createWithUser')(Self); require('../methods/client/createWithUser')(Self);
require('../methods/client/listWorkers')(Self); require('../methods/client/listWorkers')(Self);
@ -253,25 +252,28 @@ module.exports = Self => {
const salesPersonId = instance.salesPersonFk; const salesPersonId = instance.salesPersonFk;
if (salesPersonId) { if (salesPersonId) {
// Send email to client
if (instance.email) {
const worker = await models.EmailUser.findById(salesPersonId);
const params = {
authorization: authorization,
recipientId: instance.id,
recipient: instance.email,
replyTo: worker.email
};
await got.get(`${origin}/api/email/payment-update`, {
query: params
});
}
const fullUrl = `${origin}/#!/client/${instance.id}/billing-data`; const fullUrl = `${origin}/#!/client/${instance.id}/billing-data`;
const message = $t('MESSAGE_CHANGED_PAYMETHOD', { const message = $t('Changed client paymethod', {
clientId: instance.id, clientId: instance.id,
clientName: instance.name, clientName: instance.name,
url: fullUrl url: fullUrl
}); });
await models.Chat.sendCheckingPresence(httpCtx, salesPersonId, message); await models.Chat.sendCheckingPresence(httpCtx, salesPersonId, message);
} }
// Send email to client
if (!instance.email) return;
const params = {
authorization: authorization,
recipientId: instance.id,
recipient: instance.email
};
await got.get(`${origin}/api/email/payment-update`, {
query: params
});
} }
const workerIdBefore = oldInstance.salesPersonFk; const workerIdBefore = oldInstance.salesPersonFk;

View File

@ -54,11 +54,11 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="$ctrl.client.salesPersonFk" ng-model="$ctrl.client.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'salesPerson'}"
label="Salesperson" label="Salesperson"
vn-acl="salesAssistant"> vn-acl="salesAssistant">
</vn-autocomplete> </vn-autocomplete>

View File

@ -17,10 +17,10 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.buyerId" ng-model="filter.buyerId"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'buyer'}"
label="Buyer"> label="Buyer">
<tpl-item>{{nickname}}</tpl-item> <tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>

View File

@ -18,10 +18,10 @@
<vn-autocomplete <vn-autocomplete
label="Salesperson" label="Salesperson"
ng-model="$ctrl.client.salesPersonFk" ng-model="$ctrl.client.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
show-field="firstName" show-field="firstName"
where="{role: 'employee'}"> where="{role: 'salesPerson'}">
<tpl-item>{{firstName}} {{lastName}}</tpl-item> <tpl-item>{{firstName}} {{lastName}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -32,6 +32,13 @@
info="Its only used when sample is sent"> info="Its only used when sample is sent">
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Reply to"
ng-model="$ctrl.clientSample.replyTo"
info="To who should the recipient reply?">
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-id="sampleType" vn-id="sampleType"

View File

@ -18,8 +18,10 @@ class Controller extends Section {
set client(value) { set client(value) {
this._client = value; this._client = value;
if (value) if (value) {
this.clientSample.recipient = value.email; this.clientSample.recipient = value.email;
this.getWorkerEmail();
}
} }
get companyId() { get companyId() {
@ -62,7 +64,8 @@ class Controller extends Section {
const sampleType = this.$.sampleType.selection; const sampleType = this.$.sampleType.selection;
const params = { const params = {
recipientId: this.$params.id, recipientId: this.$params.id,
recipient: this.clientSample.recipient recipient: this.clientSample.recipient,
replyTo: this.clientSample.replyTo
}; };
if (!params.recipient) if (!params.recipient)
@ -77,12 +80,23 @@ class Controller extends Section {
if (sampleType.hasCompany) if (sampleType.hasCompany)
params.companyId = this.clientSample.companyFk; params.companyId = this.clientSample.companyFk;
if (isPreview) params.isPreview = true; let query = `email/${sampleType.code}`;
if (isPreview)
query = `email/${sampleType.code}/preview`;
const query = `email/${sampleType.code}`;
this.$http.get(query, {params}).then(cb); this.$http.get(query, {params}).then(cb);
} }
getWorkerEmail() {
const userId = window.localStorage.currentUserWorkerId;
const params = {filter: {where: {userFk: userId}}};
this.$http.get('EmailUsers', params).then(res => {
const [worker] = res && res.data;
this.clientSample.replyTo = worker.email;
});
}
} }
Controller.$inject = ['$element', '$scope']; Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnClientSampleCreate', { ngModule.vnComponent('vnClientSampleCreate', {

View File

@ -179,5 +179,17 @@ describe('Client', () => {
expect(controller.$state.go).toHaveBeenCalledWith('client.card.sample.index'); expect(controller.$state.go).toHaveBeenCalledWith('client.card.sample.index');
}); });
}); });
describe('getWorkerEmail()', () => {
it(`should perform a query and then set the replyTo property to the clientSample object`, () => {
const expectedEmail = 'batman@arkhamcity.com';
const serializedParams = $httpParamSerializer({filter: {where: {}}});
$httpBackend.expect('GET', `EmailUsers?${serializedParams}`).respond([{email: expectedEmail}]);
controller.getWorkerEmail();
$httpBackend.flush();
expect(controller.clientSample.replyTo).toEqual(expectedEmail);
});
});
}); });
}); });

View File

@ -2,4 +2,6 @@ Choose a sample: Selecciona una plantilla
Choose a company: Selecciona una empresa Choose a company: Selecciona una empresa
Email cannot be blank: Debes introducir un email Email cannot be blank: Debes introducir un email
Recipient: Destinatario Recipient: Destinatario
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla
Reply to: Responder a
To who should the recipient reply?: ¿A quíen debería responder el destinatario?

View File

@ -17,7 +17,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.salesPersonFk" ng-model="filter.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
show-field="firstName" show-field="firstName"
value-field="id" value-field="id"

View File

@ -13,12 +13,10 @@
<vn-th field="landed" center expand>Landed</vn-th> <vn-th field="landed" center expand>Landed</vn-th>
<vn-th>Reference</vn-th> <vn-th>Reference</vn-th>
<vn-th field="supplierFk">Supplier</vn-th> <vn-th field="supplierFk">Supplier</vn-th>
<vn-th field="currencyFk" center>Currency</vn-th>
<vn-th field="companyFk" center>Company</vn-th>
<vn-th field="isBooked" center>Booked</vn-th> <vn-th field="isBooked" center>Booked</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th> <vn-th field="isConfirmed" center>Confirmed</vn-th>
<vn-th field="isOrdered" center>Ordered</vn-th> <vn-th field="isOrdered" center>Ordered</vn-th>
<vn-th>Notes</vn-th> <vn-th shrink></vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
@ -49,18 +47,15 @@
</vn-td> </vn-td>
<vn-td expand>{{::entry.ref}}</vn-td> <vn-td expand>{{::entry.ref}}</vn-td>
<vn-td expand>{{::entry.supplierName}}</vn-td> <vn-td expand>{{::entry.supplierName}}</vn-td>
<vn-td center expand>{{::entry.currencyCode}}</vn-td>
<vn-td center expand>{{::entry.companyCode}}</vn-td>
<vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td> <vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td>
<vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td> <vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td>
<vn-td center><vn-check ng-model="entry.isOrdered" disabled="true"></vn-check></vn-td> <vn-td center><vn-check ng-model="entry.isOrdered" disabled="true"></vn-check></vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon <vn-icon-button
ng-if="entry.notes.length" vn-click-stop="$ctrl.preview(entry)"
vn-tooltip="{{::entry.notes}}" vn-tooltip="Preview"
icon="insert_drive_file" icon="preview">
class="bright"> </vn-icon-button>
</vn-icon>
</vn-td> </vn-td>
</a> </a>
</vn-tbody> </vn-tbody>
@ -70,7 +65,11 @@
<vn-travel-descriptor-popover <vn-travel-descriptor-popover
vn-id="travelDescriptor"> vn-id="travelDescriptor">
</vn-travel-descriptor-popover> </vn-travel-descriptor-popover>
<vn-popup vn-id="summary">
<vn-entry-summary
entry="$ctrl.entrySelected">
</vn-entry-summary>
</vn-popup>
<div fixed-bottom-right> <div fixed-bottom-right>
<vn-vertical style="align-items: center;"> <vn-vertical style="align-items: center;">
<a ui-sref="entry.create" vn-bind="+"> <a ui-sref="entry.create" vn-bind="+">

View File

@ -1,7 +1,12 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section {} export default class Controller extends Section {
preview(entry) {
this.entrySelected = entry;
this.$.summary.show();
}
}
ngModule.vnComponent('vnEntryIndex', { ngModule.vnComponent('vnEntryIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -39,11 +39,11 @@
<vn-autocomplete <vn-autocomplete
disabled="false" disabled="false"
ng-model="filter.salesPersonFk" ng-model="filter.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'buyer'}"
label="Buyer"> label="Buyer">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -52,6 +52,8 @@
vn-tooltip="Preview" vn-tooltip="Preview"
icon="preview"> icon="preview">
</vn-icon-button> </vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-icon-button <vn-icon-button
ng-show="invoiceIn.dmsFk" ng-show="invoiceIn.dmsFk"
vn-click-stop="$ctrl.openPdf(invoiceIn.dmsFk)" vn-click-stop="$ctrl.openPdf(invoiceIn.dmsFk)"

View File

@ -50,8 +50,9 @@ module.exports = Self => {
const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, ''); const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, '');
const file = `${fileName}.png`; const file = `${fileName}.png`;
const filePath = path.join(tempPath, file); const filePath = path.join(tempPath, file);
const imageUrl = image.url.replace('http://', 'https://');
https.get(image.url, async response => { https.get(imageUrl, async response => {
if (response.statusCode != 200) { if (response.statusCode != 200) {
const error = new Error(`Could not download the image. Status code ${response.statusCode}`); const error = new Error(`Could not download the image. Status code ${response.statusCode}`);

View File

@ -0,0 +1,69 @@
module.exports = Self => {
Self.remoteMethod('getWasteByItem', {
description: 'Returns the details of losses by worker and item',
accessType: 'READ',
accepts: [
{
arg: 'buyer',
type: 'string',
required: true,
description: 'The buyer name'
},
{
arg: 'family',
type: 'string',
required: true,
description: 'The item family name'
}
],
returns: {
type: ['Object'],
root: true
},
http: {
path: `/getWasteByItem`,
verb: 'GET'
}
});
Self.getWasteByItem = async(buyer, family) => {
const wastes = await Self.rawSql(`
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
ws.itemFk,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE buyer = ? AND family = ?
AND year = YEAR(TIMESTAMPADD(WEEK,-1,CURDATE()))
AND week = WEEK(TIMESTAMPADD(WEEK,-1,CURDATE()), 1)
GROUP BY buyer, itemFk
) sub
ORDER BY family, percentage DESC`, [buyer, family]);
const details = [];
for (let waste of wastes) {
const buyerName = waste.buyer;
let buyerDetail = details.find(waste => {
return waste.buyer == buyerName;
});
if (!buyerDetail) {
buyerDetail = {
buyer: buyerName,
family: waste.family,
lines: []
};
details.push(buyerDetail);
}
buyerDetail.lines.push(waste);
}
return details;
};
};

View File

@ -1,5 +1,5 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('getWasteDetail', { Self.remoteMethod('getWasteByWorker', {
description: 'Returns the details of losses by worker', description: 'Returns the details of losses by worker',
accessType: 'READ', accessType: 'READ',
accepts: [], accepts: [],
@ -8,13 +8,25 @@ module.exports = Self => {
root: true root: true
}, },
http: { http: {
path: `/getWasteDetail`, path: `/getWasteByWorker`,
verb: 'GET' verb: 'GET'
} }
}); });
Self.getWasteDetail = async() => { Self.getWasteByWorker = async() => {
const [wastes] = await Self.rawSql(`CALL bs.weekWaste_getDetail()`); const wastes = await Self.rawSql(`
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
ws.family,
sum(ws.saleTotal) AS total,
sum(ws.saleWaste) AS dwindle
FROM bs.waste ws
WHERE year = YEAR(TIMESTAMPADD(WEEK,-1,CURDATE()))
AND week = WEEK(TIMESTAMPADD(WEEK,-1,CURDATE()), 1)
GROUP BY buyer, family
) sub
ORDER BY percentage DESC`);
const details = []; const details = [];

View File

@ -0,0 +1,14 @@
const app = require('vn-loopback/server/server');
describe('Item getWasteByItem()', () => {
it('should check for the waste breakdown by worker and item', async() => {
const result = await app.models.Item.getWasteByItem('CharlesXavier', 'Cymbidium');
const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))];
expect(anyResult.buyer).toEqual('CharlesXavier');
expect(anyResult.family).toEqual('Cymbidium');
expect(anyResult.lines.length).toBeGreaterThanOrEqual(2);
});
});

View File

@ -1,8 +1,8 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('item getWasteDetail()', () => { describe('Item getWasteByWorker()', () => {
it('should check for the waste breakdown for every worker', async() => { it('should check for the waste breakdown for every worker', async() => {
const result = await app.models.Item.getWasteDetail(); const result = await app.models.Item.getWasteByWorker();
const length = result.length; const length = result.length;
const anyResult = result[Math.floor(Math.random() * Math.floor(length))]; const anyResult = result[Math.floor(Math.random() * Math.floor(length))];

View File

@ -11,7 +11,8 @@ module.exports = Self => {
require('../methods/item/regularize')(Self); require('../methods/item/regularize')(Self);
require('../methods/item/getVisibleAvailable')(Self); require('../methods/item/getVisibleAvailable')(Self);
require('../methods/item/new')(Self); require('../methods/item/new')(Self);
require('../methods/item/getWasteDetail')(Self); require('../methods/item/getWasteByWorker')(Self);
require('../methods/item/getWasteByItem')(Self);
require('../methods/item/createIntrastat')(Self); require('../methods/item/createIntrastat')(Self);
Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'}); Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'});

View File

@ -1,6 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import Descriptor from 'salix/components/descriptor'; import Descriptor from 'salix/components/descriptor';
import './style.scss';
class Controller extends Descriptor { class Controller extends Descriptor {
constructor($element, $, $rootScope) { constructor($element, $, $rootScope) {

View File

@ -1,9 +0,0 @@
vn-item-descriptor {
img[ng-src] {
min-height: 16em;
height: 100%;
width: 100%;
display: block;
}
}

View File

@ -44,7 +44,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.buyerFk" ng-model="filter.buyerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRolee"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"

View File

@ -20,7 +20,8 @@ import './niche';
import './botanical'; import './botanical';
import './barcode'; import './barcode';
import './summary'; import './summary';
import './waste'; import './waste/index/';
import './waste/detail';
import './fixed-price'; import './fixed-price';
import './fixed-price-search-panel'; import './fixed-price-search-panel';

View File

@ -63,4 +63,5 @@ Diary: Histórico
Item diary: Registro de compra-venta Item diary: Registro de compra-venta
Last entries: Últimas entradas Last entries: Últimas entradas
Tags: Etiquetas Tags: Etiquetas
Waste breakdown: Desglose de mermas Waste breakdown: Desglose de mermas
Waste breakdown by item: Desglose de mermas por artículo

View File

@ -17,10 +17,10 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.attenderFk" ng-model="filter.attenderFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'buyer'}"
label="Atender"> label="Atender">
<tpl-item>{{nickname}}</tpl-item> <tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>

View File

@ -8,7 +8,7 @@
"main": [ "main": [
{"state": "item.index", "icon": "icon-item"}, {"state": "item.index", "icon": "icon-item"},
{"state": "item.request", "icon": "pan_tool"}, {"state": "item.request", "icon": "pan_tool"},
{"state": "item.waste", "icon": "icon-claims"}, {"state": "item.waste.index", "icon": "icon-claims"},
{"state": "item.fixedPrice", "icon": "contact_support"} {"state": "item.fixedPrice", "icon": "contact_support"}
], ],
"card": [ "card": [
@ -155,12 +155,25 @@
"acl": ["employee"] "acl": ["employee"]
}, },
{ {
"url" : "/waste", "url": "/waste",
"state": "item.waste", "state": "item.waste",
"component": "vn-item-waste", "component": "ui-view",
"abstract": true
},
{
"url" : "/index",
"state": "item.waste.index",
"component": "vn-item-waste-index",
"description": "Waste breakdown", "description": "Waste breakdown",
"acl": ["buyer"] "acl": ["buyer"]
}, },
{
"url" : "/detail?buyer&family",
"state": "item.waste.detail",
"component": "vn-item-waste-detail",
"description": "Waste breakdown by item",
"acl": ["buyer"]
},
{ {
"url" : "/fixed-price?q", "url" : "/fixed-price?q",
"state": "item.fixedPrice", "state": "item.fixedPrice",

View File

@ -42,11 +42,11 @@
vn-one vn-one
disabled="false" disabled="false"
ng-model="filter.salesPersonFk" ng-model="filter.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'buyer'}"
label="Buyer"> label="Buyer">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -0,0 +1,42 @@
<vn-crud-model auto-load="true"
vn-id="model"
url="Items/getWasteByItem"
params="$ctrl.$params"
data="details">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-w-md">
<section ng-repeat="detail in details" class="vn-pa-md">
<vn-horizontal class="header">
<h5>{{detail.family}} ({{detail.buyer}})</h5>
</vn-horizontal>
<vn-table>
<vn-thead>
<vn-tr>
<vn-th shrink>Item</vn-th>
<vn-th number>Percentage</vn-th>
<vn-th number>Dwindle</vn-th>
<vn-th number>Total</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="waste in detail.lines">
<vn-td shrink>
<span
ng-click="descriptor.show($event, waste.itemFk)"
class="link">
{{::waste.itemFk}}
</span>
</vn-td>
<vn-td number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
<vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>
<vn-td number>{{::waste.total | currency: 'EUR'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</section>
</vn-card>
</vn-data-viewer>
<vn-item-descriptor-popover
vn-id="descriptor">
</vn-item-descriptor-popover>

View File

@ -0,0 +1,7 @@
import ngModule from '../../module';
import Section from 'salix/components/section';
ngModule.vnComponent('vnItemWasteDetail', {
template: require('./index.html'),
controller: Section
});

View File

@ -1,13 +1,14 @@
<vn-crud-model auto-load="true" <vn-crud-model auto-load="true"
vn-id="model" vn-id="model"
url="Items/getWasteDetail" url="Items/getWasteByWorker"
data="details"> data="details">
</vn-crud-model> </vn-crud-model>
<vn-data-viewer model="model"> <vn-data-viewer model="model">
<vn-card> <vn-card>
<section ng-repeat="detail in details" class="vn-pa-md"> <section ng-repeat="detail in details" class="vn-pa-md">
<vn-horizontal class="header"> <vn-horizontal class="header">
<h5><span translate>{{detail.buyer}}</span></h5> <h5><span><span translate>{{detail.buyer}}</span></h5>
</vn-horizontal> </vn-horizontal>
<vn-table> <vn-table>
<vn-thead> <vn-thead>
@ -19,7 +20,8 @@
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="waste in detail.lines"> <a ng-repeat="waste in detail.lines" class="clickable vn-tr"
ui-sref="item.waste.detail({buyer: waste.buyer, family: waste.family})" >
<vn-td class="waste-family">{{::waste.family}}</vn-td> <vn-td class="waste-family">{{::waste.family}}</vn-td>
<vn-td number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td> <vn-td number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
<vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td> <vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>

View File

@ -1,8 +1,8 @@
import ngModule from '../module'; import ngModule from '../../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import './style.scss'; import './style.scss';
ngModule.vnComponent('vnItemWaste', { ngModule.vnComponent('vnItemWasteIndex', {
template: require('./index.html'), template: require('./index.html'),
controller: Section controller: Section
}); });

View File

@ -0,0 +1,26 @@
@import "variables";
vn-item-waste-index,
vn-item-waste-detail {
.header {
margin-bottom: 16px;
text-transform: uppercase;
font-size: 1.25rem;
line-height: 1;
padding: 7px;
padding-bottom: 7px;
padding-bottom: 4px;
font-weight: lighter;
background-color: #fde6ca;
border-bottom: 1px solid #f7931e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
vn-table vn-th.waste-family,
vn-table vn-td.waste-family {
max-width: 64px;
width: 64px
}
}

View File

@ -28,11 +28,11 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.workerFk" ng-model="filter.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
show-field="nickname" show-field="nickname"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'salesPerson'}"
label="Sales person"> label="Sales person">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>

View File

@ -11,7 +11,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="$ctrl.route.workerFk" ng-model="$ctrl.route.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"

View File

@ -11,7 +11,7 @@
<vn-autocomplete <vn-autocomplete
label="Worker" label="Worker"
ng-model="$ctrl.route.workerFk" ng-model="$ctrl.route.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
where="{role: 'employee'}"> where="{role: 'employee'}">

View File

@ -13,7 +13,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.workerFk" ng-model="filter.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"

View File

@ -18,7 +18,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="$ctrl.supplier.workerFk" ng-model="$ctrl.supplier.workerFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
show-field="nickname" show-field="nickname"
value-field="id" value-field="id"

View File

@ -17,10 +17,10 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.buyerId" ng-model="filter.buyerId"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'buyer'}"
label="Buyer"> label="Buyer">
<tpl-item>{{nickname}}</tpl-item> <tpl-item>{{nickname}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>

View File

@ -165,13 +165,13 @@ class Controller extends Section {
created: this.ticket.updated created: this.ticket.updated
}; };
this.showSMSDialog({ this.showSMSDialog({
message: this.$params.message || this.$t('Minimum is needed', params) message: this.$t('Minimum is needed', params)
}); });
} }
sendPaymentSms() { sendPaymentSms() {
this.showSMSDialog({ this.showSMSDialog({
message: this.$params.message || this.$t('Make a payment') message: this.$t('Make a payment')
}); });
} }

View File

@ -18,7 +18,7 @@
<vn-autocomplete <vn-autocomplete
label="Buyer" label="Buyer"
ng-model="$ctrl.ticketRequest.attenderFk" ng-model="$ctrl.ticketRequest.attenderFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithRole"
show-field="nickname" show-field="nickname"
where="{role: 'buyer'}" where="{role: 'buyer'}"
search-function="{firstName: $search}"> search-function="{firstName: $search}">

View File

@ -27,9 +27,12 @@ class Controller extends Section {
} }
get ticketState() { get ticketState() {
if (!this.ticket) return null; const ticket = this.ticket;
if (!ticket) return null;
return this.ticket.ticketState.state.code; const ticketState = ticket.ticketState;
return ticketState && ticketState.state.code;
} }
getSaleTotal(sale) { getSaleTotal(sale) {

View File

@ -60,7 +60,7 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
ng-model="filter.salesPersonFk" ng-model="filter.salesPersonFk"
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
search-function="{firstName: $search}" search-function="{firstName: $search}"
value-field="id" value-field="id"
where="{role: 'employee'}" where="{role: 'employee'}"

View File

@ -21,7 +21,7 @@
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
url="Clients/activeWorkersWithRole" url="Workers/activeWithInheritedRole"
ng-if="$ctrl.isPickerDesignedState" ng-if="$ctrl.isPickerDesignedState"
ng-model="$ctrl.workerFk" ng-model="$ctrl.workerFk"
show-field="nickname" show-field="nickname"

View File

@ -3,26 +3,7 @@ const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters; const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('activeWorkersWithRole', { Self.activeWorkers = async(query, filter) => {
description: 'Returns actives workers with salesperson role',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: 'Worker',
root: true
},
http: {
path: `/activeWorkersWithRole`,
verb: 'get'
}
});
Self.activeWorkersWithRole = async filter => {
let conn = Self.dataSource.connector; let conn = Self.dataSource.connector;
if (filter.where && filter.where.and && Array.isArray(filter.where.and)) { if (filter.where && filter.where.and && Array.isArray(filter.where.and)) {
let where = {}; let where = {};
@ -54,14 +35,8 @@ module.exports = Self => {
myFilter = mergeFilters(myFilter, clientFilter); myFilter = mergeFilters(myFilter, clientFilter);
let stmt = new ParameterizedSQL( let stmt = new ParameterizedSQL(query);
`SELECT DISTINCT w.id, w.firstName, w.lastName, u.name, u.nickname
FROM worker w
JOIN account.user u ON u.id = w.userFk
JOIN account.roleRole i ON i.role = u.role
JOIN account.role r ON r.id = i.inheritsFrom`
);
stmt.merge(conn.makeSuffix(myFilter)); stmt.merge(conn.makeSuffix(myFilter));
return await conn.executeStmt(stmt); return conn.executeStmt(stmt);
}; };
}; };

View File

@ -0,0 +1,32 @@
module.exports = Self => {
Self.remoteMethod('activeWithInheritedRole', {
description: 'Returns active workers with a role',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/activeWithInheritedRole`,
verb: 'GET'
}
});
Self.activeWithInheritedRole = async filter => {
const query =
`SELECT DISTINCT w.id, w.firstName, w.lastName, u.name, u.nickname
FROM worker w
JOIN account.user u ON u.id = w.userFk
JOIN account.roleRole i ON i.role = u.role
JOIN account.role r ON r.id = i.inheritsFrom`;
return Self.activeWorkers(query, filter);
};
};

View File

@ -0,0 +1,31 @@
module.exports = Self => {
Self.remoteMethod('activeWithRole', {
description: 'Returns active workers with an inherited role',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'Object',
description: 'Filter defining where and paginated data',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/activeWithRole`,
verb: 'GET'
}
});
Self.activeWithRole = async filter => {
const query =
`SELECT DISTINCT w.id, w.firstName, w.lastName, u.name, u.nickname
FROM worker w
JOIN account.user u ON u.id = w.id
JOIN account.role r ON r.id = u.role`;
return Self.activeWorkers(query, filter);
};
};

View File

@ -3,7 +3,7 @@ const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('mySubordinates', { Self.remoteMethod('mySubordinates', {
description: 'Returns a list of a subordinate workers', description: 'Returns a list of a subordinated workers',
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'ctx', arg: 'ctx',
@ -11,7 +11,7 @@ module.exports = Self => {
http: {source: 'context'} http: {source: 'context'}
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -22,19 +22,16 @@ module.exports = Self => {
Self.mySubordinates = async ctx => { Self.mySubordinates = async ctx => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const myUserId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const myWorker = await Self.app.models.Worker.findOne({
where: {userFk: myUserId}
});
const stmts = []; const stmts = [];
stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [myWorker.id])); stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [userId]));
stmts.push('SELECT * FROM tmp.subordinate'); const queryIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1;
stmts.push('DROP TEMPORARY TABLE tmp.subordinate'); stmts.push('DROP TEMPORARY TABLE tmp.subordinate');
let sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql);
return result[1]; return result[queryIndex];
}; };
}; };

View File

@ -0,0 +1,29 @@
const app = require('vn-loopback/server/server');
describe('Worker activeWithInheritedRole', () => {
it('should return the workers with an inherited role of salesPerson', async() => {
const filter = {where: {role: 'salesPerson'}};
const result = await app.models.Worker.activeWithInheritedRole(filter);
const randomIndex = Math.floor(Math.random() * result.length);
const worker = result[randomIndex];
const isSalesPerson = await app.models.Account.hasRole(worker.id, 'salesPerson');
expect(result.length).toEqual(19);
expect(isSalesPerson).toBe(true);
});
it('should return the workers with an inherited role of buyer', async() => {
const filter = {where: {role: 'buyer'}};
const result = await app.models.Worker.activeWithInheritedRole(filter);
const randomIndex = Math.floor(Math.random() * result.length);
const worker = result[randomIndex];
const isBuyer = await app.models.Account.hasRole(worker.id, 'buyer');
expect(result.length).toEqual(17);
expect(isBuyer).toBe(true);
});
});

View File

@ -0,0 +1,21 @@
const app = require('vn-loopback/server/server');
describe('Worker activeWithRole', () => {
it('should return the sales people as result', async() => {
const filter = {where: {role: 'salesPerson'}};
const result = await app.models.Worker.activeWithRole(filter);
const firstWorker = result[0];
expect(result.length).toEqual(1);
expect(firstWorker.nickname).toEqual('salesPersonNick');
});
it('should return the buyers as result', async() => {
const filter = {where: {role: 'buyer'}};
const result = await app.models.Worker.activeWithRole(filter);
const firstWorker = result[0];
expect(result.length).toEqual(1);
expect(firstWorker.nickname).toEqual('buyerNick');
});
});

View File

@ -7,4 +7,7 @@ module.exports = Self => {
require('../methods/worker/createAbsence')(Self); require('../methods/worker/createAbsence')(Self);
require('../methods/worker/deleteAbsence')(Self); require('../methods/worker/deleteAbsence')(Self);
require('../methods/worker/updateAbsence')(Self); require('../methods/worker/updateAbsence')(Self);
require('../methods/worker/active')(Self);
require('../methods/worker/activeWithRole')(Self);
require('../methods/worker/activeWithInheritedRole')(Self);
}; };

View File

@ -30,13 +30,13 @@
rule> rule>
</vn-textfield> </vn-textfield>
<vn-autocomplete <vn-autocomplete
disabled="false" disabled="false"
ng-model="$ctrl.worker.bossFk" ng-model="$ctrl.worker.bossFk"
url="Clients/activeWorkersWithRole" url="Clients/activeWorkersWithInheritRole"
show-field="nickname" show-field="nickname"
search-function="{firstName: $search}" search-function="{firstName: $search}"
where="{role: 'employee'}" where="{role: 'employee'}"
label="Boss"> label="Boss">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>

View File

@ -6,7 +6,7 @@ const imageSrc = {
getEmailSrc(image) { getEmailSrc(image) {
let src = `cid:${image}`; let src = `cid:${image}`;
if (this.isPreview === 'true') if (this.isPreview)
src = `/api/${this.$options.name}/assets/images/${image}`; src = `/api/${this.$options.name}/assets/images/${image}`;
return src; return src;

View File

@ -6,17 +6,26 @@ module.exports = app => {
const reportName = req.params.name; const reportName = req.params.name;
const email = new Email(reportName, req.args); const email = new Email(reportName, req.args);
if (req.args.isPreview === 'true') { await email.send();
const rendered = await email.render();
res.send(rendered); res.status(200).json({
} else { message: 'Sent'
await email.send(); });
} catch (e) {
next(e);
}
});
res.status(200).json({ app.get(`/api/email/:name/preview`, async(req, res, next) => {
message: 'Sent' try {
}); const reportName = req.params.name;
} const args = req.args;
args.isPreview = true;
const email = new Email(reportName, args);
const rendered = await email.render();
res.send(rendered);
} catch (e) { } catch (e) {
next(e); next(e);
} }

View File

@ -1,5 +1,4 @@
const Component = require(`${appPath}/core/component`); const Component = require(`${appPath}/core/component`);
const db = require(`${appPath}/core/database`);
const emailHeader = new Component('email-header'); const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer'); const emailFooter = new Component('email-footer');
@ -20,7 +19,7 @@ module.exports = {
}, },
methods: { methods: {
fetchWastes() { fetchWastes() {
return db.findOne(`CALL bs.weekWaste()`); return this.rawSqlFromDef('wasteWeekly');
} }
}, },
components: { components: {

View File

@ -0,0 +1,11 @@
SELECT *, 100 * dwindle / total AS percentage
FROM (
SELECT buyer,
sum(saleTotal) as total,
sum(saleWaste) as dwindle
FROM bs.waste w
JOIN vn.time t ON w.year = t.year AND w.week = t.week
WHERE t.dated = DATE_ADD(CURDATE(), INTERVAL -1 WEEK)
GROUP BY buyer
) sub
ORDER BY percentage DESC;