diff --git a/.vscode/settings.json b/.vscode/settings.json index c27d01a76..b5da1e8e6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,8 @@ -// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario. -{ - // Carácter predeterminado de final de línea. - "files.eol": "\n", - "editor.bracketPairColorization.enabled": true, - "editor.guides.bracketPairs": true, - "editor.formatOnSave": true, - "editor.defaultFormatter": "dbaeumer.vscode-eslint", - "editor.codeActionsOnSave": ["source.fixAll.eslint"], - "eslint.validate": [ - "javascript", - "json" - ] -} \ No newline at end of file +// Coloque su configuración en este archivo para sobrescribir la configuración predeterminada y de usuario. +{ + // Carácter predeterminado de final de línea. + "files.eol": "\n", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} diff --git a/db/changes/10503-november/00-aclInvoiceOut.sql b/db/changes/10503-november/00-aclInvoiceOut.sql new file mode 100644 index 000000000..7715045b5 --- /dev/null +++ b/db/changes/10503-november/00-aclInvoiceOut.sql @@ -0,0 +1,7 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('InvoiceOut', 'clientsToInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'); + +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('InvoiceOut', 'invoiceClient', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'); \ No newline at end of file diff --git a/db/changes/10503-november/00-deleteInvoiceOutQueue.sql b/db/changes/10503-november/00-deleteInvoiceOutQueue.sql new file mode 100644 index 000000000..87a3059a0 --- /dev/null +++ b/db/changes/10503-november/00-deleteInvoiceOutQueue.sql @@ -0,0 +1 @@ +DROP TABLE `vn`.`invoiceOutQueue`; \ No newline at end of file diff --git a/db/changes/10503-november/00-editTrackedACL.sql b/db/changes/10503-november/00-editTrackedACL.sql new file mode 100644 index 000000000..e768fb7c7 --- /dev/null +++ b/db/changes/10503-november/00-editTrackedACL.sql @@ -0,0 +1,4 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES + ('Sale', 'editTracked', 'WRITE', 'ALLOW', 'ROLE', 'production'), + ('Sale', 'editFloramondo', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'); diff --git a/db/changes/10503-november/delete.keep b/db/changes/10503-november/delete.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 6624e99f4..49cf639ef 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -984,7 +984,7 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric (30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()), (31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, util.VN_CURDATE()), (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, util.VN_CURDATE()), - (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE()); + (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE()); INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`) VALUES @@ -1146,10 +1146,11 @@ INSERT INTO `vncontrol`.`accion`(`accion_id`, `accion`) INSERT INTO `vn`.`saleTracking`(`saleFk`, `isChecked`, `created`, `originalQuantity`, `workerFk`, `actionFk`, `id`, `stateFk`) VALUES - (1, 0, util.VN_CURDATE(), 5, 55, 3, 1, 14), - (1, 1, util.VN_CURDATE(), 5, 54, 3, 2, 8), - (2, 1, util.VN_CURDATE(), 10, 40, 4, 3, 8), - (3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8); + (1, 0, util.VN_CURDATE(), 5, 55, 3, 1, 14), + (1, 1, util.VN_CURDATE(), 5, 54, 3, 2, 8), + (2, 1, util.VN_CURDATE(), 10, 40, 4, 3, 8), + (3, 1, util.VN_CURDATE(), 2, 40, 4, 4, 8), + (31, 1, util.VN_CURDATE(), -5, 40, 4, 5, 8); INSERT INTO `vn`.`itemBarcode`(`id`, `itemFk`, `code`) VALUES @@ -1356,7 +1357,8 @@ INSERT INTO `vn`.`ticketWeekly`(`ticketFk`, `weekDay`) (2, 1), (3, 2), (4, 4), - (5, 6); + (5, 6), + (15, 6); INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseOutFk`, `agencyModeFk`, `m3`, `kg`,`ref`, `totalEntries`, `cargoSupplierFk`) VALUES @@ -2699,7 +2701,7 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`) INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`) VALUES (1, 9); - + INSERT INTO `vn`.`productionConfig` (`isPreviousPreparationRequired`, `ticketPrintedMax`, `ticketTrolleyMax`, `rookieDays`, `notBuyingMonths`, `id`, `isZoneClosedByExpeditionActivated`, `maxNotReadyCollections`, `minTicketsToCloseZone`, `movingTicketDelRoute`, `defaultZone`, `defautlAgencyMode`, `hasUniqueCollectionTime`, `maxCollectionWithoutUser`, `pendingCollectionsOrder`, `pendingCollectionsAge`) VALUES (0, 8, 80, 0, 0, 1, 0, 15, 25, -1, 697, 1328, 0, 1, 8, 6); @@ -2712,6 +2714,10 @@ INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `lev VALUES (9, 3, util.VN_NOW(), NULL, 0, NULL, NULL, NULL, NULL); +INSERT INTO `vn`.`saleCloned` (`saleClonedFk`, `saleOriginalFk`) + VALUES + (29, 25); + UPDATE `account`.`user` SET `hasGrant` = 1 WHERE `id` = 66; diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index f0d726ed6..1391731de 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -969,6 +969,7 @@ export default { manualInvoiceTaxArea: 'vn-autocomplete[ng-model="$ctrl.invoice.taxArea"]', saveInvoice: 'button[response="accept"]', globalInvoiceForm: '.vn-invoice-out-global-invoicing', + globalInvoiceClientsRange: 'vn-radio[val="clientsRange"]', globalInvoiceDate: '[ng-model="$ctrl.invoice.invoiceDate"]', globalInvoiceFromClient: '[ng-model="$ctrl.invoice.fromClientId"]', globalInvoiceToClient: '[ng-model="$ctrl.invoice.toClientId"]', diff --git a/e2e/paths/05-ticket/09_weekly.spec.js b/e2e/paths/05-ticket/09_weekly.spec.js index d04138ee5..0ba57aa9d 100644 --- a/e2e/paths/05-ticket/09_weekly.spec.js +++ b/e2e/paths/05-ticket/09_weekly.spec.js @@ -19,7 +19,7 @@ describe('Ticket descriptor path', () => { it('should count the amount of tickets in the turns section', async() => { const result = await page.countElement(selectors.ticketsIndex.weeklyTicket); - expect(result).toEqual(5); + expect(result).toEqual(6); }); it('should go back to the ticket index then search and access a ticket summary', async() => { @@ -104,7 +104,7 @@ describe('Ticket descriptor path', () => { await page.doSearch(); const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult); - expect(nResults).toEqual(5); + expect(nResults).toEqual(6); }); it('should update the agency then remove it afterwards', async() => { diff --git a/e2e/paths/09-invoice-out/04_globalInvoice.spec.js b/e2e/paths/09-invoice-out/04_globalInvoice.spec.js index b62c889db..74efafd2d 100644 --- a/e2e/paths/09-invoice-out/04_globalInvoice.spec.js +++ b/e2e/paths/09-invoice-out/04_globalInvoice.spec.js @@ -33,6 +33,7 @@ describe('InvoiceOut global invoice path', () => { it('should create a global invoice for charles xavier today', async() => { await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate); + await page.waitToClick(selectors.invoiceOutIndex.globalInvoiceClientsRange); await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceFromClient, 'Petter Parker'); await page.autocompleteSearch(selectors.invoiceOutIndex.globalInvoiceToClient, 'Petter Parker'); await page.waitToClick(selectors.invoiceOutIndex.saveInvoice); @@ -48,4 +49,15 @@ describe('InvoiceOut global invoice path', () => { expect(currentInvoices).toBeGreaterThan(invoicesBefore); }); + + it('should create a global invoice for all clients today', async() => { + await page.waitToClick(selectors.invoiceOutIndex.createInvoice); + await page.waitToClick(selectors.invoiceOutIndex.createGlobalInvoice); + await page.waitForSelector(selectors.invoiceOutIndex.globalInvoiceForm); + await page.pickDate(selectors.invoiceOutIndex.globalInvoiceDate); + await page.waitToClick(selectors.invoiceOutIndex.saveInvoice); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); }); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index f03441c0f..887a657a3 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -136,5 +136,6 @@ "Not enough privileges to edit a client": "Not enough privileges to edit a client", "Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*", "You don't have grant privilege": "You don't have grant privilege", - "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user" + "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user", + "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production" } diff --git a/loopback/server/boot/acl.js b/loopback/server/boot/acl.js new file mode 100644 index 000000000..f9375e0c1 --- /dev/null +++ b/loopback/server/boot/acl.js @@ -0,0 +1,22 @@ + +module.exports = function(app) { + app.models.ACL.checkAccessAcl = async(ctx, modelId, property, accessType = '*') => { + const models = app.models; + const context = { + accessToken: ctx.req.accessToken, + model: models[modelId], + property: property, + modelId: modelId, + accessType: accessType, + sharedMethod: { + name: property, + aliases: [], + sharedClass: true + } + }; + + const acl = await models.ACL.checkAccessForContext(context); + + return acl.permission == 'ALLOW'; + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js b/modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js similarity index 50% rename from modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js rename to modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js index 2aa277b6f..d42184ae5 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js +++ b/modules/invoiceOut/back/methods/invoiceOut/clientsToInvoice.js @@ -1,6 +1,6 @@ module.exports = Self => { - Self.remoteMethodCtx('globalInvoicing', { - description: 'Make a global invoice', + Self.remoteMethodCtx('clientsToInvoice', { + description: 'Get the clients to make global invoicing', accessType: 'WRITE', accepts: [ { @@ -29,19 +29,22 @@ module.exports = Self => { description: 'The company id to invoice' } ], - returns: { - type: 'object', - root: true + returns: [{ + arg: 'clientsAndAddresses', + type: ['object'] }, + { + arg: 'invoice', + type: 'object' + }], http: { - path: '/globalInvoicing', + path: '/clientsToInvoice', verb: 'POST' } }); - Self.globalInvoicing = async(ctx, options) => { + Self.clientsToInvoice = async(ctx, options) => { const args = ctx.args; - let tx; const myOptions = {}; @@ -53,8 +56,6 @@ module.exports = Self => { myOptions.transaction = tx; } - const invoicesIds = []; - const failedClients = []; let query; try { query = ` @@ -78,6 +79,9 @@ module.exports = Self => { const minShipped = new Date(); minShipped.setFullYear(minShipped.getFullYear() - 1); + minShipped.setMonth(1); + minShipped.setDate(1); + minShipped.setHours(0, 0, 0, 0); // Packaging liquidation const vIsAllInvoiceable = false; @@ -93,110 +97,51 @@ module.exports = Self => { const invoiceableClients = await getInvoiceableClients(ctx, myOptions); - if (!invoiceableClients.length) return; + if (!invoiceableClients) return; - for (let client of invoiceableClients) { - try { - if (client.hasToInvoiceByAddress) { - await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [ - minShipped, - args.maxShipped, - client.addressFk, - args.companyFk - ], myOptions); - } else { - await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [ - args.maxShipped, - client.id, - args.companyFk - ], myOptions); - } + const clientsAndAddresses = invoiceableClients.map(invoiceableClient => { + return { + clientId: invoiceableClient.id, + addressId: invoiceableClient.addressFk - // Make invoice - const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions); - - // Validates ticket nagative base - const hasAnyNegativeBase = await getNegativeBase(myOptions); - if (hasAnyNegativeBase && isSpanishCompany) - continue; - - query = `SELECT invoiceSerial(?, ?, ?) AS serial`; - const [invoiceSerial] = await Self.rawSql(query, [ - client.id, - args.companyFk, - 'G' - ], myOptions); - const serialLetter = invoiceSerial.serial; - - query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`; - await Self.rawSql(query, [ - serialLetter, - args.invoiceDate - ], myOptions); - - const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); - if (newInvoice.id) { - await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); - - query = `INSERT IGNORE INTO invoiceOutQueue(invoiceFk) VALUES(?)`; - await Self.rawSql(query, [newInvoice.id], myOptions); - - invoicesIds.push(newInvoice.id); - } - } catch (e) { - failedClients.push({ - id: client.id, - stacktrace: e - }); - continue; - } + }; } - - if (failedClients.length > 0) - await notifyFailures(ctx, failedClients, myOptions); + ); if (tx) await tx.commit(); + + return [ + clientsAndAddresses, + { + invoiceDate: args.invoiceDate, + maxShipped: args.maxShipped, + fromClientId: args.fromClientId, + toClientId: args.toClientId, + companyFk: args.companyFk, + minShipped: minShipped + } + ]; } catch (e) { if (tx) await tx.rollback(); throw e; } - - return invoicesIds; }; - async function getNegativeBase(options) { - const models = Self.app.models; - const query = 'SELECT hasAnyNegativeBase() AS base'; - const [result] = await models.InvoiceOut.rawSql(query, null, options); - - return result && result.base; - } - - async function getIsSpanishCompany(companyId, options) { - const models = Self.app.models; - const query = `SELECT COUNT(*) AS total - FROM supplier s - JOIN country c ON c.id = s.countryFk - AND c.code = 'ES' - WHERE s.id = ?`; - const [supplierCompany] = await models.InvoiceOut.rawSql(query, [ - companyId - ], options); - - return supplierCompany && supplierCompany.total; - } - async function getClientsWithPackaging(ctx, options) { const models = Self.app.models; const args = ctx.args; const query = `SELECT DISTINCT clientFk AS id FROM ticket t JOIN ticketPackaging tp ON t.id = tp.ticketFk + JOIN client c ON c.id = t.clientFk WHERE t.shipped BETWEEN '2017-11-21' AND ? - AND t.clientFk BETWEEN ? AND ?`; + AND t.clientFk >= ? + AND (t.clientFk <= ? OR ? IS NULL) + AND c.isActive`; return models.InvoiceOut.rawSql(query, [ args.maxShipped, args.fromClientId, + args.toClientId, args.toClientId ], options); } @@ -225,42 +170,20 @@ module.exports = Self => { LEFT JOIN ticketService ts ON ts.ticketFk = t.id JOIN address a ON a.id = t.addressFk JOIN client c ON c.id = t.clientFk - WHERE ISNULL(t.refFk) AND c.id BETWEEN ? AND ? + WHERE ISNULL(t.refFk) AND c.id >= ? + AND (t.clientFk <= ? OR ? IS NULL) AND t.shipped BETWEEN ? AND util.dayEnd(?) AND t.companyFk = ? AND c.hasToInvoice - AND c.isTaxDataChecked + AND c.isTaxDataChecked AND c.isActive GROUP BY c.id, IF(c.hasToInvoiceByAddress,a.id,TRUE) HAVING sumAmount > 0`; return models.InvoiceOut.rawSql(query, [ args.fromClientId, args.toClientId, + args.toClientId, minShipped, args.maxShipped, args.companyFk ], options); } - - async function notifyFailures(ctx, failedClients, options) { - const models = Self.app.models; - const userId = ctx.req.accessToken.userId; - const $t = ctx.req.__; // $translate - - const worker = await models.EmailUser.findById(userId, null, options); - const subject = $t('Global invoicing failed'); - let body = $t(`Wasn't able to invoice the following clients`) + ':

'; - - for (client of failedClients) { - body += `ID: ${client.id} -
${client.stacktrace}

`; - } - - await Self.rawSql(` - INSERT INTO vn.mail (sender, replyTo, sent, subject, body) - VALUES (?, ?, FALSE, ?, ?)`, [ - worker.email, - worker.email, - subject, - body - ], options); - } }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js new file mode 100644 index 000000000..b04747d16 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -0,0 +1,190 @@ +module.exports = Self => { + Self.remoteMethodCtx('invoiceClient', { + description: 'Make a invoice of a client', + accessType: 'WRITE', + accepts: [{ + arg: 'clientId', + type: 'number', + description: 'The client id to invoice', + required: true + }, + { + arg: 'addressId', + type: 'number', + description: 'The address id to invoice', + required: true + }, + { + arg: 'invoiceDate', + type: 'date', + description: 'The invoice date', + required: true + }, + { + arg: 'maxShipped', + type: 'date', + description: 'The maximum shipped date', + required: true + }, + { + arg: 'companyFk', + type: 'number', + description: 'The company id to invoice', + required: true + }, + { + arg: 'minShipped', + type: 'date', + description: 'The minium shupped date', + required: true + }], + returns: { + type: 'object', + root: true + }, + http: { + path: '/invoiceClient', + verb: 'POST' + } + }); + + Self.invoiceClient = async(ctx, options) => { + const args = ctx.args; + const models = Self.app.models; + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + let invoiceId; + let invoiceOut; + try { + const client = await models.Client.findById(args.clientId, { + fields: ['id', 'hasToInvoiceByAddress'] + }, myOptions); + try { + if (client.hasToInvoiceByAddress) { + await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [ + args.minShipped, + args.maxShipped, + args.addressId, + args.companyFk + ], myOptions); + } else { + await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [ + args.maxShipped, + client.id, + args.companyFk + ], myOptions); + } + + // Make invoice + const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions); + + // Validates ticket nagative base + const hasAnyNegativeBase = await getNegativeBase(myOptions); + if (hasAnyNegativeBase && isSpanishCompany) + return tx.rollback(); + + query = `SELECT invoiceSerial(?, ?, ?) AS serial`; + const [invoiceSerial] = await Self.rawSql(query, [ + client.id, + args.companyFk, + 'G' + ], myOptions); + const serialLetter = invoiceSerial.serial; + + query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`; + await Self.rawSql(query, [ + serialLetter, + args.invoiceDate + ], myOptions); + + const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); + if (newInvoice.id) { + await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); + + invoiceId = newInvoice.id; + } + } catch (e) { + const failedClient = { + id: client.id, + stacktrace: e + }; + await notifyFailures(ctx, failedClient, myOptions); + } + + invoiceOut = await models.InvoiceOut.findById(invoiceId, { + include: { + relation: 'client' + } + }, myOptions); + + if (tx) await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + + ctx.args = { + reference: invoiceOut.ref, + recipientId: invoiceOut.clientFk, + recipient: invoiceOut.client().email + }; + try { + await models.InvoiceOut.invoiceEmail(ctx); + } catch (err) {} + + return invoiceId; + }; + + async function getNegativeBase(options) { + const models = Self.app.models; + const query = 'SELECT hasAnyNegativeBase() AS base'; + const [result] = await models.InvoiceOut.rawSql(query, null, options); + + return result && result.base; + } + + async function getIsSpanishCompany(companyId, options) { + const models = Self.app.models; + const query = `SELECT COUNT(*) AS total + FROM supplier s + JOIN country c ON c.id = s.countryFk + AND c.code = 'ES' + WHERE s.id = ?`; + const [supplierCompany] = await models.InvoiceOut.rawSql(query, [ + companyId + ], options); + + return supplierCompany && supplierCompany.total; + } + + async function notifyFailures(ctx, failedClient, options) { + const models = Self.app.models; + const userId = ctx.req.accessToken.userId; + const $t = ctx.req.__; // $translate + + const worker = await models.EmailUser.findById(userId, null, options); + const subject = $t('Global invoicing failed'); + let body = $t(`Wasn't able to invoice the following clients`) + ':

'; + + body += `ID: ${failedClient.id} +
${failedClient.stacktrace}

`; + + await Self.rawSql(` + INSERT INTO vn.mail (sender, replyTo, sent, subject, body) + VALUES (?, ?, FALSE, ?, ?)`, [ + worker.email, + worker.email, + subject, + body + ], options); + } +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js index 83564e3ab..83cb84881 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js @@ -40,19 +40,40 @@ module.exports = Self => { } }); - Self.invoiceEmail = async ctx => { + Self.invoiceEmail = async(ctx, reference) => { const args = Object.assign({}, ctx.args); + const {InvoiceOut} = Self.app.models; const params = { recipient: args.recipient, lang: ctx.req.getLocale() }; + const invoiceOut = await InvoiceOut.findOne({ + where: { + ref: reference + } + }); + delete args.ctx; for (const param in args) params[param] = args[param]; const email = new Email('invoice', params); - return email.send(); + const [stream, ...headers] = await Self.download(ctx, invoiceOut.id); + const name = headers[1]; + const fileName = name.replace(/filename="(.*)"/gm, '$1'); + + const mailOptions = { + overrideAttachments: true, + attachments: [ + { + filename: fileName, + content: stream + } + ] + }; + + return email.send(mailOptions); }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js b/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js deleted file mode 100644 index a1730ac81..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/sendQueued.js +++ /dev/null @@ -1,133 +0,0 @@ -const {Email, Report, storage} = require('vn-print'); - -module.exports = Self => { - Self.remoteMethod('sendQueued', { - description: 'Send all queued invoices', - accessType: 'WRITE', - accepts: [], - returns: { - type: 'object', - root: true - }, - http: { - path: '/send-queued', - verb: 'POST' - } - }); - - Self.sendQueued = async() => { - const invoices = await Self.rawSql(` - SELECT - io.id, - io.clientFk, - io.issued, - io.ref, - c.email recipient, - c.salesPersonFk, - c.isToBeMailed, - c.hasToInvoice, - co.hasDailyInvoice, - eu.email salesPersonEmail - FROM invoiceOutQueue ioq - JOIN invoiceOut io ON io.id = ioq.invoiceFk - JOIN client c ON c.id = io.clientFk - JOIN province p ON p.id = c.provinceFk - JOIN country co ON co.id = p.countryFk - LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - WHERE status = ''`); - - let invoiceId; - for (const invoiceOut of invoices) { - try { - const tx = await Self.beginTransaction({}); - const myOptions = {transaction: tx}; - - invoiceId = invoiceOut.id; - - const args = { - reference: invoiceOut.ref, - recipientId: invoiceOut.clientFk, - recipient: invoiceOut.recipient, - replyTo: invoiceOut.salesPersonEmail - }; - - const invoiceReport = new Report('invoice', args); - const stream = await invoiceReport.toPdfStream(); - - const issued = invoiceOut.issued; - const year = issued.getFullYear().toString(); - const month = (issued.getMonth() + 1).toString(); - const day = issued.getDate().toString(); - - const fileName = `${year}${invoiceOut.ref}.pdf`; - - // Store invoice - storage.write(stream, { - type: 'invoice', - path: `${year}/${month}/${day}`, - fileName: fileName - }); - - await Self.rawSql(` - UPDATE invoiceOut - SET hasPdf = true - WHERE id = ?`, - [invoiceOut.id], myOptions); - - const isToBeMailed = invoiceOut.recipient && invoiceOut.salesPersonFk && invoiceOut.isToBeMailed; - - if (isToBeMailed) { - const mailOptions = { - overrideAttachments: true, - attachments: [] - }; - - const invoiceAttachment = { - filename: fileName, - content: stream - }; - - if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') { - const exportation = new Report('exportation', args); - const stream = await exportation.toPdfStream(); - const fileName = `CITES-${invoiceOut.ref}.pdf`; - - mailOptions.attachments.push({ - filename: fileName, - content: stream - }); - } - - mailOptions.attachments.push(invoiceAttachment); - - const email = new Email('invoice', args); - await email.send(mailOptions); - } - // Update queue status - const date = new Date(); - await Self.rawSql(` - UPDATE invoiceOutQueue - SET status = "printed", - printed = ? - WHERE invoiceFk = ?`, - [date, invoiceOut.id], myOptions); - - await tx.commit(); - } catch (error) { - await tx.rollback(); - - await Self.rawSql(` - UPDATE invoiceOutQueue - SET status = ? - WHERE invoiceFk = ?`, - [error.message, invoiceId]); - - throw e; - } - } - - return { - message: 'Success' - }; - }; -}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js index 7a9e184ea..08f049783 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/downloadZip.spec.js @@ -13,7 +13,7 @@ describe('InvoiceOut downloadZip()', () => { }; it('should return part of link to dowloand the zip', async() => { - const tx = await models.Order.beginTransaction({}); + const tx = await models.InvoiceOut.beginTransaction({}); try { const options = {transaction: tx}; @@ -30,7 +30,7 @@ describe('InvoiceOut downloadZip()', () => { }); it('should return an error if the size of the files is too large', async() => { - const tx = await models.Order.beginTransaction({}); + const tx = await models.InvoiceOut.beginTransaction({}); let error; try { diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js deleted file mode 100644 index f7546b72e..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/globalInvoicing.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -const models = require('vn-loopback/server/server').models; - -describe('InvoiceOut globalInvoicing()', () => { - const userId = 1; - const companyFk = 442; - const clientId = 1101; - const invoicedTicketId = 8; - const invoiceSerial = 'A'; - const activeCtx = { - accessToken: {userId: userId}, - __: value => { - return value; - } - }; - const ctx = {req: activeCtx}; - - it('should make a global invoicing', async() => { - spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); - - const tx = await models.InvoiceOut.beginTransaction({}); - const options = {transaction: tx}; - - try { - ctx.args = { - invoiceDate: new Date(), - maxShipped: new Date(), - fromClientId: clientId, - toClientId: 1106, - companyFk: companyFk - }; - const result = await models.InvoiceOut.globalInvoicing(ctx, options); - const ticket = await models.Ticket.findById(invoicedTicketId, null, options); - - expect(result.length).toBeGreaterThan(0); - expect(ticket.refFk).toContain(invoiceSerial); - - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } - }); -}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js new file mode 100644 index 000000000..5f890de26 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js @@ -0,0 +1,56 @@ +const models = require('vn-loopback/server/server').models; + +describe('InvoiceOut invoiceClient()', () => { + const userId = 1; + const clientId = 1101; + const addressId = 121; + const companyFk = 442; + const minShipped = new Date(); + minShipped.setFullYear(minShipped.getFullYear() - 1); + minShipped.setMonth(1); + minShipped.setDate(1); + minShipped.setHours(0, 0, 0, 0); + const invoiceSerial = 'A'; + const activeCtx = { + getLocale: () => { + return 'en'; + }, + accessToken: {userId: userId}, + __: value => { + return value; + } + }; + const ctx = {req: activeCtx}; + + it('should make a global invoicing', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); + spyOn(models.InvoiceOut, 'invoiceEmail'); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + ctx.args = { + clientId: clientId, + addressId: addressId, + invoiceDate: new Date(), + maxShipped: new Date(), + companyFk: companyFk, + minShipped: minShipped + }; + const invoiceOutId = await models.InvoiceOut.invoiceClient(ctx, options); + const invoiceOut = await models.InvoiceOut.findById(invoiceOutId, null, options); + const [firstTicket] = await models.Ticket.find({ + where: {refFk: invoiceOut.ref} + }, options); + + expect(invoiceOutId).toBeGreaterThan(0); + expect(firstTicket.refFk).toContain(invoiceSerial); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 9533ad484..88adae2ef 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -8,11 +8,11 @@ module.exports = Self => { require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/createPdf')(Self); require('../methods/invoiceOut/createManualInvoice')(Self); - require('../methods/invoiceOut/globalInvoicing')(Self); + require('../methods/invoiceOut/clientsToInvoice')(Self); + require('../methods/invoiceOut/invoiceClient')(Self); require('../methods/invoiceOut/refund')(Self); require('../methods/invoiceOut/invoiceEmail')(Self); require('../methods/invoiceOut/exportationPdf')(Self); - require('../methods/invoiceOut/sendQueued')(Self); require('../methods/invoiceOut/invoiceCsv')(Self); require('../methods/invoiceOut/invoiceCsvEmail')(Self); }; diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html index 3d245b8d8..c2d1c4304 100644 --- a/modules/invoiceOut/front/index/global-invoicing/index.html +++ b/modules/invoiceOut/front/index/global-invoicing/index.html @@ -13,13 +13,24 @@ url="Companies" data="companies" order="code"> - +
+ ng-if="$ctrl.packageInvoicing"> - - Adding invoices to queue... +
+ {{'Calculating packages to invoice...' | translate}} +
+
+
+
+ +
+ {{'Id Client' | translate}}: {{$ctrl.currentClientId}} + {{'of' | translate}} {{::$ctrl.lastClientId}} +
@@ -35,10 +46,24 @@ + + + + + + @@ -48,6 +73,7 @@ url="Clients" label="To client" search-function="{or: [{id: $search}, {name: {like: '%'+$search+'%'}}]}" + order="id" show-field="name" value-field="id" ng-model="$ctrl.invoice.toClientId"> @@ -66,5 +92,5 @@ - + {{$ctrl.isInvoicing}} \ No newline at end of file diff --git a/modules/invoiceOut/front/index/global-invoicing/index.js b/modules/invoiceOut/front/index/global-invoicing/index.js index 5e522f23d..f772a4936 100644 --- a/modules/invoiceOut/front/index/global-invoicing/index.js +++ b/modules/invoiceOut/front/index/global-invoicing/index.js @@ -5,11 +5,10 @@ import './style.scss'; class Controller extends Dialog { constructor($element, $, $transclude) { super($element, $, $transclude); - - this.isInvoicing = false; this.invoice = { maxShipped: new Date() }; + this.clientsNumber = 'allClients'; } $onInit() { @@ -46,6 +45,39 @@ class Controller extends Dialog { this.invoice.companyFk = value; } + restartValues() { + this.lastClientId = null; + this.$.invoiceButton.disabled = false; + } + + cancelRequest() { + this.canceler = this.$q.defer(); + return {timeout: this.canceler.promise}; + } + + invoiceOut(invoice, clientsAndAddresses) { + const [clientAndAddress] = clientsAndAddresses; + if (!clientAndAddress) return; + this.currentClientId = clientAndAddress.clientId; + const params = { + clientId: clientAndAddress.clientId, + addressId: clientAndAddress.addressId, + invoiceDate: invoice.invoiceDate, + maxShipped: invoice.maxShipped, + companyFk: invoice.companyFk, + minShipped: invoice.minShipped, + + }; + + const options = this.cancelRequest(); + + return this.$http.post(`InvoiceOuts/invoiceClient`, params, options) + .then(() => { + clientsAndAddresses.shift(); + return this.invoiceOut(invoice, clientsAndAddresses); + }); + } + responseHandler(response) { try { if (response !== 'accept') @@ -57,14 +89,30 @@ class Controller extends Dialog { if (!this.invoice.fromClientId || !this.invoice.toClientId) throw new Error('Choose a valid clients range'); - this.isInvoicing = true; - return this.$http.post(`InvoiceOuts/globalInvoicing`, this.invoice) + this.on('close', () => { + if (this.canceler) this.canceler.resolve(); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + + this.$.invoiceButton.disabled = true; + this.packageInvoicing = true; + const options = this.cancelRequest(); + + this.$http.post(`InvoiceOuts/clientsToInvoice`, this.invoice, options) + .then(res => { + this.packageInvoicing = false; + const invoice = res.data.invoice; + const clientsAndAddresses = res.data.clientsAndAddresses; + if (!clientsAndAddresses.length) return super.responseHandler(response); + this.lastClientId = clientsAndAddresses[clientsAndAddresses.length - 1].clientId; + return this.invoiceOut(invoice, clientsAndAddresses); + }) .then(() => super.responseHandler(response)) .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) - .finally(() => this.isInvoicing = false); + .finally(() => this.restartValues()); } catch (e) { this.vnApp.showError(this.$t(e.message)); - this.isInvoicing = false; + this.restartValues(); return false; } } diff --git a/modules/invoiceOut/front/index/global-invoicing/index.spec.js b/modules/invoiceOut/front/index/global-invoicing/index.spec.js index 916364007..e88b0b1d4 100644 --- a/modules/invoiceOut/front/index/global-invoicing/index.spec.js +++ b/modules/invoiceOut/front/index/global-invoicing/index.spec.js @@ -19,6 +19,7 @@ describe('InvoiceOut', () => { } }; controller = $componentController('vnInvoiceOutGlobalInvoicing', {$element, $scope, $transclude}); + controller.$.invoiceButton = {disabled: false}; })); describe('getMinClientId()', () => { @@ -87,14 +88,26 @@ describe('InvoiceOut', () => { it('should make an http POST query and then call to the showSuccess() method', () => { jest.spyOn(controller.vnApp, 'showSuccess'); + const minShipped = new Date(); + minShipped.setFullYear(minShipped.getFullYear() - 1); + minShipped.setMonth(1); + minShipped.setDate(1); + minShipped.setHours(0, 0, 0, 0); controller.invoice = { invoiceDate: new Date(), maxShipped: new Date(), fromClientId: 1101, - toClientId: 1101 + toClientId: 1101, + companyFk: 442, + minShipped: minShipped + }; + const response = { + clientsAndAddresses: [{clientId: 1101, addressId: 121}], + invoice: controller.invoice }; - $httpBackend.expect('POST', `InvoiceOuts/globalInvoicing`).respond({id: 1}); + $httpBackend.expect('POST', `InvoiceOuts/clientsToInvoice`).respond(response); + $httpBackend.expect('POST', `InvoiceOuts/invoiceClient`).respond({id: 1}); controller.responseHandler('accept'); $httpBackend.flush(); diff --git a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml index 1a6e15656..af151684f 100644 --- a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml +++ b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml @@ -6,4 +6,9 @@ Invoice date: Fecha de factura From client: Desde el cliente To client: Hasta el cliente Invoice date and the max date should be filled: La fecha de factura y la fecha límite deben rellenarse -Choose a valid clients range: Selecciona un rango válido de clientes \ No newline at end of file +Choose a valid clients range: Selecciona un rango válido de clientes +of: de +Id Client: Id Cliente +All clients: Todos los clientes +Clients range: Rango de clientes +Calculating packages to invoice...: Calculando paquetes a factura... \ No newline at end of file diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html index 632dbf90b..e2cf2120a 100644 --- a/modules/invoiceOut/front/index/index.html +++ b/modules/invoiceOut/front/index/index.html @@ -30,7 +30,6 @@ Company Due date - diff --git a/modules/invoiceOut/front/summary/index.html b/modules/invoiceOut/front/summary/index.html index a0d050efd..b83775158 100644 --- a/modules/invoiceOut/front/summary/index.html +++ b/modules/invoiceOut/front/summary/index.html @@ -85,7 +85,7 @@ {{ticket.shipped | date: 'dd/MM/yyyy' | dashIfEmpty}} - {{ticket.totalWithVat | currency: 'EUR': 2}} + {{ticket.totalWithVat | currency: 'EUR': 2}} diff --git a/modules/ticket/back/methods/sale/canEdit.js b/modules/ticket/back/methods/sale/canEdit.js index 4e0fc5f8b..f44bd6743 100644 --- a/modules/ticket/back/methods/sale/canEdit.js +++ b/modules/ticket/back/methods/sale/canEdit.js @@ -1,10 +1,12 @@ +const UserError = require('vn-loopback/util/user-error'); + module.exports = Self => { Self.remoteMethodCtx('canEdit', { description: 'Check if all the received sales are aditable', accessType: 'READ', accepts: [{ arg: 'sales', - type: ['object'], + type: ['number'], required: true }], returns: { @@ -12,29 +14,48 @@ module.exports = Self => { root: true }, http: { - path: `/isEditable`, - verb: 'get' + path: `/canEdit`, + verb: 'GET' } }); Self.canEdit = async(ctx, sales, options) => { const models = Self.app.models; - const userId = ctx.req.accessToken.userId; const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - const idsCollection = sales.map(sale => sale.id); + const salesData = await models.Sale.find({ + fields: ['id', 'itemFk', 'ticketFk'], + where: {id: {inq: sales}}, + include: + { + relation: 'item', + scope: { + fields: ['id', 'isFloramondo'], + } + } + }, myOptions); - const saleTracking = await models.SaleTracking.find({where: {saleFk: {inq: idsCollection}}}, myOptions); + const ticketId = salesData[0].ticketFk; - const hasSaleTracking = saleTracking.length; + const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions); + if (!isTicketEditable) + throw new UserError(`The sales of this ticket can't be modified`); - const isProductionRole = await models.Account.hasRole(userId, 'production', myOptions); + const hasSaleTracking = await models.SaleTracking.findOne({where: {saleFk: {inq: sales}}}, myOptions); + const hasSaleCloned = await models.SaleCloned.findOne({where: {saleClonedFk: {inq: sales}}}, myOptions); + const hasSaleFloramondo = salesData.find(sale => sale.item().isFloramondo); - const canEdit = (isProductionRole || !hasSaleTracking); + const canEditTracked = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editTracked'); + const canEditCloned = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editCloned'); + const canEditFloramondo = await models.ACL.checkAccessAcl(ctx, 'Sale', 'editFloramondo'); - return canEdit; + const shouldEditTracked = canEditTracked || !hasSaleTracking; + const shouldEditCloned = canEditCloned || !hasSaleCloned; + const shouldEditFloramondo = canEditFloramondo || !hasSaleFloramondo; + + return shouldEditTracked && shouldEditCloned && shouldEditFloramondo; }; }; diff --git a/modules/ticket/back/methods/sale/deleteSales.js b/modules/ticket/back/methods/sale/deleteSales.js index c1359569d..c045b9197 100644 --- a/modules/ticket/back/methods/sale/deleteSales.js +++ b/modules/ticket/back/methods/sale/deleteSales.js @@ -41,7 +41,11 @@ module.exports = Self => { } try { - const canEditSales = await models.Sale.canEdit(ctx, sales, myOptions); + const saleIds = sales.map(sale => sale.id); + + const canEditSales = await models.Sale.canEdit(ctx, saleIds, myOptions); + if (!canEditSales) + throw new UserError(`Sale(s) blocked, please contact production`); const ticket = await models.Ticket.findById(ticketId, { include: { @@ -57,13 +61,6 @@ module.exports = Self => { } }, 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) { diff --git a/modules/ticket/back/methods/sale/recalculatePrice.js b/modules/ticket/back/methods/sale/recalculatePrice.js index 59c7d3535..38c68d7f6 100644 --- a/modules/ticket/back/methods/sale/recalculatePrice.js +++ b/modules/ticket/back/methods/sale/recalculatePrice.js @@ -1,4 +1,5 @@ const UserError = require('vn-loopback/util/user-error'); + module.exports = Self => { Self.remoteMethodCtx('recalculatePrice', { description: 'Calculates the price of sales and its components', @@ -34,15 +35,9 @@ module.exports = Self => { } try { - const salesIds = []; - for (let sale of sales) - salesIds.push(sale.id); + const salesIds = sales.map(sale => sale.id); - const isEditable = await models.Ticket.isEditable(ctx, sales[0].ticketFk, myOptions); - if (!isEditable) - throw new UserError(`The sales of this ticket can't be modified`); - - const canEditSale = await models.Sale.canEdit(ctx, sales, myOptions); + const canEditSale = await models.Sale.canEdit(ctx, salesIds, myOptions); if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); diff --git a/modules/ticket/back/methods/sale/reserve.js b/modules/ticket/back/methods/sale/reserve.js index b368f6fc3..648e6de23 100644 --- a/modules/ticket/back/methods/sale/reserve.js +++ b/modules/ticket/back/methods/sale/reserve.js @@ -49,12 +49,9 @@ module.exports = Self => { } 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 canEditSale = await models.Sale.canEdit(ctx, sales, myOptions); + const salesIds = sales.map(sale => sale.id); + const canEditSale = await models.Sale.canEdit(ctx, salesIds, myOptions); if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); diff --git a/modules/ticket/back/methods/sale/specs/canEdit.spec.js b/modules/ticket/back/methods/sale/specs/canEdit.spec.js index 4f6747257..2aa873df5 100644 --- a/modules/ticket/back/methods/sale/specs/canEdit.spec.js +++ b/modules/ticket/back/methods/sale/specs/canEdit.spec.js @@ -1,69 +1,193 @@ const models = require('vn-loopback/server/server').models; describe('sale canEdit()', () => { - it('should return true if the role is production regardless of the saleTrackings', async() => { - const tx = await models.Sale.beginTransaction({}); + const employeeId = 1; - try { - const options = {transaction: tx}; + describe('sale editTracked', () => { + it('should return true if the role is production regardless of the saleTrackings', async() => { + const tx = await models.Sale.beginTransaction({}); - const productionUserID = 49; - const ctx = {req: {accessToken: {userId: productionUserID}}}; + try { + const options = {transaction: tx}; - const sales = [{id: 3}]; + const productionUserID = 49; + const ctx = {req: {accessToken: {userId: productionUserID}}}; - const result = await models.Sale.canEdit(ctx, sales, options); + const sales = [25]; - expect(result).toEqual(true); + const result = await models.Sale.canEdit(ctx, sales, options); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + 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() => { + const tx = await models.Sale.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const salesPersonUserID = 18; + const ctx = {req: {accessToken: {userId: salesPersonUserID}}}; + + const sales = [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() => { + const tx = await models.Sale.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const buyerId = 35; + const ctx = {req: {accessToken: {userId: buyerId}}}; + + const sales = [31]; + + const result = await models.Sale.canEdit(ctx, sales, options); + + expect(result).toEqual(false); + + 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() => { - const tx = await models.Sale.beginTransaction({}); + describe('sale editCloned', () => { + const saleCloned = [29]; + it('should return false if any of the sales is cloned', async() => { + const tx = await models.Sale.beginTransaction({}); - try { - const options = {transaction: tx}; + try { + const options = {transaction: tx}; - const salesPersonUserID = 18; - const ctx = {req: {accessToken: {userId: salesPersonUserID}}}; + const buyerId = 35; + const ctx = {req: {accessToken: {userId: buyerId}}}; - const sales = [{id: 10}]; + const result = await models.Sale.canEdit(ctx, saleCloned, options); - const result = await models.Sale.canEdit(ctx, sales, options); + expect(result).toEqual(false); - expect(result).toEqual(true); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + it('should return true if any of the sales is cloned and has the correct role', async() => { + const tx = await models.Sale.beginTransaction({}); + const roleEnabled = await models.ACL.findOne({ + where: { + model: 'Sale', + property: 'editCloned', + permission: 'ALLOW' + } + }); + if (!roleEnabled || !roleEnabled.principalId) return await tx.rollback(); + + try { + const options = {transaction: tx}; + + const role = await models.Role.findOne({ + where: { + name: roleEnabled.principalId + } + }); + const ctx = {req: {accessToken: {userId: role.id}}}; + + const result = await models.Sale.canEdit(ctx, saleCloned, 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() => { - const tx = await models.Sale.beginTransaction({}); + describe('sale editFloramondo', () => { + it('should return false if any of the sales isFloramondo', async() => { + const tx = await models.Sale.beginTransaction({}); + const sales = [26]; - try { - const options = {transaction: tx}; + try { + const options = {transaction: tx}; - const salesPersonUserID = 18; - const ctx = {req: {accessToken: {userId: salesPersonUserID}}}; + const ctx = {req: {accessToken: {userId: employeeId}}}; - const sales = [{id: 3}]; + // For test + const saleToEdit = await models.Sale.findById(sales[0], null, options); + await saleToEdit.updateAttribute('itemFk', 9, options); - const result = await models.Sale.canEdit(ctx, sales, options); + const result = await models.Sale.canEdit(ctx, sales, options); - expect(result).toEqual(false); + expect(result).toEqual(false); - await tx.rollback(); - } catch (e) { - await tx.rollback(); - throw e; - } + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return true if any of the sales is of isFloramondo and has the correct role', async() => { + const tx = await models.Sale.beginTransaction({}); + const sales = [26]; + + const roleEnabled = await models.ACL.findOne({ + where: { + model: 'Sale', + property: 'editFloramondo', + permission: 'ALLOW' + } + }); + + if (!roleEnabled || !roleEnabled.principalId) return await tx.rollback(); + + try { + const options = {transaction: tx}; + + const role = await models.Role.findOne({ + where: { + name: roleEnabled.principalId + } + }); + const ctx = {req: {accessToken: {userId: role.id}}}; + + // For test + const saleToEdit = await models.Sale.findById(sales[0], null, options); + await saleToEdit.updateAttribute('itemFk', 9, options); + + const result = await models.Sale.canEdit(ctx, sales, options); + + expect(result).toEqual(true); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); }); diff --git a/modules/ticket/back/methods/sale/specs/reserve.spec.js b/modules/ticket/back/methods/sale/specs/reserve.spec.js index c4b3b4e5d..7ab79f9c0 100644 --- a/modules/ticket/back/methods/sale/specs/reserve.spec.js +++ b/modules/ticket/back/methods/sale/specs/reserve.spec.js @@ -3,7 +3,7 @@ const models = require('vn-loopback/server/server').models; describe('sale reserve()', () => { const ctx = { req: { - accessToken: {userId: 9}, + accessToken: {userId: 1}, headers: {origin: 'localhost:5000'}, __: () => {} } diff --git a/modules/ticket/back/methods/sale/specs/updateConcept.spec.js b/modules/ticket/back/methods/sale/specs/updateConcept.spec.js index 01f610d36..0e7e9bf0f 100644 --- a/modules/ticket/back/methods/sale/specs/updateConcept.spec.js +++ b/modules/ticket/back/methods/sale/specs/updateConcept.spec.js @@ -2,7 +2,7 @@ const models = require('vn-loopback/server/server').models; describe('sale updateConcept()', () => { const ctx = {req: {accessToken: {userId: 9}}}; - const saleId = 1; + const saleId = 25; it('should throw if ID was undefined', async() => { const tx = await models.Sale.beginTransaction({}); diff --git a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js index 4c961faab..53a05cd7e 100644 --- a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js +++ b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js @@ -9,32 +9,21 @@ describe('sale updateQuantity()', () => { } }; - it('should throw an error if the quantity is not a number', async() => { - const tx = await models.Sale.beginTransaction({}); - - let error; - try { - const options = {transaction: tx}; - - await models.Sale.updateQuantity(ctx, 1, 'wrong quantity!', options); - - 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() => { + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const tx = await models.Sale.beginTransaction({}); let error; try { const options = {transaction: tx}; - await models.Sale.updateQuantity(ctx, 1, 99, options); + await models.Sale.updateQuantity(ctx, 17, 99, options); await tx.rollback(); } catch (e) { @@ -45,21 +34,60 @@ describe('sale updateQuantity()', () => { 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 add quantity if the quantity is greater than it should be and is role advanced', async() => { const tx = await models.Sale.beginTransaction({}); + const saleId = 17; + const buyerId = 35; + const ctx = { + req: { + accessToken: {userId: buyerId}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; try { const options = {transaction: tx}; - const originalLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options); + const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, options); - expect(originalLine.quantity).toEqual(5); + expect(isRoleAdvanced).toEqual(true); - await models.Sale.updateQuantity(ctx, 1, 4, options); + const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); - const modifiedLine = await models.Sale.findOne({where: {id: 1}, fields: ['quantity']}, options); + expect(originalLine.quantity).toEqual(30); - expect(modifiedLine.quantity).toEqual(4); + const newQuantity = originalLine.quantity + 1; + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(modifiedLine.quantity).toEqual(newQuantity); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should update the quantity of a given sale current line', async() => { + const tx = await models.Sale.beginTransaction({}); + const saleId = 25; + const newQuantity = 4; + + try { + const options = {transaction: tx}; + + const originalLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(originalLine.quantity).toEqual(20); + + await models.Sale.updateQuantity(ctx, saleId, newQuantity, options); + + const modifiedLine = await models.Sale.findOne({where: {id: saleId}, fields: ['quantity']}, options); + + expect(modifiedLine.quantity).toEqual(newQuantity); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/methods/sale/updatePrice.js b/modules/ticket/back/methods/sale/updatePrice.js index 302522cfb..8f27e1af5 100644 --- a/modules/ticket/back/methods/sale/updatePrice.js +++ b/modules/ticket/back/methods/sale/updatePrice.js @@ -66,12 +66,7 @@ module.exports = Self => { const sale = await models.Sale.findById(id, filter, myOptions); - const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk, myOptions); - if (!isEditable) - throw new UserError(`The sales of this ticket can't be modified`); - const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions); - if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js index 24e3736f3..8cf0720ce 100644 --- a/modules/ticket/back/methods/sale/updateQuantity.js +++ b/modules/ticket/back/methods/sale/updateQuantity.js @@ -42,13 +42,9 @@ module.exports = Self => { try { const canEditSale = await models.Sale.canEdit(ctx, [id], myOptions); - if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); - if (isNaN(newQuantity)) - throw new UserError(`The value should be a number`); - const filter = { include: { relation: 'ticket', @@ -70,7 +66,8 @@ module.exports = Self => { const sale = await models.Sale.findById(id, filter, myOptions); - if (newQuantity > sale.quantity) + const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, myOptions); + if (newQuantity > sale.quantity && !isRoleAdvanced) throw new UserError('The new quantity should be smaller than the old one'); const oldQuantity = sale.quantity; diff --git a/modules/ticket/back/methods/ticket-weekly/specs/filter.spec.js b/modules/ticket/back/methods/ticket-weekly/specs/filter.spec.js index 411bbe014..2587b6657 100644 --- a/modules/ticket/back/methods/ticket-weekly/specs/filter.spec.js +++ b/modules/ticket/back/methods/ticket-weekly/specs/filter.spec.js @@ -17,7 +17,7 @@ describe('ticket-weekly filter()', () => { const firstRow = result[0]; expect(firstRow.ticketFk).toEqual(1); - expect(result.length).toEqual(5); + expect(result.length).toEqual(6); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/methods/ticket/isEditable.js b/modules/ticket/back/methods/ticket/isEditable.js index 5b9a397a1..d8fbb86ce 100644 --- a/modules/ticket/back/methods/ticket/isEditable.js +++ b/modules/ticket/back/methods/ticket/isEditable.js @@ -20,24 +20,20 @@ module.exports = Self => { }); Self.isEditable = async(ctx, id, options) => { - const userId = ctx.req.accessToken.userId; + const models = Self.app.models; const myOptions = {}; if (typeof options == 'object') Object.assign(myOptions, options); - let state = await Self.app.models.TicketState.findOne({ + const state = await models.TicketState.findOne({ where: {ticketFk: id} }, myOptions); - const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant', myOptions); - const isDeliveryBoss = await Self.app.models.Account.hasRole(userId, 'deliveryBoss', myOptions); - const isBuyer = await Self.app.models.Account.hasRole(userId, 'buyer', myOptions); + const isRoleAdvanced = await models.Ticket.isRoleAdvanced(ctx, myOptions); - const isValidRole = isSalesAssistant || isDeliveryBoss || isBuyer; - - let alertLevel = state ? state.alertLevel : null; - let ticket = await Self.app.models.Ticket.findById(id, { + const alertLevel = state ? state.alertLevel : null; + const ticket = await models.Ticket.findById(id, { fields: ['clientFk'], include: [{ relation: 'client', @@ -48,15 +44,17 @@ module.exports = Self => { } }] }, myOptions); - const isLocked = await Self.app.models.Ticket.isLocked(id, myOptions); + + const isLocked = await models.Ticket.isLocked(id, myOptions); + const isWeekly = await models.TicketWeekly.findOne({where: {ticketFk: id}}, myOptions); const alertLevelGreaterThanZero = (alertLevel && alertLevel > 0); const isNormalClient = ticket && ticket.client().type().code == 'normal'; - const validAlertAndRoleNormalClient = (alertLevelGreaterThanZero && isNormalClient && !isValidRole); + const isEditable = !(alertLevelGreaterThanZero && isNormalClient); - if (!ticket || validAlertAndRoleNormalClient || isLocked) - return false; + if (ticket && (isEditable || isRoleAdvanced) && !isLocked && !isWeekly) + return true; - return true; + return false; }; }; diff --git a/modules/ticket/back/methods/ticket/isRoleAdvanced.js b/modules/ticket/back/methods/ticket/isRoleAdvanced.js new file mode 100644 index 000000000..7c5c8ed86 --- /dev/null +++ b/modules/ticket/back/methods/ticket/isRoleAdvanced.js @@ -0,0 +1,32 @@ +module.exports = Self => { + Self.remoteMethodCtx('isRoleAdvanced', { + description: 'Check if a ticket is editable', + accessType: 'READ', + returns: { + type: 'boolean', + root: true + }, + http: { + path: `/isRoleAdvanced`, + verb: 'GET' + } + }); + + Self.isRoleAdvanced = async(ctx, options) => { + const models = Self.app.models; + const userId = ctx.req.accessToken.userId; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); + const isDeliveryBoss = await models.Account.hasRole(userId, 'deliveryBoss', myOptions); + const isBuyer = await models.Account.hasRole(userId, 'buyer', myOptions); + const isClaimManager = await models.Account.hasRole(userId, 'claimManager', myOptions); + + const isRoleAdvanced = isSalesAssistant || isDeliveryBoss || isBuyer || isClaimManager; + + return isRoleAdvanced; + }; +}; diff --git a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js index b3291c25a..3a9c2db50 100644 --- a/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js +++ b/modules/ticket/back/methods/ticket/specs/componentUpdate.spec.js @@ -1,4 +1,5 @@ const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); describe('ticket componentUpdate()', () => { const userID = 1101; @@ -178,10 +179,15 @@ describe('ticket componentUpdate()', () => { } } }; + + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: ctx.req + }); + const oldTicket = await models.Ticket.findById(ticketID, null, options); + await models.Ticket.componentUpdate(ctx, options); const [newTicketID] = await models.Ticket.rawSql('SELECT MAX(id) as id FROM ticket', null, options); - const oldTicket = await models.Ticket.findById(ticketID, null, options); const newTicket = await models.Ticket.findById(newTicketID.id, null, options); const newTicketSale = await models.Sale.findOne({where: {ticketFk: args.id}}, options); diff --git a/modules/ticket/back/methods/ticket/specs/isEditable.spec.js b/modules/ticket/back/methods/ticket/specs/isEditable.spec.js index adc2acdee..7337017d6 100644 --- a/modules/ticket/back/methods/ticket/specs/isEditable.spec.js +++ b/modules/ticket/back/methods/ticket/specs/isEditable.spec.js @@ -134,4 +134,23 @@ describe('ticket isEditable()', () => { expect(result).toEqual(false); }); + + it('should not be able to edit if is a ticket weekly', async() => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = {transaction: tx}; + + const ctx = {req: {accessToken: {userId: 1}}}; + + const result = await models.Ticket.isEditable(ctx, 15, options); + + expect(result).toEqual(false); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); }); diff --git a/modules/ticket/back/model-config.json b/modules/ticket/back/model-config.json index 21e800b36..17bd72949 100644 --- a/modules/ticket/back/model-config.json +++ b/modules/ticket/back/model-config.json @@ -35,6 +35,9 @@ "SaleChecked": { "dataSource": "vn" }, + "SaleCloned": { + "dataSource": "vn" + }, "SaleComponent": { "dataSource": "vn" }, diff --git a/modules/ticket/back/models/sale-cloned.json b/modules/ticket/back/models/sale-cloned.json new file mode 100644 index 000000000..eb0641684 --- /dev/null +++ b/modules/ticket/back/models/sale-cloned.json @@ -0,0 +1,26 @@ +{ + "name": "SaleCloned", + "base": "VnModel", + "options": { + "mysql": { + "table": "saleCloned" + } + }, + "properties": { + "saleClonedFk": { + "id": true + } + }, + "relations": { + "saleOriginal": { + "type": "belongsTo", + "model": "Sale", + "foreignKey": "saleOriginalFk" + }, + "saleCloned": { + "type": "belongsTo", + "model": "Sale", + "foreignKey": "saleClonedFk" + } + } +} diff --git a/modules/ticket/back/models/ticket-methods.js b/modules/ticket/back/models/ticket-methods.js index f265709e7..3ac03e1de 100644 --- a/modules/ticket/back/models/ticket-methods.js +++ b/modules/ticket/back/models/ticket-methods.js @@ -33,5 +33,6 @@ module.exports = function(Self) { require('../methods/ticket/closeByTicket')(Self); require('../methods/ticket/closeByAgency')(Self); require('../methods/ticket/closeByRoute')(Self); + require('../methods/ticket/isRoleAdvanced')(Self); require('../methods/ticket/collectionLabel')(Self); }; diff --git a/modules/ticket/front/basic-data/step-two/index.js b/modules/ticket/front/basic-data/step-two/index.js index c12647aa5..32d6b2cd6 100644 --- a/modules/ticket/front/basic-data/step-two/index.js +++ b/modules/ticket/front/basic-data/step-two/index.js @@ -76,7 +76,7 @@ class Controller extends Component { haveNotNegatives = true; }); - this.ticket.withoutNegatives = false; + this.ticket.withoutNegatives = true; this.haveNegatives = (haveNegatives && haveNotNegatives && haveDifferences); } diff --git a/modules/ticket/front/expedition/index.spec.js b/modules/ticket/front/expedition/index.spec.js index b95d64fa3..5a538b1c8 100644 --- a/modules/ticket/front/expedition/index.spec.js +++ b/modules/ticket/front/expedition/index.spec.js @@ -76,9 +76,10 @@ describe('Ticket', () => { it('should make a query and then call to the $state go() method', () => { jest.spyOn(controller.$state, 'go').mockReturnThis(); + const landed = new Date(); const ticket = { clientFk: 1101, - landed: new Date(), + landed: landed, addressFk: 121, agencyModeFk: 1, warehouseFk: 1 @@ -90,7 +91,7 @@ describe('Ticket', () => { const expectedParams = { clientId: 1101, - landed: new Date(), + landed: landed, warehouseId: 1, addressId: 121, agencyModeId: 1, diff --git a/package.json b/package.json index 7c70bb430..8cc33526d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "jsdom": "^16.7.0", "jszip": "^3.10.0", "ldapjs": "^2.2.0", - "loopback": "^3.26.0", + "loopback": "^3.28.0", "loopback-boot": "3.3.1", "loopback-component-explorer": "^6.5.0", "loopback-component-storage": "3.6.1", diff --git a/print/templates/email/invoice/invoice.js b/print/templates/email/invoice/invoice.js index 8d3dcaea7..cb90e461f 100755 --- a/print/templates/email/invoice/invoice.js +++ b/print/templates/email/invoice/invoice.js @@ -15,7 +15,7 @@ module.exports = { }, props: { reference: { - type: Number, + type: String, required: true } }