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/dump/fixtures.sql b/db/dump/fixtures.sql index 6624e99f4..29b75602b 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2699,7 +2699,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); 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/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/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/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/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 } }