diff --git a/CHANGELOG.md b/CHANGELOG.md index fa2ebcd62..cbfc6e9e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2328.01] - 2023-07-13 + +### Added + +### Changed + +### Fixed + + ## [2326.01] - 2023-06-29 ### Added diff --git a/db/changes/232801/.gitkeep b/db/changes/232801/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 43ff4b86a..40df9a712 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -115,7 +115,7 @@ "This client is not invoiceable": "This client is not invoiceable", "INACTIVE_PROVIDER": "Inactive provider", "reference duplicated": "reference duplicated", - "The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option", + "The PDF document does not exist": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option", "This item is not available": "This item is not available", "Deny buy request": "Purchase request for ticket id [{{ticketId}}]({{{url}}}) has been rejected. Reason: {{observation}}", "The type of business must be filled in basic data": "The type of business must be filled in basic data", @@ -175,5 +175,6 @@ "Pass expired": "The password has expired, change it from Salix", "Can't transfer claimed sales": "Can't transfer claimed sales", "Invalid quantity": "Invalid quantity", - "Failed to upload delivery note": "Error to upload delivery note {{id}}" + "Failed to upload delivery note": "Error to upload delivery note {{id}}", + "Mail not sent": "There has been an error sending the invoice to the client [{{clientId}}]({{{clientUrl}}}), please check the email address" } diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 895d087dc..138fa7100 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -211,7 +211,7 @@ "You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito", "You can't change the credit set to zero from a financialBoss": "No puedes cambiar el cŕedito establecido a cero por un jefe de finanzas", "Amounts do not match": "Las cantidades no coinciden", - "The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'", + "The PDF document does not exist": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'", "The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos", "You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días", "The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día", @@ -265,7 +265,7 @@ "It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas", "A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.", "There is no assigned email for this client": "No hay correo asignado para este cliente", - "Exists an invoice with a previous date": "Existe una factura con fecha anterior", + "Exists an invoice with a future date": "Existe una factura con fecha posterior", "Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite", "Warehouse inventory not set": "El almacén inventario no está establecido", "This locker has already been assigned": "Esta taquilla ya ha sido asignada", @@ -294,5 +294,9 @@ "Invalid NIF for VIES": "Invalid NIF for VIES", "Ticket does not exist": "Este ticket no existe", "Ticket is already signed": "Este ticket ya ha sido firmado", + "Fecha fuera de rango": "Fecha fuera de rango", + "Error while generating PDF": "Error al generar PDF", + "Error when sending mail to client": "Error al enviar el correo al cliente", + "Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico", "The renew period has not been exceeded": "El periodo de renovación no ha sido superado" } diff --git a/modules/client/back/locale/client/en.yml b/modules/client/back/locale/client/en.yml index 74384461c..ad21a6ce6 100644 --- a/modules/client/back/locale/client/en.yml +++ b/modules/client/back/locale/client/en.yml @@ -52,4 +52,5 @@ columns: hasInvoiceSimplified: simplified invoice typeFk: type lastSalesPersonFk: last salesperson - + rating: rating + recommendedCredit: recommended credit diff --git a/modules/client/back/locale/client/es.yml b/modules/client/back/locale/client/es.yml index 47bf896d9..b17f53b1c 100644 --- a/modules/client/back/locale/client/es.yml +++ b/modules/client/back/locale/client/es.yml @@ -52,4 +52,5 @@ columns: hasInvoiceSimplified: factura simple typeFk: tipo lastSalesPersonFk: último comercial - + rating: clasificación + recommendedCredit: crédito recomendado diff --git a/modules/entry/back/methods/entry/editLatestBuys.js b/modules/entry/back/methods/entry/editLatestBuys.js index 2642d4f4d..0da2b1625 100644 --- a/modules/entry/back/methods/entry/editLatestBuys.js +++ b/modules/entry/back/methods/entry/editLatestBuys.js @@ -75,7 +75,7 @@ module.exports = Self => { value[field] = newValue; if (filter) { - ctx.args.filter = {where: filter, limit: null}; + ctx.args = {where: filter, limit: null}; lines = await models.Buy.latestBuysFilter(ctx, null, myOptions); } diff --git a/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js index 99d2df67b..a4dd185fd 100644 --- a/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js @@ -53,7 +53,36 @@ describe('Buy editLatestsBuys()', () => { const options = {transaction: tx}; try { - const filter = {'i.typeFk': 1}; + const filter = {'categoryFk': 1, 'tags': []}; + const ctx = { + args: { + filter: filter + }, + req: {accessToken: {userId: 1}} + }; + + const field = 'size'; + const newValue = 88; + + await models.Buy.editLatestBuys(ctx, field, newValue, null, filter, options); + + const [result] = await models.Buy.latestBuysFilter(ctx, null, options); + + expect(result[field]).toEqual(newValue); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should change the value of a given column for filter tags', async() => { + const tx = await models.Buy.beginTransaction({}); + const options = {transaction: tx}; + + try { + const filter = {'tags': [{tagFk: 1, value: 'Brown'}]}; const ctx = { args: { filter: filter diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index dfdb3c1a7..8cc584c94 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -1,5 +1,4 @@ const UserError = require('vn-loopback/util/user-error'); -const print = require('vn-print'); module.exports = Self => { Self.remoteMethodCtx('createPdf', { @@ -26,9 +25,6 @@ module.exports = Self => { Self.createPdf = async function(ctx, id, options) { const models = Self.app.models; - if (process.env.NODE_ENV == 'test') - throw new UserError(`Action not allowed on the test environment`); - let tx; const myOptions = {}; @@ -41,40 +37,20 @@ module.exports = Self => { } try { - const invoiceOut = await Self.findById(id, null, myOptions); - const canCreatePdf = await models.ACL.checkAccessAcl(ctx, 'InvoiceOut', 'canCreatePdf', 'WRITE'); + const invoiceOut = await Self.findById(id, {fields: ['hasPdf']}, myOptions); - if (invoiceOut.hasPdf && !canCreatePdf) - throw new UserError(`You don't have enough privileges`); + if (invoiceOut.hasPdf) { + const canCreatePdf = await models.ACL.checkAccessAcl(ctx, 'InvoiceOut', 'canCreatePdf', 'WRITE'); + if (!canCreatePdf) + throw new UserError(`You don't have enough privileges`); + } - await invoiceOut.updateAttributes({ - hasPdf: true - }, myOptions); - - const invoiceReport = new print.Report('invoice', { - reference: invoiceOut.ref, - recipientId: invoiceOut.clientFk - }); - const buffer = 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 - await print.storage.write(buffer, { - type: 'invoice', - path: `${year}/${month}/${day}`, - fileName: fileName - }); + await Self.makePdf(id, myOptions); if (tx) await tx.commit(); - } catch (e) { + } catch (err) { if (tx) await tx.rollback(); - throw e; + throw err; } }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js index 5c787428b..49bba046b 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/download.js +++ b/modules/invoiceOut/back/methods/invoiceOut/download.js @@ -43,7 +43,9 @@ module.exports = Self => { Object.assign(myOptions, options); try { - const invoiceOut = await models.InvoiceOut.findById(id, null, myOptions); + const invoiceOut = await models.InvoiceOut.findById(id, { + fields: ['ref', 'issued'] + }, myOptions); const issued = invoiceOut.issued; const year = issued.getFullYear().toString(); @@ -73,7 +75,7 @@ module.exports = Self => { return [stream, file.contentType, `filename="${file.name}"`]; } catch (error) { if (error.code === 'ENOENT') - throw new UserError('The PDF document does not exists'); + throw new UserError('The PDF document does not exist'); throw error; } diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js index 421cbaea1..d010ef631 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceClient.js @@ -51,6 +51,9 @@ module.exports = Self => { 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; + let tx; if (typeof options == 'object') @@ -109,17 +112,22 @@ module.exports = Self => { ], myOptions); const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); - if (newInvoice.id) { - await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); + if (!newInvoice) + throw new UserError('No tickets to invoice', 'notInvoiced'); - invoiceOut = await models.InvoiceOut.findById(newInvoice.id, { - include: { - relation: 'client' + 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); + } + }, myOptions); - invoiceId = newInvoice.id; - } + invoiceId = newInvoice.id; if (tx) await tx.commit(); } catch (e) { @@ -127,26 +135,45 @@ module.exports = Self => { throw e; } - if (invoiceId) { - if (!invoiceOut.client().isToBeMailed) { - const query = ` - CALL vn.report_print( - 'invoice', - ?, - account.myUser_getId(), - JSON_OBJECT('refFk', ?), - 'normal' - );`; - await models.InvoiceOut.rawSql(query, [args.printerFk, invoiceOut.ref]); - } else { + 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: invoiceOut.clientFk, + 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; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js index 83cb84881..113526484 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js +++ b/modules/invoiceOut/back/methods/invoiceOut/invoiceEmail.js @@ -1,3 +1,4 @@ +const UserError = require('vn-loopback/util/user-error'); const {Email} = require('vn-print'); module.exports = Self => { @@ -74,6 +75,10 @@ module.exports = Self => { ] }; - return email.send(mailOptions); + try { + return email.send(mailOptions); + } catch (err) { + throw new UserError('Error when sending mail to client', 'mailNotSent'); + } }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js index 9a0574dba..0faa8fe1a 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/invoiceClient.spec.js @@ -18,12 +18,14 @@ describe('InvoiceOut invoiceClient()', () => { accessToken: {userId: userId}, __: value => { return value; - } + }, + headers: {origin: 'http://localhost'} + }; 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, 'makePdf').and.returnValue(new Promise(resolve => resolve(true))); spyOn(models.InvoiceOut, 'invoiceEmail'); const tx = await models.InvoiceOut.beginTransaction({}); diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 5fcef7744..0d5ed021c 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -1,3 +1,5 @@ +const print = require('vn-print'); + module.exports = Self => { require('../methods/invoiceOut/filter')(Self); require('../methods/invoiceOut/summary')(Self); @@ -19,4 +21,34 @@ module.exports = Self => { require('../methods/invoiceOut/getInvoiceDate')(Self); require('../methods/invoiceOut/negativeBases')(Self); require('../methods/invoiceOut/negativeBasesCsv')(Self); + + Self.makePdf = async function(id, options) { + const fields = ['id', 'hasPdf', 'ref', 'issued']; + const invoiceOut = await Self.findById(id, {fields}, options); + const invoiceReport = new print.Report('invoice', { + reference: invoiceOut.ref + }); + const buffer = 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 + + await invoiceOut.updateAttributes({ + hasPdf: true + }, options); + + if (process.env.NODE_ENV !== 'test') { + await print.storage.write(buffer, { + type: 'invoice', + path: `${year}/${month}/${day}`, + fileName: fileName + }); + } + }; }; diff --git a/modules/invoiceOut/front/global-invoicing/index.html b/modules/invoiceOut/front/global-invoicing/index.html index 3f0a10650..52bf8b619 100644 --- a/modules/invoiceOut/front/global-invoicing/index.html +++ b/modules/invoiceOut/front/global-invoicing/index.html @@ -55,7 +55,11 @@ {{::error.address.nickname}} - {{::error.message}} + + {{::error.message}} + diff --git a/modules/invoiceOut/front/global-invoicing/index.js b/modules/invoiceOut/front/global-invoicing/index.js index 5a078dfbb..930dfd459 100644 --- a/modules/invoiceOut/front/global-invoicing/index.js +++ b/modules/invoiceOut/front/global-invoicing/index.js @@ -7,29 +7,29 @@ class Controller extends Section { $onInit() { const date = Date.vnNew(); Object.assign(this, { - maxShipped: new Date(date.getFullYear(), date.getMonth(), 0), - clientsToInvoice: 'all', + maxShipped: new Date(date.getFullYear(), date.getMonth(), 0), + clientsToInvoice: 'all', }); this.$http.get('UserConfigs/getUserConfig') - .then(res => { - this.companyFk = res.data.companyFk; - this.getInvoiceDate(this.companyFk); - }); - } + .then(res => { + this.companyFk = res.data.companyFk; + this.getInvoiceDate(this.companyFk); + }); + } - getInvoiceDate(companyFk) { - const params = { companyFk: companyFk }; + getInvoiceDate(companyFk) { + const params = {companyFk: companyFk}; this.fetchInvoiceDate(params); - } + } - fetchInvoiceDate(params) { - this.$http.get('InvoiceOuts/getInvoiceDate', { params }) - .then(res => { - this.minInvoicingDate = res.data.issued ? new Date(res.data.issued) : null; - this.invoiceDate = this.minInvoicingDate; - }); - } + fetchInvoiceDate(params) { + 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'; @@ -49,7 +49,7 @@ class Controller extends Section { if (this.invoiceDate < this.maxShipped) throw new UserError('Invoice date can\'t be less than max date'); if (this.minInvoicingDate && this.invoiceDate.getTime() < this.minInvoicingDate.getTime()) - throw new UserError('Exists an invoice with a previous date'); + throw new UserError('Exists an invoice with a future date'); if (!this.companyFk) throw new UserError('Choose a valid company'); if (!this.printerFk) @@ -108,16 +108,30 @@ class Controller extends Section { this.$http.post(`InvoiceOuts/invoiceClient`, params) .then(() => this.invoiceNext()) .catch(res => { - const message = res.data?.error?.message || res.message; if (res.status >= 400 && res.status < 500) { - this.errors.unshift({address, message}); + 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.invoiceNext(); } else { this.invoicing = false; this.status = 'done'; throw new UserError(`Critical invoicing error, proccess stopped`); } - }) + }); } invoiceNext() { diff --git a/modules/invoiceOut/front/global-invoicing/locale/es.yml b/modules/invoiceOut/front/global-invoicing/locale/es.yml index 5b1f7e883..ed61e832a 100644 --- a/modules/invoiceOut/front/global-invoicing/locale/es.yml +++ b/modules/invoiceOut/front/global-invoicing/locale/es.yml @@ -18,4 +18,4 @@ Invoice out: Facturar One client: Un solo cliente Choose a valid client: Selecciona un cliente válido Stop: Parar -Critical invoicing error, proccess stopped: Error crítico al facturar, proceso detenido \ No newline at end of file +Critical invoicing error, proccess stopped: Error crítico al facturar, proceso detenido diff --git a/package.json b/package.json index 4358c86a7..64c374841 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-back", - "version": "23.26.01", + "version": "23.28.01", "author": "Verdnatura Levante SL", "description": "Salix backend", "license": "GPL-3.0", diff --git a/print/core/smtp.js b/print/core/smtp.js index 61b115b5a..04bede5b5 100644 --- a/print/core/smtp.js +++ b/print/core/smtp.js @@ -46,14 +46,21 @@ module.exports = { const fileNames = attachments.join(',\n'); await db.rawSql(` - INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) - VALUES (?, ?, 1, ?, ?, ?, ?)`, [ + INSERT INTO vn.mail + SET receiver = ?, + replyTo = ?, + sent = ?, + subject = ?, + body = ?, + attachment = ?, + status = ?`, [ options.to, options.replyTo, + error ? 2 : 1, options.subject, options.text || options.html, fileNames, - error && error.message || 'Sent' + error && error.message || 'OK' ]); } diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css index 9fda2a613..d4526ce56 100644 --- a/print/templates/reports/invoice/assets/css/style.css +++ b/print/templates/reports/invoice/assets/css/style.css @@ -16,6 +16,10 @@ h2 { font-size: 22px } +.column-oriented td, +.column-oriented th { + padding: 6px +} #nickname h2 { max-width: 400px; @@ -39,4 +43,4 @@ h2 { .phytosanitary-info { margin-top: 10px -} \ No newline at end of file +} diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html index d74325dfd..45b6c3934 100644 --- a/print/templates/reports/invoice/invoice.html +++ b/print/templates/reports/invoice/invoice.html @@ -62,7 +62,7 @@ -
+

{{$t('deliveryNote')}}

@@ -106,13 +106,6 @@ {{sale.vatType}} {{saleImport(sale) | currency('EUR', $i18n.locale)}} - - - {{sale.tag5}} {{sale.value5}} - {{sale.tag6}} {{sale.value6}} - {{sale.tag7}} {{sale.value7}} - -