Merge branch 'dev' into 2873-create-supplier-beneficiary
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
f7fa363fa5
|
@ -15,15 +15,24 @@ module.exports = Self => {
|
|||
Self.notifyIssues = async ctx => {
|
||||
const models = Self.app.models;
|
||||
const $t = ctx.req.__; // $translate
|
||||
const [urgentIssue] = await Self.rawSql(`
|
||||
SELECT * FROM managedesktop.vn_workOrderInmediata LIMIT 1
|
||||
`);
|
||||
const tickets = await models.OsTicket.rawSql(`
|
||||
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`, {
|
||||
title: urgentIssue.title,
|
||||
issueId: urgentIssue.workOrderId
|
||||
let message = $t(`There's a new urgent ticket:`);
|
||||
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
|
||||
tickets.forEach(ticket => {
|
||||
message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} (@${ticket.username})](${ostUri + ticket.id})`;
|
||||
});
|
||||
|
||||
const department = await models.Department.findOne({
|
||||
|
|
|
@ -6,11 +6,12 @@ describe('Chat notifyIssue()', () => {
|
|||
return value;
|
||||
};
|
||||
const chatModel = app.models.Chat;
|
||||
const osTicketModel = app.models.OsTicket;
|
||||
const departmentId = 31;
|
||||
|
||||
it(`should not call to the send() method and neither return a response`, async() => {
|
||||
spyOn(chatModel, 'send').and.callThrough();
|
||||
spyOn(chatModel, 'rawSql').and.returnValue([]);
|
||||
spyOn(osTicketModel, 'rawSql').and.returnValue([]);
|
||||
|
||||
const response = await chatModel.notifyIssues(ctx);
|
||||
|
||||
|
@ -20,7 +21,13 @@ describe('Chat notifyIssue()', () => {
|
|||
|
||||
it(`should return a response calling the send() method`, async() => {
|
||||
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);
|
||||
let orgChatName = department.chatName;
|
||||
|
@ -30,7 +37,7 @@ describe('Chat notifyIssue()', () => {
|
|||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
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
|
||||
await department.updateAttribute('chatName', orgChatName);
|
||||
|
|
|
@ -94,6 +94,9 @@
|
|||
},
|
||||
"Warehouse": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"OsTicket": {
|
||||
"dataSource": "osticket"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "OsTicket",
|
||||
"base": "VnModel",
|
||||
"acls": [{
|
||||
"property": "validations",
|
||||
"accessType": "EXECUTE",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -1300,18 +1300,23 @@ INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRa
|
|||
(103, 2000, 0.00, 0.00, 0.02, 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
|
||||
('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 Colombia', '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 Short', '9182', '59', '0.6'),
|
||||
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Containers', '-74', '0', '0.0'),
|
||||
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Packagings', '-7', '0', '0.0'),
|
||||
('DavidCharlesHaller', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Freight', '1100', '0', '0.0'),
|
||||
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Funeral Accessories', '848', '-187', '-22.1'),
|
||||
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', '186', '0', '0.0'),
|
||||
('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', '277', '0', '0.0');
|
||||
('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', 2, 1, '35074', '687', '2.0'),
|
||||
('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', 4, 1, '3182', '59', '0.6'),
|
||||
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Crisantemo', 5, 1, '1747', '13', '0.7'),
|
||||
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Lilium Oriental', 6, 1, '7182', '59', '0.6'),
|
||||
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Alstroemeria', 7, 1, '1777', '13', '0.7'),
|
||||
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Cymbidium', 1, 1, '4181', '59', '0.6'),
|
||||
('CharlesXavier', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Cymbidium', 2, 1, '7268', '59', '0.6'),
|
||||
('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`)
|
||||
VALUES
|
||||
|
@ -1579,9 +1584,9 @@ INSERT INTO `vn`.`claimState`(`id`, `code`, `description`, `roleFk`, `priority`)
|
|||
( 2, 'managed', 'Gestionado', 1, 5),
|
||||
( 3, 'resolved', 'Resuelto', 72, 7),
|
||||
( 4, 'canceled', 'Anulado', 72, 6),
|
||||
( 5, 'disputed', 'Cuestionado', 72, 3),
|
||||
( 5, 'incomplete', 'Incompleta', 72, 3),
|
||||
( 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` )
|
||||
VALUES
|
||||
|
|
|
@ -30193,7 +30193,7 @@ BEGIN
|
|||
NEW.currencyFk != OLD.currencyFk
|
||||
THEN
|
||||
|
||||
CALL vn2008.recibidaIvaDivisaUpdate(NEW.id);
|
||||
-- CALL vn2008.recibidaIvaDivisaUpdate(NEW.id);
|
||||
|
||||
END IF;
|
||||
|
||||
|
|
|
@ -904,6 +904,25 @@ export default {
|
|||
ticketOne: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(1)',
|
||||
ticketTwo: 'vn-invoice-out-summary > vn-card > vn-horizontal > vn-auto > vn-table > div > vn-tbody > vn-tr:nth-child(2)'
|
||||
},
|
||||
invoiceInSummary: {
|
||||
supplierRef: 'vn-invoice-in-summary vn-label-value:nth-child(2) > section > span'
|
||||
},
|
||||
invoiceInDescriptor: {
|
||||
moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]',
|
||||
moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]',
|
||||
acceptDeleteButton: '.vn-confirm.shown button[response="accept"]'
|
||||
},
|
||||
invoiceInBasicData: {
|
||||
issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]',
|
||||
operated: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.operated"]',
|
||||
supplier: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.supplierFk"]',
|
||||
supplierRef: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.supplierRef"]',
|
||||
bookEntried: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.bookEntried"]',
|
||||
booked: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.booked"]',
|
||||
currency: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.currencyFk"]',
|
||||
company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]',
|
||||
save: 'vn-invoice-in-basic-data button[type=submit]'
|
||||
},
|
||||
travelIndex: {
|
||||
anySearchResult: 'vn-travel-index vn-tbody > a',
|
||||
firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)',
|
||||
|
|
|
@ -31,7 +31,7 @@ describe('Client create path', () => {
|
|||
await page.write(selectors.createClientView.taxNumber, '74451390E');
|
||||
await page.write(selectors.createClientView.userName, 'CaptainMarvel');
|
||||
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);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ describe('Client Edit basicData path', () => {
|
|||
await page.write(selectors.clientBasicData.phone, '333333333');
|
||||
await page.clearInput(selectors.clientBasicData.mobile);
|
||||
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.waitToClick(selectors.clientBasicData.saveButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
@ -143,7 +143,7 @@ describe('Client Edit basicData path', () => {
|
|||
const result = await page
|
||||
.waitToGetProperty(selectors.clientBasicData.salesPerson, 'value');
|
||||
|
||||
expect(result).toEqual('replenisherNick');
|
||||
expect(result).toEqual('salesPersonNick');
|
||||
});
|
||||
|
||||
it('should now confirm the channel have been selected', async() => {
|
||||
|
|
|
@ -184,7 +184,7 @@ describe('Ticket descriptor path', () => {
|
|||
await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('SMS sent!');
|
||||
expect(message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should send the import SMS using the descriptor menu', async() => {
|
||||
|
@ -196,7 +196,7 @@ describe('Ticket descriptor path', () => {
|
|||
await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('SMS sent!');
|
||||
expect(message).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import selectors from '../../helpers/selectors.js';
|
||||
import getBrowser from '../../helpers/puppeteer';
|
||||
|
||||
describe('InvoiceIn summary path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('administrative', 'invoiceIn');
|
||||
await page.accessToSearchResult('1');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should reach the summary section', async() => {
|
||||
await page.waitForState('invoiceIn.card.summary');
|
||||
});
|
||||
|
||||
it('should contain some basic data from the invoice', async() => {
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInSummary.supplierRef, 'innerText');
|
||||
|
||||
expect(result).toEqual('1234');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import selectors from '../../helpers/selectors.js';
|
||||
import getBrowser from '../../helpers/puppeteer';
|
||||
|
||||
describe('InvoiceIn descriptor path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('administrative', 'invoiceIn');
|
||||
await page.accessToSearchResult('10');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should delete the invoiceIn using the descriptor more menu', async() => {
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('InvoiceIn deleted');
|
||||
});
|
||||
|
||||
it('should have been relocated to the invoiceOut index', async() => {
|
||||
await page.waitForState('invoiceIn.index');
|
||||
});
|
||||
|
||||
it(`should search for the deleted invouceOut to find no results`, async() => {
|
||||
await page.doSearch('10');
|
||||
const nResults = await page.countElement(selectors.invoiceOutIndex.searchResult);
|
||||
|
||||
expect(nResults).toEqual(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import selectors from '../../helpers/selectors.js';
|
||||
import getBrowser from '../../helpers/puppeteer';
|
||||
|
||||
describe('InvoiceIn basic data path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
beforeAll(async() => {
|
||||
browser = await getBrowser();
|
||||
page = browser.page;
|
||||
await page.loginAndModule('administrative', 'invoiceIn');
|
||||
await page.accessToSearchResult('1');
|
||||
await page.accessToSection('invoiceIn.card.basicData');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it(`should edit the invoiceIn basic data`, async() => {
|
||||
const now = new Date();
|
||||
await page.pickDate(selectors.invoiceInBasicData.issued, now);
|
||||
await page.pickDate(selectors.invoiceInBasicData.operated, now);
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura');
|
||||
await page.clearInput(selectors.invoiceInBasicData.supplierRef);
|
||||
await page.write(selectors.invoiceInBasicData.supplierRef, '9999');
|
||||
await page.pickDate(selectors.invoiceInBasicData.bookEntried, now);
|
||||
await page.pickDate(selectors.invoiceInBasicData.booked, now);
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'Dollar USA');
|
||||
await page.autocompleteSearch(selectors.invoiceInBasicData.company, 'ORN');
|
||||
await page.waitToClick(selectors.invoiceInBasicData.save);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it(`should confirm the invoiceIn supplier was edited`, async() => {
|
||||
await page.reloadSection('invoiceIn.card.basicData');
|
||||
const result = await page.waitToGetProperty(selectors.invoiceInBasicData.supplier, 'value');
|
||||
|
||||
expect(result).toContain('Verdnatura');
|
||||
});
|
||||
|
||||
it(`should confirm the invoiceIn supplierRef was edited`, async() => {
|
||||
const result = await page
|
||||
.waitToGetProperty(selectors.invoiceInBasicData.supplierRef, 'value');
|
||||
|
||||
expect(result).toEqual('9999');
|
||||
});
|
||||
|
||||
it(`should confirm the invoiceIn currency was edited`, async() => {
|
||||
const result = await page
|
||||
.waitToGetProperty(selectors.invoiceInBasicData.currency, 'value');
|
||||
|
||||
expect(result).toEqual('Dollar USA');
|
||||
});
|
||||
|
||||
it(`should confirm the invoiceIn company was edited`, async() => {
|
||||
const result = await page
|
||||
.waitToGetProperty(selectors.invoiceInBasicData.company, 'value');
|
||||
|
||||
expect(result).toEqual('ORN');
|
||||
});
|
||||
});
|
|
@ -83,9 +83,6 @@ export default class Field extends FormInput {
|
|||
this._required = value;
|
||||
let required = this.element.querySelector('.required');
|
||||
display(required, this._required);
|
||||
|
||||
this.$.$applyAsync(() =>
|
||||
this.input.setAttribute('required', value));
|
||||
}
|
||||
|
||||
get required() {
|
||||
|
|
|
@ -59,9 +59,10 @@
|
|||
"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}}})",
|
||||
"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}}})",
|
||||
"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",
|
||||
"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",
|
||||
|
|
|
@ -123,9 +123,10 @@
|
|||
"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}}})",
|
||||
"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}}})",
|
||||
"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",
|
||||
"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",
|
||||
|
@ -172,7 +173,7 @@
|
|||
"Swift / BIC cannot be empty": "Swift / BIC no puede estar vacío",
|
||||
"This BIC already exist.": "Este BIC ya 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",
|
||||
"Compensation account is empty": "La cuenta para compensar está vacia",
|
||||
"This genus already exist": "Este genus ya existe",
|
||||
|
|
|
@ -17,6 +17,15 @@
|
|||
"connectTimeout": 40000,
|
||||
"acquireTimeout": 20000
|
||||
},
|
||||
"osticket": {
|
||||
"connector": "vn-mysql",
|
||||
"database": "vn",
|
||||
"debug": false,
|
||||
"host": "localhost",
|
||||
"port": "3306",
|
||||
"username": "root",
|
||||
"password": "root"
|
||||
},
|
||||
"tempStorage": {
|
||||
"name": "tempStorage",
|
||||
"connector": "loopback-component-storage",
|
||||
|
|
|
@ -58,24 +58,21 @@
|
|||
label="Save">
|
||||
</vn-submit>
|
||||
<vn-button
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Synchronize all"
|
||||
ng-click="$ctrl.onSynchronizeAll()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Synchronize user"
|
||||
ng-click="syncUser.show()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Synchronize roles"
|
||||
ng-click="$ctrl.onSynchronizeRoles()">
|
||||
</vn-button>
|
||||
<vn-button
|
||||
disabled="!watcher.dataChanged()"
|
||||
class="cancel"
|
||||
label="Undo changes"
|
||||
disabled="!watcher.dataChanged()"
|
||||
ng-click="watcher.loadOriginalData()">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
|
|
|
@ -43,7 +43,6 @@ module.exports = Self => {
|
|||
const models = Self.app.models;
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const args = ctx.args;
|
||||
const $t = ctx.req.__; // $translate
|
||||
let tx;
|
||||
let myOptions = {};
|
||||
|
||||
|
@ -66,10 +65,14 @@ module.exports = Self => {
|
|||
}
|
||||
}
|
||||
}, myOptions);
|
||||
// Get sales person from claim client
|
||||
const salesPerson = claim.client().salesPersonUser();
|
||||
|
||||
let changedHasToPickUp = false;
|
||||
if (args.hasToPickUp)
|
||||
changedHasToPickUp = true;
|
||||
|
||||
// Validate when claimState has been changed
|
||||
if (args.claimStateFk) {
|
||||
const canUpdate = await canChangeState(ctx, claim.claimStateFk, myOptions);
|
||||
const hasRights = await canChangeState(ctx, args.claimStateFk, myOptions);
|
||||
|
@ -78,18 +81,20 @@ module.exports = Self => {
|
|||
if (!canUpdate || !hasRights || changedHasToPickUp && !isClaimManager)
|
||||
throw new UserError(`You don't have enough privileges to change that field`);
|
||||
}
|
||||
|
||||
delete args.ctx;
|
||||
const updatedClaim = await claim.updateAttributes(args, myOptions);
|
||||
// Get sales person from claim client
|
||||
const salesPerson = claim.client().salesPersonUser();
|
||||
if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
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, salesPerson.id, message);
|
||||
|
||||
// When hasToPickUp has been changed
|
||||
if (salesPerson && changedHasToPickUp && updatedClaim.hasToPickUp)
|
||||
notifyPickUp(ctx, salesPerson.id, claim);
|
||||
|
||||
// When claimState has been changed
|
||||
if (args.claimStateFk) {
|
||||
const newState = await models.ClaimState.findById(args.claimStateFk, null, options);
|
||||
|
||||
if (newState.code == 'incomplete')
|
||||
notifyStateChange(ctx, salesPerson.id, claim);
|
||||
}
|
||||
|
||||
if (tx) await tx.commit();
|
||||
|
@ -115,4 +120,29 @@ module.exports = Self => {
|
|||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,10 +28,10 @@
|
|||
<vn-autocomplete
|
||||
disabled="false"
|
||||
ng-model="$ctrl.claim.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Attended by">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
ng-model="claimDevelopment.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
|
|
|
@ -25,20 +25,20 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Salesperson">
|
||||
<tpl-item>{{firstName}} {{name}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.attenderFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Attended by">
|
||||
<tpl-item>{{firstName}} {{name}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const soap = require('soap');
|
||||
|
||||
describe('client sendSms()', () => {
|
||||
let createdLog;
|
||||
|
@ -9,7 +10,8 @@ describe('client sendSms()', () => {
|
|||
done();
|
||||
});
|
||||
|
||||
it('should send a message and log it', async() => {
|
||||
it('should now send a message and log it', async() => {
|
||||
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
let id = 101;
|
||||
let destination = 222222222;
|
||||
|
|
|
@ -4,34 +4,11 @@ const soap = require('soap');
|
|||
describe('sms send()', () => {
|
||||
it('should return the expected message and status code', async() => {
|
||||
const code = 200;
|
||||
const smsConfig = await app.models.SmsConfig.findOne();
|
||||
const soapClient = await soap.createClientAsync(smsConfig.uri);
|
||||
spyOn(soap, 'createClientAsync').and.returnValue(soapClient);
|
||||
spyOn(soapClient, 'sendSMSAsync').and.returnValue([{
|
||||
result: {
|
||||
$value:
|
||||
`<xtratelecom-sms-response>
|
||||
<sms>
|
||||
<codigo>
|
||||
${code}
|
||||
</codigo>
|
||||
<descripcion>
|
||||
Envio en procesamiento
|
||||
</descripcion>
|
||||
<messageId>
|
||||
1
|
||||
</messageId>
|
||||
</sms>
|
||||
<procesoId>
|
||||
444328681
|
||||
</procesoId>
|
||||
</xtratelecom-sms-response>`
|
||||
}
|
||||
}]);
|
||||
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
|
||||
let ctx = {req: {accessToken: {userId: 1}}};
|
||||
let result = await app.models.Sms.send(ctx, 105, 'destination', 'My SMS Body');
|
||||
|
||||
expect(result.statusCode).toEqual(200);
|
||||
expect(result.statusCode).toEqual(code);
|
||||
expect(result.status).toContain('Fake response');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@ const LoopBackContext = require('loopback-context');
|
|||
|
||||
module.exports = Self => {
|
||||
// Methods
|
||||
require('../methods/client/activeWorkersWithRole')(Self);
|
||||
require('../methods/client/getCard')(Self);
|
||||
require('../methods/client/createWithUser')(Self);
|
||||
require('../methods/client/listWorkers')(Self);
|
||||
|
@ -253,25 +252,28 @@ module.exports = Self => {
|
|||
const salesPersonId = instance.salesPersonFk;
|
||||
|
||||
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 message = $t('MESSAGE_CHANGED_PAYMETHOD', {
|
||||
const message = $t('Changed client paymethod', {
|
||||
clientId: instance.id,
|
||||
clientName: instance.name,
|
||||
url: fullUrl
|
||||
});
|
||||
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;
|
||||
|
|
|
@ -54,11 +54,11 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="$ctrl.client.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Salesperson"
|
||||
vn-acl="salesAssistant">
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.buyerId"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'buyer'}"
|
||||
label="Buyer">
|
||||
<tpl-item>{{nickname}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
<vn-autocomplete
|
||||
label="Salesperson"
|
||||
ng-model="$ctrl.client.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
search-function="{firstName: $search}"
|
||||
show-field="firstName"
|
||||
where="{role: 'employee'}">
|
||||
where="{role: 'salesPerson'}">
|
||||
<tpl-item>{{firstName}} {{lastName}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
|
|
@ -32,6 +32,13 @@
|
|||
info="Its only used when sample is sent">
|
||||
</vn-textfield>
|
||||
</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-autocomplete
|
||||
vn-id="sampleType"
|
||||
|
|
|
@ -18,8 +18,10 @@ class Controller extends Section {
|
|||
set client(value) {
|
||||
this._client = value;
|
||||
|
||||
if (value)
|
||||
if (value) {
|
||||
this.clientSample.recipient = value.email;
|
||||
this.getWorkerEmail();
|
||||
}
|
||||
}
|
||||
|
||||
get companyId() {
|
||||
|
@ -62,7 +64,8 @@ class Controller extends Section {
|
|||
const sampleType = this.$.sampleType.selection;
|
||||
const params = {
|
||||
recipientId: this.$params.id,
|
||||
recipient: this.clientSample.recipient
|
||||
recipient: this.clientSample.recipient,
|
||||
replyTo: this.clientSample.replyTo
|
||||
};
|
||||
|
||||
if (!params.recipient)
|
||||
|
@ -77,12 +80,23 @@ class Controller extends Section {
|
|||
if (sampleType.hasCompany)
|
||||
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);
|
||||
}
|
||||
|
||||
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'];
|
||||
|
||||
ngModule.vnComponent('vnClientSampleCreate', {
|
||||
|
|
|
@ -179,5 +179,17 @@ describe('Client', () => {
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,4 +2,6 @@ Choose a sample: Selecciona una plantilla
|
|||
Choose a company: Selecciona una empresa
|
||||
Email cannot be blank: Debes introducir un email
|
||||
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?
|
|
@ -17,7 +17,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
search-function="{firstName: $search}"
|
||||
show-field="firstName"
|
||||
value-field="id"
|
||||
|
|
|
@ -10,76 +10,81 @@ module.exports = Self => {
|
|||
accepts: [
|
||||
{
|
||||
arg: 'filter',
|
||||
type: 'Object',
|
||||
type: 'object',
|
||||
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'search',
|
||||
type: 'String',
|
||||
type: 'string',
|
||||
description: 'Searchs the entry by id',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'id',
|
||||
type: 'Integer',
|
||||
type: 'integer',
|
||||
description: 'The entry id',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'created',
|
||||
type: 'Date',
|
||||
type: 'date',
|
||||
description: 'The created date to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'travelFk',
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
description: 'The travel id to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'companyFk',
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
description: 'The company to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'isBooked',
|
||||
type: 'Boolean',
|
||||
type: 'boolean',
|
||||
description: 'The isBokked filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'isConfirmed',
|
||||
type: 'Boolean',
|
||||
type: 'boolean',
|
||||
description: 'The isConfirmed filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'isOrdered',
|
||||
type: 'Boolean',
|
||||
type: 'boolean',
|
||||
description: 'The isOrdered filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'ref',
|
||||
type: 'String',
|
||||
type: 'string',
|
||||
description: 'The ref filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'supplierFk',
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
description: 'The supplier id to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'invoiceInFk',
|
||||
type: 'number',
|
||||
description: 'The invoiceIn id to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'currencyFk',
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
description: 'The currency id to filter',
|
||||
http: {source: 'query'}
|
||||
}, {
|
||||
arg: 'from',
|
||||
type: 'Date',
|
||||
type: 'date',
|
||||
description: `The from date filter`
|
||||
}, {
|
||||
arg: 'to',
|
||||
type: 'Date',
|
||||
type: 'date',
|
||||
description: `The to date filter`
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -116,6 +121,7 @@ module.exports = Self => {
|
|||
case 'travelFk':
|
||||
case 'currencyFk':
|
||||
case 'supplierFk':
|
||||
case 'invoiceInFk':
|
||||
param = `e.${param}`;
|
||||
return {[param]: value};
|
||||
}
|
||||
|
|
|
@ -39,11 +39,11 @@
|
|||
<vn-autocomplete
|
||||
disabled="false"
|
||||
ng-model="filter.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'buyer'}"
|
||||
label="Buyer">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = Self => {
|
|||
{
|
||||
arg: 'search',
|
||||
type: 'string',
|
||||
description: 'Searchs the invoiceOut by id',
|
||||
description: 'Searchs the invoiceIn by id',
|
||||
http: {source: 'query'}
|
||||
},
|
||||
{
|
||||
|
@ -25,6 +25,11 @@ module.exports = Self => {
|
|||
type: 'string',
|
||||
description: 'The supplier reference'
|
||||
},
|
||||
{
|
||||
arg: 'supplierFk',
|
||||
type: 'number',
|
||||
description: 'The supplier id'
|
||||
},
|
||||
{
|
||||
arg: 'fi',
|
||||
type: 'string',
|
||||
|
@ -108,6 +113,7 @@ module.exports = Self => {
|
|||
case 'fi':
|
||||
return {[`s.${param}`]: value};
|
||||
case 'supplierRef':
|
||||
case 'supplierFk':
|
||||
case 'serialNumber':
|
||||
case 'serial':
|
||||
case 'issued':
|
||||
|
@ -132,7 +138,7 @@ module.exports = Self => {
|
|||
ii.isBooked,
|
||||
ii.supplierRef,
|
||||
ii.docFk AS dmsFk,
|
||||
s.id AS supplierFk,
|
||||
ii.supplierFk,
|
||||
s.name AS supplierName,
|
||||
s.account,
|
||||
SUM(iid.amount) AS amount,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('invoiceIn summary()', () => {
|
||||
it('should return a summary object containing data from one invoiceIn', async() => {
|
||||
const summary = await app.models.InvoiceIn.summary(1);
|
||||
|
||||
expect(summary.supplierRef).toEqual('1234');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('summary', {
|
||||
description: 'The invoiceIn summary',
|
||||
accessType: 'READ',
|
||||
accepts: [{
|
||||
arg: 'id',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The invoiceIn id',
|
||||
http: {source: 'path'}
|
||||
}],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/:id/summary`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.summary = async id => {
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'company',
|
||||
scope: {
|
||||
fields: ['id', 'code']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'supplier',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'sageWithholding',
|
||||
scope: {
|
||||
fields: ['withholding']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return Self.app.models.InvoiceIn.findById(id, filter);
|
||||
};
|
||||
};
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"InvoiceIn": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"InvoiceInDueDay": {
|
||||
"dataSource": "vn"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "InvoiceInDueDay",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "invoiceInDueDay"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"id": true,
|
||||
"type": "number",
|
||||
"description": "Identifier"
|
||||
},
|
||||
"invoiceInFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"dueDated": {
|
||||
"type": "date"
|
||||
},
|
||||
"bankFk": {
|
||||
"type": "number"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/invoice-in/filter')(Self);
|
||||
require('../methods/invoice-in/summary')(Self);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
"serial": {
|
||||
"type": "string"
|
||||
},
|
||||
"supplierRef": {
|
||||
"type": "string"
|
||||
},
|
||||
"issued": {
|
||||
"type": "date"
|
||||
},
|
||||
|
@ -27,12 +30,18 @@
|
|||
"isBooked": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isVatDeductible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"booked": {
|
||||
"type": "date"
|
||||
},
|
||||
"operated": {
|
||||
"type": "date"
|
||||
},
|
||||
"bookEntried": {
|
||||
"type": "date"
|
||||
},
|
||||
"dmsFk": {
|
||||
"type": "number",
|
||||
"mysql": {
|
||||
|
@ -41,6 +50,16 @@
|
|||
}
|
||||
},
|
||||
"relations": {
|
||||
"invoiceInDueDay": {
|
||||
"type": "hasMany",
|
||||
"model": "InvoiceInDueDay",
|
||||
"foreignKey": "invoiceInFk"
|
||||
},
|
||||
"sageWithholding": {
|
||||
"type": "belongsTo",
|
||||
"model": "SageWithholding",
|
||||
"foreignKey": "withholdingSageFk"
|
||||
},
|
||||
"company": {
|
||||
"type": "belongsTo",
|
||||
"model": "Company",
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<mg-ajax path="InvoiceIns/{{patch.params.id}}" options="vnPatch"></mg-ajax>
|
||||
<vn-watcher
|
||||
vn-id="watcher"
|
||||
data="$ctrl.invoiceIn"
|
||||
form="form"
|
||||
save="patch">
|
||||
</vn-watcher>
|
||||
<form name="form" ng-submit="watcher.submit()" class="vn-w-md">
|
||||
<vn-card class="vn-pa-lg">
|
||||
<vn-horizontal>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Expedition date"
|
||||
ng-model="$ctrl.invoiceIn.issued"
|
||||
vn-focus
|
||||
rule>
|
||||
</vn-date-picker>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Operation date"
|
||||
ng-model="$ctrl.invoiceIn.operated"
|
||||
rule>
|
||||
</vn-date-picker>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="$ctrl.invoiceIn.supplierFk"
|
||||
url="Suppliers"
|
||||
show-field="nickname"
|
||||
search-function="{or: [{id: $search}, {nickname: {like: '%'+ $search +'%'}}]}"
|
||||
value-field="id"
|
||||
order="nickname"
|
||||
label="Supplier"
|
||||
required="true"
|
||||
rule>
|
||||
<tpl-item>
|
||||
{{::id}} - {{::nickname}}
|
||||
</tpl-item>
|
||||
</vn-autocomplete>
|
||||
<vn-textfield
|
||||
label="Supplier ref"
|
||||
ng-model="$ctrl.invoiceIn.supplierRef"
|
||||
rule>
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Entry date"
|
||||
ng-model="$ctrl.invoiceIn.bookEntried"
|
||||
rule>
|
||||
</vn-date-picker>
|
||||
<vn-date-picker
|
||||
vn-one
|
||||
label="Accounted date"
|
||||
ng-model="$ctrl.invoiceIn.booked"
|
||||
rule>
|
||||
</vn-date-picker>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
label="Currency"
|
||||
ng-model="$ctrl.invoiceIn.currencyFk"
|
||||
url="Currencies"
|
||||
show-field="name"
|
||||
value-field="id"
|
||||
rule>
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
url="Companies"
|
||||
label="Company"
|
||||
show-field="code"
|
||||
value-field="id"
|
||||
ng-model="$ctrl.invoiceIn.companyFk"
|
||||
rule>
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
</vn-card>
|
||||
<vn-button-bar>
|
||||
<vn-submit
|
||||
disabled="!watcher.dataChanged()"
|
||||
label="Save">
|
||||
</vn-submit>
|
||||
<vn-button
|
||||
class="cancel"
|
||||
label="Undo changes"
|
||||
disabled="!watcher.dataChanged()"
|
||||
ng-click="watcher.loadOriginalData()">
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
</form>
|
|
@ -0,0 +1,10 @@
|
|||
import ngModule from '../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
ngModule.vnComponent('vnInvoiceInBasicData', {
|
||||
template: require('./index.html'),
|
||||
controller: Section,
|
||||
bindings: {
|
||||
invoiceIn: '<'
|
||||
}
|
||||
});
|
|
@ -3,10 +3,27 @@ import ModuleCard from 'salix/components/module-card';
|
|||
|
||||
class Controller extends ModuleCard {
|
||||
reload() {
|
||||
const filter = {};
|
||||
const filter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'supplier'
|
||||
},
|
||||
{
|
||||
relation: 'invoiceInDueDay'
|
||||
},
|
||||
{
|
||||
relation: 'company'
|
||||
}
|
||||
]};
|
||||
|
||||
this.$http.get(`InvoiceIns/${this.$params.id}`, {filter})
|
||||
.then(res => this.invoiceIn = res.data);
|
||||
.then(res => {
|
||||
this.invoiceIn = res.data;
|
||||
this.invoiceIn.amount = res.data.invoiceInDueDay.reduce(
|
||||
(accumulator, currentValue) => {
|
||||
return accumulator + (currentValue['amount'] || 0);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,12 @@ import './index.js';
|
|||
describe('vnInvoiceIn', () => {
|
||||
let controller;
|
||||
let $httpBackend;
|
||||
let data = {id: 1, name: 'fooName'};
|
||||
const expectedAmount = 99;
|
||||
const data = {
|
||||
id: 1,
|
||||
name: 'fooName',
|
||||
invoiceInDueDay: [{amount: expectedAmount}]
|
||||
};
|
||||
|
||||
beforeEach(ngModule('invoiceIn'));
|
||||
|
||||
|
@ -21,7 +26,8 @@ describe('vnInvoiceIn', () => {
|
|||
controller.reload();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.invoiceIn).toEqual(data);
|
||||
expect(controller.invoiceIn).toBeDefined();
|
||||
expect(controller.invoiceIn.amount).toEqual(expectedAmount);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<vn-descriptor-content module="invoiceIn" description="$ctrl.invoiceIn.supplierRef">
|
||||
<slot-menu>
|
||||
<vn-item
|
||||
ng-click="deleteConfirmation.show()"
|
||||
vn-acl="invoicing"
|
||||
vn-acl-action="remove"
|
||||
name="deleteInvoice"
|
||||
translate>
|
||||
Delete Invoice
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
<slot-body>
|
||||
<div class="attributes">
|
||||
<vn-label-value label="Date" value="{{$ctrl.invoiceIn.issued | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Booked" value="{{$ctrl.invoiceIn.booked | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Import" value="{{$ctrl.invoiceIn.amount | currency: 'EUR': 2}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Supplier">
|
||||
<span ng-click="supplierDescriptor.show($event, $ctrl.invoiceIn.supplier.id)" class="link">
|
||||
{{$ctrl.invoiceIn.supplier.nickname}}
|
||||
</span>
|
||||
</vn-label-value>
|
||||
</div>
|
||||
<div class="quicklinks">
|
||||
<div ng-transclude="btnOne">
|
||||
<vn-quick-link tooltip="Supplier" state="['supplier.card.summary', {id: $ctrl.invoiceIn.supplier.id}]"
|
||||
icon="icon-supplier">
|
||||
</vn-quick-link>
|
||||
</div>
|
||||
<div ng-transclude="btnTwo">
|
||||
<vn-quick-link tooltip="Entries list" state="['entry.index', {q: $ctrl.entryFilter}]"
|
||||
icon="icon-entry">
|
||||
</vn-quick-link>
|
||||
</div>
|
||||
<div ng-transclude="btnThree">
|
||||
<vn-quick-link tooltip="Invoice list" state="['invoiceIn.index', {q: $ctrl.invoiceInFilter}]"
|
||||
icon="icon-invoiceIn">
|
||||
</vn-quick-link>
|
||||
</div>
|
||||
</div>
|
||||
</slot-body>
|
||||
</vn-descriptor-content>
|
||||
<vn-confirm vn-id="deleteConfirmation" on-accept="$ctrl.deleteInvoiceIn()"
|
||||
question="Are you sure you want to delete this invoice?">
|
||||
</vn-confirm>
|
||||
<vn-supplier-descriptor-popover vn-id="supplierDescriptor">
|
||||
</vn-supplier-descriptor-popover>
|
|
@ -10,6 +10,26 @@ class Controller extends Descriptor {
|
|||
this.entity = value;
|
||||
}
|
||||
|
||||
get entryFilter() {
|
||||
if (this.invoiceIn)
|
||||
return JSON.stringify({invoiceInFk: this.invoiceIn.id});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get invoiceInFilter() {
|
||||
if (this.invoiceIn)
|
||||
return JSON.stringify({supplierFk: this.invoiceIn.supplierFk});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
deleteInvoiceIn() {
|
||||
return this.$http.delete(`InvoiceIns/${this.id}`)
|
||||
.then(() => this.$state.go('invoiceIn.index'))
|
||||
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn deleted')));
|
||||
}
|
||||
|
||||
loadData() {
|
||||
const filter = {
|
||||
include: [
|
||||
|
|
|
@ -6,3 +6,5 @@ import './search-panel';
|
|||
import './card';
|
||||
import './descriptor';
|
||||
import './descriptor-popover';
|
||||
import './summary';
|
||||
import './basic-data';
|
||||
|
|
|
@ -22,26 +22,28 @@
|
|||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<a ng-repeat="invoiceIn in model.data"
|
||||
class="clickable vn-tr search-result">
|
||||
<a
|
||||
ng-repeat="invoiceIn in model.data"
|
||||
class="clickable vn-tr search-result"
|
||||
ui-sref="invoiceIn.card.summary({id: {{::invoiceIn.id}}})">
|
||||
<vn-td>{{::invoiceIn.id}}</vn-td>
|
||||
<vn-td>
|
||||
<span
|
||||
class="link"
|
||||
class="link"
|
||||
vn-click-stop="supplierDescriptor.show($event, invoiceIn.supplierFk)">
|
||||
{{::invoiceIn.supplierName}}
|
||||
</span>
|
||||
</vn-td>
|
||||
</vn-td>
|
||||
<vn-td>{{::invoiceIn.supplierRef | dashIfEmpty}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.serialNumber}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.serial}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.account}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.serialNumber}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.serial}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.account}}</vn-td>
|
||||
<vn-td expand>{{::invoiceIn.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
|
||||
<vn-td center>
|
||||
<vn-check disabled="true"
|
||||
ng-model="invoiceIn.isBooked">
|
||||
</vn-check>
|
||||
</vn-td>
|
||||
</vn-td>
|
||||
<vn-td>{{::invoiceIn.awbCode}}</vn-td>
|
||||
<vn-td>{{::invoiceIn.amount | currency:'EUR'}}</vn-td>
|
||||
<vn-td shrink>
|
||||
|
@ -67,38 +69,6 @@
|
|||
</vn-data-viewer>
|
||||
<vn-popup vn-id="summary">
|
||||
<vn-invoice-in-summary
|
||||
invoice-in="$ctrl.selectedinvoiceIn">
|
||||
invoice-in="$ctrl.selectedInvoiceIn">
|
||||
</vn-invoice-in-summary>
|
||||
</vn-popup>
|
||||
<vn-supplier-descriptor-popover
|
||||
vn-id="supplierDescriptor">
|
||||
</vn-supplier-descriptor-popover>
|
||||
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
|
||||
expr-builder="$ctrl.exprBuilder(param, value)">
|
||||
<slot-menu>
|
||||
<vn-item translate
|
||||
ng-if="contextmenu.isFilterAllowed()"
|
||||
ng-click="contextmenu.filterBySelection()">
|
||||
Filter by selection
|
||||
</vn-item>
|
||||
<vn-item translate
|
||||
ng-if="contextmenu.isFilterAllowed()"
|
||||
ng-click="contextmenu.excludeSelection()">
|
||||
Exclude selection
|
||||
</vn-item>
|
||||
<vn-item translate
|
||||
ng-if="contextmenu.isFilterAllowed()"
|
||||
ng-click="contextmenu.removeFilter()">
|
||||
Remove filter
|
||||
</vn-item>
|
||||
<vn-item translate
|
||||
ng-click="contextmenu.removeAllFilters()">
|
||||
Remove all filters
|
||||
</vn-item>
|
||||
<vn-item translate
|
||||
ng-if="contextmenu.isActionAllowed()"
|
||||
ng-click="contextmenu.copyValue()">
|
||||
Copy value
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
</vn-contextmenu>
|
||||
</vn-popup>
|
|
@ -34,6 +34,11 @@ export default class Controller extends Section {
|
|||
|
||||
return [minHour, maxHour];
|
||||
}
|
||||
|
||||
preview(invoiceIn) {
|
||||
this.selectedInvoiceIn = invoiceIn;
|
||||
this.$.summary.show();
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceInIndex', {
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
InvoiceIn: Facturas recibidas
|
||||
Search invoices in by reference: Buscar facturas recibidas por referencia
|
||||
Search invoices in by reference: Buscar facturas recibidas por referencia
|
||||
Entries list: Listado de entradas
|
||||
Invoice list: Listado de entradas
|
||||
InvoiceIn deleted: Factura eliminada
|
|
@ -7,6 +7,12 @@
|
|||
"menus": {
|
||||
"main": [
|
||||
{"state": "invoiceIn.index", "icon": "icon-invoiceIn"}
|
||||
],
|
||||
"card": [
|
||||
{
|
||||
"state": "invoiceIn.card.basicData",
|
||||
"icon": "settings"
|
||||
}
|
||||
]
|
||||
},
|
||||
"routes": [
|
||||
|
@ -36,9 +42,18 @@
|
|||
"component": "vn-invoice-in-summary",
|
||||
"description": "Summary",
|
||||
"params": {
|
||||
"invoice-In": "$ctrl.invoiceIn"
|
||||
"invoice-in": "$ctrl.invoiceIn"
|
||||
},
|
||||
"acl": ["developer"]
|
||||
"acl": ["administrative"]
|
||||
},
|
||||
{
|
||||
"url": "/basic-data",
|
||||
"state": "invoiceIn.card.basicData",
|
||||
"component": "vn-invoice-in-basic-data",
|
||||
"description": "Basic data",
|
||||
"params": {
|
||||
"invoice-in": "$ctrl.invoiceIn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<vn-card class="summary">
|
||||
<h5>
|
||||
<a
|
||||
ng-if="::$ctrl.summary.id"
|
||||
vn-tooltip="Go to the Invoice In"
|
||||
ui-sref="invoiceIn.card.summary({id: {{::$ctrl.summary.id}}})"
|
||||
name="goToSummary">
|
||||
<vn-icon-button icon="launch"></vn-icon-button>
|
||||
</a>
|
||||
<span>{{$ctrl.summary.id}} - {{$ctrl.summary.supplier.name}}</span>
|
||||
</h5>
|
||||
<vn-horizontal>
|
||||
<vn-auto>
|
||||
<h4 translate>Basic data</h4>
|
||||
<vn-horizontal>
|
||||
<vn-one>
|
||||
<vn-label-value label="Supplier" value="{{$ctrl.summary.supplier.name}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Supplier ref" value="{{$ctrl.summary.supplierRef}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Doc number" value="{{$ctrl.summary.serial}}/{{$ctrl.summary.serialNumber}}">
|
||||
</vn-label-value>
|
||||
</vn-one>
|
||||
<vn-one>
|
||||
<vn-label-value label="Expedition date" value="{{$ctrl.summary.issued | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Operation date" value="{{$ctrl.summary.operated | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Entry date" value="{{$ctrl.summary.bookEntried | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Booked date" value="{{$ctrl.summary.booked | date: 'dd/MM/yyyy'}}">
|
||||
</vn-label-value>
|
||||
</vn-one>
|
||||
<vn-one>
|
||||
<vn-label-value label="Sage withholding" value="{{$ctrl.summary.sageWithholding.withholding}}">
|
||||
</vn-label-value>
|
||||
<vn-label-value label="Company" value="{{$ctrl.summary.company.code}}">
|
||||
</vn-label-value>
|
||||
<vn-vertical>
|
||||
<vn-check label="Deductible" ng-model="$ctrl.summary.isVatDeductible" disabled="true">
|
||||
</vn-check>
|
||||
<vn-check label="Booked" ng-model="$ctrl.summary.isBooked" disabled="true">
|
||||
</vn-check>
|
||||
</vn-vertical>
|
||||
</vn-one>
|
||||
</vn-horizontal>
|
||||
</vn-auto>
|
||||
</vn-horizontal>
|
||||
</vn-card>
|
||||
<vn-supplier-descriptor-popover
|
||||
vn-id="supplierDescriptor">
|
||||
</vn-supplier-descriptor-popover>
|
|
@ -0,0 +1,28 @@
|
|||
import ngModule from '../module';
|
||||
import Summary from 'salix/components/summary';
|
||||
import './style.scss';
|
||||
|
||||
class Controller extends Summary {
|
||||
set invoiceIn(value) {
|
||||
this._invoiceIn = value;
|
||||
if (value && value.id)
|
||||
this.getSummary();
|
||||
}
|
||||
|
||||
get invoiceIn() {
|
||||
return this._invoiceIn;
|
||||
}
|
||||
|
||||
getSummary() {
|
||||
return this.$http.get(`InvoiceIns/${this.invoiceIn.id}/summary`)
|
||||
.then(res => this.summary = res.data);
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceInSummary', {
|
||||
template: require('./index.html'),
|
||||
controller: Controller,
|
||||
bindings: {
|
||||
invoiceIn: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import './index.js';
|
||||
|
||||
describe('InvoiceIn', () => {
|
||||
describe('Component summary', () => {
|
||||
let controller;
|
||||
let $httpBackend;
|
||||
let $scope;
|
||||
|
||||
beforeEach(ngModule('invoiceIn'));
|
||||
|
||||
beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$scope = $rootScope.$new();
|
||||
const $element = angular.element('<vn-invoice-in-summary></vn-invoice-in-summary>');
|
||||
controller = $componentController('vnInvoiceInSummary', {$element, $scope});
|
||||
controller.invoiceIn = {id: 1};
|
||||
}));
|
||||
|
||||
describe('getSummary()', () => {
|
||||
it('should perform a query to set summary', () => {
|
||||
$httpBackend.when('GET', `InvoiceIns/1/summary`).respond(200, 'the data you are looking for');
|
||||
controller.getSummary();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.summary).toEqual('the data you are looking for');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
Go to the Invoice In: Ir a la factura recibida
|
||||
Expedition date: Fecha expedición
|
||||
Operation date: Fecha operación
|
||||
Supplier ref: Ref. proveedor
|
||||
Entry date: Fecha asiento
|
||||
Booked date: Fecha contable
|
||||
Accounted date: Fecha contable
|
||||
Doc number: Numero documento
|
||||
Sage withholding: Retención sage
|
||||
Deductible: Deducible
|
|
@ -0,0 +1,5 @@
|
|||
@import "variables";
|
||||
|
||||
vn-invoice-in-summary .summary {
|
||||
width: $width-lg;
|
||||
}
|
|
@ -50,8 +50,9 @@ module.exports = Self => {
|
|||
const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, '');
|
||||
const file = `${fileName}.png`;
|
||||
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) {
|
||||
const error = new Error(`Could not download the image. Status code ${response.statusCode}`);
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = Self => {
|
||||
Self.remoteMethod('getWasteDetail', {
|
||||
Self.remoteMethod('getWasteByWorker', {
|
||||
description: 'Returns the details of losses by worker',
|
||||
accessType: 'READ',
|
||||
accepts: [],
|
||||
|
@ -8,13 +8,25 @@ module.exports = Self => {
|
|||
root: true
|
||||
},
|
||||
http: {
|
||||
path: `/getWasteDetail`,
|
||||
path: `/getWasteByWorker`,
|
||||
verb: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
Self.getWasteDetail = async() => {
|
||||
const [wastes] = await Self.rawSql(`CALL bs.weekWaste_getDetail()`);
|
||||
Self.getWasteByWorker = async() => {
|
||||
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 = [];
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('item getWasteDetail()', () => {
|
||||
describe('Item getWasteByWorker()', () => {
|
||||
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 anyResult = result[Math.floor(Math.random() * Math.floor(length))];
|
|
@ -11,7 +11,8 @@ module.exports = Self => {
|
|||
require('../methods/item/regularize')(Self);
|
||||
require('../methods/item/getVisibleAvailable')(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);
|
||||
|
||||
Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'});
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.buyerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRolee"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
|
|
|
@ -20,7 +20,8 @@ import './niche';
|
|||
import './botanical';
|
||||
import './barcode';
|
||||
import './summary';
|
||||
import './waste';
|
||||
import './waste/index/';
|
||||
import './waste/detail';
|
||||
import './fixed-price';
|
||||
import './fixed-price-search-panel';
|
||||
|
||||
|
|
|
@ -63,4 +63,5 @@ Diary: Histórico
|
|||
Item diary: Registro de compra-venta
|
||||
Last entries: Últimas entradas
|
||||
Tags: Etiquetas
|
||||
Waste breakdown: Desglose de mermas
|
||||
Waste breakdown: Desglose de mermas
|
||||
Waste breakdown by item: Desglose de mermas por artículo
|
|
@ -17,10 +17,10 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.attenderFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'buyer'}"
|
||||
label="Atender">
|
||||
<tpl-item>{{nickname}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"main": [
|
||||
{"state": "item.index", "icon": "icon-item"},
|
||||
{"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"}
|
||||
],
|
||||
"card": [
|
||||
|
@ -155,12 +155,25 @@
|
|||
"acl": ["employee"]
|
||||
},
|
||||
{
|
||||
"url" : "/waste",
|
||||
"url": "/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",
|
||||
"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",
|
||||
"state": "item.fixedPrice",
|
||||
|
|
|
@ -42,11 +42,11 @@
|
|||
vn-one
|
||||
disabled="false"
|
||||
ng-model="filter.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'buyer'}"
|
||||
label="Buyer">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,7 @@
|
|||
import ngModule from '../../module';
|
||||
import Section from 'salix/components/section';
|
||||
|
||||
ngModule.vnComponent('vnItemWasteDetail', {
|
||||
template: require('./index.html'),
|
||||
controller: Section
|
||||
});
|
|
@ -1,13 +1,14 @@
|
|||
<vn-crud-model auto-load="true"
|
||||
vn-id="model"
|
||||
url="Items/getWasteDetail"
|
||||
url="Items/getWasteByWorker"
|
||||
data="details">
|
||||
</vn-crud-model>
|
||||
|
||||
<vn-data-viewer model="model">
|
||||
<vn-card>
|
||||
<section ng-repeat="detail in details" class="vn-pa-md">
|
||||
<vn-horizontal class="header">
|
||||
<h5><span translate>{{detail.buyer}}</span></h5>
|
||||
<h5><span><span translate>{{detail.buyer}}</span></h5>
|
||||
</vn-horizontal>
|
||||
<vn-table>
|
||||
<vn-thead>
|
||||
|
@ -19,7 +20,8 @@
|
|||
</vn-tr>
|
||||
</vn-thead>
|
||||
<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 number>{{::(waste.percentage / 100) | percentage: 2}}</vn-td>
|
||||
<vn-td number>{{::waste.dwindle | currency: 'EUR'}}</vn-td>
|
|
@ -1,8 +1,8 @@
|
|||
import ngModule from '../module';
|
||||
import ngModule from '../../module';
|
||||
import Section from 'salix/components/section';
|
||||
import './style.scss';
|
||||
|
||||
ngModule.vnComponent('vnItemWaste', {
|
||||
ngModule.vnComponent('vnItemWasteIndex', {
|
||||
template: require('./index.html'),
|
||||
controller: Section
|
||||
});
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -28,11 +28,11 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
search-function="{firstName: $search}"
|
||||
show-field="nickname"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'salesPerson'}"
|
||||
label="Sales person">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="$ctrl.route.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<vn-autocomplete
|
||||
label="Worker"
|
||||
ng-model="$ctrl.route.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
where="{role: 'employee'}">
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
show-field="nickname"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="$ctrl.supplier.workerFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
search-function="{firstName: $search}"
|
||||
show-field="nickname"
|
||||
value-field="id"
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.buyerId"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
where="{role: 'buyer'}"
|
||||
label="Buyer">
|
||||
<tpl-item>{{nickname}}</tpl-item>
|
||||
</vn-autocomplete>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
const soap = require('soap');
|
||||
|
||||
describe('ticket sendSms()', () => {
|
||||
let logId;
|
||||
|
@ -10,6 +11,7 @@ describe('ticket sendSms()', () => {
|
|||
});
|
||||
|
||||
it('should send a message and log it', async() => {
|
||||
spyOn(soap, 'createClientAsync').and.returnValue('a so fake client');
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
let id = 11;
|
||||
let destination = 222222222;
|
||||
|
|
|
@ -165,13 +165,13 @@ class Controller extends Section {
|
|||
created: this.ticket.updated
|
||||
};
|
||||
this.showSMSDialog({
|
||||
message: this.$params.message || this.$t('Minimum is needed', params)
|
||||
message: this.$t('Minimum is needed', params)
|
||||
});
|
||||
}
|
||||
|
||||
sendPaymentSms() {
|
||||
this.showSMSDialog({
|
||||
message: this.$params.message || this.$t('Make a payment')
|
||||
message: this.$t('Make a payment')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<vn-autocomplete
|
||||
label="Buyer"
|
||||
ng-model="$ctrl.ticketRequest.attenderFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithRole"
|
||||
show-field="nickname"
|
||||
where="{role: 'buyer'}"
|
||||
search-function="{firstName: $search}">
|
||||
|
|
|
@ -27,9 +27,12 @@ class Controller extends Section {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<vn-autocomplete
|
||||
vn-one
|
||||
ng-model="filter.salesPersonFk"
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
search-function="{firstName: $search}"
|
||||
value-field="id"
|
||||
where="{role: 'employee'}"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
vn-one
|
||||
url="Clients/activeWorkersWithRole"
|
||||
url="Workers/activeWithInheritedRole"
|
||||
ng-if="$ctrl.isPickerDesignedState"
|
||||
ng-model="$ctrl.workerFk"
|
||||
show-field="nickname"
|
||||
|
|
|
@ -3,26 +3,7 @@ const buildFilter = require('vn-loopback/util/filter').buildFilter;
|
|||
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
|
||||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('activeWorkersWithRole', {
|
||||
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 => {
|
||||
Self.activeWorkers = async(query, filter) => {
|
||||
let conn = Self.dataSource.connector;
|
||||
if (filter.where && filter.where.and && Array.isArray(filter.where.and)) {
|
||||
let where = {};
|
||||
|
@ -54,14 +35,8 @@ module.exports = Self => {
|
|||
|
||||
myFilter = mergeFilters(myFilter, clientFilter);
|
||||
|
||||
let stmt = new ParameterizedSQL(
|
||||
`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`
|
||||
);
|
||||
let stmt = new ParameterizedSQL(query);
|
||||
stmt.merge(conn.makeSuffix(myFilter));
|
||||
return await conn.executeStmt(stmt);
|
||||
return conn.executeStmt(stmt);
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -3,7 +3,7 @@ const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
|
|||
|
||||
module.exports = Self => {
|
||||
Self.remoteMethod('mySubordinates', {
|
||||
description: 'Returns a list of a subordinate workers',
|
||||
description: 'Returns a list of a subordinated workers',
|
||||
accessType: 'READ',
|
||||
accepts: [{
|
||||
arg: 'ctx',
|
||||
|
@ -11,7 +11,7 @@ module.exports = Self => {
|
|||
http: {source: 'context'}
|
||||
}],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -22,19 +22,16 @@ module.exports = Self => {
|
|||
|
||||
Self.mySubordinates = async ctx => {
|
||||
const conn = Self.dataSource.connector;
|
||||
const myUserId = ctx.req.accessToken.userId;
|
||||
const myWorker = await Self.app.models.Worker.findOne({
|
||||
where: {userFk: myUserId}
|
||||
});
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const stmts = [];
|
||||
|
||||
stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [myWorker.id]));
|
||||
stmts.push('SELECT * FROM tmp.subordinate');
|
||||
stmts.push(new ParameterizedSQL('CALL vn.subordinateGetList(?)', [userId]));
|
||||
const queryIndex = stmts.push('SELECT * FROM tmp.subordinate') - 1;
|
||||
stmts.push('DROP TEMPORARY TABLE tmp.subordinate');
|
||||
|
||||
let sql = ParameterizedSQL.join(stmts, ';');
|
||||
let result = await conn.executeStmt(sql);
|
||||
const sql = ParameterizedSQL.join(stmts, ';');
|
||||
const result = await conn.executeStmt(sql);
|
||||
|
||||
return result[1];
|
||||
return result[queryIndex];
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue