Merge branch 'dev' into 3111-uploadPhoto_vertical
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Jimenez Ruiz 2021-10-18 09:14:44 +00:00
commit b737bb69d6
154 changed files with 45356 additions and 1818 deletions

View File

@ -11,7 +11,6 @@ Required applications.
* Node.js = 14.x LTS * Node.js = 14.x LTS
* Docker * Docker
* Git * Git
* Docker
You will need to install globally the following items. You will need to install globally the following items.
``` ```

View File

@ -89,6 +89,7 @@ module.exports = Self => {
await uploadNewFile(ctx, dms, myOptions); await uploadNewFile(ctx, dms, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
return dms; return dms;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -96,6 +96,7 @@ module.exports = Self => {
} }
if (tx) await tx.commit(); if (tx) await tx.commit();
return addedDms; return addedDms;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -151,6 +151,7 @@ module.exports = Self => {
await fs.unlink(srcFilePath); await fs.unlink(srcFilePath);
await tx.commit(); await tx.commit();
return newImage; return newImage;
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();

View File

@ -0,0 +1,48 @@
drop procedure `vn`.`ticket_getProblems`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`ticket_getProblems`(IN vIsTodayRelative tinyint(1))
BEGIN
/**
* Calcula los problemas para un conjunto de tickets.
* Agrupados por ticket
*
* @table tmp.sale_getProblems(ticketFk, clientFk, warehouseFk, shipped) Identificadores de los tickets a calcular
* @return tmp.ticket_problems
*/
CALL sale_getProblems(vIsTodayRelative);
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_problems;
CREATE TEMPORARY TABLE tmp.ticket_problems
(PRIMARY KEY (ticketFk))
ENGINE = MEMORY
SELECT
ticketFk,
MAX(p.isFreezed) AS isFreezed,
MAX(p.risk) AS risk,
MAX(p.hasHighRisk) AS hasHighRisk,
MAX(p.hasTicketRequest) AS hasTicketRequest,
MIN(p.isAvailable) AS isAvailable,
MAX(p.itemShortage) AS itemShortage,
MIN(p.isTaxDataChecked) AS isTaxDataChecked,
MAX(p.hasComponentLack) AS hasComponentLack,
0 AS totalProblems
FROM tmp.sale_problems p
GROUP BY ticketFk;
UPDATE tmp.ticket_problems tp
SET tp.totalProblems = (
(tp.isFreezed) +
IF(tp.risk, TRUE, FALSE) +
(tp.hasTicketRequest) +
(tp.isAvailable = 0) +
(tp.isTaxDataChecked = 0) +
(tp.hasComponentLack)
);
DROP TEMPORARY TABLE
tmp.sale_problems;
END;;$$
DELIMITER ;

View File

@ -108,7 +108,7 @@ let actions = {
}, },
getState: async function() { getState: async function() {
return await this.evaluate(() => { return this.evaluate(() => {
let $state = angular.element(document.body).injector().get('$state'); let $state = angular.element(document.body).injector().get('$state');
return $state.current.name; return $state.current.name;
}); });
@ -194,7 +194,7 @@ let actions = {
}, },
getProperty: async function(selector, property) { getProperty: async function(selector, property) {
return await this.evaluate((selector, property) => { return this.evaluate((selector, property) => {
return document.querySelector(selector)[property].replace(/\s+/g, ' ').trim(); return document.querySelector(selector)[property].replace(/\s+/g, ' ').trim();
}, selector, property); }, selector, property);
}, },
@ -202,7 +202,7 @@ let actions = {
getClassName: async function(selector) { getClassName: async function(selector) {
const element = await this.$(selector); const element = await this.$(selector);
const handle = await element.getProperty('className'); const handle = await element.getProperty('className');
return await handle.jsonValue(); return handle.jsonValue();
}, },
waitPropertyLength: async function(selector, property, minLength) { waitPropertyLength: async function(selector, property, minLength) {
@ -210,7 +210,7 @@ let actions = {
const element = document.querySelector(selector); const element = document.querySelector(selector);
return element && element[property] != null && element[property] !== '' && element[property].length >= minLength; return element && element[property] != null && element[property] !== '' && element[property].length >= minLength;
}, {}, selector, property, minLength); }, {}, selector, property, minLength);
return await this.getProperty(selector, property); return this.getProperty(selector, property);
}, },
expectPropertyValue: async function(selector, property, value) { expectPropertyValue: async function(selector, property, value) {
@ -219,7 +219,7 @@ let actions = {
builtSelector = await this.selectorFormater(selector); builtSelector = await this.selectorFormater(selector);
try { try {
return await this.waitForFunction((selector, property, value) => { return this.waitForFunction((selector, property, value) => {
const element = document.querySelector(selector); const element = document.querySelector(selector);
return element[property] == value; return element[property] == value;
}, {}, builtSelector, property, value); }, {}, builtSelector, property, value);
@ -239,7 +239,7 @@ let actions = {
return element && element[property] != null && element[property] !== ''; return element && element[property] != null && element[property] !== '';
}, {}, builtSelector, property); }, {}, builtSelector, property);
return await this.getProperty(builtSelector, property); return this.getProperty(builtSelector, property);
} catch (error) { } catch (error) {
throw new Error(`couldn't get property: ${property} of ${builtSelector}, ${error}`); throw new Error(`couldn't get property: ${property} of ${builtSelector}, ${error}`);
} }
@ -261,7 +261,7 @@ let actions = {
await this.waitForSelector(selector); await this.waitForSelector(selector);
await this.waitForFunction(checkVisibility, {}, selector); await this.waitForFunction(checkVisibility, {}, selector);
return await this.click(selector); return this.click(selector);
}, },
writeOnEditableTD: async function(selector, text) { writeOnEditableTD: async function(selector, text) {
@ -274,7 +274,7 @@ let actions = {
focusElement: async function(selector) { focusElement: async function(selector) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.evaluate(selector => { return this.evaluate(selector => {
let element = document.querySelector(selector); let element = document.querySelector(selector);
element.focus(); element.focus();
}, selector); }, selector);
@ -282,19 +282,19 @@ let actions = {
isVisible: async function(selector) { isVisible: async function(selector) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.evaluate(checkVisibility, selector); return this.evaluate(checkVisibility, selector);
}, },
waitImgLoad: async function(selector) { waitImgLoad: async function(selector) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.waitForFunction(selector => { return this.waitForFunction(selector => {
const imageReady = document.querySelector(selector).complete; const imageReady = document.querySelector(selector).complete;
return imageReady; return imageReady;
}, {}, selector); }, {}, selector);
}, },
countElement: async function(selector) { countElement: async function(selector) {
return await this.evaluate(selector => { return this.evaluate(selector => {
return document.querySelectorAll(selector).length; return document.querySelectorAll(selector).length;
}, selector); }, selector);
}, },
@ -312,7 +312,7 @@ let actions = {
waitForClassNotPresent: async function(selector, className) { waitForClassNotPresent: async function(selector, className) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.waitForFunction((selector, className) => { return this.waitForFunction((selector, className) => {
if (!document.querySelector(selector).classList.contains(className)) if (!document.querySelector(selector).classList.contains(className))
return true; return true;
}, {}, selector, className); }, {}, selector, className);
@ -320,7 +320,7 @@ let actions = {
waitForClassPresent: async function(selector, className) { waitForClassPresent: async function(selector, className) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.waitForFunction((elementSelector, targetClass) => { return this.waitForFunction((elementSelector, targetClass) => {
if (document.querySelector(elementSelector).classList.contains(targetClass)) if (document.querySelector(elementSelector).classList.contains(targetClass))
return true; return true;
}, {}, selector, className); }, {}, selector, className);
@ -387,13 +387,13 @@ let actions = {
const innerText = document.querySelector(selector).innerText; const innerText = document.querySelector(selector).innerText;
return innerText != null && innerText != ''; return innerText != null && innerText != '';
}, {}, selector); }, {}, selector);
return await this.evaluate(selector => { return this.evaluate(selector => {
return document.querySelector(selector).innerText; return document.querySelector(selector).innerText;
}, selector); }, selector);
}, },
waitForEmptyInnerText: async function(selector) { waitForEmptyInnerText: async function(selector) {
return await this.waitFunction(selector => { return this.waitFunction(selector => {
return document.querySelector(selector).innerText == ''; return document.querySelector(selector).innerText == '';
}, selector); }, selector);
}, },
@ -521,7 +521,7 @@ let actions = {
checkboxState: async function(selector) { checkboxState: async function(selector) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.evaluate(selector => { return this.evaluate(selector => {
let checkbox = document.querySelector(selector); let checkbox = document.querySelector(selector);
switch (checkbox.$ctrl.field) { switch (checkbox.$ctrl.field) {
case null: case null:
@ -536,14 +536,14 @@ let actions = {
isDisabled: async function(selector) { isDisabled: async function(selector) {
await this.waitForSelector(selector); await this.waitForSelector(selector);
return await this.evaluate(selector => { return this.evaluate(selector => {
let element = document.querySelector(selector); let element = document.querySelector(selector);
return element.$ctrl.disabled; return element.$ctrl.disabled;
}, selector); }, selector);
}, },
waitForStylePresent: async function(selector, property, value) { waitForStylePresent: async function(selector, property, value) {
return await this.waitForFunction((selector, property, value) => { return this.waitForFunction((selector, property, value) => {
const element = document.querySelector(selector); const element = document.querySelector(selector);
return element.style[property] == value; return element.style[property] == value;
}, {}, selector, property, value); }, {}, selector, property, value);
@ -631,7 +631,7 @@ export function extendPage(page) {
for (let name in actions) { for (let name in actions) {
page[name] = async(...args) => { page[name] = async(...args) => {
try { try {
return await actions[name].apply(page, args); return actions[name].apply(page, args);
} catch (err) { } catch (err) {
let stringArgs = args let stringArgs = args
.map(i => typeof i == 'function' ? 'Function' : i) .map(i => typeof i == 'function' ? 'Function' : i)

View File

@ -5,11 +5,16 @@ import {url as defaultURL} from './config';
export async function getBrowser() { export async function getBrowser() {
const args = [ const args = [
`--no-sandbox`, '--no-sandbox',
`--window-size=${ 1920 },${ 1080 }` `--window-size=${ 1920 },${ 1080 }`,
'--single-process',
'--no-zygote'
// '--disable-dev-shm-usage'
// '--full-memory-crash-report',
// '--unlimited-storage'
]; ];
let env = process.env; const env = process.env;
if (env.E2E_DEBUG) { if (env.E2E_DEBUG) {
args.push('--auto-open-devtools-for-tabs'); args.push('--auto-open-devtools-for-tabs');
@ -22,6 +27,9 @@ export async function getBrowser() {
defaultViewport: null, defaultViewport: null,
headless: headless, headless: headless,
slowMo: 0, // slow down by ms slowMo: 0, // slow down by ms
// ignoreDefaultArgs: ['--disable-extensions'],
// executablePath: '/usr/bin/google-chrome-stable',
// executablePath: '/usr/bin/firefox-developer-edition',
}); });
let page = (await browser.pages())[0]; let page = (await browser.pages())[0];
@ -38,7 +46,7 @@ export async function getBrowser() {
}); });
}); });
page = extendPage(page); page = extendPage(page);
page.setDefaultTimeout(4000); page.setDefaultTimeout(5000);
await page.goto(defaultURL, {waitUntil: 'load'}); await page.goto(defaultURL, {waitUntil: 'load'});
return {page, close: browser.close.bind(browser)}; return {page, close: browser.close.bind(browser)};
} }

View File

@ -544,7 +544,7 @@ export default {
}, },
ticketSales: { ticketSales: {
setOk: 'vn-ticket-sale vn-tool-bar > vn-button[label="Ok"] > button', setOk: 'vn-ticket-sale vn-tool-bar > vn-button[label="Ok"] > button',
saleLine: 'vn-table div > vn-tbody > vn-tr', saleLine: 'vn-table div > vn-tbody > vn-tr vn-check',
saleDescriptorPopover: '.vn-popover.shown vn-item-descriptor', saleDescriptorPopover: '.vn-popover.shown vn-item-descriptor',
saleDescriptorPopoverSummaryButton: '.vn-popover.shown vn-item-descriptor a[ui-sref="item.card.summary({id: $ctrl.descriptor.id})"]', saleDescriptorPopoverSummaryButton: '.vn-popover.shown vn-item-descriptor a[ui-sref="item.card.summary({id: $ctrl.descriptor.id})"]',
newItemButton: 'vn-ticket-sale vn-card vn-icon-button[icon="add_circle"]', newItemButton: 'vn-ticket-sale vn-card vn-icon-button[icon="add_circle"]',

View File

@ -107,6 +107,7 @@ describe('Item regularize path', () => {
}); });
it('should regularize the item once more', async() => { it('should regularize the item once more', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.itemDescriptor.moreMenu); await page.waitToClick(selectors.itemDescriptor.moreMenu);
await page.waitToClick(selectors.itemDescriptor.moreMenuRegularizeButton); await page.waitToClick(selectors.itemDescriptor.moreMenuRegularizeButton);
await page.write(selectors.itemDescriptor.regularizeQuantity, '100'); await page.write(selectors.itemDescriptor.regularizeQuantity, '100');

View File

@ -196,6 +196,7 @@ describe('Ticket Edit sale path', () => {
}); });
it('should select the third sale and create a claim of it', async() => { it('should select the third sale and create a claim of it', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim); await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);
@ -340,9 +341,10 @@ describe('Ticket Edit sale path', () => {
}); });
it('should confirm the new ticket received the line', async() => { it('should confirm the new ticket received the line', async() => {
const expectedLines = 1;
const result = await page.countElement(selectors.ticketSales.saleLine); const result = await page.countElement(selectors.ticketSales.saleLine);
expect(result).toEqual(1); expect(result).toEqual(expectedLines);
}); });
it('should check the first sale reserved icon isnt visible', async() => { it('should check the first sale reserved icon isnt visible', async() => {
@ -353,6 +355,7 @@ describe('Ticket Edit sale path', () => {
it('should mark the first sale as reserved', async() => { it('should mark the first sale as reserved', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuReserve); await page.waitToClick(selectors.ticketSales.moreMenuReserve);
await page.closePopup(); await page.closePopup();
@ -363,6 +366,7 @@ describe('Ticket Edit sale path', () => {
}); });
it('should unmark the first sale as reserved', async() => { it('should unmark the first sale as reserved', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuUnmarkReseved); await page.waitToClick(selectors.ticketSales.moreMenuUnmarkReseved);
await page.waitForClassPresent(selectors.ticketSales.firstSaleReservedIcon, 'ng-hide'); await page.waitForClassPresent(selectors.ticketSales.firstSaleReservedIcon, 'ng-hide');

View File

@ -28,6 +28,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should add the ticket to thursday turn using the descriptor more menu', async() => { it('should add the ticket to thursday turn using the descriptor more menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn); await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn);
await page.waitToClick(selectors.ticketDescriptor.thursdayButton); await page.waitToClick(selectors.ticketDescriptor.thursdayButton);
@ -63,6 +64,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should add the ticket to saturday turn using the descriptor more menu', async() => { it('should add the ticket to saturday turn using the descriptor more menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn); await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn);
await page.waitToClick(selectors.ticketDescriptor.saturdayButton); await page.waitToClick(selectors.ticketDescriptor.saturdayButton);

View File

@ -22,6 +22,7 @@ describe('Ticket descriptor path', () => {
}); });
it(`should update the shipped hour using the descriptor menu`, async() => { it(`should update the shipped hour using the descriptor menu`, async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuChangeShippedHour); await page.waitToClick(selectors.ticketDescriptor.moreMenuChangeShippedHour);
await page.pickTime(selectors.ticketDescriptor.changeShippedHour, '08:15'); await page.pickTime(selectors.ticketDescriptor.changeShippedHour, '08:15');
@ -65,6 +66,7 @@ describe('Ticket descriptor path', () => {
describe('Restore ticket', () => { describe('Restore ticket', () => {
it('should restore the ticket using the descriptor menu', async() => { it('should restore the ticket using the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuRestoreTicket); await page.waitToClick(selectors.ticketDescriptor.moreMenuRestoreTicket);
await page.waitToClick(selectors.ticketDescriptor.acceptDialog); await page.waitToClick(selectors.ticketDescriptor.acceptDialog);
@ -82,6 +84,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should open the add stowaway dialog', async() => { it('should open the add stowaway dialog', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitForFunction(() => { await page.waitForFunction(() => {
let element = document.querySelector('vn-ticket-descriptor-menu'); let element = document.querySelector('vn-ticket-descriptor-menu');
return element.$ctrl.canShowStowaway === true; return element.$ctrl.canShowStowaway === true;
@ -114,6 +117,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should delete the stowaway', async() => { it('should delete the stowaway', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded(); await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteStowawayButton); await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteStowawayButton);
@ -145,6 +149,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should invoice the ticket using the descriptor menu', async() => { it('should invoice the ticket using the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded(); await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuMakeInvoice); await page.waitToClick(selectors.ticketDescriptor.moreMenuMakeInvoice);
@ -176,6 +181,7 @@ describe('Ticket descriptor path', () => {
describe('SMS', () => { describe('SMS', () => {
it('should send the payment SMS using the descriptor menu', async() => { it('should send the payment SMS using the descriptor menu', async() => {
await page.waitForTimeout(2000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded(); await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuPaymentSMS); await page.waitToClick(selectors.ticketDescriptor.moreMenuPaymentSMS);
@ -188,6 +194,7 @@ describe('Ticket descriptor path', () => {
}); });
it('should send the import SMS using the descriptor menu', async() => { it('should send the import SMS using the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded(); await page.waitForContentLoaded();
await page.waitToClick(selectors.ticketDescriptor.moreMenuSendImportSms); await page.waitToClick(selectors.ticketDescriptor.moreMenuSendImportSms);

View File

@ -62,6 +62,7 @@ describe('Ticket create path', () => {
}); });
it('should make the previously created ticket the stowaway of the current ticket', async() => { it('should make the previously created ticket the stowaway of the current ticket', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuAddStowaway); await page.waitToClick(selectors.ticketDescriptor.moreMenuAddStowaway);
await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket); await page.waitToClick(selectors.ticketDescriptor.addStowawayDialogFirstTicket);
@ -71,6 +72,7 @@ describe('Ticket create path', () => {
}); });
it('should delete the current ticket', async() => { it('should delete the current ticket', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket);
await page.waitToClick(selectors.ticketDescriptor.acceptDialog); await page.waitToClick(selectors.ticketDescriptor.acceptDialog);

View File

@ -17,6 +17,7 @@ describe('Ticket create from client path', () => {
}); });
it('should click the create simple ticket on the descriptor menu', async() => { it('should click the create simple ticket on the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.clientDescriptor.moreMenu); await page.waitToClick(selectors.clientDescriptor.moreMenu);
await page.waitToClick(selectors.clientDescriptor.simpleTicketButton); await page.waitToClick(selectors.clientDescriptor.simpleTicketButton);
await page.waitForState('ticket.create'); await page.waitForState('ticket.create');

View File

@ -18,6 +18,7 @@ describe('InvoiceIn descriptor path', () => {
}); });
it('should clone the invoiceIn using the descriptor more menu', async() => { it('should clone the invoiceIn using the descriptor more menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuCloneInvoiceIn); await page.waitToClick(selectors.invoiceInDescriptor.moreMenuCloneInvoiceIn);
await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); await page.waitToClick(selectors.invoiceInDescriptor.acceptButton);
@ -31,6 +32,7 @@ describe('InvoiceIn descriptor path', () => {
}); });
it('should delete the cloned invoiceIn using the descriptor more menu', async() => { it('should delete the cloned invoiceIn using the descriptor more menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn); await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn);
await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); await page.waitToClick(selectors.invoiceInDescriptor.acceptButton);

View File

@ -38,6 +38,7 @@ describe('InvoiceOut descriptor path', () => {
}); });
it('should delete the invoiceOut using the descriptor more menu', async() => { it('should delete the invoiceOut using the descriptor more menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutDescriptor.moreMenu); await page.waitToClick(selectors.invoiceOutDescriptor.moreMenu);
await page.waitToClick(selectors.invoiceOutDescriptor.moreMenuDeleteInvoiceOut); await page.waitToClick(selectors.invoiceOutDescriptor.moreMenuDeleteInvoiceOut);
await page.waitToClick(selectors.invoiceOutDescriptor.acceptDeleteButton); await page.waitToClick(selectors.invoiceOutDescriptor.acceptDeleteButton);

View File

@ -16,6 +16,7 @@ describe('InvoiceOut manual invoice path', () => {
}); });
it('should open the manual invoice form', async() => { it('should open the manual invoice form', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createInvoice); await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice); await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm); await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);
@ -44,6 +45,7 @@ describe('InvoiceOut manual invoice path', () => {
}); });
it('should now open the manual invoice form', async() => { it('should now open the manual invoice form', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createInvoice); await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice); await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm); await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);

View File

@ -34,6 +34,7 @@ describe('Travel descriptor path', () => {
}); });
it('should be redirected to the create entry view', async() => { it('should be redirected to the create entry view', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.travelDescriptor.dotMenu); await page.waitToClick(selectors.travelDescriptor.dotMenu);
await page.waitToClick(selectors.travelDescriptor.dotMenuAddEntry); await page.waitToClick(selectors.travelDescriptor.dotMenuAddEntry);
await page.waitForState('entry.create'); await page.waitForState('entry.create');
@ -89,6 +90,7 @@ describe('Travel descriptor path', () => {
}); });
it('should be redirected to the create travel when using the clone option of the dot menu', async() => { it('should be redirected to the create travel when using the clone option of the dot menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.travelDescriptor.dotMenu); await page.waitToClick(selectors.travelDescriptor.dotMenu);
await page.waitToClick(selectors.travelDescriptor.dotMenuClone); await page.waitToClick(selectors.travelDescriptor.dotMenuClone);
await page.respondToDialog('accept'); await page.respondToDialog('accept');
@ -114,6 +116,7 @@ describe('Travel descriptor path', () => {
}); });
it('should atempt to clone the travel and its entries using the descriptor menu but receive an error', async() => { it('should atempt to clone the travel and its entries using the descriptor menu but receive an error', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.travelDescriptor.dotMenu); await page.waitToClick(selectors.travelDescriptor.dotMenu);
await page.waitToClick(selectors.travelDescriptor.dotMenuCloneWithEntries); await page.waitToClick(selectors.travelDescriptor.dotMenuCloneWithEntries);
await page.waitToClick(selectors.travelDescriptor.acceptClonation); await page.waitToClick(selectors.travelDescriptor.acceptClonation);

View File

@ -17,6 +17,7 @@ describe('Zone descriptor path', () => {
}); });
it('should eliminate the zone using the descriptor option', async() => { it('should eliminate the zone using the descriptor option', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.zoneDescriptor.menu); await page.waitToClick(selectors.zoneDescriptor.menu);
await page.waitToClick(selectors.zoneDescriptor.deleteZone); await page.waitToClick(selectors.zoneDescriptor.deleteZone);
await page.respondToDialog('accept'); await page.respondToDialog('accept');

View File

@ -91,6 +91,7 @@ describe('Account create and basic data path', () => {
}); });
it('should activate the account using the descriptor menu', async() => { it('should activate the account using the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.accountDescriptor.menuButton); await page.waitToClick(selectors.accountDescriptor.menuButton);
await page.waitToClick(selectors.accountDescriptor.activateAccount); await page.waitToClick(selectors.accountDescriptor.activateAccount);
await page.waitToClick(selectors.accountDescriptor.acceptButton); await page.waitToClick(selectors.accountDescriptor.acceptButton);
@ -138,6 +139,7 @@ describe('Account create and basic data path', () => {
describe('Set password', () => { describe('Set password', () => {
it('should set the password using the descriptor menu', async() => { it('should set the password using the descriptor menu', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.accountDescriptor.menuButton); await page.waitToClick(selectors.accountDescriptor.menuButton);
await page.waitToClick(selectors.accountDescriptor.setPassword); await page.waitToClick(selectors.accountDescriptor.setPassword);
await page.write(selectors.accountDescriptor.newPassword, 'quantum.crypt0graphy'); await page.write(selectors.accountDescriptor.newPassword, 'quantum.crypt0graphy');

View File

@ -3,8 +3,8 @@ import Popover from '../popover';
import './style.scss'; import './style.scss';
export default class Menu extends Popover { export default class Menu extends Popover {
show(parent) { show(parent, direction) {
super.show(parent); super.show(parent, direction);
this.windowEl.addEventListener('click', () => this.hide()); this.windowEl.addEventListener('click', () => this.hide());
} }
} }

View File

@ -1,7 +1,18 @@
@import "./effects"; @import "./effects";
@import "variables";
.vn-menu { .vn-menu {
vn-item, .vn-item { vn-item, .vn-item {
@extend %clickable; @extend %clickable;
} }
vn-item.dropdown:after,
.vn-item.dropdown:after {
font-family: 'Material Icons';
content: 'keyboard_arrow_right';
position: absolute;
color: $color-spacer;
font-size: 1.5em;
right: 0
}
} }

View File

@ -1,9 +1,10 @@
<div ng-if="$ctrl.model.moreRows"> <div ng-if="$ctrl.model.moreRows" class="vn-py-md">
<vn-button <div
ng-if="!$ctrl.model.isPaging" ng-if="!$ctrl.model.isPaging"
label="Load more results"
ng-click="$ctrl.onLoadClick()"> ng-click="$ctrl.onLoadClick()">
</vn-button> <vn-button label="Load more results"></vn-button>
<vn-icon icon="arrow_drop_down"/>
</div>
<vn-spinner <vn-spinner
ng-if="$ctrl.model.isPaging" ng-if="$ctrl.model.isPaging"
enable="::true"> enable="::true">

View File

@ -1,7 +1,13 @@
@import "variables";
vn-pagination { vn-pagination {
display: block; display: block;
text-align: center; text-align: center;
color: $color-primary;
vn-button, vn-icon {
display: block
}
& > div > vn-icon-button { & > div > vn-icon-button {
font-size: 2rem; font-size: 2rem;

View File

@ -23,12 +23,15 @@ export default class Popover extends Popup {
* it is shown in a visible relative position to it. * it is shown in a visible relative position to it.
* *
* @param {HTMLElement|Event} parent Overrides the parent property * @param {HTMLElement|Event} parent Overrides the parent property
* @param {String} direction - Direction [left]
*/ */
show(parent) { show(parent, direction) {
if (parent instanceof Event) if (parent instanceof Event)
parent = event.target; parent = event.target;
if (parent) this.parent = parent; if (parent) this.parent = parent;
if (direction) this.direction = direction;
super.show(); super.show();
this.content = this.popup.querySelector('.content'); this.content = this.popup.querySelector('.content');
this.$timeout(() => this.relocate(), 10); this.$timeout(() => this.relocate(), 10);
@ -89,21 +92,40 @@ export default class Popover extends Popup {
let width = clamp(popoverRect.width, parentRect.width, maxWith); let width = clamp(popoverRect.width, parentRect.width, maxWith);
let height = popoverRect.height; let height = popoverRect.height;
let left = parentRect.left + parentRect.width / 2 - width / 2; let left;
left = clamp(left, margin, maxRight - width); if (this.direction == 'left') {
left = parentRect.left + parentRect.width;
left = clamp(left, margin, maxRight - width);
} else {
left = parentRect.left + parentRect.width / 2 - width / 2;
left = clamp(left, margin, maxRight - width);
}
let top = parentRect.top + parentRect.height + arrowOffset; let top;
if (this.direction == 'left')
top = parentRect.top;
else
top = parentRect.top + parentRect.height + arrowOffset;
let showTop = top + height > maxBottom; let showTop = top + height > maxBottom;
if (showTop) top = parentRect.top - height - arrowOffset; if (showTop) top = parentRect.top - height - arrowOffset;
top = Math.max(top, margin); top = Math.max(top, margin);
if (showTop) if (this.direction == 'left')
arrowStyle.left = `0`;
else if (showTop)
arrowStyle.bottom = `0`; arrowStyle.bottom = `0`;
else else
arrowStyle.top = `0`; arrowStyle.top = `0`;
let arrowLeft = (parentRect.left - left) + parentRect.width / 2; let arrowLeft;
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight); if (this.direction == 'left') {
arrowLeft = 0;
let arrowTop = arrowOffset;
arrowStyle.top = `${arrowTop}px`;
} else {
arrowLeft = (parentRect.left - left) + parentRect.width / 2;
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight);
}
arrowStyle.left = `${arrowLeft}px`; arrowStyle.left = `${arrowLeft}px`;
style.top = `${top}px`; style.top = `${top}px`;

View File

@ -18,6 +18,18 @@ class Email {
return this.$http.get(`email/${template}`, {params}) return this.$http.get(`email/${template}`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!'))); .then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
} }
/**
* Sends an email displaying a notification when it's sent.
*
* @param {String} template The email report name
* @param {Object} params The email parameters
* @return {Promise} Promise resolved when it's sent
*/
sendCsv(template, params) {
return this.$http.get(`csv/${template}/send`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
}
} }
Email.$inject = ['$http', '$translate', 'vnApp']; Email.$inject = ['$http', '$translate', 'vnApp'];

View File

@ -20,6 +20,21 @@ class Report {
const serializedParams = this.$httpParamSerializer(params); const serializedParams = this.$httpParamSerializer(params);
window.open(`api/report/${report}?${serializedParams}`); window.open(`api/report/${report}?${serializedParams}`);
} }
/**
* Shows a report in another window, automatically adds the authorization
* token to params.
*
* @param {String} report The report name
* @param {Object} params The report parameters
*/
showCsv(report, params) {
params = Object.assign({
authorization: this.vnToken.token
}, params);
const serializedParams = this.$httpParamSerializer(params);
window.open(`api/csv/${report}/download?${serializedParams}`);
}
} }
Report.$inject = ['$httpParamSerializer', 'vnToken']; Report.$inject = ['$httpParamSerializer', 'vnToken'];

View File

@ -94,8 +94,11 @@ async function launchBackTest(done) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const jasmine = require('gulp-jasmine'); const jasmine = require('gulp-jasmine');
let options = { const options = {
verbose: false,
includeStackTrace: false,
errorOnFail: false, errorOnFail: false,
timeout: 5000,
config: {} config: {}
}; };

View File

@ -116,5 +116,6 @@
"This client is not invoiceable": "This client is not invoiceable", "This client is not invoiceable": "This client is not invoiceable",
"INACTIVE_PROVIDER": "Inactive provider", "INACTIVE_PROVIDER": "Inactive provider",
"reference duplicated": "reference duplicated", "reference duplicated": "reference duplicated",
"The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option" "The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"This item is not available": "This item is not available"
} }

View File

@ -8,12 +8,10 @@ describe('client canBeInvoiced()', () => {
accessToken: {userId: userId} accessToken: {userId: userId}
}; };
beforeAll(async done => { beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
done();
}); });
it('should return falsy for a client without the data checked', async() => { it('should return falsy for a client without the data checked', async() => {

View File

@ -4,13 +4,11 @@ describe('loopback model address', () => {
let createdAddressId; let createdAddressId;
const clientId = 1101; const clientId = 1101;
afterAll(async done => { afterAll(async() => {
let client = await app.models.Client.findById(clientId); let client = await app.models.Client.findById(clientId);
await app.models.Address.destroyById(createdAddressId); await app.models.Address.destroyById(createdAddressId);
await client.updateAttribute('isEqualizated', false); await client.updateAttribute('isEqualizated', false);
done();
}); });
describe('observe()', () => { describe('observe()', () => {

View File

@ -41,6 +41,7 @@ module.exports = Self => {
const deleted = await Promise.all(promises); const deleted = await Promise.all(promises);
if (tx) await tx.commit(); if (tx) await tx.commit();
return deleted; return deleted;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -97,6 +97,7 @@ module.exports = Self => {
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
await conn.executeStmt(sql, myOptions); await conn.executeStmt(sql, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();

View File

@ -10,12 +10,10 @@ describe('entry import()', () => {
accessToken: {userId: buyerId}, accessToken: {userId: buyerId},
}; };
beforeAll(async done => { beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
done();
}); });
it('should import the buy rows', async() => { it('should import the buy rows', async() => {

View File

@ -3,12 +3,10 @@ const LoopBackContext = require('loopback-context');
describe('entry importBuysPreview()', () => { describe('entry importBuysPreview()', () => {
const entryId = 1; const entryId = 1;
beforeAll(async done => { beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
done();
}); });
it('should return the buys with the calculated packageFk', async() => { it('should return the buys with the calculated packageFk', async() => {

View File

@ -1,9 +1,3 @@
vn-entry-buy-import {
.vn-table > tbody td:nth-child(1) {
width: 250px
}
}
.itemFilter { .itemFilter {
vn-table.scrollable { vn-table.scrollable {
height: 500px height: 500px

View File

@ -2,18 +2,49 @@
module="invoiceOut" module="invoiceOut"
description="$ctrl.invoiceOut.ref"> description="$ctrl.invoiceOut.ref">
<slot-menu> <slot-menu>
<a class="vn-item" <vn-item class="dropdown"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}" vn-click-stop="showInvoiceMenu.show($event, 'left')"
target="_blank"
name="showInvoicePdf" name="showInvoicePdf"
translate> translate>
Show invoice PDF Show invoice...
</a>
<vn-item <vn-menu vn-id="showInvoiceMenu">
ng-click="invoiceConfirmation.show({email: $ctrl.invoiceOut.client.email})" <vn-list>
<a class="vn-item"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
target="_blank"
name="showInvoicePdf"
translate>
Show as PDF
</a>
<vn-item
ng-click="$ctrl.showCsvInvoice()"
translate>
Show as CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item class="dropdown"
vn-click-stop="sendInvoiceMenu.show($event, 'left')"
name="sendInvoice" name="sendInvoice"
translate> translate>
Send invoice PDF Send invoice...
<vn-menu vn-id="sendInvoiceMenu">
<vn-list>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send PDF
</vn-item>
<vn-item
ng-click="sendCsvConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
@ -104,15 +135,32 @@
message="Generate PDF invoice document"> message="Generate PDF invoice document">
</vn-confirm> </vn-confirm>
<!-- Send invoice confirmation popup --> <!-- Send PDF invoice confirmation popup -->
<vn-dialog class="edit" <vn-dialog
vn-id="invoiceConfirmation" vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendInvoice($data)" on-accept="$ctrl.sendPdfInvoice($data)"
message="Send invoice PDF"> message="Send PDF invoice">
<tpl-body> <tpl-body>
<span translate>Are you sure you want to send it?</span> <span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one <vn-textfield vn-one
ng-model="invoiceConfirmation.data.email"> ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Send CSV invoice confirmation popup -->
<vn-dialog
vn-id="sendCsvConfirmation"
on-accept="$ctrl.sendCsvInvoice($data)"
message="Send CSV invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendCsvConfirmation.data.email">
</vn-textfield> </vn-textfield>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>

View File

@ -14,29 +14,6 @@ class Controller extends Descriptor {
return this.aclService.hasAny(['invoicing']); return this.aclService.hasAny(['invoicing']);
} }
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createInvoicePdf() {
const invoiceId = this.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
get filter() { get filter() {
if (this.invoiceOut) if (this.invoiceOut)
return JSON.stringify({refFk: this.invoiceOut.ref}); return JSON.stringify({refFk: this.invoiceOut.ref});
@ -55,7 +32,7 @@ class Controller extends Descriptor {
}, { }, {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'name'] fields: ['id', 'name', 'email']
} }
} }
] ]
@ -76,13 +53,51 @@ class Controller extends Descriptor {
// Prevents error when not defined // Prevents error when not defined
} }
sendInvoice($data) { deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createPdfInvoice() {
const invoiceId = this.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
showCsvInvoice() {
this.vnReport.showCsv('invoice', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id,
});
}
sendPdfInvoice($data) {
return this.vnEmail.send('invoice', { return this.vnEmail.send('invoice', {
recipientId: this.invoiceOut.client.id, recipientId: this.invoiceOut.client.id,
recipient: $data.email, recipient: $data.email,
invoiceId: this.id invoiceId: this.id
}); });
} }
sendCsvInvoice($data) {
return this.vnEmail.sendCsv('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
} }
ngModule.vnComponent('vnInvoiceOutDescriptor', { ngModule.vnComponent('vnInvoiceOutDescriptor', {

View File

@ -3,30 +3,20 @@ import './index';
describe('vnInvoiceOutDescriptor', () => { describe('vnInvoiceOutDescriptor', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
const invoiceOut = {id: 1}; let $httpParamSerializer;
const invoiceOut = {
id: 1,
client: {id: 1101}
};
beforeEach(ngModule('invoiceOut')); beforeEach(ngModule('invoiceOut'));
beforeEach(inject(($componentController, _$httpBackend_) => { beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnInvoiceOutDescriptor', {$element: null}); controller = $componentController('vnInvoiceOutDescriptor', {$element: null});
})); }));
describe('createInvoicePdf()', () => {
it('should make a query and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createInvoicePdf();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('loadData()', () => { describe('loadData()', () => {
it(`should perform a get query to store the invoice in data into the controller`, () => { it(`should perform a get query to store the invoice in data into the controller`, () => {
const id = 1; const id = 1;
@ -39,4 +29,81 @@ describe('vnInvoiceOutDescriptor', () => {
expect(controller.invoiceOut).toEqual(response); expect(controller.invoiceOut).toEqual(response);
}); });
}); });
describe('createPdfInvoice()', () => {
it('should make a query to the createPdf() endpoint and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createPdfInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('showCsvInvoice()', () => {
it('should make a query to the csv invoice download endpoint and show a message snackbar', () => {
jest.spyOn(window, 'open').mockReturnThis();
controller.invoiceOut = invoiceOut;
const expectedParams = {
invoiceId: invoiceOut.id,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/csv/invoice/download?${serializedParams}`;
controller.showCsvInvoice();
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`email/invoice?${serializedParams}`).respond();
controller.sendPdfInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
describe('sendCsvInvoice()', () => {
it('should make a query to the csv invoice send endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`csv/invoice/send?${serializedParams}`).respond();
controller.sendCsvInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
}); });

View File

@ -2,8 +2,10 @@ Volume exceded: Volumen excedido
Volume: Volumen Volume: Volumen
Client card: Ficha del cliente Client card: Ficha del cliente
Invoice ticket list: Listado de tickets de la factura Invoice ticket list: Listado de tickets de la factura
Show invoice PDF: Ver factura en PDF Show invoice...: Ver factura...
Send invoice PDF: Enviar factura en PDF Send invoice...: Enviar factura...
Send PDF invoice: Enviar factura en PDF
Send CSV invoice: Enviar factura en CSV
Delete Invoice: Eliminar factura Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura Clone Invoice: Clonar factura
InvoiceOut deleted: Factura eliminada InvoiceOut deleted: Factura eliminada

View File

@ -40,7 +40,7 @@ module.exports = Self => {
if (!tax.taxClassFk) if (!tax.taxClassFk)
throw new UserError('Tax class cannot be blank'); throw new UserError('Tax class cannot be blank');
promises.push(Self.app.models.ItemTaxCountry.update( promises.push(Self.app.models.ItemTaxCountry.updateAll(
{id: tax.id}, {id: tax.id},
{taxClassFk: tax.taxClassFk} {taxClassFk: tax.taxClassFk}
), myOptions); ), myOptions);

View File

@ -129,54 +129,20 @@ module.exports = Self => {
const where = buildFilter(ctx.args, (param, value) => { const where = buildFilter(ctx.args, (param, value) => {
switch (param) { switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': {inq: value}}
: {'t.nickname': {like: `%${value}%`}};
case 'from': case 'from':
return {'t.shipped': {gte: value}}; return {'t.shipped': {gte: value}};
case 'to': case 'to':
return {'t.shipped': {lte: value}}; return {'t.shipped': {lte: value}};
case 'nickname':
return {'t.nickname': {like: `%${value}%`}};
case 'refFk':
return {'t.refFk': value};
case 'salesPersonFk': case 'salesPersonFk':
return {'c.salesPersonFk': value}; return {'c.salesPersonFk': value};
case 'provinceFk':
return {'a.provinceFk': value};
case 'stateFk':
return {'ts.stateFk': value};
case 'mine': case 'mine':
case 'myTeam': case 'myTeam':
if (value) if (value)
return {'c.salesPersonFk': {inq: teamMembersId}}; return {'c.salesPersonFk': {inq: teamMembersId}};
else else
return {'c.salesPersonFk': {nin: teamMembersId}}; return {'c.salesPersonFk': {nin: teamMembersId}};
case 'alertLevel':
return {'ts.alertLevel': value};
case 'pending':
if (value) {
return {and: [
{'st.alertLevel': 0},
{'st.code': {nin: [
'OK',
'BOARDING',
'PRINTED',
'PRINTED_AUTO',
'PICKER_DESIGNED'
]}}
]};
} else {
return {and: [
{'st.alertLevel': {gt: 0}}
]};
}
case 'id': case 'id':
case 'clientFk': case 'clientFk':
case 'agencyModeFk':
case 'warehouseFk':
param = `t.${param}`; param = `t.${param}`;
return {[param]: value}; return {[param]: value};
} }
@ -196,7 +162,6 @@ module.exports = Self => {
t.id, t.id,
t.shipped, t.shipped,
CAST(DATE(t.shipped) AS CHAR) AS shippedDate, CAST(DATE(t.shipped) AS CHAR) AS shippedDate,
HOUR(t.shipped) AS shippedHour,
t.nickname, t.nickname,
t.refFk, t.refFk,
t.routeFk, t.routeFk,
@ -217,12 +182,12 @@ module.exports = Self => {
ts.code AS alertLevelCode, ts.code AS alertLevelCode,
u.name AS userName, u.name AS userName,
c.salesPersonFk, c.salesPersonFk,
c.credit,
z.hour AS zoneLanding, z.hour AS zoneLanding,
HOUR(z.hour) AS zoneHour,
MINUTE(z.hour) AS zoneMinute,
z.name AS zoneName, z.name AS zoneName,
z.id AS zoneFk, z.id AS zoneFk,
CAST(z.hour AS CHAR) AS hour, TIME_FORMAT(t.shipped, '%H:%i') AS preparationHour,
TIME_FORMAT(z.hour, '%H:%i') AS theoreticalhour,
TIME_FORMAT(zed.etc, '%H:%i') AS practicalHour TIME_FORMAT(zed.etc, '%H:%i') AS practicalHour
FROM ticket t FROM ticket t
LEFT JOIN invoiceOut io ON t.refFk = io.ref LEFT JOIN invoiceOut io ON t.refFk = io.ref
@ -248,6 +213,47 @@ module.exports = Self => {
stmt.merge(conn.makeWhere(filter.where)); stmt.merge(conn.makeWhere(filter.where));
stmts.push(stmt); stmts.push(stmt);
// Get client debt balance
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.clientGetDebt');
stmts.push(`
CREATE TEMPORARY TABLE tmp.clientGetDebt
(INDEX (clientFk))
ENGINE = MEMORY
SELECT DISTINCT clientFk FROM tmp.filter`);
stmt = new ParameterizedSQL('CALL clientGetDebt(?)', [args.to]);
stmts.push(stmt);
stmts.push('DROP TEMPORARY TABLE tmp.clientGetDebt');
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.tickets');
stmt = new ParameterizedSQL(`
CREATE TEMPORARY TABLE tmp.tickets
(INDEX (id))
ENGINE = MEMORY
SELECT f.*, r.risk AS debt
FROM tmp.filter f
LEFT JOIN tmp.risk r ON f.clientFk = r.clientFk`);
stmts.push(stmt);
// Sum risk to future
stmts.push(`SET @client:= 0`);
stmts.push('SET @risk := 0');
stmts.push(`
UPDATE tmp.tickets
SET debt = IF(@client <> @client:= clientFk,
-totalWithVat + @risk:= - debt + totalWithVat,
-totalWithVat + @risk:= @risk + totalWithVat
)
ORDER BY clientFk, shipped DESC
`);
// Remove positive risks
stmts.push(`
UPDATE tmp.tickets t
SET debt = 0
WHERE t.debt + t.credit >= 0
`);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getProblems'); stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getProblems');
stmts.push(` stmts.push(`
CREATE TEMPORARY TABLE tmp.sale_getProblems CREATE TEMPORARY TABLE tmp.sale_getProblems
@ -260,27 +266,39 @@ module.exports = Self => {
AND f.shipped >= CURDATE()`); AND f.shipped >= CURDATE()`);
stmts.push('CALL ticket_getProblems(FALSE)'); stmts.push('CALL ticket_getProblems(FALSE)');
stmts.push(`
INSERT INTO tmp.ticket_problems (ticketFk, risk, totalProblems)
SELECT t.id, t.debt + t.credit AS risk, 1
FROM tmp.tickets t
WHERE (t.debt + t.credit) < 0
ON DUPLICATE KEY UPDATE
risk = t.debt + t.credit, totalProblems = totalProblems + 1
`);
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT f.*, tp.* SELECT t.*, tp.*,
FROM tmp.filter f ((tp.risk) + cc.riskTolerance < 0) AS hasHighRisk
LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = f.id`); FROM tmp.tickets t
LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = t.id
JOIN clientConfig cc`);
const hasProblems = args.problems; const hasProblems = args.problems;
if (hasProblems != undefined && (!args.from && !args.to)) if (hasProblems != undefined && (!args.from && !args.to))
throw new UserError('Choose a date range or days forward'); throw new UserError('Choose a date range or days forward');
let problemsFilter; let finalFilter = {};
let whereProblems;
if (hasProblems === true) { if (hasProblems === true) {
problemsFilter = {or: [ whereProblems = {or: [
{'tp.isFreezed': true}, {'tp.isFreezed': true},
{'tp.risk': {gt: 0}}, {'tp.risk': {lt: 0}},
{'tp.hasTicketRequest': true}, {'tp.hasTicketRequest': true},
{'tp.hasComponentLack': true}, {'tp.hasComponentLack': true},
{'tp.isTaxDataChecked': false}, {'tp.isTaxDataChecked': false},
{'tp.isAvailable': false} {'tp.isAvailable': false}
]}; ]};
} else if (hasProblems === false) { } else if (hasProblems === false) {
problemsFilter = {and: [ whereProblems = {and: [
{'tp.isFreezed': false}, {'tp.isFreezed': false},
{'tp.risk': 0}, {'tp.risk': 0},
{'tp.hasTicketRequest': false}, {'tp.hasTicketRequest': false},
@ -290,8 +308,53 @@ module.exports = Self => {
]}; ]};
} }
if (problemsFilter) if (whereProblems) finalFilter.where = whereProblems;
stmt.merge(conn.makeWhere(problemsFilter));
const myWhere = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': {inq: value}}
: {'t.nickname': {like: `%${value}%`}};
case 'nickname':
return {'t.nickname': {like: `%${value}%`}};
case 'refFk':
return {'t.refFk': value};
case 'provinceFk':
return {'t.provinceFk': value};
case 'stateFk':
return {'t.stateFk': value};
case 'alertLevel':
return {'t.alertLevel': value};
case 'pending':
if (value) {
return {and: [
{'t.alertLevel': 0},
{'t.alertLevelCode': {nin: [
'OK',
'BOARDING',
'PRINTED',
'PRINTED_AUTO',
'PICKER_DESIGNED'
]}}
]};
} else {
return {and: [
{'t.alertLevel': {gt: 0}}
]};
}
case 'agencyModeFk':
case 'warehouseFk':
param = `t.${param}`;
return {[param]: value};
}
});
finalFilter = mergeFilters(finalFilter, {where: myWhere});
if (finalFilter.where)
stmt.merge(conn.makeWhere(finalFilter.where));
stmt.merge(conn.makeOrderBy(filter.order)); stmt.merge(conn.makeOrderBy(filter.order));
stmt.merge(conn.makeLimit(filter)); stmt.merge(conn.makeLimit(filter));
@ -300,7 +363,9 @@ module.exports = Self => {
stmts.push( stmts.push(
`DROP TEMPORARY TABLE `DROP TEMPORARY TABLE
tmp.filter, tmp.filter,
tmp.ticket_problems`); tmp.ticket_problems,
tmp.sale_getProblems,
tmp.risk`);
let sql = ParameterizedSQL.join(stmts, ';'); let sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); let result = await conn.executeStmt(sql);

View File

@ -1,15 +1,15 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('SalesMonitor salesFilter()', () => { describe('SalesMonitor salesFilter()', () => {
it('should return the tickets matching the filter', async() => { it('should now return the tickets matching the filter', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {}}; const ctx = {req: {accessToken: {userId: 9}}, args: {}};
const filter = {order: 'id DESC'}; const filter = {order: 'id DESC'};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(24); expect(result.length).toEqual(24);
}); });
it('should return the tickets matching the problems on true', async() => { it('should now return the tickets matching the problems on true', async() => {
const yesterday = new Date(); const yesterday = new Date();
yesterday.setHours(0, 0, 0, 0); yesterday.setHours(0, 0, 0, 0);
const today = new Date(); const today = new Date();
@ -21,12 +21,12 @@ describe('SalesMonitor salesFilter()', () => {
to: today to: today
}}; }};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(9); expect(result.length).toEqual(13);
}); });
it('should return the tickets matching the problems on false', async() => { it('should now return the tickets matching the problems on false', async() => {
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0); yesterday.setHours(0, 0, 0, 0);
@ -39,33 +39,33 @@ describe('SalesMonitor salesFilter()', () => {
to: today to: today
}}; }};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(0); expect(result.length).toEqual(0);
}); });
it('should return the tickets matching the problems on null', async() => { it('should now return the tickets matching the problems on null', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {problems: null}}; const ctx = {req: {accessToken: {userId: 9}}, args: {problems: null}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(24); expect(result.length).toEqual(24);
}); });
it('should return the tickets matching the orderId 11', async() => { it('should now return the tickets matching the orderId 11', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {orderFk: 11}}; const ctx = {req: {accessToken: {userId: 9}}, args: {orderFk: 11}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
const firstRow = result[0]; const firstRow = result[0];
expect(result.length).toEqual(1); expect(result.length).toEqual(1);
expect(firstRow.id).toEqual(11); expect(firstRow.id).toEqual(11);
}); });
it('should return the tickets with grouped state "Pending" and not "Ok" nor "BOARDING"', async() => { it('should now return the tickets with grouped state "Pending" and not "Ok" nor "BOARDING"', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: true}}; const ctx = {req: {accessToken: {userId: 9}}, args: {pending: true}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
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))];
@ -74,10 +74,10 @@ describe('SalesMonitor salesFilter()', () => {
expect(anyResult.state).toMatch(/(Libre|Arreglar)/); expect(anyResult.state).toMatch(/(Libre|Arreglar)/);
}); });
it('should return the tickets that are not pending', async() => { it('should now return the tickets that are not pending', async() => {
const ctx = {req: {accessToken: {userId: 9}}, args: {pending: false}}; const ctx = {req: {accessToken: {userId: 9}}, args: {pending: false}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
const firstRow = result[0]; const firstRow = result[0];
const secondRow = result[1]; const secondRow = result[1];
const thirdRow = result[2]; const thirdRow = result[2];
@ -88,18 +88,18 @@ describe('SalesMonitor salesFilter()', () => {
expect(thirdRow.state).toEqual('Entregado'); expect(thirdRow.state).toEqual('Entregado');
}); });
it('should return the tickets from the worker team', async() => { it('should now return the tickets from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: true}}; const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: true}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(20); expect(result.length).toEqual(20);
}); });
it('should return the tickets that are not from the worker team', async() => { it('should now return the tickets that are not from the worker team', async() => {
const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: false}}; const ctx = {req: {accessToken: {userId: 18}}, args: {myTeam: false}};
const filter = {}; const filter = {};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
expect(result.length).toEqual(4); expect(result.length).toEqual(4);
}); });
@ -113,7 +113,7 @@ describe('SalesMonitor salesFilter()', () => {
const ctx = {req: {accessToken: {userId: 18}}, args: {}}; const ctx = {req: {accessToken: {userId: 18}}, args: {}};
const filter = {order: 'totalProblems DESC'}; const filter = {order: 'totalProblems DESC'};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
const firstTicket = result.shift(); const firstTicket = result.shift();
const secondTicket = result.shift(); const secondTicket = result.shift();
@ -131,7 +131,7 @@ describe('SalesMonitor salesFilter()', () => {
const ctx = {req: {accessToken: {userId: 18}}, args: {}}; const ctx = {req: {accessToken: {userId: 18}}, args: {}};
const filter = {order: 'totalProblems ASC'}; const filter = {order: 'totalProblems ASC'};
const result = await app.models.SalesMonitor.salesFilter(ctx, filter); const result = await models.SalesMonitor.salesFilter(ctx, filter);
const firstTicket = result.shift(); const firstTicket = result.shift();
const secondTicket = result.shift(); const secondTicket = result.shift();

View File

@ -70,7 +70,7 @@
<vn-autocomplete vn-one <vn-autocomplete vn-one
data="$ctrl.groupedStates" data="$ctrl.groupedStates"
label="Grouped States" label="Grouped States"
value-field="alertLevel" value-field="id"
show-field="name" show-field="name"
ng-model="filter.alertLevel"> ng-model="filter.alertLevel">
<tpl-item> <tpl-item>

View File

@ -14,7 +14,7 @@ class Controller extends SearchPanel {
this.$http.get('AlertLevels').then(res => { this.$http.get('AlertLevels').then(res => {
for (let state of res.data) { for (let state of res.data) {
groupedStates.push({ groupedStates.push({
alertLevel: state.alertLevel, id: state.id,
code: state.code, code: state.code,
name: this.$t(state.code) name: this.$t(state.code)
}); });

View File

@ -18,7 +18,7 @@ describe('Monitor Component vnMonitorSalesSearchPanel', () => {
jest.spyOn(controller, '$t').mockReturnValue('miCodigo'); jest.spyOn(controller, '$t').mockReturnValue('miCodigo');
const data = [ const data = [
{ {
alertLevel: 9999, id: 9999,
code: 'myCode' code: 'myCode'
} }
]; ];
@ -27,7 +27,7 @@ describe('Monitor Component vnMonitorSalesSearchPanel', () => {
$httpBackend.flush(); $httpBackend.flush();
expect(controller.groupedStates).toEqual([{ expect(controller.groupedStates).toEqual([{
alertLevel: 9999, id: 9999,
code: 'myCode', code: 'myCode',
name: 'miCodigo' name: 'miCodigo'
}]); }]);

View File

@ -1,9 +1,8 @@
<vn-crud-model auto-load="true" <vn-crud-model
vn-id="model" vn-id="model"
params="::$ctrl.filterParams"
url="SalesMonitors/salesFilter" url="SalesMonitors/salesFilter"
limit="20" limit="20"
order="shippedDate DESC, shippedHour ASC, zoneLanding ASC, id"> order="shippedDate DESC, preparationHour ASC, zoneLanding ASC, id">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar
@ -37,8 +36,8 @@
<vn-th field="nickname">Client</vn-th> <vn-th field="nickname">Client</vn-th>
<vn-th field="salesPersonFk" class="expendable" shrink>Salesperson</vn-th> <vn-th field="salesPersonFk" class="expendable" shrink>Salesperson</vn-th>
<vn-th field="shipped" shrink-date>Date</vn-th> <vn-th field="shipped" shrink-date>Date</vn-th>
<vn-th>Prep.</vn-th> <vn-th field="preparationHour" filter-enabled="false">Prep.</vn-th>
<vn-th field="hour" shrink>Theoretical</vn-th> <vn-th field="theoreticalHour">Theoretical</vn-th>
<vn-th field="practicalHour">Practical</vn-th> <vn-th field="practicalHour">Practical</vn-th>
<vn-th field="provinceFk" class="expendable">Province</vn-th> <vn-th field="provinceFk" class="expendable">Province</vn-th>
<vn-th field="stateFk">State</vn-th> <vn-th field="stateFk">State</vn-th>
@ -51,7 +50,7 @@
<a ng-repeat="ticket in model.data" <a ng-repeat="ticket in model.data"
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="ticket.card.summary({id: {{::ticket.id}}})" target="_blank"> ui-sref="ticket.card.summary({id: {{::ticket.id}}})" target="_blank">
<vn-td class="icon-field"> <vn-td expand>
<vn-icon <vn-icon
ng-show="::ticket.isTaxDataChecked === 0" ng-show="::ticket.isTaxDataChecked === 0"
translate-attr="{title: 'No verified data'}" translate-attr="{title: 'No verified data'}"

View File

@ -6,13 +6,30 @@ export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.filterParams = this.fetchParams({ this.filterParams = this.fetchParams();
scopeDays: 1
});
} }
fetchParams($params) { $onInit() {
if (!Object.entries($params).length) if (!this.$params.q) {
this.$.$applyAsync(
() => this.$.model.applyFilter(null, this.filterParams));
}
}
fetchParams($params = {}) {
const excludedParams = [
'search',
'clientFk',
'orderFk',
'refFk',
'scopeDays'
];
const hasExcludedParams = excludedParams.some(param => {
return $params && $params[param];
});
const hasParams = Object.entries($params).length;
if (!hasParams || !hasExcludedParams)
$params.scopeDays = 1; $params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') { if (typeof $params.scopeDays === 'number') {
@ -68,8 +85,10 @@ export default class Controller extends Section {
return {'c.salesPersonFk': value}; return {'c.salesPersonFk': value};
case 'provinceFk': case 'provinceFk':
return {'a.provinceFk': value}; return {'a.provinceFk': value};
case 'hour': case 'theoreticalHour':
return {'z.hour': value}; return {'z.hour': value};
case 'practicalHour':
return {'zed.etc': value};
case 'shipped': case 'shipped':
return {'t.shipped': { return {'t.shipped': {
between: this.dateRange(value)} between: this.dateRange(value)}

View File

@ -3,10 +3,8 @@ const app = require('vn-loopback/server/server');
describe('order addToOrder()', () => { describe('order addToOrder()', () => {
const orderId = 8; const orderId = 8;
let rowToDelete; let rowToDelete;
afterAll(async done => { afterAll(async() => {
await app.models.OrderRow.removes({rows: [rowToDelete], actualOrderId: orderId}); await app.models.OrderRow.removes({rows: [rowToDelete], actualOrderId: orderId});
done();
}); });
it('should add a row to a given order', async() => { it('should add a row to a given order', async() => {

View File

@ -4,12 +4,10 @@ describe('order removes()', () => {
let row; let row;
let newRow; let newRow;
beforeAll(async done => { beforeAll(async() => {
row = await app.models.OrderRow.findOne({where: {id: 12}}); row = await app.models.OrderRow.findOne({where: {id: 12}});
row.id = null; row.id = null;
newRow = await app.models.OrderRow.create(row); newRow = await app.models.OrderRow.create(row);
done();
}); });
it('should throw an error if rows property is empty', async() => { it('should throw an error if rows property is empty', async() => {

View File

@ -119,7 +119,7 @@ module.exports = Self => {
case 'sourceApp': case 'sourceApp':
return {'o.source_app': value}; return {'o.source_app': value};
case 'ticketFk': case 'ticketFk':
return {'ort.ticketFk': value}; return {'ot.ticketFk': value};
case 'isConfirmed': case 'isConfirmed':
return {'o.confirmed': value ? 1 : 0}; return {'o.confirmed': value ? 1 : 0};
case 'myTeam': case 'myTeam':
@ -137,7 +137,10 @@ module.exports = Self => {
let stmt; let stmt;
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`SELECT `CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
ENGINE = MEMORY
SELECT
o.id, o.id,
o.total, o.total,
o.date_send landed, o.date_send landed,
@ -168,20 +171,20 @@ module.exports = Self => {
LEFT JOIN ticket t ON t.id = ot.ticketFk LEFT JOIN ticket t ON t.id = ot.ticketFk
LEFT JOIN zoneEstimatedDelivery zed ON zed.zoneFk = t.zoneFk`); LEFT JOIN zoneEstimatedDelivery zed ON zed.zoneFk = t.zoneFk`);
if (args && args.ticketFk) {
stmt.merge({
sql: `LEFT JOIN orderTicket ort ON ort.orderFk = o.id`
});
}
stmt.merge(conn.makeWhere(filter.where)); stmt.merge(conn.makeWhere(filter.where));
stmt.merge(`GROUP BY o.id`);
stmt.merge(conn.makePagination(filter)); stmt.merge(conn.makePagination(filter));
stmts.push(stmt); stmts.push(stmt);
stmt = new ParameterizedSQL(`SELECT * FROM tmp.filter`);
stmt.merge(`GROUP BY id`);
stmt.merge(conn.makeOrderBy(filter.order));
const ordersIndex = stmts.push(stmt) - 1;
stmts.push(`DROP TEMPORARY TABLE tmp.filter`);
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql);
return result; return result[ordersIndex];
}; };
}; };

View File

@ -4,10 +4,8 @@ let UserError = require('vn-loopback/util/user-error');
describe('order new()', () => { describe('order new()', () => {
let orderId; let orderId;
afterAll(async done => { afterAll(async() => {
await app.models.Order.destroyById(orderId); await app.models.Order.destroyById(orderId);
done();
}); });
it('should throw an error if the client isnt active', async() => { it('should throw an error if the client isnt active', async() => {

View File

@ -2,12 +2,10 @@ const app = require('vn-loopback/server/server');
describe('Order updateBasicData', () => { describe('Order updateBasicData', () => {
const orderId = 21; const orderId = 21;
afterAll(async done => { afterAll(async() => {
let validparams = {note: null}; let validparams = {note: null};
await app.models.Order.updateBasicData(orderId, validparams); await app.models.Order.updateBasicData(orderId, validparams);
done();
}); });
it('should return an error if the order is confirmed', async() => { it('should return an error if the order is confirmed', async() => {

View File

@ -17,7 +17,7 @@
<vn-th field="landed" shrink-date>Landed</vn-th> <vn-th field="landed" shrink-date>Landed</vn-th>
<vn-th field="created" center>Hour</vn-th> <vn-th field="created" center>Hour</vn-th>
<vn-th field="agencyName" center>Agency</vn-th> <vn-th field="agencyName" center>Agency</vn-th>
<vn-th center>Total</vn-th> <vn-th field="total" center>Total</vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>

View File

@ -4,14 +4,12 @@ describe('route guessPriority()', () => {
const targetRouteId = 7; const targetRouteId = 7;
let routeTicketsToRestore; let routeTicketsToRestore;
afterAll(async done => { afterAll(async() => {
let restoreFixtures = []; let restoreFixtures = [];
routeTicketsToRestore.forEach(ticket => { routeTicketsToRestore.forEach(ticket => {
restoreFixtures.push(ticket.updateAttribute('priority', null)); restoreFixtures.push(ticket.updateAttribute('priority', null));
}); });
await Promise.all(restoreFixtures); await Promise.all(restoreFixtures);
done();
}); });
it('should call guessPriority() and then check the tickets in the target route now have their priorities defined', async() => { it('should call guessPriority() and then check the tickets in the target route now have their priorities defined', async() => {

View File

@ -8,12 +8,10 @@ describe('route insertTicket()', () => {
accessToken: {userId: deliveryId}, accessToken: {userId: deliveryId},
}; };
beforeAll(async done => { beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
done();
}); });
it('should add the ticket to a route', async() => { it('should add the ticket to a route', async() => {

View File

@ -4,18 +4,14 @@ describe('loopback model Supplier', () => {
let supplierOne; let supplierOne;
let supplierTwo; let supplierTwo;
beforeAll(async done => { beforeAll(async() => {
supplierOne = await app.models.Supplier.findById(1); supplierOne = await app.models.Supplier.findById(1);
supplierTwo = await app.models.Supplier.findById(442); supplierTwo = await app.models.Supplier.findById(442);
done();
}); });
afterAll(async done => { afterAll(async() => {
await supplierOne.updateAttribute('payMethodFk', supplierOne.payMethodFk); await supplierOne.updateAttribute('payMethodFk', supplierOne.payMethodFk);
await supplierTwo.updateAttribute('payMethodFk', supplierTwo.payMethodFk); await supplierTwo.updateAttribute('payMethodFk', supplierTwo.payMethodFk);
done();
}); });
describe('payMethodFk', () => { describe('payMethodFk', () => {

View File

@ -7,13 +7,13 @@ module.exports = Self => {
accepts: [ accepts: [
{ {
arg: 'filter', arg: 'filter',
type: 'Object', type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'}, http: {source: 'query'},
}, },
], ],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true, root: true,
}, },
http: { http: {
@ -22,7 +22,12 @@ module.exports = Self => {
}, },
}); });
Self.filter = async filter => { Self.filter = async(filter, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmt = new ParameterizedSQL( const stmt = new ParameterizedSQL(
`SELECT `SELECT
e.id, e.id,
@ -55,6 +60,6 @@ module.exports = Self => {
`); `);
stmt.merge(Self.buildSuffix(filter, 'e')); stmt.merge(Self.buildSuffix(filter, 'e'));
return await Self.rawStmt(stmt); return Self.rawStmt(stmt, myOptions);
}; };
}; };

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('expedition filter()', () => {
it('should return the expeditions matching the filter', async() => {
const tx = await models.Expedition.beginTransaction({});
try {
const options = {transaction: tx};
const filter = {where: {packagingFk: 1}};
const response = await models.Expedition.filter(filter, options);
expect(response.length).toEqual(10);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -7,13 +7,13 @@ module.exports = Self => {
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'filter', arg: 'filter',
type: 'Object', type: 'object',
required: false, required: false,
description: 'Filter defining where and paginated data', description: 'Filter defining where and paginated data',
http: {source: 'query'} http: {source: 'query'}
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -22,9 +22,14 @@ module.exports = Self => {
} }
}); });
Self.listPackaging = async filter => { Self.listPackaging = async(filter, options) => {
let conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
let stmt = new ParameterizedSQL( const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmt = new ParameterizedSQL(
`SELECT name, itemFk, packagingFk `SELECT name, itemFk, packagingFk
FROM (SELECT i.name, i.id itemFk, p.id packagingFk FROM (SELECT i.name, i.id itemFk, p.id packagingFk
FROM item i FROM item i
@ -33,6 +38,7 @@ module.exports = Self => {
); );
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
return conn.executeStmt(stmt);
return conn.executeStmt(stmt, myOptions);
}; };
}; };

View File

@ -1,12 +1,22 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket listPackaging()', () => { describe('ticket listPackaging()', () => {
it('should return the packaging', async() => { it('should return the packaging', async() => {
let filter = {where: {packagingFk: 1}}; const tx = await models.Packaging.beginTransaction({});
let response = await app.models.Packaging.listPackaging(filter);
expect(response[0].name).toBeDefined(); try {
expect(response[0].name).toEqual('Container ammo box 1m'); const options = {transaction: tx};
const filter = {where: {packagingFk: 1}};
const response = await models.Packaging.listPackaging(filter, options);
expect(response[0].name).toEqual('Container ammo box 1m');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -7,11 +7,11 @@ module.exports = Self => {
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'filter', arg: 'filter',
type: 'Object', type: 'object',
description: 'Filter defining where and paginated data' description: 'Filter defining where and paginated data'
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -20,8 +20,13 @@ module.exports = Self => {
} }
}); });
Self.listSaleTracking = async filter => { Self.listSaleTracking = async(filter, options) => {
let stmt = new ParameterizedSQL(` const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmt = new ParameterizedSQL(`
SELECT SELECT
st.id, st.id,
s.ticketFk, s.ticketFk,
@ -41,9 +46,9 @@ module.exports = Self => {
stmt.merge(Self.makeSuffix(filter)); stmt.merge(Self.makeSuffix(filter));
let trackings = await Self.rawStmt(stmt); const trackings = await Self.rawStmt(stmt, myOptions);
let salesFilter = { const salesFilter = {
include: [ include: [
{ {
relation: 'item' relation: 'item'
@ -52,14 +57,14 @@ module.exports = Self => {
where: {ticketFk: filter.where.ticketFk} where: {ticketFk: filter.where.ticketFk}
}; };
let sales = await Self.app.models.Sale.find(salesFilter); const sales = await Self.app.models.Sale.find(salesFilter, myOptions);
trackings.forEach(tracking => { for (const tracking of trackings) {
sales.forEach(sale => { for (const sale of sales) {
if (tracking.itemFk == sale.itemFk) if (tracking.itemFk == sale.itemFk)
tracking.item = sale.item(); tracking.item = sale.item();
}); }
}); }
return trackings; return trackings;
}; };

View File

@ -1,17 +1,39 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket listSaleTracking()', () => { describe('ticket listSaleTracking()', () => {
it('should call the listSaleTracking method and return the response', async() => { it('should call the listSaleTracking method and return the response', async() => {
let filter = {where: {ticketFk: 1}}; const tx = await models.SaleTracking.beginTransaction({});
let result = await app.models.SaleTracking.listSaleTracking(filter);
expect(result.length).toEqual(4); try {
const options = {transaction: tx};
const filter = {where: {ticketFk: 1}};
const result = await models.SaleTracking.listSaleTracking(filter, options);
expect(result.length).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it(`should call the listSaleTracking method and return zero if doesn't have lines`, async() => { it(`should call the listSaleTracking method and return zero if doesn't have lines`, async() => {
let filter = {where: {ticketFk: 2}}; const tx = await models.SaleTracking.beginTransaction({});
let result = await app.models.SaleTracking.listSaleTracking(filter);
expect(result.length).toEqual(0); try {
const options = {transaction: tx};
const filter = {where: {ticketFk: 2}};
const result = await models.SaleTracking.listSaleTracking(filter, options);
expect(result.length).toEqual(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -26,54 +26,73 @@ module.exports = Self => {
} }
}); });
Self.deleteSales = async(ctx, sales, ticketId) => { Self.deleteSales = async(ctx, sales, ticketId, options) => {
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const myOptions = {};
let tx;
const canEditSales = await models.Sale.canEdit(ctx, sales); if (typeof options == 'object')
Object.assign(myOptions, options);
const ticket = await models.Ticket.findById(ticketId, { if (!myOptions.transaction) {
include: { tx = await Self.beginTransaction({});
relation: 'client', myOptions.transaction = tx;
scope: { }
include: {
relation: 'salesPersonUser', try {
scope: { const canEditSales = await models.Sale.canEdit(ctx, sales, myOptions);
fields: ['id', 'name']
const ticket = await models.Ticket.findById(ticketId, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
} }
} }
} }
}, myOptions);
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);
if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`);
if (!canEditSales)
throw new UserError(`Sale(s) blocked, please contact production`);
const promises = [];
let deletions = '';
for (let sale of sales) {
const deletedSale = models.Sale.destroyById(sale.id, myOptions);
deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
promises.push(deletedSale);
} }
});
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); const salesPerson = ticket.client().salesPersonUser();
if (!isTicketEditable) if (salesPerson) {
throw new UserError(`The sales of this ticket can't be modified`); const origin = ctx.req.headers.origin;
if (!canEditSales) const message = $t('Deleted sales from ticket', {
throw new UserError(`Sale(s) blocked, please contact production`); ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
deletions: deletions
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
}
const promises = []; const deletedSales = await Promise.all(promises);
let deletions = '';
for (let sale of sales) {
const deletedSale = models.Sale.destroyById(sale.id);
deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
promises.push(deletedSale); if (tx) await tx.commit();
return deletedSales;
} catch (e) {
if (tx) await tx.rollback();
throw e;
} }
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Deleted sales from ticket', {
ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
deletions: deletions
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
}
return Promise.all(promises);
}; };
}; };

View File

@ -18,26 +18,32 @@ module.exports = Self => {
} }
}); });
Self.getClaimableFromTicket = async ticketFk => { Self.getClaimableFromTicket = async(ticketFk, options) => {
let query = `SELECT const myOptions = {};
s.id AS saleFk,
t.id AS ticketFk,
t.landed,
s.concept,
s.itemFk,
s.quantity,
s.price,
s.discount,
t.nickname
FROM vn.ticket t
INNER JOIN vn.sale s ON s.ticketFk = t.id
LEFT JOIN vn.claimBeginning cb ON cb.saleFk = s.id
WHERE (t.landed) >= TIMESTAMPADD(DAY, -7, CURDATE()) if (typeof options == 'object')
AND t.id = ? AND cb.id IS NULL Object.assign(myOptions, options);
ORDER BY t.landed DESC, t.id DESC`;
let claimableSales = await Self.rawSql(query, [ticketFk]); const query = `
SELECT
s.id AS saleFk,
t.id AS ticketFk,
t.landed,
s.concept,
s.itemFk,
s.quantity,
s.price,
s.discount,
t.nickname
FROM vn.ticket t
INNER JOIN vn.sale s ON s.ticketFk = t.id
LEFT JOIN vn.claimBeginning cb ON cb.saleFk = s.id
WHERE (t.landed) >= TIMESTAMPADD(DAY, -7, CURDATE())
AND t.id = ? AND cb.id IS NULL
ORDER BY t.landed DESC, t.id DESC`;
const claimableSales = await Self.rawSql(query, [ticketFk], myOptions);
return claimableSales; return claimableSales;
}; };

View File

@ -20,20 +20,39 @@ module.exports = Self => {
} }
}); });
Self.recalculatePrice = async(ctx, id) => { Self.recalculatePrice = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {};
let tx;
const sale = await Self.findById(id); if (typeof options == 'object')
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk); Object.assign(myOptions, options);
if (!isEditable) if (!myOptions.transaction) {
throw new UserError(`The sales of this ticket can't be modified`); tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
const canEditSale = await models.Sale.canEdit(ctx, [id]); try {
const sale = await Self.findById(id, null, myOptions);
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk, myOptions);
if (!canEditSale) if (!isEditable)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`The sales of this ticket can't be modified`);
return Self.rawSql('CALL vn.sale_calculateComponent(?, null)', [id]); const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);
const recalculation = await Self.rawSql('CALL vn.sale_calculateComponent(?, null)', [id], myOptions);
if (tx) await tx.commit();
return recalculation;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -34,63 +34,80 @@ module.exports = Self => {
} }
}); });
Self.reserve = async(ctx, ticketId, sales, reserved) => { Self.reserve = async(ctx, ticketId, sales, reserved, options) => {
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const myOptions = {};
let tx;
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); if (typeof options == 'object')
if (!isTicketEditable) Object.assign(myOptions, options);
throw new UserError(`The sales of this ticket can't be modified`);
const canEditSale = await models.Sale.canEdit(ctx, sales); if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
if (!canEditSale) myOptions.transaction = tx;
throw new UserError(`Sale(s) blocked, please contact production`);
let changesMade = '';
const promises = [];
for (let sale of sales) {
if (sale.reserved != reserved) {
const oldState = sale.reserved ? 'reserved' : 'regular';
const newState = reserved ? 'reserved' : 'regular';
const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved});
promises.push(reservedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`;
}
} }
const result = await Promise.all(promises); try {
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);
if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const ticket = await models.Ticket.findById(ticketId, { const canEditSale = await models.Sale.canEdit(ctx, sales, myOptions);
include: {
relation: 'client', if (!canEditSale)
scope: { throw new UserError(`Sale(s) blocked, please contact production`);
include: {
relation: 'salesPersonUser', let changesMade = '';
scope: { const promises = [];
fields: ['id', 'name']
for (let sale of sales) {
if (sale.reserved != reserved) {
const oldState = sale.reserved ? 'reserved' : 'regular';
const newState = reserved ? 'reserved' : 'regular';
const reservedSale = models.Sale.updateAll({id: sale.id}, {reserved: reserved}, myOptions);
promises.push(reservedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`;
}
}
const result = await Promise.all(promises);
const ticket = await models.Ticket.findById(ticketId, {
include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
} }
} }
} }
}, myOptions);
const salesPerson = ticket.client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale reserved state', {
ticketId: ticketId,
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
changes: changesMade
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
} }
});
const salesPerson = ticket.client().salesPersonUser(); if (tx) await tx.commit();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale reserved state', { return result;
ticketId: ticketId, } catch (e) {
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, if (tx) await tx.rollback();
changes: changesMade throw e;
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
} }
return result;
}; };
}; };

View File

@ -1,36 +1,69 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale canEdit()', () => { describe('sale canEdit()', () => {
it('should return true if the role is production regardless of the saleTrackings', async() => { it('should return true if the role is production regardless of the saleTrackings', async() => {
const productionUserID = 49; const tx = await models.Sale.beginTransaction({});
let ctx = {req: {accessToken: {userId: productionUserID}}};
const sales = [{id: 3}]; try {
const options = {transaction: tx};
const result = await app.models.Sale.canEdit(ctx, sales); const productionUserID = 49;
const ctx = {req: {accessToken: {userId: productionUserID}}};
expect(result).toEqual(true); const sales = [{id: 3}];
const result = await models.Sale.canEdit(ctx, sales, options);
expect(result).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return true if the role is not production and none of the sales has saleTracking', async() => { it('should return true if the role is not production and none of the sales has saleTracking', async() => {
const salesPersonUserID = 18; const tx = await models.Sale.beginTransaction({});
let ctx = {req: {accessToken: {userId: salesPersonUserID}}};
const sales = [{id: 10}]; try {
const options = {transaction: tx};
const result = await app.models.Sale.canEdit(ctx, sales); const salesPersonUserID = 18;
const ctx = {req: {accessToken: {userId: salesPersonUserID}}};
expect(result).toEqual(true); const sales = [{id: 10}];
const result = await models.Sale.canEdit(ctx, sales, options);
expect(result).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return false if any of the sales has a saleTracking record', async() => { it('should return false if any of the sales has a saleTracking record', async() => {
const salesPersonUserID = 18; const tx = await models.Sale.beginTransaction({});
let ctx = {req: {accessToken: {userId: salesPersonUserID}}};
const sales = [{id: 3}]; try {
const options = {transaction: tx};
const result = await app.models.Sale.canEdit(ctx, sales); const salesPersonUserID = 18;
const ctx = {req: {accessToken: {userId: salesPersonUserID}}};
expect(result).toEqual(false); const sales = [{id: 3}];
const result = await models.Sale.canEdit(ctx, sales, options);
expect(result).toEqual(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,38 +1,29 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale deleteSales()', () => { describe('sale deleteSales()', () => {
let sale;
let newSale;
beforeAll(async done => {
try {
sale = await app.models.Sale.findOne({where: {id: 9}});
sale.id = null;
newSale = await app.models.Sale.create(sale);
} catch (error) {
console.error(error);
}
done();
});
it('should throw an error if the ticket of the given sales is not editable', async() => { it('should throw an error if the ticket of the given sales is not editable', async() => {
let ctx = { const tx = await models.Sale.beginTransaction({});
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let error; let error;
const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}];
const ticketId = 2;
try { try {
await app.models.Sale.deleteSales(ctx, sales, ticketId); const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}];
const ticketId = 2;
await models.Sale.deleteSales(ctx, sales, ticketId, options);
await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback();
error = e; error = e;
} }
@ -40,19 +31,33 @@ describe('sale deleteSales()', () => {
}); });
it('should delete the sale', async() => { it('should delete the sale', async() => {
let ctx = { const tx = await models.Sale.beginTransaction({});
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const sales = [{id: newSale.id, instance: 0}]; try {
const ticketId = 16; const options = {transaction: tx};
let res = await app.models.Sale.deleteSales(ctx, sales, ticketId); const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const sale = await models.Sale.findOne({where: {id: 9}}, options);
sale.id = null;
const newSale = await models.Sale.create(sale, options);
expect(res).toEqual([{count: 1}]); const sales = [{id: newSale.id, instance: 0}];
const ticketId = 16;
const deletions = await models.Sale.deleteSales(ctx, sales, ticketId, options);
expect(deletions).toEqual([{count: 1}]);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,10 +1,21 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale getClaimableFromTicket()', () => { describe('sale getClaimableFromTicket()', () => {
it('should return the claimable sales of a given ticket', async() => { it('should return the claimable sales of a given ticket', async() => {
let claimableFromTicket = await app.models.Sale.getClaimableFromTicket(16); const tx = await models.Sale.beginTransaction({});
expect(claimableFromTicket[0].concept).toBe('Ranged weapon longbow 2m'); try {
expect(claimableFromTicket.length).toBe(3); const options = {transaction: tx};
const claimableFromTicket = await models.Sale.getClaimableFromTicket(16, options);
expect(claimableFromTicket[0].concept).toBe('Ranged weapon longbow 2m');
expect(claimableFromTicket.length).toBe(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,24 +1,43 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale recalculatePrice()', () => { describe('sale recalculatePrice()', () => {
const saleId = 7; const saleId = 7;
it('should update the sale price', async() => { it('should update the sale price', async() => {
const ctx = {req: {accessToken: {userId: 9}}}; const tx = await models.Sale.beginTransaction({});
const response = await app.models.Sale.recalculatePrice(ctx, saleId);
expect(response.affectedRows).toBeDefined(); try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}};
const response = await models.Sale.recalculatePrice(ctx, saleId, options);
expect(response.affectedRows).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should throw an error if the ticket is not editable', async() => { it('should throw an error if the ticket is not editable', async() => {
const ctx = {req: {accessToken: {userId: 9}}}; const tx = await models.Sale.beginTransaction({});
const immutableSaleId = 1;
await app.models.Sale.recalculatePrice(ctx, immutableSaleId)
.catch(response => {
expect(response).toEqual(new Error(`The sales of this ticket can't be modified`));
error = response;
});
expect(error).toBeDefined(); let error;
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}};
const immutableSaleId = 1;
await models.Sale.recalculatePrice(ctx, immutableSaleId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error(`The sales of this ticket can't be modified`));
}); });
}); });

View File

@ -1,4 +1,4 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale reserve()', () => { describe('sale reserve()', () => {
const ctx = { const ctx = {
@ -9,51 +9,54 @@ describe('sale reserve()', () => {
} }
}; };
afterAll(async done => {
let ctx = {req: {accessToken: {userId: 9}}};
let params = {
sales: [
{id: 7},
{id: 8}],
ticketFk: 11,
reserved: false
};
await app.models.Sale.reserve(ctx, params);
done();
});
it('should throw an error if the ticket can not be modified', async() => { it('should throw an error if the ticket can not be modified', async() => {
const tx = await models.Sale.beginTransaction({});
let error; let error;
const ticketId = 2; try {
const sales = [{id: 5}]; const options = {transaction: tx};
const reserved = false;
await app.models.Sale.reserve(ctx, ticketId, sales, reserved) const ticketId = 2;
.catch(response => { const sales = [{id: 5}];
expect(response).toEqual(new Error(`The sales of this ticket can't be modified`)); const reserved = false;
error = response;
});
expect(error).toBeDefined(); await models.Sale.reserve(ctx, ticketId, sales, reserved, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error(`The sales of this ticket can't be modified`));
}); });
it('should update the given sales of a ticket to reserved', async() => { it('should update the given sales of a ticket to reserved', async() => {
originalTicketSales = await app.models.Ticket.getSales(11); const tx = await models.Sale.beginTransaction({});
expect(originalTicketSales[0].reserved).toEqual(false); try {
expect(originalTicketSales[1].reserved).toEqual(false); const options = {transaction: tx};
const ticketId = 11; originalTicketSales = await models.Ticket.getSales(11, options);
const sales = [{id: 7}, {id: 8}];
const reserved = true;
await app.models.Sale.reserve(ctx, ticketId, sales, reserved); expect(originalTicketSales[0].reserved).toEqual(false);
expect(originalTicketSales[1].reserved).toEqual(false);
const reservedTicketSales = await app.models.Ticket.getSales(ticketId); const ticketId = 11;
const sales = [{id: 7}, {id: 8}];
const reserved = true;
expect(reservedTicketSales[0].reserved).toEqual(true); await models.Sale.reserve(ctx, ticketId, sales, reserved, options);
expect(reservedTicketSales[1].reserved).toEqual(true);
const reservedTicketSales = await models.Ticket.getSales(ticketId, options);
expect(reservedTicketSales[0].reserved).toEqual(true);
expect(reservedTicketSales[1].reserved).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,40 +1,45 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale updateConcept()', () => { describe('sale updateConcept()', () => {
const ctx = {req: {accessToken: {userId: 9}}}; const ctx = {req: {accessToken: {userId: 9}}};
const saleId = 1; const saleId = 1;
let originalSale;
beforeAll(async done => {
originalSale = await app.models.Sale.findById(saleId);
done();
});
afterAll(async done => {
await originalSale.save();
done();
});
it('should throw if ID was undefined', async() => { it('should throw if ID was undefined', async() => {
let err; const tx = await models.Sale.beginTransaction({});
const newConcept = 'I am he new concept';
let error;
try { try {
await app.models.Sale.updateConcept(ctx, undefined, newConcept); const options = {transaction: tx};
const newConcept = 'not going to heppen';
await models.Sale.updateConcept(ctx, undefined, newConcept, options);
await tx.rollback();
} catch (e) { } catch (e) {
err = e; await tx.rollback();
error = e;
} }
expect(err).toBeDefined(); expect(error).toBeDefined();
}); });
it('should update the sale concept', async() => { it('should update the sale concept', async() => {
const newConcept = 'I am the new concept'; const tx = await models.Sale.beginTransaction({});
let response = await app.models.Sale.updateConcept(ctx, saleId, newConcept); try {
const options = {transaction: tx};
expect(response.concept).toEqual(newConcept); const newConcept = 'I am the new concept';
let response = await models.Sale.updateConcept(ctx, saleId, newConcept, options);
expect(response.concept).toEqual(newConcept);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,104 +1,100 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale updatePrice()', () => { describe('sale updatePrice()', () => {
let originalSale; const ctx = {
let originalSalesPersonMana; req: {
let createdSaleComponent; accessToken: {userId: 18},
let saleId = 7; headers: {origin: 'localhost:5000'},
let manaComponentId; __: () => {}
}
beforeAll(async done => { };
let component = await app.models.Component.findOne({where: {code: 'mana'}}); const saleId = 7;
manaComponentId = component.id;
originalSale = await app.models.Sale.findById(saleId);
originalSalesPersonMana = await app.models.WorkerMana.findById(18);
done();
});
it('should throw an error if the ticket is not editable', async() => { it('should throw an error if the ticket is not editable', async() => {
const ctx = { const tx = await models.Sale.beginTransaction({});
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let immutableSaleId = 1;
let price = 5;
await app.models.Sale.updatePrice(ctx, immutableSaleId, price) try {
.catch(response => { const options = {transaction: tx};
expect(response).toEqual(new Error(`The sales of this ticket can't be modified`));
error = response;
});
expect(error).toBeDefined(); const immutableSaleId = 1;
const price = 5;
await models.Sale.updatePrice(ctx, immutableSaleId, price, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error(`The sales of this ticket can't be modified`));
}); });
it('should return 0 if the price is an empty string', async() => { it('should return 0 if the price is an empty string', async() => {
const ctx = { const tx = await models.Sale.beginTransaction({});
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let price = ''; try {
const options = {transaction: tx};
await app.models.Sale.updatePrice(ctx, saleId, price); const price = '';
let updatedSale = await app.models.Sale.findById(saleId);
expect(updatedSale.price).toEqual(0); await models.Sale.updatePrice(ctx, saleId, price, options);
const updatedSale = await models.Sale.findById(saleId, null, options);
// restores expect(updatedSale.price).toEqual(0);
await originalSale.updateAttributes(originalSale);
await app.models.SaleComponent.updateAll({componentFk: manaComponentId, saleFk: saleId}, {value: 0}); await tx.rollback();
await originalSalesPersonMana.updateAttributes(originalSalesPersonMana); } catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should now set price as a number in a string', async() => { it('should now set price as a number in a string', async() => {
const ctx = { const tx = await models.Sale.beginTransaction({});
req: {
accessToken: {userId: 18},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
let price = '8'; try {
const options = {transaction: tx};
await app.models.Sale.updatePrice(ctx, saleId, price); const price = '8';
let updatedSale = await app.models.Sale.findById(saleId);
expect(updatedSale.price).toEqual(8); await models.Sale.updatePrice(ctx, saleId, price, options);
const updatedSale = await models.Sale.findById(saleId, null, options);
// restores expect(updatedSale.price).toEqual(8);
await originalSale.updateAttributes(originalSale);
await app.models.SaleComponent.updateAll({componentFk: manaComponentId, saleFk: saleId}, {value: 0}); await tx.rollback();
await originalSalesPersonMana.updateAttributes(originalSalesPersonMana); } catch (e) {
await tx.rollback();
throw e;
}
}); });
// #2736 sale updatePrice() returns inconsistent values it('should set price as a decimal number and check the sale has the mana component changing the salesPersonMana', async() => {
xit('should set price as a decimal number and check the sale has the mana component changing the salesPersonMana', async() => { const tx = await models.Sale.beginTransaction({});
let ctx = {req: {accessToken: {userId: 18}}};
let price = 5.4;
await app.models.Sale.updatePrice(ctx, saleId, price); try {
let updatedSale = await app.models.Sale.findById(saleId); const options = {transaction: tx};
createdSaleComponent = await app.models.SaleComponent.findOne({where: {saleFk: saleId, componentFk: manaComponentId}});
expect(updatedSale.price).toBe(price); const price = 5.4;
expect(createdSaleComponent.value).toEqual(-2.04); const originalSalesPersonMana = await models.WorkerMana.findById(18, null, options);
const manaComponent = await models.Component.findOne({where: {code: 'mana'}}, options);
let updatedSalesPersonMana = await app.models.WorkerMana.findById(18); await models.Sale.updatePrice(ctx, saleId, price, options);
const updatedSale = await models.Sale.findById(saleId, null, options);
createdSaleComponent = await models.SaleComponent.findOne({where: {saleFk: saleId, componentFk: manaComponent.id}}, options);
expect(updatedSalesPersonMana.amount).not.toEqual(originalSalesPersonMana.amount); expect(updatedSale.price).toBe(price);
expect(createdSaleComponent.value).toEqual(-2.04);
// restores const updatedSalesPersonMana = await models.WorkerMana.findById(18, null, options);
await originalSale.updateAttributes(originalSale);
await app.models.SaleComponent.updateAll({componentFk: manaComponentId, saleFk: saleId}, {value: 0}); expect(updatedSalesPersonMana.amount).not.toEqual(originalSalesPersonMana.amount);
await originalSalesPersonMana.updateAttributes(originalSalesPersonMana);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,4 +1,4 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('sale updateQuantity()', () => { describe('sale updateQuantity()', () => {
const ctx = { const ctx = {
@ -10,44 +10,61 @@ describe('sale updateQuantity()', () => {
}; };
it('should throw an error if the quantity is not a number', async() => { it('should throw an error if the quantity is not a number', async() => {
const tx = await models.Sale.beginTransaction({});
let error; let error;
try {
const options = {transaction: tx};
await app.models.Sale.updateQuantity(ctx, 1, 'wrong quantity!') await models.Sale.updateQuantity(ctx, 1, 'wrong quantity!', options);
.catch(response => {
expect(response).toEqual(new Error('The value should be a number'));
error = response;
});
expect(error).toBeDefined(); await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('The value should be a number'));
}); });
it('should throw an error if the quantity is greater than it should be', async() => { it('should throw an error if the quantity is greater than it should be', async() => {
const tx = await models.Sale.beginTransaction({});
let error; let error;
try {
const options = {transaction: tx};
await app.models.Sale.updateQuantity(ctx, 1, 99) await models.Sale.updateQuantity(ctx, 1, 99, options);
.catch(response => {
expect(response).toEqual(new Error('The new quantity should be smaller than the old one'));
error = response;
});
expect(error).toBeDefined(); await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new Error('The new quantity should be smaller than the old one'));
}); });
it('should update the quantity of a given sale current line', async() => { it('should update the quantity of a given sale current line', async() => {
let originalLineData = await app.models.Sale.findOne({where: {id: 1}, fields: ['quantity']}); const tx = await models.Sale.beginTransaction({});
expect(originalLineData.quantity).toEqual(5); try {
const options = {transaction: tx};
await app.models.Sale.updateQuantity(ctx, 1, 4); const originalLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options);
let modifiedLineData = await app.models.Sale.findOne({where: {id: 1}, fields: ['quantity']}); expect(originalLine.quantity).toEqual(5);
expect(modifiedLineData.quantity).toEqual(4); await models.Sale.updateQuantity(ctx, 1, 4, options);
await app.models.Sale.update({id: 1}, {quantity: 5}); const modifiedLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options);
let resetLineDataValues = await app.models.Sale.findOne({where: {id: 1}, fields: ['quantity']}); expect(modifiedLine.quantity).toEqual(4);
expect(resetLineDataValues.quantity).toEqual(5); await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -24,15 +24,35 @@ module.exports = Self => {
} }
}); });
Self.updateConcept = async(ctx, id, newConcept) => { Self.updateConcept = async(ctx, id, newConcept, options) => {
const models = Self.app.models; const models = Self.app.models;
const currentLine = await models.Sale.findById(id); const myOptions = {};
let tx;
const canEditSale = await models.Sale.canEdit(ctx, [id]); if (typeof options == 'object')
Object.assign(myOptions, options);
if (!canEditSale) if (!myOptions.transaction) {
throw new UserError(`Sale(s) blocked, please contact production`); tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
return await currentLine.updateAttributes({concept: newConcept}); try {
const currentLine = await models.Sale.findById(id, null, myOptions);
const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`);
const line = await currentLine.updateAttributes({concept: newConcept}, myOptions);
if (tx) await tx.commit();
return line;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -28,14 +28,21 @@ module.exports = Self => {
} }
}); });
Self.updatePrice = async(ctx, id, newPrice) => { Self.updatePrice = async(ctx, id, newPrice, options) => {
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({}); const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try { try {
const options = {transaction: tx};
const filter = { const filter = {
include: { include: {
relation: 'ticket', relation: 'ticket',
@ -57,22 +64,22 @@ module.exports = Self => {
} }
}; };
const sale = await models.Sale.findById(id, filter, options); const sale = await models.Sale.findById(id, filter, myOptions);
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk); const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk, myOptions);
if (!isEditable) if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
const canEditSale = await models.Sale.canEdit(ctx, [id]); const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
const oldPrice = sale.price; const oldPrice = sale.price;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options); const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, myOptions);
const componentCode = usesMana ? 'mana' : 'buyerDiscount'; const componentCode = usesMana ? 'mana' : 'buyerDiscount';
const discount = await models.Component.findOne({where: {code: componentCode}}, options); const discount = await models.Component.findOne({where: {code: componentCode}}, myOptions);
const componentId = discount.id; const componentId = discount.id;
const componentValue = newPrice - sale.price; const componentValue = newPrice - sale.price;
@ -80,23 +87,23 @@ module.exports = Self => {
componentFk: componentId, componentFk: componentId,
saleFk: id saleFk: id
}; };
const saleComponent = await models.SaleComponent.findOne({where}, options); const saleComponent = await models.SaleComponent.findOne({where}, myOptions);
if (saleComponent) { if (saleComponent) {
await models.SaleComponent.updateAll(where, { await models.SaleComponent.updateAll(where, {
value: saleComponent.value + componentValue value: saleComponent.value + componentValue
}, options); }, myOptions);
} else { } else {
await models.SaleComponent.create({ await models.SaleComponent.create({
saleFk: id, saleFk: id,
componentFk: componentId, componentFk: componentId,
value: componentValue value: componentValue
}, options); }, myOptions);
} }
await sale.updateAttributes({price: newPrice}, options); await sale.updateAttributes({price: newPrice}, myOptions);
query = `CALL vn.manaSpellersRequery(?)`; query = `CALL vn.manaSpellersRequery(?)`;
await Self.rawSql(query, [userId], options); await Self.rawSql(query, [userId], myOptions);
const salesPerson = sale.ticket().client().salesPersonUser(); const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) { if (salesPerson) {
@ -111,14 +118,14 @@ module.exports = Self => {
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
} }
await tx.commit(); if (tx) await tx.commit();
return sale; return sale;
} catch (error) { } catch (error) {
await tx.rollback(); if (tx) await tx.rollback();
throw error; throw error;
} }
}; };

View File

@ -26,60 +26,77 @@ module.exports = Self => {
} }
}); });
Self.updateQuantity = async(ctx, id, newQuantity) => { Self.updateQuantity = async(ctx, id, newQuantity, options) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const $t = ctx.req.__; // $translate
const myOptions = {};
let tx;
const canEditSale = await models.Sale.canEdit(ctx, [id]); if (typeof options == 'object')
Object.assign(myOptions, options);
if (!canEditSale) if (!myOptions.transaction) {
throw new UserError(`Sale(s) blocked, please contact production`); tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
if (isNaN(newQuantity)) try {
throw new UserError(`The value should be a number`); const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions);
const filter = { if (!canEditSale)
include: { throw new UserError(`Sale(s) blocked, please contact production`);
relation: 'ticket',
scope: { if (isNaN(newQuantity))
include: { throw new UserError(`The value should be a number`);
relation: 'client',
scope: { const filter = {
include: { include: {
relation: 'salesPersonUser', relation: 'ticket',
scope: { scope: {
fields: ['id', 'name'] include: {
relation: 'client',
scope: {
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
} }
} }
} }
} }
} }
};
const sale = await models.Sale.findById(id, filter, myOptions);
if (newQuantity > sale.quantity)
throw new UserError('The new quantity should be smaller than the old one');
const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions);
const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale quantity', {
ticketId: sale.ticket().id,
itemId: sale.itemFk,
concept: sale.concept,
oldQuantity: oldQuantity,
newQuantity: newQuantity,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message, myOptions);
} }
};
const sale = await models.Sale.findById(id, filter); if (tx) await tx.commit();
if (newQuantity > sale.quantity) return result;
throw new UserError('The new quantity should be smaller than the old one'); } catch (error) {
if (tx) await tx.rollback();
const oldQuantity = sale.quantity; throw error;
const result = await sale.updateAttributes({quantity: newQuantity});
const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) {
const origin = ctx.req.headers.origin;
const message = $t('Changed sale quantity', {
ticketId: sale.ticket().id,
itemId: sale.itemFk,
concept: sale.concept,
oldQuantity: oldQuantity,
newQuantity: newQuantity,
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
} }
return result;
}; };
}; };

View File

@ -1,4 +1,3 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('editableStates', { Self.remoteMethodCtx('editableStates', {
description: 'Gets the editable states according the user role ', description: 'Gets the editable states according the user role ',
@ -8,7 +7,7 @@ module.exports = Self => {
type: 'object' type: 'object'
}, },
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -17,14 +16,18 @@ module.exports = Self => {
} }
}); });
Self.editableStates = async(ctx, filter) => { Self.editableStates = async(ctx, filter, options) => {
let userId = ctx.req.accessToken.userId; const models = Self.app.models;
let models = Self.app.models; const userId = ctx.req.accessToken.userId;
let statesList = await models.State.find({where: filter.where}); const myOptions = {};
let isProduction = await models.Account.hasRole(userId, 'production'); if (typeof options == 'object')
let isSalesPerson = await models.Account.hasRole(userId, 'salesPerson'); Object.assign(myOptions, options);
let isAdministrative = await models.Account.hasRole(userId, 'administrative');
let statesList = await models.State.find({where: filter.where}, myOptions);
const isProduction = await models.Account.hasRole(userId, 'production', myOptions);
const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson', myOptions);
const isAdministrative = await models.Account.hasRole(userId, 'administrative', myOptions);
if (isProduction || isAdministrative) if (isProduction || isAdministrative)
return statesList; return statesList;

View File

@ -18,19 +18,23 @@ module.exports = Self => {
} }
}); });
Self.isEditable = async(ctx, stateId) => { Self.isEditable = async(ctx, stateId, options) => {
const accessToken = ctx.req.accessToken; const accessToken = ctx.req.accessToken;
const models = Self.app.models; const models = Self.app.models;
const userId = accessToken.userId; const userId = accessToken.userId;
const myOptions = {};
let isProduction = await models.Account.hasRole(userId, 'production'); if (typeof options == 'object')
let isSalesPerson = await models.Account.hasRole(userId, 'salesPerson'); Object.assign(myOptions, options);
let isAdministrative = await models.Account.hasRole(userId, 'administrative');
let state = await models.State.findById(stateId);
let salesPersonAllowed = (isSalesPerson && (state.code == 'PICKER_DESIGNED' || state.code == 'PRINTED')); const isProduction = await models.Account.hasRole(userId, 'production', myOptions);
const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson', myOptions);
const isAdministrative = await models.Account.hasRole(userId, 'administrative', myOptions);
const state = await models.State.findById(stateId, null, myOptions);
let isAllowed = isProduction || isAdministrative || salesPersonAllowed || state.alertLevel == 0; const salesPersonAllowed = (isSalesPerson && (state.code == 'PICKER_DESIGNED' || state.code == 'PRINTED'));
const isAllowed = isProduction || isAdministrative || salesPersonAllowed || state.alertLevel == 0;
return isAllowed; return isAllowed;
}; };
}; };

View File

@ -1,34 +1,73 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket editableStates()', () => { describe('ticket editableStates()', () => {
const filter = {where: {name: {like: '%%'}}}; const filter = {where: {name: {like: '%%'}}};
it('should return the expected state for the given role', async() => { it('should return the expected state for the given role', async() => {
const productionRole = 49; const tx = await models.State.beginTransaction({});
const ctx = {req: {accessToken: {userId: productionRole}}};
let result = await app.models.State.editableStates(ctx, filter); try {
let deliveredState = result.some(state => state.code == 'DELIVERED'); const options = {transaction: tx};
expect(deliveredState).toBeTruthy(); const productionRole = 49;
const ctx = {req: {accessToken: {userId: productionRole}}};
const editableStates = await models.State.editableStates(ctx, filter, options);
const deliveredState = editableStates.some(state => state.code == 'DELIVERED');
expect(deliveredState).toBeTruthy();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it(`should return the expected states by a specific role`, async() => { it(`should return the expected states by a specific role`, async() => {
const productionRole = 18; const tx = await models.State.beginTransaction({});
const ctx = {req: {accessToken: {userId: productionRole}}};
let result = await app.models.State.editableStates(ctx, filter);
let deliveredState = result.some(state => state.code == 'DELIVERED');
let pickerDesignedState = result.some(state => state.code == 'PICKER_DESIGNED');
expect(deliveredState).toBeFalsy(); try {
expect(pickerDesignedState).toBeTruthy(); const options = {transaction: tx};
const productionRole = 18;
const ctx = {req: {accessToken: {userId: productionRole}}};
const editableStates = await models.State.editableStates(ctx, filter, options);
const deliveredState = editableStates.some(state => state.code == 'DELIVERED');
const pickerDesignedState = editableStates.some(state => state.code == 'PICKER_DESIGNED');
expect(deliveredState).toBeFalsy();
expect(pickerDesignedState).toBeTruthy();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it(`should return again the expected state by a specific role`, async() => { it(`should return again the expected state by a specific role`, async() => {
const employeeRole = 1; const tx = await models.State.beginTransaction({});
const ctx = {req: {accessToken: {userId: employeeRole}}};
let result = await app.models.State.editableStates(ctx, filter);
let pickerDesignedState = result.some(state => state.code == 'PICKER_DESIGNED');
expect(pickerDesignedState).toBeTruthy(); try {
const options = {transaction: tx};
const employeeRole = 1;
const ctx = {req: {accessToken: {userId: employeeRole}}};
const editableStates = await models.State.editableStates(ctx, filter, options);
const pickerDesignedState = editableStates.some(state => state.code == 'PICKER_DESIGNED');
expect(pickerDesignedState).toBeTruthy();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,61 +1,127 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('state isEditable()', () => { describe('state isEditable()', () => {
it('should return false if the state is not editable by a specific role', async() => { it('should return false if the state is not editable by a specific role', async() => {
const salesPersonRole = 18; const tx = await models.State.beginTransaction({});
const onDeliveryState = 13;
let ctx = {req: {accessToken: {userId: salesPersonRole}}};
let result = await app.models.State.isEditable(ctx, onDeliveryState);
expect(result).toBe(false); try {
const options = {transaction: tx};
const salesPersonRole = 18;
const onDeliveryState = 13;
const ctx = {req: {accessToken: {userId: salesPersonRole}}};
const result = await models.State.isEditable(ctx, onDeliveryState, options);
expect(result).toBe(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return true if the state is editable by a specific role', async() => { it('should return true if the state is editable by a specific role', async() => {
const salesPersonRole = 18; const tx = await models.State.beginTransaction({});
const asignedState = 20;
let ctx = {req: {accessToken: {userId: salesPersonRole}}};
let result = await app.models.State.isEditable(ctx, asignedState);
expect(result).toBe(true); try {
const options = {transaction: tx};
const salesPersonRole = 18;
const asignedState = 20;
const ctx = {req: {accessToken: {userId: salesPersonRole}}};
const result = await models.State.isEditable(ctx, asignedState, options);
expect(result).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return true again if the state is editable by a specific role', async() => { it('should return true again if the state is editable by a specific role', async() => {
const employeeRole = 1; const tx = await models.State.beginTransaction({});
const fixingState = 1;
let ctx = {req: {accessToken: {userId: employeeRole}}};
let result = await app.models.State.isEditable(ctx, fixingState);
expect(result).toBe(true); try {
const options = {transaction: tx};
const employeeRole = 1;
const fixingState = 1;
const ctx = {req: {accessToken: {userId: employeeRole}}};
const result = await models.State.isEditable(ctx, fixingState, options);
expect(result).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return false if the state is not editable for the given role', async() => { it('should return false if the state is not editable for the given role', async() => {
const employeeRole = 1; const tx = await models.State.beginTransaction({});
const asignedState = 13;
let ctx = {req: {accessToken: {userId: employeeRole}}};
let result = await app.models.State.isEditable(ctx, asignedState);
expect(result).toBe(false); try {
const options = {transaction: tx};
const employeeRole = 1;
const asignedState = 13;
const ctx = {req: {accessToken: {userId: employeeRole}}};
const result = await models.State.isEditable(ctx, asignedState, options);
expect(result).toBe(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return true if the state is editable for the given role', async() => { it('should return true if the state is editable for the given role', async() => {
const productionRole = 49; const tx = await models.State.beginTransaction({});
const onDeliveryState = 13;
let ctx = {req: {accessToken: {userId: productionRole}}};
let result = await app.models.State.isEditable(ctx, onDeliveryState);
expect(result).toBe(true); try {
const options = {transaction: tx};
const productionRole = 49;
const onDeliveryState = 13;
const ctx = {req: {accessToken: {userId: productionRole}}};
const result = await models.State.isEditable(ctx, onDeliveryState, options);
expect(result).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return true if the ticket is editable, the role is salesPerson and the ticket state is printed', async() => { it('should return true if the ticket is editable, the role is salesPerson and the ticket state is printed', async() => {
const salesPersonRole = 18; const tx = await models.State.beginTransaction({});
const printedState = 4;
const okState = 3;
const ctx = {req: {accessToken: {userId: salesPersonRole}}};
let canEditCurrent = await app.models.State.isEditable(ctx, printedState); try {
let canAsignNew = await app.models.State.isEditable(ctx, okState); const options = {transaction: tx};
let result = canEditCurrent && canAsignNew;
expect(result).toBe(true); const salesPersonRole = 18;
const printedState = 4;
const okState = 3;
const ctx = {req: {accessToken: {userId: salesPersonRole}}};
const canEditCurrent = await models.State.isEditable(ctx, printedState, options);
const canAsignNew = await models.State.isEditable(ctx, okState, options);
const result = canEditCurrent && canAsignNew;
expect(result).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -4,12 +4,12 @@ module.exports = Self => {
accessType: 'WRITE', accessType: 'WRITE',
accepts: { accepts: {
arg: 'id', arg: 'id',
type: 'Number', type: 'number',
description: 'The document id', description: 'The document id',
http: {source: 'path'} http: {source: 'path'}
}, },
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -18,16 +18,36 @@ module.exports = Self => {
} }
}); });
Self.removeFile = async(ctx, id) => { Self.removeFile = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const targetTicketDms = await models.TicketDms.findById(id); const myOptions = {};
const targetDms = await models.Dms.findById(targetTicketDms.dmsFk); let tx;
const trashDmsType = await models.DmsType.findOne({where: {code: 'trash'}});
await models.Dms.removeFile(ctx, targetTicketDms.dmsFk); if (typeof options == 'object')
await targetTicketDms.destroy(); Object.assign(myOptions, options);
return targetDms.updateAttribute('dmsTypeFk', trashDmsType.id); if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const targetTicketDms = await models.TicketDms.findById(id, null, myOptions);
const targetDms = await models.Dms.findById(targetTicketDms.dmsFk, null, myOptions);
const trashDmsType = await models.DmsType.findOne({where: {code: 'trash'}}, myOptions);
await models.Dms.removeFile(ctx, targetTicketDms.dmsFk, myOptions);
await targetTicketDms.destroy(myOptions);
await targetDms.updateAttribute('dmsTypeFk', trashDmsType.id, myOptions);
if (tx) await tx.commit();
return targetDms;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -1,18 +1,25 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('TicketDms removeFile()', () => { describe('TicketDms removeFile()', () => {
const ticketDmsId = 1; const ticketDmsId = 1;
it(`should return an error for a user without enough privileges`, async() => { it(`should return an error for a user without enough privileges`, async() => {
let clientId = 1101; const tx = await models.TicketDms.beginTransaction({});
let ctx = {req: {accessToken: {userId: clientId}}};
let error; let error;
await app.models.TicketDms.removeFile(ctx, ticketDmsId).catch(e => { try {
error = e; const options = {transaction: tx};
}).finally(() => {
expect(error.message).toEqual(`You don't have enough privileges`);
});
expect(error).toBeDefined(); const clientId = 1101;
const ctx = {req: {accessToken: {userId: clientId}}};
await models.TicketDms.removeFile(ctx, ticketDmsId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`You don't have enough privileges`);
}); });
}); });

View File

@ -6,22 +6,22 @@ module.exports = Self => {
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',
type: 'Integer', type: 'number',
required: true, required: true,
description: 'The request ID', description: 'The request ID',
}, { }, {
arg: 'itemFk', arg: 'itemFk',
type: 'Integer', type: 'number',
required: true, required: true,
description: 'The requested item ID', description: 'The requested item ID',
}, { }, {
arg: 'quantity', arg: 'quantity',
type: 'Integer', type: 'number',
required: true, required: true,
description: 'The requested item quantity', description: 'The requested item quantity',
}], }],
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -30,25 +30,37 @@ module.exports = Self => {
} }
}); });
Self.confirm = async ctx => { Self.confirm = async(ctx, options) => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({});
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
let sale; const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try { try {
let options = {transaction: tx}; const item = await models.Item.findById(ctx.args.itemFk, null, myOptions);
let item = await models.Item.findById(ctx.args.itemFk, null, options);
if (!item) if (!item)
throw new UserError(`That item doesn't exists`); throw new UserError(`That item doesn't exists`);
let request = await models.TicketRequest.findById(ctx.args.id, { const request = await models.TicketRequest.findById(ctx.args.id, {
include: {relation: 'ticket'} include: {relation: 'ticket'}
}, options); }, myOptions);
const itemStock = await models.Item.getVisibleAvailable(
ctx.args.itemFk,
request.ticket().warehouseFk,
request.ticket().shipped,
myOptions
);
const itemStock = await models.Item.getVisibleAvailable(ctx.args.itemFk, request.ticket().warehouseFk, request.ticket().shipped);
const isAvailable = itemStock.available > 0; const isAvailable = itemStock.available > 0;
if (!isAvailable) if (!isAvailable)
@ -57,23 +69,24 @@ module.exports = Self => {
if (request.saleFk) if (request.saleFk)
throw new UserError(`This request already contains a sale`); throw new UserError(`This request already contains a sale`);
sale = await models.Sale.create({ const sale = await models.Sale.create({
ticketFk: request.ticketFk, ticketFk: request.ticketFk,
itemFk: ctx.args.itemFk, itemFk: ctx.args.itemFk,
quantity: ctx.args.quantity, quantity: ctx.args.quantity,
concept: item.name concept: item.name
}, options); }, myOptions);
await request.updateAttributes({ await request.updateAttributes({
saleFk: sale.id, saleFk: sale.id,
itemFk: sale.itemFk, itemFk: sale.itemFk,
isOk: true isOk: true
}, options); }, myOptions);
query = `CALL vn.sale_calculateComponent(?, NULL)`; const query = `CALL vn.sale_calculateComponent(?, NULL)`;
await Self.rawSql(query, [sale.id], options); await Self.rawSql(query, [sale.id], myOptions);
const origin = ctx.req.headers.origin; const origin = ctx.req.headers.origin;
const requesterId = request.requesterFk; const requesterId = request.requesterFk;
const message = $t('Bought units from buy request', { const message = $t('Bought units from buy request', {
quantity: sale.quantity, quantity: sale.quantity,
concept: sale.concept, concept: sale.concept,
@ -82,10 +95,9 @@ module.exports = Self => {
url: `${origin}/#!/ticket/${sale.ticketFk}/summary`, url: `${origin}/#!/ticket/${sale.ticketFk}/summary`,
urlItem: `${origin}/#!/item/${sale.itemFk}/summary` urlItem: `${origin}/#!/item/${sale.itemFk}/summary`
}); });
await models.Chat.sendCheckingPresence(ctx, requesterId, message); await models.Chat.sendCheckingPresence(ctx, requesterId, message, myOptions);
// log const logRecord = {
let logRecord = {
originFk: sale.ticketFk, originFk: sale.ticketFk,
userFk: userId, userFk: userId,
action: 'update', action: 'update',
@ -99,14 +111,14 @@ module.exports = Self => {
} }
}; };
await Self.app.models.TicketLog.create(logRecord); await Self.app.models.TicketLog.create(logRecord, myOptions);
await tx.commit(); if (tx) await tx.commit();
return sale; return sale;
} catch (error) { } catch (e) {
await tx.rollback(); if (tx) await tx.rollback();
throw error; throw e;
} }
}; };
}; };

View File

@ -4,7 +4,7 @@ module.exports = Self => {
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',
type: 'Integer', type: 'number',
required: true, required: true,
description: 'The request ID', description: 'The request ID',
}, { }, {
@ -23,17 +23,37 @@ module.exports = Self => {
} }
}); });
Self.deny = async ctx => { Self.deny = async(ctx, options) => {
let userId = ctx.req.accessToken.userId; const myOptions = {};
let worker = await Self.app.models.Worker.findOne({where: {userFk: userId}}); let tx;
let params = { if (typeof options == 'object')
isOk: false, Object.assign(myOptions, options);
attenderFk: worker.id,
response: ctx.args.observation,
};
let request = await Self.app.models.TicketRequest.findById(ctx.args.id); if (!myOptions.transaction) {
return request.updateAttributes(params); tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const userId = ctx.req.accessToken.userId;
const worker = await Self.app.models.Worker.findOne({where: {userFk: userId}}, myOptions);
const params = {
isOk: false,
attenderFk: worker.id,
response: ctx.args.observation,
};
const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions);
await request.updateAttributes(params, myOptions);
if (tx) await tx.commit();
return request;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -68,9 +68,13 @@ module.exports = Self => {
} }
}); });
Self.filter = async(ctx, filter) => { Self.filter = async(ctx, filter, options) => {
let conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
let userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (ctx.args.mine) if (ctx.args.mine)
ctx.args.attenderFk = userId; ctx.args.attenderFk = userId;
@ -111,9 +115,7 @@ module.exports = Self => {
filter = mergeFilters(filter, {where}); filter = mergeFilters(filter, {where});
let stmt; const stmt = new ParameterizedSQL(
stmt = new ParameterizedSQL(
`SELECT `SELECT
tr.id, tr.id,
tr.ticketFk, tr.ticketFk,
@ -149,8 +151,6 @@ module.exports = Self => {
LEFT JOIN account.user ua ON ua.id = wka.userFk`); LEFT JOIN account.user ua ON ua.id = wka.userFk`);
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
let result = await conn.executeStmt(stmt); return conn.executeStmt(stmt, myOptions);
return result;
}; };
}; };

View File

@ -1,4 +1,4 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket-request confirm()', () => { describe('ticket-request confirm()', () => {
let ctx = { let ctx = {
@ -12,74 +12,89 @@ describe('ticket-request confirm()', () => {
}; };
it(`should throw an error if the item doesn't exist`, async() => { it(`should throw an error if the item doesn't exist`, async() => {
ctx.args = {itemFk: 999}; const tx = await models.TicketRequest.beginTransaction({});
let error; let error;
try { try {
await app.models.TicketRequest.confirm(ctx); const options = {transaction: tx};
} catch (err) {
error = err; ctx.args = {itemFk: 999};
await models.TicketRequest.confirm(ctx, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
} }
expect(error.message).toEqual(`That item doesn't exists`); expect(error.message).toEqual(`That item doesn't exists`);
}); });
it('should throw an error if the item is not available', async() => { it('should throw an error if the item is not available', async() => {
const requestId = 5; const tx = await models.TicketRequest.beginTransaction({});
const itemId = 4;
const quantity = 99999;
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
let error; let error;
try { try {
await app.models.TicketRequest.confirm(ctx); const options = {transaction: tx};
} catch (err) {
error = err; const requestId = 5;
const itemId = 4;
const quantity = 99999;
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
await models.TicketRequest.confirm(ctx, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
} }
expect(error.message).toEqual(`This item is not available`); expect(error.message).toEqual(`This item is not available`);
}); });
it(`should throw if there's a sale id`, async() => { it(`should throw if there's a sale id`, async() => {
const requestId = 4; const tx = await models.TicketRequest.beginTransaction({});
const itemId = 1;
const quantity = 10;
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
const request = await app.models.TicketRequest.findById(requestId);
expect(request.saleFk).toBeNull();
await request.updateAttributes({saleFk: 2});
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
let error; let error;
try { try {
await app.models.TicketRequest.confirm(ctx); const options = {transaction: tx};
} catch (err) {
error = err; const requestId = 4;
const itemId = 1;
const quantity = 10;
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
const request = await models.TicketRequest.findById(requestId, null, options);
expect(request.saleFk).toBeNull();
await request.updateAttributes({saleFk: 2}, options);
ctx.args = {
itemFk: itemId,
id: requestId,
quantity: quantity
};
await models.TicketRequest.confirm(ctx, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
} }
expect(error.message).toEqual(`This request already contains a sale`); expect(error.message).toEqual(`This request already contains a sale`);
// restores
await request.updateAttributes({saleFk: null});
}); });
}); });

View File

@ -1,25 +1,22 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket-request deny()', () => { describe('ticket-request deny()', () => {
let request; it('should return the dinied ticket request', async() => {
afterAll(async done => { const tx = await models.TicketRequest.beginTransaction({});
let params = {
isOk: null,
attenderFk: request.attenderFk,
response: null,
};
await request.updateAttributes(params); try {
done(); const options = {transaction: tx};
});
it('should return all ticket requests', async() => { const ctx = {req: {accessToken: {userId: 9}}, args: {id: 4, observation: 'my observation'}};
let ctx = {req: {accessToken: {userId: 9}}, args: {id: 4, observation: 'my observation'}};
request = await app.models.TicketRequest.findById(ctx.args.id); const result = await models.TicketRequest.deny(ctx, options);
let result = await app.models.TicketRequest.deny(ctx); expect(result.id).toEqual(4);
expect(result.id).toEqual(4); await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,85 +1,184 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket-request filter()', () => { describe('ticket-request filter()', () => {
const userId = 9; const userId = 9;
let ctx = {req: {accessToken: {userId: userId}}}; let ctx = {req: {accessToken: {userId: userId}}};
it('should now return all ticket requests', async() => { it('should now return all ticket requests', async() => {
ctx.args = {}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const options = {transaction: tx};
expect(result.length).toEqual(3); ctx.args = {};
const result = await models.TicketRequest.filter(ctx, options);
expect(result.length).toEqual(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching a generic search value which is the ticket ID', async() => { it('should return the ticket request matching a generic search value which is the ticket ID', async() => {
ctx.args = {search: 11}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(4); ctx.args = {search: 11};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching a generic search value which is the client address alias', async() => { it('should return the ticket request matching a generic search value which is the client address alias', async() => {
ctx.args = {search: 'NY roofs'}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(4); ctx.args = {search: 'NY roofs'};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the ticket ID', async() => { it('should return the ticket request matching the ticket ID', async() => {
ctx.args = {ticketFk: 11}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx);
const requestId = result[0].id;
expect(requestId).toEqual(4); try {
const options = {transaction: tx};
ctx.args = {ticketFk: 11};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the atender ID', async() => { it('should return the ticket request matching the atender ID', async() => {
ctx.args = {attenderFk: 35}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(3); ctx.args = {attenderFk: 35};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the isOk triple-state', async() => { it('should return the ticket request matching the isOk triple-state', async() => {
ctx.args = {isOk: null}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(3); ctx.args = {isOk: null};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the client ID', async() => { it('should return the ticket request matching the client ID', async() => {
ctx.args = {clientFk: 1102}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(4); ctx.args = {clientFk: 1102};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(4);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the warehouse ID', async() => { it('should return the ticket request matching the warehouse ID', async() => {
ctx.args = {warehouse: 1}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx, {order: 'id'}); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(3); ctx.args = {warehouse: 1};
const result = await models.TicketRequest.filter(ctx, {order: 'id'}, options);
const requestId = result[0].id;
expect(requestId).toEqual(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket request matching the salesPerson ID', async() => { it('should return the ticket request matching the salesPerson ID', async() => {
ctx.args = {salesPersonFk: 18}; const tx = await models.TicketRequest.beginTransaction({});
const result = await app.models.TicketRequest.filter(ctx); try {
const requestId = result[0].id; const options = {transaction: tx};
expect(requestId).toEqual(3); ctx.args = {salesPersonFk: 18};
const result = await models.TicketRequest.filter(ctx, options);
const requestId = result[0].id;
expect(requestId).toEqual(3);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -23,38 +23,64 @@ module.exports = Self => {
} }
}); });
Self.changeState = async(ctx, params) => { Self.changeState = async(ctx, params, options) => {
let userId = ctx.req.accessToken.userId; const models = Self.app.models;
let models = Self.app.models; const myOptions = {};
let tx;
if (!params.stateFk && !params.code) if (typeof options == 'object')
throw new UserError('State cannot be blank'); Object.assign(myOptions, options);
if (params.code) { if (!myOptions.transaction) {
let state = await models.State.findOne({where: {code: params.code}, fields: ['id']}); tx = await Self.beginTransaction({});
params.stateFk = state.id; myOptions.transaction = tx;
} }
if (!params.workerFk) { try {
let worker = await models.Worker.findOne({where: {userFk: userId}}); const userId = ctx.req.accessToken.userId;
params.workerFk = worker.id;
if (!params.stateFk && !params.code)
throw new UserError('State cannot be blank');
if (params.code) {
const state = await models.State.findOne({
where: {code: params.code},
fields: ['id']
}, myOptions);
params.stateFk = state.id;
}
if (!params.workerFk) {
const worker = await models.Worker.findOne({
where: {userFk: userId}
}, myOptions);
params.workerFk = worker.id;
}
const ticketState = await models.TicketState.findById(params.ticketFk, {
fields: ['stateFk']
}, myOptions);
let oldStateAllowed;
if (ticketState)
oldStateAllowed = await models.State.isEditable(ctx, ticketState.stateFk, myOptions);
const newStateAllowed = await models.State.isEditable(ctx, params.stateFk, myOptions);
const isAllowed = (!ticketState || oldStateAllowed == true) && newStateAllowed == true;
if (!isAllowed)
throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
const ticketTracking = await models.TicketTracking.create(params, myOptions);
if (tx) await tx.commit();
return ticketTracking;
} catch (e) {
if (tx) await tx.rollback();
throw e;
} }
let ticketState = await models.TicketState.findById(
params.ticketFk,
{fields: ['stateFk']}
);
let oldStateAllowed;
if (ticketState)
oldStateAllowed = await models.State.isEditable(ctx, ticketState.stateFk);
let newStateAllowed = await models.State.isEditable(ctx, params.stateFk);
let isAllowed = (!ticketState || oldStateAllowed == true) && newStateAllowed == true;
if (!isAllowed)
throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
return models.TicketTracking.create(params);
}; };
}; };

View File

@ -6,13 +6,13 @@ module.exports = Self => {
{ {
arg: 'ticketIds', arg: 'ticketIds',
description: 'the array of ticket ids to set as delivered', description: 'the array of ticket ids to set as delivered',
type: ['Number'], type: ['number'],
required: true, required: true,
http: {source: 'body'} http: {source: 'body'}
} }
], ],
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -21,30 +21,47 @@ module.exports = Self => {
} }
}); });
Self.setDelivered = async(ctx, ticketIds) => { Self.setDelivered = async(ctx, ticketIds, options) => {
let userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
let models = Self.app.models; const models = Self.app.models;
const myOptions = {};
let tx;
let state = await models.State.findOne({ if (typeof options == 'object')
where: { Object.assign(myOptions, options);
code: 'delivered'
},
fields: ['id', 'name', 'alertLevel', 'code']
});
let worker = await models.Worker.findOne({where: {userFk: userId}}); if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
let promises = []; myOptions.transaction = tx;
for (let id of ticketIds) {
let promise = models.TicketTracking.changeState(ctx, {
stateFk: state.id,
workerFk: worker.id,
ticketFk: id
});
promises.push(promise);
} }
await Promise.all(promises);
return state; try {
const state = await models.State.findOne({
where: {
code: 'delivered'
},
fields: ['id', 'name', 'alertLevel', 'code']
}, myOptions);
const worker = await models.Worker.findOne({where: {userFk: userId}}, myOptions);
const promises = [];
for (const id of ticketIds) {
const promise = models.TicketTracking.changeState(ctx, {
stateFk: state.id,
workerFk: worker.id,
ticketFk: id
}, myOptions);
promises.push(promise);
}
await Promise.all(promises);
if (tx) await tx.commit();
return state;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -1,110 +1,133 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('ticket changeState()', () => { describe('ticket changeState()', () => {
const salesPersonId = 18; const salesPersonId = 18;
const employeeId = 1; const employeeId = 1;
const productionId = 49; const productionId = 49;
let activeCtx = { const activeCtx = {
accessToken: {userId: 9}, accessToken: {userId: 9},
}; };
let ctx = {req: activeCtx}; const ctx = {req: activeCtx};
let ticket; const now = new Date();
const sampleTicket = {
shipped: now,
landed: now,
nickname: 'Many Places',
packages: 0,
updated: now,
priority: 1,
zoneFk: 3,
zonePrice: 5,
zoneBonus: 1,
totalWithVat: 120,
totalWithoutVat: 100,
clientFk: 1106,
warehouseFk: 1,
addressFk: 126,
routeFk: 6,
companyFk: 442,
agencyModeFk: 7
};
beforeAll(async done => { beforeAll(async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
done();
});
beforeEach(async done => {
try {
let originalTicket = await app.models.Ticket.findOne({where: {id: 16}});
originalTicket.id = null;
ticket = await app.models.Ticket.create(originalTicket);
} catch (error) {
console.error(error);
}
done();
});
afterEach(async done => {
try {
await app.models.Ticket.destroyById(ticket.id);
} catch (error) {
console.error(error);
}
done();
});
afterAll(async done => {
try {
await app.models.Ticket.destroyById(ticket.id);
} catch (error) {
console.error(error);
}
done();
}); });
it('should throw if the ticket is not editable and the user isnt production', async() => { it('should throw if the ticket is not editable and the user isnt production', async() => {
activeCtx.accessToken.userId = salesPersonId; const tx = await models.TicketTracking.beginTransaction({});
let params = {ticketFk: 2, stateFk: 3};
let error;
let errCode;
try { try {
await app.models.TicketTracking.changeState(ctx, params); const options = {transaction: tx};
activeCtx.accessToken.userId = salesPersonId;
const params = {ticketFk: 2, stateFk: 3};
await models.TicketTracking.changeState(ctx, params, options);
await tx.rollback();
} catch (e) { } catch (e) {
errCode = e.code; await tx.rollback();
error = e;
} }
expect(errCode).toBe('ACCESS_DENIED'); expect(error.code).toBe('ACCESS_DENIED');
}); });
it('should throw an error if a worker with employee role attemps to a forbidden state', async() => { it('should throw an error if a worker with employee role attemps to a forbidden state', async() => {
activeCtx.accessToken.userId = employeeId; const tx = await models.TicketTracking.beginTransaction({});
let params = {ticketFk: 11, stateFk: 13};
let error;
let errCode;
try { try {
await app.models.TicketTracking.changeState(ctx, params); const options = {transaction: tx};
activeCtx.accessToken.userId = employeeId;
const params = {ticketFk: 11, stateFk: 13};
await models.TicketTracking.changeState(ctx, params, options);
await tx.rollback();
} catch (e) { } catch (e) {
errCode = e.code; await tx.rollback();
error = e;
} }
expect(errCode).toBe('ACCESS_DENIED'); expect(error.code).toBe('ACCESS_DENIED');
}); });
it('should be able to create a ticket tracking line for a not editable ticket if the user has the production role', async() => { it('should be able to create a ticket tracking line for a not editable ticket if the user has the production role', async() => {
activeCtx.accessToken.userId = productionId; const tx = await models.TicketTracking.beginTransaction({});
let params = {ticketFk: ticket.id, stateFk: 3};
let ticketTracking = await app.models.TicketTracking.changeState(ctx, params); try {
const options = {transaction: tx};
expect(ticketTracking.__data.ticketFk).toBe(params.ticketFk); const ticket = await models.Ticket.create(sampleTicket, options);
expect(ticketTracking.__data.stateFk).toBe(params.stateFk);
expect(ticketTracking.__data.workerFk).toBe(49);
expect(ticketTracking.__data.id).toBeDefined();
// restores activeCtx.accessToken.userId = productionId;
await app.models.TicketTracking.destroyById(ticketTracking.__data.id); const params = {ticketFk: ticket.id, stateFk: 3};
const ticketTracking = await models.TicketTracking.changeState(ctx, params, options);
expect(ticketTracking.__data.ticketFk).toBe(params.ticketFk);
expect(ticketTracking.__data.stateFk).toBe(params.stateFk);
expect(ticketTracking.__data.workerFk).toBe(49);
expect(ticketTracking.__data.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should update the ticket tracking line when the user is salesperson, uses the state assigned and a valid worker id', async() => { it('should update the ticket tracking line when the user is salesperson, uses the state assigned and a valid worker id', async() => {
let ctx = {req: {accessToken: {userId: 18}}}; const tx = await models.TicketTracking.beginTransaction({});
let assignedState = await app.models.State.findOne({where: {code: 'PICKER_DESIGNED'}});
let params = {ticketFk: ticket.id, stateFk: assignedState.id, workerFk: 1};
let res = await app.models.TicketTracking.changeState(ctx, params);
expect(res.__data.ticketFk).toBe(params.ticketFk); try {
expect(res.__data.stateFk).toBe(params.stateFk); const options = {transaction: tx};
expect(res.__data.workerFk).toBe(params.workerFk);
expect(res.__data.workerFk).toBe(1); const ticket = await models.Ticket.create(sampleTicket, options);
expect(res.__data.id).toBeDefined(); const ctx = {req: {accessToken: {userId: 18}}};
const assignedState = await models.State.findOne({where: {code: 'PICKER_DESIGNED'}}, options);
const params = {ticketFk: ticket.id, stateFk: assignedState.id, workerFk: 1};
const res = await models.TicketTracking.changeState(ctx, params, options);
expect(res.__data.ticketFk).toBe(params.ticketFk);
expect(res.__data.stateFk).toBe(params.stateFk);
expect(res.__data.workerFk).toBe(params.workerFk);
expect(res.__data.workerFk).toBe(1);
expect(res.__data.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -1,4 +1,4 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('ticket setDelivered()', () => { describe('ticket setDelivered()', () => {
@ -7,50 +7,40 @@ describe('ticket setDelivered()', () => {
accessToken: {userId: userId}, accessToken: {userId: userId},
}; };
let ticketOne; beforeAll(async() => {
let ticketTwo;
beforeAll(async done => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
});
it('should return the state which has been applied to the given tickets', async() => {
const tx = await models.TicketTracking.beginTransaction({});
try { try {
let originalTicketOne = await app.models.Ticket.findById(8); const options = {transaction: tx};
let originalTicketTwo = await app.models.Ticket.findById(10);
const ctx = {req: {accessToken: {userId: 49}}};
const originalTicketOne = await models.Ticket.findById(8, null, options);
const originalTicketTwo = await models.Ticket.findById(10, null, options);
originalTicketOne.id = null; originalTicketOne.id = null;
originalTicketTwo.id = null; originalTicketTwo.id = null;
ticketOne = await app.models.Ticket.create(originalTicketOne); const ticketOne = await models.Ticket.create(originalTicketOne, options);
ticketTwo = await app.models.Ticket.create(originalTicketTwo); const ticketTwo = await models.Ticket.create(originalTicketTwo, options);
} catch (error) {
console.error(error); const delivered = await models.State.findOne({where: {code: 'delivered'}, fields: ['id']}, options);
const params = [ticketOne.id, ticketTwo.id];
const state = await models.TicketTracking.setDelivered(ctx, params, options);
expect(state.id).toEqual(delivered.id);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
} }
done();
});
afterAll(async done => {
try {
await app.models.Ticket.destroyById(ticketOne.id);
await app.models.Ticket.destroyById(ticketTwo.id);
} catch (error) {
console.error(error);
}
done();
});
it('should return the state which has been applied to the given tickets', async() => {
let ctx = {req: {accessToken: {userId: 49}}};
let delivered = await app.models.State.findOne({where: {code: 'delivered'}, fields: ['id']});
let params = [ticketOne.id, ticketTwo.id];
let state = await app.models.TicketTracking.setDelivered(ctx, params);
expect(state.id).toEqual(delivered.id);
// restores
await app.models.TicketTracking.destroyById(state.id);
}); });
}); });

View File

@ -10,18 +10,18 @@ module.exports = Self => {
accepts: [ accepts: [
{ {
arg: 'filter', arg: 'filter',
type: 'Object', type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
http: {source: 'query'} http: {source: 'query'}
}, { }, {
arg: 'search', arg: 'search',
type: 'String', type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by client id`, description: `If it's and integer searchs by id, otherwise it searchs by client id`,
http: {source: 'query'} http: {source: 'query'}
} }
], ],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -30,10 +30,14 @@ module.exports = Self => {
} }
}); });
Self.filter = async(ctx, filter) => { Self.filter = async(ctx, filter, options) => {
let conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const myOptions = {};
let where = buildFilter(ctx.args, (param, value) => { if (typeof options == 'object')
Object.assign(myOptions, options);
const where = buildFilter(ctx.args, (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
return {or: [ return {or: [
@ -46,10 +50,9 @@ module.exports = Self => {
filter = mergeFilters(ctx.args.filter, {where}); filter = mergeFilters(ctx.args.filter, {where});
let stmts = []; const stmts = [];
let stmt;
stmt = new ParameterizedSQL( const stmt = new ParameterizedSQL(
`SELECT t.id AS ticketFk, c.id AS clientFk, c.name AS clientName, tw.weekDay, `SELECT t.id AS ticketFk, c.id AS clientFk, c.name AS clientName, tw.weekDay,
wh.name AS warehouseName, u.id AS workerFk, u.name AS userName, u.nickName, tw.agencyModeFk wh.name AS warehouseName, u.id AS workerFk, u.name AS userName, u.nickName, tw.agencyModeFk
FROM ticketWeekly tw FROM ticketWeekly tw
@ -60,10 +63,10 @@ module.exports = Self => {
); );
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));
let itemsIndex = stmts.push(stmt) - 1; const itemsIndex = stmts.push(stmt) - 1;
let sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
let result = await conn.executeStmt(sql); const result = await conn.executeStmt(sql, myOptions);
return itemsIndex === 0 ? result : result[itemsIndex]; return itemsIndex === 0 ? result : result[itemsIndex];
}; };
}; };

View File

@ -1,43 +1,89 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
describe('ticket-weekly filter()', () => { describe('ticket-weekly filter()', () => {
const authUserId = 9; const authUserId = 9;
it('should all return the tickets matching the filter', async() => { it('should all return the tickets matching the filter', async() => {
const filter = {order: 't.id ASC'}; const tx = await models.TicketWeekly.beginTransaction({});
const ctx = {req: {accessToken: {userId: authUserId}}, args: {filter: filter}};
const result = await app.models.TicketWeekly.filter(ctx);
const firstRow = result[0]; try {
const options = {transaction: tx};
expect(firstRow.ticketFk).toEqual(1); const filter = {order: 't.id ASC'};
expect(result.length).toEqual(5);
const ctx = {req: {accessToken: {userId: authUserId}}, args: {filter: filter}};
const result = await models.TicketWeekly.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.ticketFk).toEqual(1);
expect(result.length).toEqual(5);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket with id one', async() => { it('should return the ticket with id one', async() => {
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 2}}; const tx = await models.TicketWeekly.beginTransaction({});
const filter = {};
const result = await app.models.TicketWeekly.filter(ctx, filter);
const firstRow = result[0];
expect(firstRow.ticketFk).toEqual(2); try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 2}};
const result = await models.TicketWeekly.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.ticketFk).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket matching the client name', async() => { it('should return the ticket matching the client name', async() => {
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'bruce'}}; const tx = await models.TicketWeekly.beginTransaction({});
const filter = {};
const result = await app.models.TicketWeekly.filter(ctx, filter);
const firstRow = result[0];
expect(firstRow.clientName).toEqual('Bruce Wayne'); try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 'bruce'}};
const result = await models.TicketWeekly.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.clientName).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
it('should return the ticket matching the client id', async() => { it('should return the ticket matching the client id', async() => {
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 1101}}; const tx = await models.TicketWeekly.beginTransaction({});
const filter = {};
const result = await app.models.TicketWeekly.filter(ctx, filter);
const firstRow = result[0];
expect(firstRow.clientFk).toEqual(1101); try {
expect(firstRow.clientName).toEqual('Bruce Wayne'); const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: authUserId}}, args: {search: 1101}};
const result = await models.TicketWeekly.filter(ctx, null, options);
const firstRow = result[0];
expect(firstRow.clientFk).toEqual(1101);
expect(firstRow.clientName).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -117,9 +117,6 @@ module.exports = Self => {
if (!zoneShipped || zoneShipped.zoneFk != args.zoneFk) if (!zoneShipped || zoneShipped.zoneFk != args.zoneFk)
throw new UserError(`You don't have privileges to change the zone`); throw new UserError(`You don't have privileges to change the zone`);
} }
const observationTypeDelivery = await models.ObservationType.findOne({
where: {code: 'delivery'}
}, myOptions);
const originalTicket = await models.Ticket.findOne({ const originalTicket = await models.Ticket.findOne({
where: {id: args.id}, where: {id: args.id},

View File

@ -12,12 +12,12 @@ module.exports = Self => {
}, },
{ {
arg: 'shipped', arg: 'shipped',
type: 'Date', type: 'date',
description: `The shipment date filter` description: `The shipment date filter`
}, },
{ {
arg: 'landed', arg: 'landed',
type: 'Date', type: 'date',
description: `The landing date filter` description: `The landing date filter`
}, },
{ {
@ -142,7 +142,7 @@ module.exports = Self => {
if (tx) await tx.commit(); if (tx) await tx.commit();
return await ticket; return ticket;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;

View File

@ -66,70 +66,77 @@ module.exports = Self => {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
const isEditable = await Self.isEditable(ctx, args.id, myOptions); try {
const isEditable = await Self.isEditable(ctx, args.id, myOptions);
if (!isEditable) if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
const isProductionBoss = await models.Account.hasRole(userId, 'productionBoss', myOptions); const isProductionBoss = await models.Account.hasRole(userId, 'productionBoss', myOptions);
if (!isProductionBoss) { if (!isProductionBoss) {
const zoneShipped = await models.Agency.getShipped( const zoneShipped = await models.Agency.getShipped(
args.landed, args.landed,
args.addressId, args.addressId,
args.agencyModeId, args.agencyModeId,
args.warehouseId, args.warehouseId,
myOptions); myOptions);
if (!zoneShipped || zoneShipped.zoneFk != args.zoneId) if (!zoneShipped || zoneShipped.zoneFk != args.zoneId)
throw new UserError(`You don't have privileges to change the zone`); throw new UserError(`You don't have privileges to change the zone`);
}
const items = await models.Sale.find({
where: {
ticketFk: args.id
},
order: 'concept ASC',
include: 'item'
}, myOptions);
const salesObj = {
items: items,
totalUnitPrice: 0.00,
totalNewPrice: 0.00,
totalDifference: 0.00,
};
const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`;
const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId];
const [difComponents] = await Self.rawSql(query, params, myOptions);
const map = new Map();
// Sale price component, one per sale
for (difComponent of difComponents)
map.set(difComponent.saleFk, difComponent);
function round(value) {
return Math.round(value * 100) / 100;
}
for (sale of salesObj.items) {
const difComponent = map.get(sale.id);
if (difComponent) {
sale.component = difComponent;
salesObj.totalDifference += difComponent.difference;
salesObj.totalDifference = round(salesObj.totalDifference);
salesObj.totalNewPrice += difComponent.newPrice;
salesObj.totalNewPrice = round(salesObj.totalNewPrice);
} }
salesObj.totalUnitPrice += sale.price; const items = await models.Sale.find({
salesObj.totalUnitPrice = round(salesObj.totalUnitPrice); where: {
} ticketFk: args.id
},
order: 'concept ASC',
include: 'item'
}, myOptions);
return salesObj; const salesObj = {
items: items,
totalUnitPrice: 0.00,
totalNewPrice: 0.00,
totalDifference: 0.00,
};
const query = `CALL vn.ticket_priceDifference(?, ?, ?, ?, ?)`;
const params = [args.id, args.landed, args.addressId, args.zoneId, args.warehouseId];
const [difComponents] = await Self.rawSql(query, params, myOptions);
const map = new Map();
// Sale price component, one per sale
for (difComponent of difComponents)
map.set(difComponent.saleFk, difComponent);
for (sale of salesObj.items) {
const difComponent = map.get(sale.id);
if (difComponent) {
sale.component = difComponent;
salesObj.totalDifference += difComponent.difference;
salesObj.totalDifference = round(salesObj.totalDifference);
salesObj.totalNewPrice += difComponent.newPrice;
salesObj.totalNewPrice = round(salesObj.totalNewPrice);
}
salesObj.totalUnitPrice += sale.price;
salesObj.totalUnitPrice = round(salesObj.totalUnitPrice);
}
if (tx) await tx.commit();
return salesObj;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
}; };
function round(value) {
return Math.round(value * 100) / 100;
}
}; };

Some files were not shown because too many files have changed in this diff Show More