diff --git a/.eslintrc.yml b/.eslintrc.yml index ee20324ff..edbc47195 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -17,7 +17,7 @@ rules: camelcase: 0 default-case: 0 no-eq-null: 0 - no-console: ["error"] + no-console: ["warn"] no-warning-comments: 0 no-empty: [error, allowEmptyCatch: true] complexity: 0 diff --git a/db/changes/232601/01-invoiceOutPdf.sql b/db/changes/232601/01-invoiceOutPdf.sql new file mode 100644 index 000000000..38e0b8bbb --- /dev/null +++ b/db/changes/232601/01-invoiceOutPdf.sql @@ -0,0 +1,13 @@ +INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId) + VALUES + ('InvoiceOut','makePdfAndNotify','WRITE','ALLOW','ROLE','invoicing'), + ('InvoiceOutConfig','*','READ','ALLOW','ROLE','invoicing'); + +CREATE OR REPLACE TABLE vn.invoiceOutConfig ( + id INT UNSIGNED auto_increment NOT NULL, + parallelism int UNSIGNED DEFAULT 1 NOT NULL, + PRIMARY KEY (id) +) +ENGINE=InnoDB +DEFAULT CHARSET=utf8mb3 +COLLATE=utf8mb3_unicode_ci; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 9c930222f..c4338a555 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -603,6 +603,9 @@ UPDATE `vn`.`invoiceOut` SET ref = 'T3333333' WHERE id = 3; UPDATE `vn`.`invoiceOut` SET ref = 'T4444444' WHERE id = 4; UPDATE `vn`.`invoiceOut` SET ref = 'A1111111' WHERE id = 5; +INSERT INTO vn.invoiceOutConfig + SET parallelism = 8; + INSERT INTO `vn`.`invoiceOutTax` (`invoiceOutFk`, `taxableBase`, `vat`, `pgcFk`) VALUES (1, 895.76, 89.58, 4722000010), diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js index d010ef631..ab076b46f 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -30,15 +30,10 @@ module.exports = Self => { type: 'number', description: 'The company id to invoice', required: true - }, { - arg: 'printerFk', - type: 'number', - description: 'The printer to print', - required: true } ], returns: { - type: 'object', + type: 'number', root: true }, http: { @@ -50,29 +45,23 @@ module.exports = Self => { Self.invoiceClient = async(ctx, options) => { const args = ctx.args; const models = Self.app.models; - const myOptions = {userId: ctx.req.accessToken.userId}; - const $t = ctx.req.__; // $translate - const origin = ctx.req.headers.origin; + + options = typeof options == 'object' + ? Object.assign({}, options) : {}; + options.userId = ctx.req.accessToken.userId; let tx; - - if (typeof options == 'object') - Object.assign(myOptions, options); - - if (!myOptions.transaction) { - tx = await Self.beginTransaction({}); - myOptions.transaction = tx; - } + if (!options.transaction) + tx = options.transaction = await Self.beginTransaction({}); const minShipped = Date.vnNew(); minShipped.setFullYear(args.maxShipped.getFullYear() - 1); let invoiceId; - let invoiceOut; try { const client = await models.Client.findById(args.clientId, { fields: ['id', 'hasToInvoiceByAddress'] - }, myOptions); + }, options); if (client.hasToInvoiceByAddress) { await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [ @@ -80,53 +69,57 @@ module.exports = Self => { args.maxShipped, args.addressId, args.companyFk - ], myOptions); + ], options); } else { await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [ args.maxShipped, client.id, args.companyFk - ], myOptions); + ], options); } - // Make invoice - const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions); + // Check negative bases - // Validates ticket nagative base - const hasAnyNegativeBase = await getNegativeBase(myOptions); + let query = + `SELECT COUNT(*) isSpanishCompany + FROM supplier s + JOIN country c ON c.id = s.countryFk + AND c.code = 'ES' + WHERE s.id = ?`; + const [supplierCompany] = await Self.rawSql(query, [ + args.companyFk + ], options); + + const isSpanishCompany = supplierCompany?.isSpanishCompany; + + query = 'SELECT hasAnyNegativeBase() AS base'; + const [result] = await Self.rawSql(query, null, options); + + const hasAnyNegativeBase = result?.base; if (hasAnyNegativeBase && isSpanishCompany) throw new UserError('Negative basis'); + // Invoicing + query = `SELECT invoiceSerial(?, ?, ?) AS serial`; const [invoiceSerial] = await Self.rawSql(query, [ client.id, args.companyFk, 'G' - ], myOptions); + ], options); const serialLetter = invoiceSerial.serial; query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`; await Self.rawSql(query, [ serialLetter, args.invoiceDate - ], myOptions); + ], options); - const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); + const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, options); if (!newInvoice) throw new UserError('No tickets to invoice', 'notInvoiced'); - await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); - - invoiceOut = await models.InvoiceOut.findById(newInvoice.id, { - fields: ['id', 'ref', 'clientFk'], - include: { - relation: 'client', - scope: { - fields: ['email', 'isToBeMailed', 'salesPersonFk'] - } - } - }, myOptions); - + await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], options); invoiceId = newInvoice.id; if (tx) await tx.commit(); @@ -135,66 +128,6 @@ module.exports = Self => { throw e; } - try { - await Self.makePdf(invoiceId); - } catch (err) { - console.error(err); - throw new UserError('Error while generating PDF', 'pdfError'); - } - - if (invoiceOut.client().isToBeMailed) { - try { - ctx.args = { - reference: invoiceOut.ref, - recipientId: args.clientId, - recipient: invoiceOut.client().email - }; - await models.InvoiceOut.invoiceEmail(ctx, invoiceOut.ref); - } catch (err) { - const message = $t('Mail not sent', { - clientId: args.clientId, - clientUrl: `${origin}/#!/claim/${args.id}/summary` - }); - const salesPersonId = invoiceOut.client().salesPersonFk; - - if (salesPersonId) - await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); - - throw new UserError('Error when sending mail to client', 'mailNotSent'); - } - } else { - const query = ` - CALL vn.report_print( - 'invoice', - ?, - account.myUser_getId(), - JSON_OBJECT('refFk', ?), - 'normal' - );`; - await models.InvoiceOut.rawSql(query, [args.printerFk, invoiceOut.ref]); - } - 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(*) isSpanishCompany - 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.isSpanishCompany; - } }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js new file mode 100644 index 000000000..a999437c0 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/makePdfAndNotify.js @@ -0,0 +1,87 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('makePdfAndNotify', { + description: 'Create invoice PDF and send it to client', + accessType: 'WRITE', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'The invoice id', + required: true, + http: {source: 'path'} + }, { + arg: 'printerFk', + type: 'number', + description: 'The printer to print', + required: true + } + ], + http: { + path: '/:id/makePdfAndNotify', + verb: 'POST' + } + }); + + Self.makePdfAndNotify = async function(ctx, id, printerFk) { + const models = Self.app.models; + + options = typeof options == 'object' + ? Object.assign({}, options) : {}; + options.userId = ctx.req.accessToken.userId; + + try { + await Self.makePdf(id, options); + } catch (err) { + console.error(err); + throw new UserError('Error while generating PDF', 'pdfError'); + } + + const invoiceOut = await Self.findById(id, { + fields: ['ref', 'clientFk'], + include: { + relation: 'client', + scope: { + fields: ['id', 'email', 'isToBeMailed', 'salesPersonFk'] + } + } + }, options); + + const ref = invoiceOut.ref; + const client = invoiceOut.client(); + + if (client.isToBeMailed) { + try { + ctx.args = { + reference: ref, + recipientId: client.id, + recipient: client.email + }; + await Self.invoiceEmail(ctx, ref); + } catch (err) { + const origin = ctx.req.headers.origin; + const message = ctx.req.__('Mail not sent', { + clientId: client.id, + clientUrl: `${origin}/#!/claim/${id}/summary` + }); + const salesPersonId = client.salesPersonFk; + + if (salesPersonId) + await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); + + throw new UserError('Error when sending mail to client', 'mailNotSent'); + } + } else { + const query = ` + CALL vn.report_print( + 'invoice', + ?, + account.myUser_getId(), + JSON_OBJECT('refFk', ?), + 'normal' + );`; + await Self.rawSql(query, [printerFk, ref], options); + } + }; +}; diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index b190126ea..9e8b119ab 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -2,6 +2,9 @@ "InvoiceOut": { "dataSource": "vn" }, + "InvoiceOutConfig": { + "dataSource": "vn" + }, "InvoiceOutSerial": { "dataSource": "vn" }, diff --git a/modules/invoiceOut/back/models/invoice-out config.json b/modules/invoiceOut/back/models/invoice-out config.json new file mode 100644 index 000000000..d5595f687 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-out config.json @@ -0,0 +1,22 @@ +{ + "name": "InvoiceOutConfig", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceOutConfig" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "parallelism": { + "type": "number", + "required": true + } + } +} + + diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 0d5ed021c..9baa1e7e6 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -12,6 +12,7 @@ module.exports = Self => { require('../methods/invoiceOut/createManualInvoice')(Self); require('../methods/invoiceOut/clientsToInvoice')(Self); require('../methods/invoiceOut/invoiceClient')(Self); + require('../methods/invoiceOut/makePdfAndNotify')(Self); require('../methods/invoiceOut/refund')(Self); require('../methods/invoiceOut/invoiceEmail')(Self); require('../methods/invoiceOut/exportationPdf')(Self); diff --git a/modules/invoiceOut/front/global-invoicing/index.html b/modules/invoiceOut/front/global-invoicing/index.html index 52bf8b619..3ece30862 100644 --- a/modules/invoiceOut/front/global-invoicing/index.html +++ b/modules/invoiceOut/front/global-invoicing/index.html @@ -1,7 +1,6 @@ + class="status vn-w-lg vn-pa-md"> @@ -20,8 +19,15 @@ Ended process -
- {{$ctrl.percentage | percentage: 0}} ({{$ctrl.addressNumber}} {{'of' | translate}} {{$ctrl.nAddresses}}) +
+
+ {{$ctrl.percentage | percentage: 0}} + ({{$ctrl.addressNumber}} of {{$ctrl.nAddresses}}) +
+
+ {{$ctrl.nPdfs}} of {{$ctrl.totalPdfs}} + PDFs +
@@ -141,7 +147,7 @@ + ng-click="$ctrl.status = 'stopping'"> diff --git a/modules/invoiceOut/front/global-invoicing/index.js b/modules/invoiceOut/front/global-invoicing/index.js index 930dfd459..9a936611a 100644 --- a/modules/invoiceOut/front/global-invoicing/index.js +++ b/modules/invoiceOut/front/global-invoicing/index.js @@ -9,30 +9,27 @@ class Controller extends Section { Object.assign(this, { maxShipped: new Date(date.getFullYear(), date.getMonth(), 0), clientsToInvoice: 'all', + companyFk: this.vnConfig.companyFk, + parallelism: 1 }); - this.$http.get('UserConfigs/getUserConfig') - .then(res => { - this.companyFk = res.data.companyFk; - this.getInvoiceDate(this.companyFk); - }); - } - - getInvoiceDate(companyFk) { - const params = {companyFk: companyFk}; - this.fetchInvoiceDate(params); - } - - fetchInvoiceDate(params) { + const params = {companyFk: this.companyFk}; this.$http.get('InvoiceOuts/getInvoiceDate', {params}) .then(res => { this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null; this.invoiceDate = this.minInvoicingDate; }); - } - stopInvoicing() { - this.status = 'stopping'; + const filter = {fields: ['parallelism']}; + this.$http.get('InvoiceOutConfigs/findOne', {filter}) + .then(res => { + if (res.data.parallelism) + this.parallelism = res.data.parallelism; + }) + .catch(res => { + if (res.status == 404) return; + throw res; + }); } makeInvoice() { @@ -70,8 +67,11 @@ class Controller extends Section { if (!this.addresses.length) throw new UserError(`There aren't tickets to invoice`); + this.nRequests = 0; + this.nPdfs = 0; + this.totalPdfs = 0; this.addressIndex = 0; - return this.invoiceOut(); + this.invoiceClient(); }) .catch(err => this.handleError(err)); } catch (err) { @@ -85,8 +85,11 @@ class Controller extends Section { throw err; } - invoiceOut() { - if (this.addressIndex == this.addresses.length || this.status == 'stopping') { + invoiceClient() { + if (this.nRequests == this.parallelism || this.isInvoicing) return; + + if (this.addressIndex >= this.addresses.length || this.status == 'stopping') { + if (this.nRequests) return; this.invoicing = false; this.status = 'done'; return; @@ -95,36 +98,27 @@ class Controller extends Section { this.status = 'invoicing'; const address = this.addresses[this.addressIndex]; this.currentAddress = address; + this.isInvoicing = true; const params = { clientId: address.clientId, addressId: address.id, invoiceDate: this.invoiceDate, maxShipped: this.maxShipped, - companyFk: this.companyFk, - printerFk: this.printerFk, + companyFk: this.companyFk }; this.$http.post(`InvoiceOuts/invoiceClient`, params) - .then(() => this.invoiceNext()) + .then(res => { + this.isInvoicing = false; + if (res.data) + this.makePdfAndNotify(res.data, address); + this.invoiceNext(); + }) .catch(res => { + this.isInvoicing = false; if (res.status >= 400 && res.status < 500) { - const error = res.data?.error; - let isWarning; - const filter = { - where: { - id: address.clientId - } - }; - switch (error?.code) { - case 'pdfError': - case 'mailNotSent': - isWarning = true; - break; - } - - const message = error?.message || res.message; - this.errors.unshift({address, message, isWarning}); + this.invoiceError(address, res); this.invoiceNext(); } else { this.invoicing = false; @@ -136,7 +130,27 @@ class Controller extends Section { invoiceNext() { this.addressIndex++; - this.invoiceOut(); + this.invoiceClient(); + } + + makePdfAndNotify(invoiceId, address) { + this.nRequests++; + this.totalPdfs++; + const params = {printerFk: this.printerFk}; + this.$http.post(`InvoiceOuts/${invoiceId}/makePdfAndNotify`, params) + .catch(res => { + this.invoiceError(address, res, true); + }) + .finally(() => { + this.nPdfs++; + this.nRequests--; + this.invoiceClient(); + }); + } + + invoiceError(address, res, isWarning) { + const message = res.data?.error?.message || res.message; + this.errors.unshift({address, message, isWarning}); } get nAddresses() { diff --git a/modules/invoiceOut/front/global-invoicing/locale/es.yml b/modules/invoiceOut/front/global-invoicing/locale/es.yml index ed61e832a..f1a411ba1 100644 --- a/modules/invoiceOut/front/global-invoicing/locale/es.yml +++ b/modules/invoiceOut/front/global-invoicing/locale/es.yml @@ -10,6 +10,7 @@ Build packaging tickets: Generando tickets de embalajes Address id: Id dirección Printer: Impresora of: de +PDFs: PDFs Client: Cliente Current client id: Id cliente actual Invoicing client: Facturando cliente diff --git a/modules/invoiceOut/front/global-invoicing/style.scss b/modules/invoiceOut/front/global-invoicing/style.scss index 6fdfac0ba..3ad767aba 100644 --- a/modules/invoiceOut/front/global-invoicing/style.scss +++ b/modules/invoiceOut/front/global-invoicing/style.scss @@ -1,17 +1,21 @@ @import "variables"; -vn-invoice-out-global-invoicing{ - - h5{ +vn-invoice-out-global-invoicing { + h5 { color: $color-primary; } - + .status { + height: 80px; + display: flex; + align-items: center; + justify-content: center; + gap: 20px; + } #error { line-break: normal; overflow-wrap: break-word; white-space: normal; } - }