From af5f0db7f8336bf55fe499ddb58b2b00cd02a1ab Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 17 Feb 2021 13:00:06 +0100 Subject: [PATCH 01/21] 2734 - Added invoice report --- .../reports/invoice/assets/css/import.js | 9 + .../reports/invoice/assets/css/style.css | 40 +++ .../reports/invoice/assets/images/europe.png | Bin 0 -> 55634 bytes print/templates/reports/invoice/invoice.html | 276 ++++++++++++++++++ print/templates/reports/invoice/invoice.js | 129 ++++++++ print/templates/reports/invoice/locale/es.yml | 27 ++ .../templates/reports/invoice/sql/address.sql | 11 + .../templates/reports/invoice/sql/client.sql | 8 + .../reports/invoice/sql/corrected.sql | 5 + .../templates/reports/invoice/sql/invoice.sql | 17 ++ .../reports/invoice/sql/packagings.sql | 9 + print/templates/reports/invoice/sql/sales.sql | 42 +++ .../reports/invoice/sql/services.sql | 8 + .../reports/invoice/sql/signature.sql | 8 + print/templates/reports/invoice/sql/taxes.sql | 8 + 15 files changed, 597 insertions(+) create mode 100644 print/templates/reports/invoice/assets/css/import.js create mode 100644 print/templates/reports/invoice/assets/css/style.css create mode 100644 print/templates/reports/invoice/assets/images/europe.png create mode 100644 print/templates/reports/invoice/invoice.html create mode 100755 print/templates/reports/invoice/invoice.js create mode 100644 print/templates/reports/invoice/locale/es.yml create mode 100644 print/templates/reports/invoice/sql/address.sql create mode 100644 print/templates/reports/invoice/sql/client.sql create mode 100644 print/templates/reports/invoice/sql/corrected.sql create mode 100644 print/templates/reports/invoice/sql/invoice.sql create mode 100644 print/templates/reports/invoice/sql/packagings.sql create mode 100644 print/templates/reports/invoice/sql/sales.sql create mode 100644 print/templates/reports/invoice/sql/services.sql create mode 100644 print/templates/reports/invoice/sql/signature.sql create mode 100644 print/templates/reports/invoice/sql/taxes.sql diff --git a/print/templates/reports/invoice/assets/css/import.js b/print/templates/reports/invoice/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/invoice/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css new file mode 100644 index 000000000..cbe894097 --- /dev/null +++ b/print/templates/reports/invoice/assets/css/style.css @@ -0,0 +1,40 @@ +#signature { + padding-right: 10px +} + +#signature img { + -webkit-filter: brightness(0%); + filter: brightness(0%); + margin-bottom: 10px; + max-width: 150px +} + +.description strong { + text-transform: uppercase; +} + +h2 { + font-weight: 100; + color: #555 +} + +.ticket-info { + font-size: 26px +} + +#phytosanitary { + padding-right: 10px +} + +#phytosanitary .flag img { + width: 100% +} + +#phytosanitary .flag .flag-text { + padding-left: 10px; + box-sizing: border-box; +} + +.phytosanitary-info { + margin-top: 10px +} \ No newline at end of file diff --git a/print/templates/reports/invoice/assets/images/europe.png b/print/templates/reports/invoice/assets/images/europe.png new file mode 100644 index 0000000000000000000000000000000000000000..673be92ae2f0647fd1748e12a36bf073aa145d64 GIT binary patch literal 55634 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 + + + + + + + + +
+ + + + +
+
+
+
+
+

{{$t('title')}}

+ + + + + + + + + + + + + + + +
{{$t('clientId')}}{{client.id}}
{{$t('invoice')}}{{invoice.ref}}
{{$t('date')}}{{invoice.issued | date('%d-%m-%Y')}}
+
+
+
+
+
{{$t('invoiceData')}}
+
+

{{client.socialName}}

+
+ {{client.street}} +
+
+ {{client.fi}} +
+
+
+
+
+ + +

{{$t('saleLines')}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('discount')}}{{$t('vat')}}{{$t('amount')}}
{{sale.itemFk | zerofill('000000')}}{{sale.quantity}}{{sale.concept}}{{sale.price | currency('EUR', $i18n.locale)}}{{(sale.discount / 100) | percentage}}{{sale.vatType}}{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', $i18n.locale)}}
+ + {{sale.tag5}} {{sale.value5}} + + + {{sale.tag6}} {{sale.value6}} + + + {{sale.tag7}} {{sale.value7}} + +
+ {{$t('subtotal')}} + {{getSubTotal() | currency('EUR', $i18n.locale)}}
+ + +
+ +
+

{{$t('services')}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('vat')}}{{$t('amount')}}
{{service.quantity}}{{service.description}}{{service.price | currency('EUR', $i18n.locale)}}{{service.taxDescription}}{{service.price | currency('EUR', $i18n.locale)}}
+ {{$t('subtotal')}} + {{serviceTotal | currency('EUR', $i18n.locale)}}
+
+ +
+
+ +
+

{{$t('packagings')}}

+ + + + + + + + + + + + + + + +
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}
{{packaging.itemFk | zerofill('000000')}}{{packaging.quantity}}{{packaging.name}}
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('taxBreakdown')}}
{{$t('type')}} + {{$t('taxBase')}} + {{$t('tax')}}{{$t('fee')}}
{{tax.name}} + {{tax.Base | currency('EUR', $i18n.locale)}} + {{tax.vatPercent | percentage}}{{tax.tax | currency('EUR', $i18n.locale)}}
{{$t('subtotal')}} + {{getTotalBase() | currency('EUR', $i18n.locale)}} + {{getTotalTax()| currency('EUR', $i18n.locale)}}
{{$t('total')}}{{getTotal() | currency('EUR', $i18n.locale)}}
+
+ + + + + +
+
+ + + +
+
+
+ + + +
+ + \ No newline at end of file diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js new file mode 100755 index 000000000..ac4f62155 --- /dev/null +++ b/print/templates/reports/invoice/invoice.js @@ -0,0 +1,129 @@ +const config = require(`${appPath}/core/config`); +const Component = require(`${appPath}/core/component`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); +const md5 = require('md5'); +const fs = require('fs-extra'); + +module.exports = { + name: 'invoice', + async serverPrefetch() { + this.invoice = await this.fetchInvoice(this.invoiceId); + this.client = {}; + this.address = {}; + this.sales = []; + this.services = []; + this.taxes = []; + this.packagings = []; + /* this.client = await this.fetchClient(this.ticketId); + this.ticket = await this.fetchTicket(this.ticketId); + this.sales = await this.fetchSales(this.ticketId); + this.address = await this.fetchAddress(this.ticketId); + this.services = await this.fetchServices(this.ticketId); + this.taxes = await this.fetchTaxes(this.ticketId); + this.packagings = await this.fetchPackagings(this.ticketId); + this.signature = await this.fetchSignature(this.ticketId); + + */ + if (!this.invoice) + throw new Error('Something went wrong'); + }, + data() { + return {totalBalance: 0.00}; + }, + computed: { + /* dmsPath() { + if (!this.signature) return; + + const hash = md5(this.signature.id.toString()).substring(0, 3); + const file = `${config.storage.root}/${hash}/${this.signature.id}.png`; + const src = fs.readFileSync(file); + const base64 = Buffer.from(src, 'utf8').toString('base64'); + + return `data:image/png;base64, ${base64}`; + }, + serviceTotal() { + let total = 0.00; + this.services.forEach(service => { + total += parseFloat(service.price) * service.quantity; + }); + + return total; + } */ + }, + methods: { + fetchInvoice(invoiceId) { + return this.findOneFromDef('invoice', [invoiceId]); + }, + fetchClient(ticketId) { + return this.findOneFromDef('client', [ticketId]); + }, + fetchAddress(ticketId) { + return this.findOneFromDef(`address`, [ticketId]); + }, + fetchSignature(ticketId) { + return this.findOneFromDef('signature', [ticketId]); + }, + fetchTaxes(ticketId) { + return this.findOneFromDef(`taxes`, [ticketId]); + }, + fetchSales(ticketId) { + return this.rawSqlFromDef('sales', [ticketId]); + }, + fetchPackagings(ticketId) { + return this.rawSqlFromDef('packagings', [ticketId]); + }, + fetchServices(ticketId) { + return this.rawSqlFromDef('services', [ticketId]); + }, + + getSubTotal() { + let subTotal = 0.00; + this.sales.forEach(sale => { + subTotal += sale.quantity * sale.price * (1 - sale.discount / 100); + }); + + return subTotal; + }, + getTotalBase() { + let totalBase = 0.00; + this.taxes.forEach(tax => { + totalBase += parseFloat(tax.Base); + }); + + return totalBase; + }, + getTotalTax() { + let totalTax = 0.00; + this.taxes.forEach(tax => { + totalTax += parseFloat(tax.tax); + }); + + return totalTax; + }, + getTotal() { + return this.getTotalBase() + this.getTotalTax(); + }, + getBotanical() { + let phytosanitary = []; + this.sales.forEach(sale => { + if (sale.botanical) + phytosanitary.push(sale.botanical); + }); + + return phytosanitary.filter((item, index) => + phytosanitary.indexOf(item) == index + ).join(', '); + } + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build() + }, + props: { + invoiceId: { + type: String, + required: true + } + } +}; diff --git a/print/templates/reports/invoice/locale/es.yml b/print/templates/reports/invoice/locale/es.yml new file mode 100644 index 000000000..783babfb7 --- /dev/null +++ b/print/templates/reports/invoice/locale/es.yml @@ -0,0 +1,27 @@ +title: Factura +invoice: Factura +clientId: Cliente +deliveryAddress: Dirección de entrega +fiscalData: Datos fiscales +saleLines: Líneas de pedido +date: Fecha +reference: Ref. +quantity: Cant. +concept: Concepto +price: PVP/u +discount: Dto. +vat: IVA +amount: Importe +type: Tipo +taxBase: Base imp. +tax: Tasa +fee: Cuota +total: Total +subtotal: Subtotal +taxBreakdown: Desglose impositivo +packagings: Cubos y embalajes +services: Servicios +vatType: Tipo de IVA +digitalSignature: Firma digital +invoiceRef: Factura {0} +plantPassport: Pasaporte fitosanitario \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/address.sql b/print/templates/reports/invoice/sql/address.sql new file mode 100644 index 000000000..86414635a --- /dev/null +++ b/print/templates/reports/invoice/sql/address.sql @@ -0,0 +1,11 @@ +SELECT + a.nickname, + a.street, + a.postalCode, + a.city, + p.name province +FROM ticket t + JOIN address a ON a.clientFk = t.clientFk + AND a.id = t.addressFk + LEFT JOIN province p ON p.id = a.provinceFk +WHERE t.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/client.sql b/print/templates/reports/invoice/sql/client.sql new file mode 100644 index 000000000..5318a98c7 --- /dev/null +++ b/print/templates/reports/invoice/sql/client.sql @@ -0,0 +1,8 @@ +SELECT + c.id, + c.socialName, + c.street, + c.fi +FROM ticket t + JOIN client c ON c.id = t.clientFk +WHERE t.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/corrected.sql b/print/templates/reports/invoice/sql/corrected.sql new file mode 100644 index 000000000..4ea56f38d --- /dev/null +++ b/print/templates/reports/invoice/sql/corrected.sql @@ -0,0 +1,5 @@ +SELECT io.amount, io.ref, io.issued, ict.description +FROM vn.invoiceCorrection ic + JOIN vn.invoiceOut io ON io.id = ic.correctedFk + JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk +where ic.correctingFk = # \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoice.sql b/print/templates/reports/invoice/sql/invoice.sql new file mode 100644 index 000000000..3e141d660 --- /dev/null +++ b/print/templates/reports/invoice/sql/invoice.sql @@ -0,0 +1,17 @@ +SELECT + io.issued, + io.clientFk, + io.companyFk, + io.ref, + c.socialName, + c.street postalAddress, + cny.code companyCode, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + CONCAT(c.postcode,' - ',c.city) postcodeCity +FROM vn.invoiceOut io + JOIN vn.client c ON c.id = io.clientFk + JOIN vn.country cty ON cty.id = c.countryFk + JOIN company cny ON cny.id = io.companyFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + AND ios.taxAreaFk = 'CEE' +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/packagings.sql b/print/templates/reports/invoice/sql/packagings.sql new file mode 100644 index 000000000..75a82a0aa --- /dev/null +++ b/print/templates/reports/invoice/sql/packagings.sql @@ -0,0 +1,9 @@ +SELECT + tp.quantity, + i.name, + p.itemFk +FROM ticketPackaging tp + JOIN packaging p ON p.id = tp.packagingFk + JOIN item i ON i.id = p.itemFk +WHERE tp.ticketFk = ? +ORDER BY itemFk \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/sales.sql b/print/templates/reports/invoice/sql/sales.sql new file mode 100644 index 000000000..d17f6feee --- /dev/null +++ b/print/templates/reports/invoice/sql/sales.sql @@ -0,0 +1,42 @@ +SELECT + s.id, + s.itemFk, + s.concept, + s.quantity, + s.price, + s.price - SUM(IF(ctr.id = 6, sc.value, 0)) netPrice, + s.discount, + i.size, + i.stems, + i.category, + it.id itemTypeId, + o.code AS origin, + i.inkFk, + s.ticketFk, + tcl.code vatType, + ib.ediBotanic botanical, + i.tag5, + i.value5, + i.tag6, + i.value6, + i.tag7, + i.value7 +FROM vn.sale s + LEFT JOIN saleComponent sc ON sc.saleFk = s.id + LEFT JOIN component cr ON cr.id = sc.componentFk + LEFT JOIN componentType ctr ON ctr.id = cr.typeFk + LEFT JOIN item i ON i.id = s.itemFk + LEFT JOIN ticket t ON t.id = s.ticketFk + LEFT JOIN origin o ON o.id = i.originFk + LEFT JOIN country c ON c.id = o.countryFk + LEFT JOIN supplier sp ON sp.id = t.companyFk + LEFT JOIN itemType it ON it.id = i.typeFk + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN itemTaxCountry itc ON itc.itemFk = i.id + AND itc.countryFk = sp.countryFk + LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk + LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id + AND ic.code = 'plant' +WHERE s.ticketFk = ? +GROUP BY s.id +ORDER BY (it.isPackaging), s.concept, s.itemFk \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/services.sql b/print/templates/reports/invoice/sql/services.sql new file mode 100644 index 000000000..d64e8dc26 --- /dev/null +++ b/print/templates/reports/invoice/sql/services.sql @@ -0,0 +1,8 @@ +SELECT + tc.code taxDescription, + ts.description, + ts.quantity, + ts.price +FROM ticketService ts + JOIN taxClass tc ON tc.id = ts.taxClassFk +WHERE ts.ticketFk = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/signature.sql b/print/templates/reports/invoice/sql/signature.sql new file mode 100644 index 000000000..2eb83b3ac --- /dev/null +++ b/print/templates/reports/invoice/sql/signature.sql @@ -0,0 +1,8 @@ +SELECT + d.id, + d.created +FROM ticket t + JOIN ticketDms dt ON dt.ticketFk = t.id + JOIN dms d ON d.id = dt.dmsFk + AND d.file LIKE '%.png' +WHERE t.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/taxes.sql b/print/templates/reports/invoice/sql/taxes.sql new file mode 100644 index 000000000..576074df7 --- /dev/null +++ b/print/templates/reports/invoice/sql/taxes.sql @@ -0,0 +1,8 @@ +SELECT iot.* , pgc.*, IF(pe.equFk IS NULL, taxableBase, 0) AS Base, pgc.rate / 100 as vatPercent, ios.footNotes + FROM vn.invoiceOutTax iot + JOIN vn.pgc ON pgc.code = iot.pgcFk + LEFT JOIN vn.pgcEqu pe ON pe.equFk = pgc.code + JOIN vn.invoiceOut io ON io.id = iot.invoiceOutFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + WHERE invoiceOutFk = # + ORDER BY iot.id \ No newline at end of file From 20d9a99f6b4eb1cebb841f836fda1abeb5084717 Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 18 Feb 2021 13:01:01 +0100 Subject: [PATCH 02/21] Changes --- print/templates/reports/invoice/invoice.html | 7 +++++-- print/templates/reports/invoice/invoice.js | 2 +- print/templates/reports/invoice/locale/es.yml | 8 +++----- print/templates/reports/invoice/sql/client.sql | 18 +++++++++++------- .../templates/reports/invoice/sql/invoice.sql | 9 +-------- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html index a1015c28c..29477f4a3 100644 --- a/print/templates/reports/invoice/invoice.html +++ b/print/templates/reports/invoice/invoice.html @@ -40,10 +40,13 @@

{{client.socialName}}

- {{client.street}} + {{client.postalAddress}}
- {{client.fi}} + {{client.postcodeCity}} +
+
+ {{$t('fiscalId')}}: {{client.fi}}
diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js index ac4f62155..eee4eec96 100755 --- a/print/templates/reports/invoice/invoice.js +++ b/print/templates/reports/invoice/invoice.js @@ -9,7 +9,7 @@ module.exports = { name: 'invoice', async serverPrefetch() { this.invoice = await this.fetchInvoice(this.invoiceId); - this.client = {}; + this.client = await this.fetchClient(this.invoiceId); this.address = {}; this.sales = []; this.services = []; diff --git a/print/templates/reports/invoice/locale/es.yml b/print/templates/reports/invoice/locale/es.yml index 783babfb7..9b5694e3c 100644 --- a/print/templates/reports/invoice/locale/es.yml +++ b/print/templates/reports/invoice/locale/es.yml @@ -1,8 +1,9 @@ title: Factura invoice: Factura clientId: Cliente -deliveryAddress: Dirección de entrega -fiscalData: Datos fiscales +invoiceData: Datos de facturación +fiscalId: CIF / NIF +invoiceRef: Factura {0} saleLines: Líneas de pedido date: Fecha reference: Ref. @@ -21,7 +22,4 @@ subtotal: Subtotal taxBreakdown: Desglose impositivo packagings: Cubos y embalajes services: Servicios -vatType: Tipo de IVA -digitalSignature: Firma digital -invoiceRef: Factura {0} plantPassport: Pasaporte fitosanitario \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/client.sql b/print/templates/reports/invoice/sql/client.sql index 5318a98c7..dd6035222 100644 --- a/print/templates/reports/invoice/sql/client.sql +++ b/print/templates/reports/invoice/sql/client.sql @@ -1,8 +1,12 @@ SELECT - c.id, - c.socialName, - c.street, - c.fi -FROM ticket t - JOIN client c ON c.id = t.clientFk -WHERE t.id = ? \ No newline at end of file + c.id, + c.socialName, + c.street AS postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + CONCAT(c.postcode, ' - ', c.city) postcodeCity +FROM vn.invoiceOut io + JOIN vn.client c ON c.id = io.clientFk + JOIN vn.country cty ON cty.id = c.countryFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + AND ios.taxAreaFk = 'CEE' +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoice.sql b/print/templates/reports/invoice/sql/invoice.sql index 3e141d660..599e34db1 100644 --- a/print/templates/reports/invoice/sql/invoice.sql +++ b/print/templates/reports/invoice/sql/invoice.sql @@ -3,15 +3,8 @@ SELECT io.clientFk, io.companyFk, io.ref, - c.socialName, - c.street postalAddress, - cny.code companyCode, - IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, - CONCAT(c.postcode,' - ',c.city) postcodeCity + cny.code companyCode FROM vn.invoiceOut io JOIN vn.client c ON c.id = io.clientFk - JOIN vn.country cty ON cty.id = c.countryFk JOIN company cny ON cny.id = io.companyFk - LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial - AND ios.taxAreaFk = 'CEE' WHERE io.id = ? \ No newline at end of file From bb45d780624d73c4591ae08184b912924bdd74bd Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 24 Feb 2021 07:51:05 +0100 Subject: [PATCH 03/21] Changes made --- print/core/database.js | 5 +- print/core/mixins/db-helper.js | 5 +- print/methods/report.js | 13 +- .../reports/delivery-note/sql/sales.sql | 1 + .../reports/invoice/assets/css/style.css | 23 +- print/templates/reports/invoice/invoice.html | 296 ++++++++++-------- print/templates/reports/invoice/invoice.js | 145 ++++----- print/templates/reports/invoice/locale/es.yml | 18 +- .../templates/reports/invoice/sql/address.sql | 11 - .../reports/invoice/sql/corrected.sql | 5 - .../reports/invoice/sql/intrastat.sql | 88 ++++++ .../templates/reports/invoice/sql/invoice.sql | 12 +- .../reports/invoice/sql/invoiceTickets.sql | 20 ++ .../reports/invoice/sql/packagings.sql | 9 - .../reports/invoice/sql/phytosanitary.sql | 14 + .../reports/invoice/sql/rectified.sql | 9 + print/templates/reports/invoice/sql/sales.sql | 93 +++--- .../reports/invoice/sql/services.sql | 8 - .../reports/invoice/sql/signature.sql | 8 - print/templates/reports/invoice/sql/taxes.sql | 17 +- .../templates/reports/invoice/sql/tickets.sql | 7 + 21 files changed, 491 insertions(+), 316 deletions(-) delete mode 100644 print/templates/reports/invoice/sql/address.sql delete mode 100644 print/templates/reports/invoice/sql/corrected.sql create mode 100644 print/templates/reports/invoice/sql/intrastat.sql create mode 100644 print/templates/reports/invoice/sql/invoiceTickets.sql delete mode 100644 print/templates/reports/invoice/sql/packagings.sql create mode 100644 print/templates/reports/invoice/sql/phytosanitary.sql create mode 100644 print/templates/reports/invoice/sql/rectified.sql delete mode 100644 print/templates/reports/invoice/sql/services.sql delete mode 100644 print/templates/reports/invoice/sql/signature.sql create mode 100644 print/templates/reports/invoice/sql/tickets.sql diff --git a/print/core/database.js b/print/core/database.js index e879d0e3a..dee7a2933 100644 --- a/print/core/database.js +++ b/print/core/database.js @@ -36,13 +36,14 @@ module.exports = { * Makes a query from a SQL file * @param {String} queryName - The SQL file name * @param {Object} params - Parameterized values + * @param {Object} connection - Optional pool connection * * @return {Object} - Result promise */ - rawSqlFromDef(queryName, params) { + rawSqlFromDef(queryName, params, connection) { const query = fs.readFileSync(`${queryName}.sql`, 'utf8'); - return this.rawSql(query, params); + return this.rawSql(query, params, connection); }, /** diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js index 4a6f9e460..ee45d65d4 100644 --- a/print/core/mixins/db-helper.js +++ b/print/core/mixins/db-helper.js @@ -19,12 +19,13 @@ const dbHelper = { * Makes a query from a SQL file * @param {String} queryName - The SQL file name * @param {Object} params - Parameterized values + * @param {Object} connection - Optional pool connection * * @return {Object} - Result promise */ - rawSqlFromDef(queryName, params) { + rawSqlFromDef(queryName, params, connection) { const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); - return db.rawSqlFromDef(absolutePath, params); + return db.rawSqlFromDef(absolutePath, params, connection); }, /** diff --git a/print/methods/report.js b/print/methods/report.js index eea249a42..750fec4c8 100644 --- a/print/methods/report.js +++ b/print/methods/report.js @@ -6,11 +6,16 @@ module.exports = app => { const reportName = req.params.name; const fileName = getFileName(reportName, req.args); const report = new Report(reportName, req.args); - const stream = await report.toPdfStream(); + if (req.args.preview) { + const template = await report.render(); + res.send(template); + } else { + const stream = await report.toPdfStream(); - res.setHeader('Content-type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="${fileName}"`); - res.end(stream); + res.setHeader('Content-type', 'application/pdf'); + res.setHeader('Content-Disposition', `inline; filename="${fileName}"`); + res.end(stream); + } } catch (error) { next(error); } diff --git a/print/templates/reports/delivery-note/sql/sales.sql b/print/templates/reports/delivery-note/sql/sales.sql index d17f6feee..c449030cf 100644 --- a/print/templates/reports/delivery-note/sql/sales.sql +++ b/print/templates/reports/delivery-note/sql/sales.sql @@ -37,6 +37,7 @@ FROM vn.sale s LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id AND ic.code = 'plant' + AND ib.ediBotanic IS NOT NULL WHERE s.ticketFk = ? GROUP BY s.id ORDER BY (it.isPackaging), s.concept, s.itemFk \ No newline at end of file diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css index cbe894097..6e2a495e8 100644 --- a/print/templates/reports/invoice/assets/css/style.css +++ b/print/templates/reports/invoice/assets/css/style.css @@ -1,23 +1,16 @@ -#signature { - padding-right: 10px -} - -#signature img { - -webkit-filter: brightness(0%); - filter: brightness(0%); - margin-bottom: 10px; - max-width: 150px -} - -.description strong { - text-transform: uppercase; -} - h2 { font-weight: 100; color: #555 } +.table-title { + margin-bottom: 15px +} + +.table-title h2 { + margin: 0 15px 0 0 +} + .ticket-info { font-size: 26px } diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html index 29477f4a3..e21507f7d 100644 --- a/print/templates/reports/invoice/invoice.html +++ b/print/templates/reports/invoice/invoice.html @@ -5,6 +5,11 @@ + + + + + @@ -12,7 +17,7 @@
-
+

{{$t('title')}}

@@ -52,122 +57,105 @@
+ + +
+

{{$t('rectifiedInvoices')}}

+ + + + + + + + + + + + + + + + + +
{{$t('invoice')}}{{$t('issued')}}{{$t('amount')}}{{$t('description')}}
{{row.ref}}{{row.issued | date}}{{row.amount | currency('EUR', $i18n.locale)}}{{row.description}}
+
+ -

{{$t('saleLines')}}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('discount')}}{{$t('vat')}}{{$t('amount')}}
{{sale.itemFk | zerofill('000000')}}{{sale.quantity}}{{sale.concept}}{{sale.price | currency('EUR', $i18n.locale)}}{{(sale.discount / 100) | percentage}}{{sale.vatType}}{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', $i18n.locale)}}
- - {{sale.tag5}} {{sale.value5}} - - - {{sale.tag6}} {{sale.value6}} - - - {{sale.tag7}} {{sale.value7}} - -
- {{$t('subtotal')}} - {{getSubTotal() | currency('EUR', $i18n.locale)}}
+
+
+
+

{{$t('deliveryNote')}} +

+
+
+ {{ticket.id}} +
+
+
+

Shipped

+
+
+
+ {{ticket.shipped | date}} +
+
+ +

{{ticket.nickname}}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('discount')}}{{$t('vat')}}{{$t('amount')}}
{{sale.itemFk | zerofill('000000')}}{{sale.quantity}}{{sale.concept}}{{sale.price | currency('EUR', $i18n.locale)}}{{(sale.discount / 100) | percentage}}{{sale.vatType}}{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', $i18n.locale)}}
+ + {{sale.tag5}} {{sale.value5}} + + + {{sale.tag6}} {{sale.value6}} + + + {{sale.tag7}} {{sale.value7}} + +
+ {{$t('subtotal')}} + {{subTotal(ticket) | currency('EUR', $i18n.locale)}}
+
-
- -
-

{{$t('services')}}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('vat')}}{{$t('amount')}}
{{service.quantity}}{{service.description}}{{service.price | currency('EUR', $i18n.locale)}}{{service.taxDescription}}{{service.price | currency('EUR', $i18n.locale)}}
- {{$t('subtotal')}} - {{serviceTotal | currency('EUR', $i18n.locale)}}
-
- -
-
- -
-

{{$t('packagings')}}

- - - - - - - - - - - - - - - -
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}
{{packaging.itemFk | zerofill('000000')}}{{packaging.quantity}}{{packaging.name}}
-
- -
- @@ -188,32 +176,39 @@ - + - + - +
{{tax.name}} - {{tax.Base | currency('EUR', $i18n.locale)}} + {{tax.base | currency('EUR', $i18n.locale)}} {{tax.vatPercent | percentage}}{{tax.tax | currency('EUR', $i18n.locale)}}{{tax.vat | currency('EUR', $i18n.locale)}}
{{$t('subtotal')}} - {{getTotalBase() | currency('EUR', $i18n.locale)}} + {{sumTotal(taxes, 'base') | currency('EUR', $i18n.locale)}} {{getTotalTax()| currency('EUR', $i18n.locale)}}{{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}}
{{$t('total')}}{{getTotal() | currency('EUR', $i18n.locale)}}{{taxTotal | currency('EUR', $i18n.locale)}}
+ +
+
{{$t('notes')}}
+
+ {{invoice.footNotes}} +
+
- +
-
- - +
+

{{$t('intrastat')}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('code')}}{{$t('description')}}{{$t('stems')}}{{$t('netKg')}}{{$t('amount')}}
{{row.code}}{{row.description}}{{row.stems | number($i18n.locale)}}{{row.netKg | number($i18n.locale)}}{{row.subtotal | currency('EUR', $i18n.locale)}}
+ {{sumTotal(intrastat, 'stems') | number($i18n.locale)}} + + {{sumTotal(intrastat, 'netKg') | number($i18n.locale)}} + + {{sumTotal(intrastat, 'subtotal') | currency('EUR', $i18n.locale)}} +
+
+ + + +
+
+
+
{{$t('observations')}}
+
+
{{$t('wireTransfer')}}
+
{{$t('accountNumber', [invoice.iban])}}
-
--> - +
+ +
diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js index eee4eec96..72f27a69a 100755 --- a/print/templates/reports/invoice/invoice.js +++ b/print/templates/reports/invoice/invoice.js @@ -1,30 +1,34 @@ -const config = require(`${appPath}/core/config`); const Component = require(`${appPath}/core/component`); +const Report = require(`${appPath}/core/report`); const reportHeader = new Component('report-header'); const reportFooter = new Component('report-footer'); -const md5 = require('md5'); -const fs = require('fs-extra'); +const db = require(`${appPath}/core/database`); module.exports = { name: 'invoice', async serverPrefetch() { this.invoice = await this.fetchInvoice(this.invoiceId); this.client = await this.fetchClient(this.invoiceId); - this.address = {}; - this.sales = []; - this.services = []; - this.taxes = []; - this.packagings = []; - /* this.client = await this.fetchClient(this.ticketId); - this.ticket = await this.fetchTicket(this.ticketId); - this.sales = await this.fetchSales(this.ticketId); - this.address = await this.fetchAddress(this.ticketId); - this.services = await this.fetchServices(this.ticketId); - this.taxes = await this.fetchTaxes(this.ticketId); - this.packagings = await this.fetchPackagings(this.ticketId); - this.signature = await this.fetchSignature(this.ticketId); + this.taxes = await this.fetchTaxes(this.invoiceId); + this.intrastat = await this.fetchIntrastat(this.invoiceId); + this.rectified = await this.fetchRectified(this.invoiceId); + + const tickets = await this.fetchTickets(this.invoiceId); + const sales = await this.fetchSales(this.invoiceId); + + const map = new Map(); + + for (let ticket of tickets) + map.set(ticket.id, ticket); + + for (let sale of sales) { + const ticket = map.get(sale.ticketFk); + if (!ticket.sales) ticket.sales = []; + ticket.sales.push(sale); + } + + this.tickets = tickets; - */ if (!this.invoice) throw new Error('Something went wrong'); }, @@ -32,16 +36,30 @@ module.exports = { return {totalBalance: 0.00}; }, computed: { - /* dmsPath() { - if (!this.signature) return; + ticketsId() { + const tickets = this.tickets.map(ticket => ticket.id); - const hash = md5(this.signature.id.toString()).substring(0, 3); - const file = `${config.storage.root}/${hash}/${this.signature.id}.png`; - const src = fs.readFileSync(file); - const base64 = Buffer.from(src, 'utf8').toString('base64'); - - return `data:image/png;base64, ${base64}`; + return tickets.join(', '); }, + botanical() { + let phytosanitary = []; + for (let ticket of this.tickets) { + for (let sale of ticket.sales) { + if (sale.botanical) + phytosanitary.push(sale.botanical); + } + } + + return phytosanitary.filter((item, index) => + phytosanitary.indexOf(item) == index + ).join(', '); + }, + taxTotal() { + const base = this.sumTotal(this.taxes, 'base'); + const vat = this.sumTotal(this.taxes, 'vat'); + return base + vat; + } + /* serviceTotal() { let total = 0.00; this.services.forEach(service => { @@ -55,65 +73,50 @@ module.exports = { fetchInvoice(invoiceId) { return this.findOneFromDef('invoice', [invoiceId]); }, - fetchClient(ticketId) { - return this.findOneFromDef('client', [ticketId]); + fetchClient(invoiceId) { + return this.findOneFromDef('client', [invoiceId]); }, - fetchAddress(ticketId) { - return this.findOneFromDef(`address`, [ticketId]); - }, - fetchSignature(ticketId) { - return this.findOneFromDef('signature', [ticketId]); - }, - fetchTaxes(ticketId) { - return this.findOneFromDef(`taxes`, [ticketId]); - }, - fetchSales(ticketId) { - return this.rawSqlFromDef('sales', [ticketId]); - }, - fetchPackagings(ticketId) { - return this.rawSqlFromDef('packagings', [ticketId]); - }, - fetchServices(ticketId) { - return this.rawSqlFromDef('services', [ticketId]); + fetchTickets(invoiceId) { + return this.rawSqlFromDef('tickets', [invoiceId]); }, + async fetchSales(invoiceId) { + const connection = await db.pool.getConnection(); + await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection); + await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection); - getSubTotal() { + const sales = this.rawSqlFromDef('sales', connection); + + await this.rawSql(`DROP TEMPORARY TABLE tmp.invoiceTickets`, connection); + await connection.release(); + + return sales; + }, + fetchTaxes(invoiceId) { + return this.rawSqlFromDef(`taxes`, [invoiceId]); + }, + fetchIntrastat(invoiceId) { + return this.rawSqlFromDef(`intrastat`, [invoiceId]); + }, + fetchRectified(invoiceId) { + return this.rawSqlFromDef(`rectified`, [invoiceId]); + }, + subTotal(ticket) { let subTotal = 0.00; - this.sales.forEach(sale => { + ticket.sales.forEach(sale => { subTotal += sale.quantity * sale.price * (1 - sale.discount / 100); }); return subTotal; }, - getTotalBase() { - let totalBase = 0.00; - this.taxes.forEach(tax => { - totalBase += parseFloat(tax.Base); - }); - - return totalBase; - }, - getTotalTax() { - let totalTax = 0.00; - this.taxes.forEach(tax => { - totalTax += parseFloat(tax.tax); - }); - - return totalTax; - }, getTotal() { return this.getTotalBase() + this.getTotalTax(); }, - getBotanical() { - let phytosanitary = []; - this.sales.forEach(sale => { - if (sale.botanical) - phytosanitary.push(sale.botanical); - }); + sumTotal(rows, prop) { + let total = 0.00; + for (let row of rows) + total += parseFloat(row[prop]); - return phytosanitary.filter((item, index) => - phytosanitary.indexOf(item) == index - ).join(', '); + return total; } }, components: { diff --git a/print/templates/reports/invoice/locale/es.yml b/print/templates/reports/invoice/locale/es.yml index 9b5694e3c..6fdfc8a14 100644 --- a/print/templates/reports/invoice/locale/es.yml +++ b/print/templates/reports/invoice/locale/es.yml @@ -4,7 +4,8 @@ clientId: Cliente invoiceData: Datos de facturación fiscalId: CIF / NIF invoiceRef: Factura {0} -saleLines: Líneas de pedido +deliveryNote: Albarán +shipped: F. envío date: Fecha reference: Ref. quantity: Cant. @@ -20,6 +21,15 @@ fee: Cuota total: Total subtotal: Subtotal taxBreakdown: Desglose impositivo -packagings: Cubos y embalajes -services: Servicios -plantPassport: Pasaporte fitosanitario \ No newline at end of file +notes: Notas +intrastat: Intrastat +code: Código +description: Descripción +stems: Tallos +netKg: KG Neto +rectifiedInvoices: Facturas rectificadas +issued: F. emisión +plantPassport: Pasaporte fitosanitario +observations: Observaciones +wireTransfer: "Forma de pago: Transferencia" +accountNumber: "Número de cuenta: {0}" \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/address.sql b/print/templates/reports/invoice/sql/address.sql deleted file mode 100644 index 86414635a..000000000 --- a/print/templates/reports/invoice/sql/address.sql +++ /dev/null @@ -1,11 +0,0 @@ -SELECT - a.nickname, - a.street, - a.postalCode, - a.city, - p.name province -FROM ticket t - JOIN address a ON a.clientFk = t.clientFk - AND a.id = t.addressFk - LEFT JOIN province p ON p.id = a.provinceFk -WHERE t.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/corrected.sql b/print/templates/reports/invoice/sql/corrected.sql deleted file mode 100644 index 4ea56f38d..000000000 --- a/print/templates/reports/invoice/sql/corrected.sql +++ /dev/null @@ -1,5 +0,0 @@ -SELECT io.amount, io.ref, io.issued, ict.description -FROM vn.invoiceCorrection ic - JOIN vn.invoiceOut io ON io.id = ic.correctedFk - JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk -where ic.correctingFk = # \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/intrastat.sql b/print/templates/reports/invoice/sql/intrastat.sql new file mode 100644 index 000000000..ca2ed3f33 --- /dev/null +++ b/print/templates/reports/invoice/sql/intrastat.sql @@ -0,0 +1,88 @@ +SELECT + ir.id AS code, + ir.description AS description, + CAST(SUM(IFNULL(i.stems,1) * s.quantity) AS DECIMAL(10,2)) as stems, + CAST(SUM( weight) AS DECIMAL(10,2)) as netKg, + CAST(SUM((s.quantity * s.price * (100 - s.discount) / 100 )) AS DECIMAL(10,2)) AS subtotal + FROM vn.sale s + LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id + LEFT JOIN vn.ticket t ON t.id = s.ticketFk + LEFT JOIN vn.invoiceOut io ON io.ref = t.refFk + LEFT JOIN vn.item i ON i.id = s.itemFk + JOIN vn.intrastat ir ON ir.id = i.intrastatFk + WHERE io.id = ? + GROUP BY i.intrastatFk; + + +/* SELECT io.issued, + c.socialName, + c.street postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + io.clientFk, + c.postcode, + c.city, + io.companyFk, + io.ref, + tc.code, + s.concept, + s.quantity, + s.price, + s.discount, + s.ticketFk, + t.shipped, + t.refFk, + a.nickname, + s.itemFk, + s.id saleFk, + pm.name AS pmname, + sa.iban, + c.phone, + MAX(t.packages) packages, + a.incotermsFk, + ic.name incotermsName , + sub.description weight, + t.observations, + ca.fiscalName customsAgentName, + ca.street customsAgentStreet, + ca.nif customsAgentNif, + ca.phone customsAgentPhone, + ca.email customsAgentEmail, + CAST(sub2.volume AS DECIMAL (10,2)) volume, + sub3.intrastat + FROM vn.invoiceOut io + JOIN vn.supplier su ON su.id = io.companyFk + JOIN vn.client c ON c.id = io.clientFk + LEFT JOIN vn.province p ON p.id = c.provinceFk + JOIN vn.ticket t ON t.refFk = io.ref + LEFT JOIN (SELECT tob.ticketFk,tob.description + FROM vn.ticketObservation tob + LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk + WHERE ot.description = "Peso Aduana" + )sub ON sub.ticketFk = t.id + JOIN vn.address a ON a.id = t.addressFk + LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk + LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN (SELECT SUM(volume) volume + FROM vn.invoiceOut io + JOIN vn.ticket t ON t.refFk = io.ref + JOIN vn.saleVolume sv ON sv.ticketFk = t.id + WHERE io.id = :invoiceId + )sub2 ON TRUE + JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk + JOIN vn.taxClass tc ON tc.id = itc.taxClassFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE' + JOIN vn.country cty ON cty.id = c.countryFk + JOIN vn.payMethod pm ON pm.id = c .payMethodFk + JOIN vn.company co ON co.id=io.companyFk + JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk + LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat + FROM vn.ticket t + JOIN vn.invoiceOut io ON io.ref = t.refFk + JOIN vn.sale s ON t.id = s.ticketFk + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.intrastat ir ON ir.id = i.intrastatFk + WHERE io.id = :invoiceId + )sub3 ON TRUE + WHERE io.id = :invoiceId + */ \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoice.sql b/print/templates/reports/invoice/sql/invoice.sql index 599e34db1..aacbb0016 100644 --- a/print/templates/reports/invoice/sql/invoice.sql +++ b/print/templates/reports/invoice/sql/invoice.sql @@ -3,8 +3,14 @@ SELECT io.clientFk, io.companyFk, io.ref, - cny.code companyCode -FROM vn.invoiceOut io - JOIN vn.client c ON c.id = io.clientFk + pm.code AS payMethodCode, + cny.code companyCode, + sa.iban, + ios.footNotes +FROM invoiceOut io + JOIN client c ON c.id = io.clientFk + JOIN payMethod pm ON pm.id = c.payMethodFk JOIN company cny ON cny.id = io.companyFk + JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk + LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoiceTickets.sql b/print/templates/reports/invoice/sql/invoiceTickets.sql new file mode 100644 index 000000000..089911a63 --- /dev/null +++ b/print/templates/reports/invoice/sql/invoiceTickets.sql @@ -0,0 +1,20 @@ +CREATE TEMPORARY TABLE tmp.invoiceTickets + ENGINE = MEMORY + SELECT + t.id AS ticketFk, + t.clientFk, + t.shipped, + t.nickname, + io.ref, + c.socialName, + sa.iban, + pm.name AS payMethod, + su.countryFk AS supplierCountryFk + FROM vn.invoiceOut io + JOIN vn.supplier su ON su.id = io.companyFk + JOIN vn.ticket t ON t.refFk = io.ref + JOIN vn.client c ON c.id = t.clientFk + JOIN vn.payMethod pm ON pm.id = c.payMethodFk + JOIN vn.company co ON co.id = io.companyFk + JOIN vn.supplierAccount sa ON sa.id = co.supplierAccountFk + WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/packagings.sql b/print/templates/reports/invoice/sql/packagings.sql deleted file mode 100644 index 75a82a0aa..000000000 --- a/print/templates/reports/invoice/sql/packagings.sql +++ /dev/null @@ -1,9 +0,0 @@ -SELECT - tp.quantity, - i.name, - p.itemFk -FROM ticketPackaging tp - JOIN packaging p ON p.id = tp.packagingFk - JOIN item i ON i.id = p.itemFk -WHERE tp.ticketFk = ? -ORDER BY itemFk \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/phytosanitary.sql b/print/templates/reports/invoice/sql/phytosanitary.sql new file mode 100644 index 000000000..1ae178975 --- /dev/null +++ b/print/templates/reports/invoice/sql/phytosanitary.sql @@ -0,0 +1,14 @@ +SELECT CONCAT( 'A ', GROUP_CONCAT(DISTINCT(ib.ediBotanic) SEPARATOR ', '), CHAR(13,10), CHAR(13,10), + 'B ES17462130', CHAR(13,10), CHAR(13,10), + 'C ', GROUP_CONCAT(DISTINCT(t.id) SEPARATOR ', '), CHAR(13,10), CHAR(13,10), + 'D ES' ) phytosanitary + FROM vn.ticket t + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.itemType it ON it.id = i.typeFk + JOIN vn.itemCategory ic ON ic.id = it.categoryFk + JOIN vn.itemBotanicalWithGenus ib ON ib.itemfk = i.id + WHERE t.refFk = # AND + ic.`code` = 'plant' AND + ib.ediBotanic IS NOT NULL + ORDER BY ib.ediBotanic \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/rectified.sql b/print/templates/reports/invoice/sql/rectified.sql new file mode 100644 index 000000000..1255b973c --- /dev/null +++ b/print/templates/reports/invoice/sql/rectified.sql @@ -0,0 +1,9 @@ +SELECT + io.amount, + io.ref, + io.issued, + ict.description +FROM vn.invoiceCorrection ic + JOIN vn.invoiceOut io ON io.id = ic.correctedFk + JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk +where ic.correctingFk = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/sales.sql b/print/templates/reports/invoice/sql/sales.sql index d17f6feee..0665fc0ff 100644 --- a/print/templates/reports/invoice/sql/sales.sql +++ b/print/templates/reports/invoice/sql/sales.sql @@ -1,42 +1,59 @@ -SELECT - s.id, - s.itemFk, - s.concept, - s.quantity, - s.price, - s.price - SUM(IF(ctr.id = 6, sc.value, 0)) netPrice, - s.discount, - i.size, - i.stems, - i.category, - it.id itemTypeId, - o.code AS origin, - i.inkFk, - s.ticketFk, - tcl.code vatType, - ib.ediBotanic botanical, - i.tag5, - i.value5, - i.tag6, - i.value6, - i.tag7, - i.value7 -FROM vn.sale s - LEFT JOIN saleComponent sc ON sc.saleFk = s.id - LEFT JOIN component cr ON cr.id = sc.componentFk - LEFT JOIN componentType ctr ON ctr.id = cr.typeFk - LEFT JOIN item i ON i.id = s.itemFk - LEFT JOIN ticket t ON t.id = s.ticketFk - LEFT JOIN origin o ON o.id = i.originFk - LEFT JOIN country c ON c.id = o.countryFk - LEFT JOIN supplier sp ON sp.id = t.companyFk +SELECT + it.ref, + it.socialName, + it.iban, + it.payMethod, + it.clientFk, + it.shipped, + it.nickname, + s.ticketFk, + s.itemFk, + s.concept, + s.quantity, + s.price, + s.discount, + i.tag5, + i.value5, + i.tag6, + i.value6, + i.tag7, + i.value7, + tc.code AS vatType, + ib.ediBotanic botanical + FROM tmp.invoiceTickets it + JOIN vn.sale s ON s.ticketFk = it.ticketFk + JOIN item i ON i.id = s.itemFk LEFT JOIN itemType it ON it.id = i.typeFk LEFT JOIN itemCategory ic ON ic.id = it.categoryFk - LEFT JOIN itemTaxCountry itc ON itc.itemFk = i.id - AND itc.countryFk = sp.countryFk - LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id AND ic.code = 'plant' -WHERE s.ticketFk = ? -GROUP BY s.id -ORDER BY (it.isPackaging), s.concept, s.itemFk \ No newline at end of file + AND ib.ediBotanic IS NOT NULL + JOIN vn.itemTaxCountry itc ON itc.countryFk = it.supplierCountryFk + AND itc.itemFk = s.itemFk + JOIN vn.taxClass tc ON tc.id = itc.taxClassFk +UNION ALL +SELECT + it.ref, + it.socialName, + it.iban, + it.payMethod, + it.clientFk, + it.shipped, + it.nickname, + it.ticketFk, + '', + ts.description concept, + ts.quantity, + ts.price, + 0 discount, + NULL AS tag5, + NULL AS value5, + NULL AS tag6, + NULL AS value6, + NULL AS tag7, + NULL AS value7, + tc.code AS vatType, + NULL AS botanical + FROM tmp.invoiceTickets it + JOIN vn.ticketService ts ON ts.ticketFk = it.ticketFk + JOIN vn.taxClass tc ON tc.id = ts.taxClassFk \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/services.sql b/print/templates/reports/invoice/sql/services.sql deleted file mode 100644 index d64e8dc26..000000000 --- a/print/templates/reports/invoice/sql/services.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - tc.code taxDescription, - ts.description, - ts.quantity, - ts.price -FROM ticketService ts - JOIN taxClass tc ON tc.id = ts.taxClassFk -WHERE ts.ticketFk = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/signature.sql b/print/templates/reports/invoice/sql/signature.sql deleted file mode 100644 index 2eb83b3ac..000000000 --- a/print/templates/reports/invoice/sql/signature.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - d.id, - d.created -FROM ticket t - JOIN ticketDms dt ON dt.ticketFk = t.id - JOIN dms d ON d.id = dt.dmsFk - AND d.file LIKE '%.png' -WHERE t.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/taxes.sql b/print/templates/reports/invoice/sql/taxes.sql index 576074df7..19b1cc00e 100644 --- a/print/templates/reports/invoice/sql/taxes.sql +++ b/print/templates/reports/invoice/sql/taxes.sql @@ -1,8 +1,11 @@ -SELECT iot.* , pgc.*, IF(pe.equFk IS NULL, taxableBase, 0) AS Base, pgc.rate / 100 as vatPercent, ios.footNotes - FROM vn.invoiceOutTax iot - JOIN vn.pgc ON pgc.code = iot.pgcFk - LEFT JOIN vn.pgcEqu pe ON pe.equFk = pgc.code - JOIN vn.invoiceOut io ON io.id = iot.invoiceOutFk - LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial - WHERE invoiceOutFk = # +SELECT + iot.vat, + pgc.name, + IF(pe.equFk IS NULL, taxableBase, 0) AS base, + pgc.rate / 100 AS vatPercent + FROM invoiceOutTax iot + JOIN pgc ON pgc.code = iot.pgcFk + LEFT JOIN pgcEqu pe ON pe.equFk = pgc.code + JOIN invoiceOut io ON io.id = iot.invoiceOutFk + WHERE invoiceOutFk = ? ORDER BY iot.id \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/tickets.sql b/print/templates/reports/invoice/sql/tickets.sql new file mode 100644 index 000000000..feca81ead --- /dev/null +++ b/print/templates/reports/invoice/sql/tickets.sql @@ -0,0 +1,7 @@ +SELECT + t.id, + t.shipped, + t.nickname +FROM invoiceOut io + JOIN ticket t ON t.refFk = io.ref +WHERE io.id = ? \ No newline at end of file From cc00587b66d6eedd3eb6421ee6d8041abab36d9a Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 25 Feb 2021 09:00:49 +0100 Subject: [PATCH 04/21] Small refactor --- print/common/css/report.css | 2 +- .../delivery-note/assets/css/style.css | 2 +- .../reports/invoice/assets/css/style.css | 10 ++++++-- print/templates/reports/invoice/invoice.html | 8 +++---- print/templates/reports/invoice/invoice.js | 24 +++++++------------ 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/print/common/css/report.css b/print/common/css/report.css index 9331481f4..e8161f1fb 100644 --- a/print/common/css/report.css +++ b/print/common/css/report.css @@ -9,6 +9,6 @@ body { .title { margin-bottom: 20px; font-weight: 100; - font-size: 3em; + font-size: 2.6rem; margin-top: 0 } \ No newline at end of file diff --git a/print/templates/reports/delivery-note/assets/css/style.css b/print/templates/reports/delivery-note/assets/css/style.css index cbe894097..f99c385fa 100644 --- a/print/templates/reports/delivery-note/assets/css/style.css +++ b/print/templates/reports/delivery-note/assets/css/style.css @@ -19,7 +19,7 @@ h2 { } .ticket-info { - font-size: 26px + font-size: 22px } #phytosanitary { diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css index 6e2a495e8..cd605db9b 100644 --- a/print/templates/reports/invoice/assets/css/style.css +++ b/print/templates/reports/invoice/assets/css/style.css @@ -4,7 +4,8 @@ h2 { } .table-title { - margin-bottom: 15px + margin-bottom: 15px; + font-size: 0.8rem } .table-title h2 { @@ -12,7 +13,12 @@ h2 { } .ticket-info { - font-size: 26px + font-size: 22px +} + +#nickname h2 { + max-width: 400px; + word-wrap: break-word } #phytosanitary { diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html index e21507f7d..fa888dafc 100644 --- a/print/templates/reports/invoice/invoice.html +++ b/print/templates/reports/invoice/invoice.html @@ -101,8 +101,8 @@ {{ticket.shipped | date}} - -

{{ticket.nickname}}

+ +

{{ticket.nickname}}

@@ -125,7 +125,7 @@ - + - +
{{sale.price | currency('EUR', $i18n.locale)}} {{(sale.discount / 100) | percentage}} {{sale.vatType}}{{sale.price * sale.quantity * (1 - sale.discount / 100) | currency('EUR', $i18n.locale)}}{{saleImport(sale) | currency('EUR', $i18n.locale)}}
@@ -146,7 +146,7 @@ {{$t('subtotal')}} {{subTotal(ticket) | currency('EUR', $i18n.locale)}}{{ticketSubtotal(ticket) | currency('EUR', $i18n.locale)}}
diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js index 72f27a69a..d6f490333 100755 --- a/print/templates/reports/invoice/invoice.js +++ b/print/templates/reports/invoice/invoice.js @@ -59,15 +59,6 @@ module.exports = { const vat = this.sumTotal(this.taxes, 'vat'); return base + vat; } - /* - serviceTotal() { - let total = 0.00; - this.services.forEach(service => { - total += parseFloat(service.price) * service.quantity; - }); - - return total; - } */ }, methods: { fetchInvoice(invoiceId) { @@ -100,17 +91,18 @@ module.exports = { fetchRectified(invoiceId) { return this.rawSqlFromDef(`rectified`, [invoiceId]); }, - subTotal(ticket) { + saleImport(sale) { + const price = sale.quantity * sale.price; + + return price * (1 - sale.discount / 100); + }, + ticketSubtotal(ticket) { let subTotal = 0.00; - ticket.sales.forEach(sale => { - subTotal += sale.quantity * sale.price * (1 - sale.discount / 100); - }); + for (let sale of ticket.sales) + subTotal += this.saleImport(sale); return subTotal; }, - getTotal() { - return this.getTotalBase() + this.getTotalTax(); - }, sumTotal(rows, prop) { let total = 0.00; for (let row of rows) From 1e36b460a130c05798f9b162c450a44cab8fd73b Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 1 Mar 2021 11:25:37 +0100 Subject: [PATCH 05/21] Added invoice incoterms report --- print/common/css/misc.css | 4 + print/core/component.js | 7 +- print/core/mixins/db-helper.js | 6 +- .../invoice-incoterms/assets/css/import.js | 9 ++ .../invoice-incoterms/assets/css/style.css | 29 ++++ .../invoice-incoterms/invoice-incoterms.html | 124 ++++++++++++++++++ .../invoice-incoterms/invoice-incoterms.js | 40 ++++++ .../reports/invoice-incoterms/locale/es.yml | 16 +++ .../reports/invoice-incoterms/sql/client.sql | 12 ++ .../invoice-incoterms/sql/incoterms.sql | 71 ++++++++++ .../reports/invoice-incoterms/sql/invoice.sql | 17 +++ print/templates/reports/invoice/invoice.html | 8 +- print/templates/reports/invoice/invoice.js | 8 +- .../reports/invoice/sql/hasIncoterms.sql | 8 ++ 14 files changed, 351 insertions(+), 8 deletions(-) create mode 100644 print/templates/reports/invoice-incoterms/assets/css/import.js create mode 100644 print/templates/reports/invoice-incoterms/assets/css/style.css create mode 100644 print/templates/reports/invoice-incoterms/invoice-incoterms.html create mode 100755 print/templates/reports/invoice-incoterms/invoice-incoterms.js create mode 100644 print/templates/reports/invoice-incoterms/locale/es.yml create mode 100644 print/templates/reports/invoice-incoterms/sql/client.sql create mode 100644 print/templates/reports/invoice-incoterms/sql/incoterms.sql create mode 100644 print/templates/reports/invoice-incoterms/sql/invoice.sql create mode 100644 print/templates/reports/invoice/sql/hasIncoterms.sql diff --git a/print/common/css/misc.css b/print/common/css/misc.css index 09d7706b3..df8bf571a 100644 --- a/print/common/css/misc.css +++ b/print/common/css/misc.css @@ -45,4 +45,8 @@ .no-page-break { page-break-inside: avoid; break-inside: avoid +} + +.page-break-after { + page-break-after: always; } \ No newline at end of file diff --git a/print/core/component.js b/print/core/component.js index 4985cd061..12474566e 100644 --- a/print/core/component.js +++ b/print/core/component.js @@ -83,6 +83,11 @@ class Component { component.template = juice.inlineContent(this.template, this.stylesheet, { inlinePseudoElements: true }); + const tplPath = this.path; + if (!component.computed) component.computed = {}; + component.computed.path = function() { + return tplPath; + }; return component; } @@ -93,7 +98,7 @@ class Component { const component = this.build(); const i18n = new VueI18n(config.i18n); - const props = {tplPath: this.path, ...this.args}; + const props = {...this.args}; this._component = new Vue({ i18n: i18n, render: h => h(component, { diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js index ee45d65d4..a953a6ef1 100644 --- a/print/core/mixins/db-helper.js +++ b/print/core/mixins/db-helper.js @@ -24,7 +24,7 @@ const dbHelper = { * @return {Object} - Result promise */ rawSqlFromDef(queryName, params, connection) { - const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); + const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); return db.rawSqlFromDef(absolutePath, params, connection); }, @@ -78,11 +78,11 @@ const dbHelper = { * @return {Object} - SQL */ getSqlFromDef(queryName) { - const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); + const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); return db.getSqlFromDef(absolutePath); }, }, - props: ['tplPath'] + props: ['tplPath', 'name'] }; Vue.mixin(dbHelper); diff --git a/print/templates/reports/invoice-incoterms/assets/css/import.js b/print/templates/reports/invoice-incoterms/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/invoice-incoterms/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/invoice-incoterms/assets/css/style.css b/print/templates/reports/invoice-incoterms/assets/css/style.css new file mode 100644 index 000000000..d754d050f --- /dev/null +++ b/print/templates/reports/invoice-incoterms/assets/css/style.css @@ -0,0 +1,29 @@ +h2 { + font-weight: 100; + color: #555 +} + +.table-title { + margin-bottom: 15px; + font-size: 0.8rem +} + +.table-title h2 { + margin: 0 15px 0 0 +} + +.ticket-info { + font-size: 22px +} + +#incoterms table { + font-size: 1.2rem +} + +#incoterms table th { + width: 10% +} + +#incoterms p { + font-size: 1.2rem +} \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.html b/print/templates/reports/invoice-incoterms/invoice-incoterms.html new file mode 100644 index 000000000..2cceccc93 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.html @@ -0,0 +1,124 @@ + + + + + + + + + +
+ + + + +
+
+
+
+
+

{{$t('title')}}

+ + + + + + + + + + + + + + + +
{{$t('clientId')}}{{client.id}}
{{$t('invoice')}}{{invoice.ref}}
{{$t('date')}}{{invoice.issued | date('%d-%m-%Y')}}
+
+
+
+
+
{{$t('invoiceData')}}
+
+

{{client.socialName}}

+
+ {{client.postalAddress}} +
+
+ {{client.postcodeCity}} +
+
+ {{$t('fiscalId')}}: {{client.fi}} +
+
+
+
+
+ +
+
{{$t('incotermsTitle')}}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{$t('incoterms')}} +
asd
+
{{incoterms.incotermsFk}} - {{incoterms.incotermsName}}
+ {{$t('productDescription')}} + {{incoterms.intrastat}}
{{$t('expeditionDescription')}}
{{$t('packageNumber')}}{{incoterms.packages}}
{{$t('packageGrossWeight')}}{{incoterms.weight}} KG
{{$t('packageCubing')}}{{incoterms.volume}} m3
+ +

+

+ {{$t('customsInfo')}} + {{incoterms.customsAgentName}} +
+
+ ( + {{incoterms.customsAgentNif}} + {{incoterms.customsAgentStreet}} + + ☎ {{incoterms.customsAgentPhone}} + + + ✉ {{incoterms.customsAgentEmail}} + + ) +
+

+

+ {{$t('productDisclaimer')}} +

+
+
+
+
+
+ + \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.js b/print/templates/reports/invoice-incoterms/invoice-incoterms.js new file mode 100755 index 000000000..cdf08bed0 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.js @@ -0,0 +1,40 @@ +const Component = require(`${appPath}/core/component`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); +const db = require(`${appPath}/core/database`); + +module.exports = { + name: 'invoice-incoterms', + async serverPrefetch() { + this.invoice = await this.fetchInvoice(this.invoiceId); + this.client = await this.fetchClient(this.invoiceId); + this.incoterms = await this.fetchIncoterms(this.invoiceId); + + if (!this.invoice) + throw new Error('Something went wrong'); + }, + computed: { + + }, + methods: { + fetchInvoice(invoiceId) { + return this.findOneFromDef('invoice', [invoiceId]); + }, + fetchClient(invoiceId) { + return this.findOneFromDef('client', [invoiceId]); + }, + fetchIncoterms(invoiceId) { + return this.findOneFromDef('incoterms', {invoiceId}); + } + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build() + }, + props: { + invoiceId: { + type: String, + required: true + } + } +}; diff --git a/print/templates/reports/invoice-incoterms/locale/es.yml b/print/templates/reports/invoice-incoterms/locale/es.yml new file mode 100644 index 000000000..9828564d7 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/locale/es.yml @@ -0,0 +1,16 @@ +title: Factura +invoice: Factura +clientId: Cliente +date: Fecha +invoiceData: Datos de facturación +fiscalId: CIF / NIF +invoiceRef: Factura {0} +incotermsTitle: Información para la exportación +incoterms: Incoterms +productDescription: Descripción de la mercancia +expeditionDescription: INFORMACIÓN DE LA EXPEDICIÓN +packageNumber: Número de bultos +packageGrossWeight: Peso bruto +packageCubing: Cubicaje +customsInfo: A despachar por la agencia de aduanas +productDisclaimer: Mercancía destinada a la exportación, EXENTA de IVA (Ley 37/1992 - Art. 21) \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/sql/client.sql b/print/templates/reports/invoice-incoterms/sql/client.sql new file mode 100644 index 000000000..dd6035222 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/client.sql @@ -0,0 +1,12 @@ +SELECT + c.id, + c.socialName, + c.street AS postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + CONCAT(c.postcode, ' - ', c.city) postcodeCity +FROM vn.invoiceOut io + JOIN vn.client c ON c.id = io.clientFk + JOIN vn.country cty ON cty.id = c.countryFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + AND ios.taxAreaFk = 'CEE' +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/sql/incoterms.sql b/print/templates/reports/invoice-incoterms/sql/incoterms.sql new file mode 100644 index 000000000..e3d3f19c1 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/incoterms.sql @@ -0,0 +1,71 @@ +SELECT io.issued, + c.socialName, + c.street postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + io.clientFk, + c.postcode, + c.city, + io.companyFk, + io.ref, + tc.code, + s.concept, + s.quantity, + s.price, + s.discount, + s.ticketFk, + t.shipped, + t.refFk, + a.nickname, + s.itemFk, + s.id saleFk, + pm.name AS pmname, + sa.iban, + c.phone, + MAX(t.packages) packages, + a.incotermsFk, + ic.name incotermsName , + sub.description weight, + t.observations, + ca.fiscalName customsAgentName, + ca.street customsAgentStreet, + ca.nif customsAgentNif, + ca.phone customsAgentPhone, + ca.email customsAgentEmail, + CAST(sub2.volume AS DECIMAL (10,2)) volume, + sub3.intrastat + FROM vn.invoiceOut io + JOIN vn.supplier su ON su.id = io.companyFk + JOIN vn.client c ON c.id = io.clientFk + LEFT JOIN vn.province p ON p.id = c.provinceFk + JOIN vn.ticket t ON t.refFk = io.ref + LEFT JOIN (SELECT tob.ticketFk,tob.description + FROM vn.ticketObservation tob + LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk + WHERE ot.description = "Peso Aduana" + )sub ON sub.ticketFk = t.id + JOIN vn.address a ON a.id = t.addressFk + LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk + LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN (SELECT SUM(volume) volume + FROM vn.invoiceOut io + JOIN vn.ticket t ON t.refFk = io.ref + JOIN vn.saleVolume sv ON sv.ticketFk = t.id + WHERE io.id = :invoiceId + )sub2 ON TRUE + JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk + JOIN vn.taxClass tc ON tc.id = itc.taxClassFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE' + JOIN vn.country cty ON cty.id = c.countryFk + JOIN vn.payMethod pm ON pm.id = c .payMethodFk + JOIN vn.company co ON co.id=io.companyFk + JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk + LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat + FROM vn.ticket t + JOIN vn.invoiceOut io ON io.ref = t.refFk + JOIN vn.sale s ON t.id = s.ticketFk + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.intrastat ir ON ir.id = i.intrastatFk + WHERE io.id = :invoiceId + )sub3 ON TRUE + WHERE io.id = :invoiceId diff --git a/print/templates/reports/invoice-incoterms/sql/invoice.sql b/print/templates/reports/invoice-incoterms/sql/invoice.sql new file mode 100644 index 000000000..b9a929183 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/invoice.sql @@ -0,0 +1,17 @@ +SELECT + io.id, + io.issued, + io.clientFk, + io.companyFk, + io.ref, + pm.code AS payMethodCode, + cny.code companyCode, + sa.iban, + ios.footNotes +FROM invoiceOut io + JOIN client c ON c.id = io.clientFk + JOIN payMethod pm ON pm.id = c.payMethodFk + JOIN company cny ON cny.id = io.companyFk + JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk + LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html index fa888dafc..671bb8c7f 100644 --- a/print/templates/reports/invoice/invoice.html +++ b/print/templates/reports/invoice/invoice.html @@ -6,9 +6,11 @@ - - - + + + Date: Thu, 11 Mar 2021 08:58:34 +0100 Subject: [PATCH 06/21] 2734 - Recreate invoice --- loopback/server/datasources.json | 11 +++ .../back/methods/invoiceOut/createPdf.js | 74 +++++++++++++++++++ .../back/methods/invoiceOut/regenerate.js | 9 +-- modules/invoiceOut/back/model-config.json | 3 + .../back/models/invoice-container.json | 10 +++ modules/invoiceOut/back/models/invoiceOut.js | 1 + .../ticket/back/methods/ticket/makeInvoice.js | 6 +- package.json | 2 +- print/core/mixins/db-helper.js | 2 +- 9 files changed, 104 insertions(+), 14 deletions(-) create mode 100644 modules/invoiceOut/back/methods/invoiceOut/createPdf.js create mode 100644 modules/invoiceOut/back/models/invoice-container.json diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index 8ce442b8e..343bcedd8 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -68,5 +68,16 @@ "image/jpeg", "image/jpg" ] + }, + "invoiceStorage": { + "name": "invoiceStorage", + "connector": "loopback-component-storage", + "provider": "filesystem", + "root": "./storage/pdfs/invoice", + "maxFileSize": "52428800", + "allowedContentTypes": [ + "application/octet-stream", + "application/pdf" + ] } } diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js new file mode 100644 index 000000000..c423d2cdb --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -0,0 +1,74 @@ +const fs = require('fs-extra'); +const got = require('got'); +const path = require('path'); + +module.exports = Self => { + Self.remoteMethodCtx('createPdf', { + description: 'Creates an invoice PDF', + accessType: 'READ', + accepts: [ + { + arg: 'id', + type: 'String', + description: 'The invoice id', + http: {source: 'path'} + } + ], + returns: [ + { + arg: 'body', + type: 'file', + root: true + }, { + arg: 'Content-Type', + type: 'String', + http: {target: 'header'} + }, { + arg: 'Content-Disposition', + type: 'String', + http: {target: 'header'} + } + ], + http: { + path: `/:id/createPdf`, + verb: 'GET' + } + }); + + Self.createPdf = async function(ctx, id, options = {}) { + const models = Self.app.models; + const headers = ctx.req.headers; + const origin = headers.origin; + const authorization = headers.authorization; + + if (process.env.NODE_ENV == 'test') + throw new UserError(`Action not allowed on the test environment`); + + const invoiceOut = await Self.findById(id); + await invoiceOut.updateAttributes({ + hasPdf: true + }); + + const response = got.stream(`${origin}/api/report/invoice`, { + query: { + authorization: authorization, + invoiceId: id + } + }); + + const invoiceYear = invoiceOut.created.getFullYear().toString(); + const container = await models.InvoiceContainer.container(invoiceYear); + const rootPath = container.client.root; + const fileName = `${invoiceOut.ref}.pdf`; + const fileSrc = path.join(rootPath, invoiceYear, fileName); + + const writeStream = fs.createWriteStream(fileSrc); + writeStream.on('open', () => { + response.pipe(writeStream); + }); + + writeStream.on('finish', async function() { + writeStream.end(); + }); + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js index fda12c3cb..828db4c98 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js +++ b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js @@ -20,10 +20,7 @@ module.exports = Self => { }); Self.regenerate = async(ctx, id) => { - const userId = ctx.req.accessToken.userId; const models = Self.app.models; - const invoiceReportFk = 30; // Should be deprecated - const worker = await models.Worker.findOne({where: {userFk: userId}}); const tx = await Self.beginTransaction({}); try { @@ -35,10 +32,8 @@ module.exports = Self => { hasPdf: false }); - // Send to print queue - await Self.rawSql(` - INSERT INTO vn.printServerQueue (reportFk, param1, workerFk) - VALUES (?, ?, ?)`, [invoiceReportFk, id, worker.id], options); + // Create invoice PDF + await models.InvoiceOut.createPdf(ctx, id); await tx.commit(); diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index f3492dbe6..e144ce80e 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -1,5 +1,8 @@ { "InvoiceOut": { "dataSource": "vn" + }, + "InvoiceContainer": { + "dataSource": "invoiceStorage" } } diff --git a/modules/invoiceOut/back/models/invoice-container.json b/modules/invoiceOut/back/models/invoice-container.json new file mode 100644 index 000000000..5b713c0c4 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-container.json @@ -0,0 +1,10 @@ +{ + "name": "InvoiceContainer", + "base": "Container", + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoiceOut.js b/modules/invoiceOut/back/models/invoiceOut.js index 8046f1dc4..10c36b37c 100644 --- a/modules/invoiceOut/back/models/invoiceOut.js +++ b/modules/invoiceOut/back/models/invoiceOut.js @@ -5,4 +5,5 @@ module.exports = Self => { require('../methods/invoiceOut/regenerate')(Self); require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/book')(Self); + require('../methods/invoiceOut/createPdf')(Self); }; diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js index 29099e379..9500ab2a2 100644 --- a/modules/ticket/back/methods/ticket/makeInvoice.js +++ b/modules/ticket/back/methods/ticket/makeInvoice.js @@ -67,11 +67,7 @@ module.exports = function(Self) { if (serial != 'R' && invoiceId) { await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options); - await models.PrintServerQueue.create({ - reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas - param1: invoiceId, - workerFk: userId - }, options); + await models.InvoiceOut.createPdf(ctx, invoiceId); } await tx.commit(); diff --git a/package.json b/package.json index 0e1bc8e32..8fa785987 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "loopback": "^3.26.0", "loopback-boot": "^2.27.1", "loopback-component-explorer": "^6.5.0", - "loopback-component-storage": "^3.6.1", + "loopback-component-storage": "3.6.1", "loopback-connector-mysql": "^5.4.3", "loopback-connector-remote": "^3.4.1", "loopback-context": "^3.4.0", diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js index a953a6ef1..add0ed96c 100644 --- a/print/core/mixins/db-helper.js +++ b/print/core/mixins/db-helper.js @@ -67,7 +67,7 @@ const dbHelper = { */ findValueFromDef(queryName, params) { return this.findOneFromDef(queryName, params).then(row => { - return Object.values(row)[0]; + if (row) return Object.values(row)[0]; }); }, From c74b635a1a5cc78ce7ce1e36aac9f072934128ff Mon Sep 17 00:00:00 2001 From: jorgebl Date: Fri, 12 Mar 2021 12:24:08 +0100 Subject: [PATCH 07/21] Translate and show url of item --- loopback/locale/en.json | 2 +- loopback/locale/es.json | 2 +- modules/ticket/back/methods/ticket-request/confirm.js | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 5eb81edd6..65028a8a8 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -57,7 +57,7 @@ "The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket", "Swift / BIC can't be empty": "Swift / BIC can't be empty", - "MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} ({{itemId}}) for the ticket id [{{ticketId}}]({{{url}}})", + "Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})", "Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})", diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 8d5156842..9a9b6a071 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -121,7 +121,7 @@ "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", - "MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} ({{itemId}}) para el ticket id [{{ticketId}}]({{{url}}})", + "Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", diff --git a/modules/ticket/back/methods/ticket-request/confirm.js b/modules/ticket/back/methods/ticket-request/confirm.js index b80971183..ce4bfdccb 100644 --- a/modules/ticket/back/methods/ticket-request/confirm.js +++ b/modules/ticket/back/methods/ticket-request/confirm.js @@ -74,12 +74,13 @@ module.exports = Self => { const origin = ctx.req.headers.origin; const requesterId = request.requesterFk; - const message = $t('MESSAGE_BOUGHT_UNITS', { + const message = $t('Bought units from buy request', { quantity: sale.quantity, concept: sale.concept, itemId: sale.itemFk, ticketId: sale.ticketFk, - url: `${origin}/#!/ticket/${sale.ticketFk}/summary` + url: `${origin}/#!/ticket/${sale.ticketFk}/summary`, + urlItem: `${origin}/#!/item/${sale.itemFk}/summary` }); await models.Chat.sendCheckingPresence(ctx, requesterId, message); From bd081f9fd63b4ff5f2eda41ad4b155771e5316b3 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 15 Mar 2021 13:43:22 +0100 Subject: [PATCH 08/21] 2734 - Make invoice PDF refactor --- back/methods/image/upload.js | 2 +- loopback/locale/es.json | 2 +- modules/claim/front/summary/index.html | 2 +- .../back/methods/invoiceOut/book.js | 18 +++- .../back/methods/invoiceOut/createPdf.js | 94 +++++++++++-------- .../back/methods/invoiceOut/regenerate.js | 46 --------- .../invoiceOut/specs/regenerate.spec.js | 36 ------- modules/invoiceOut/back/models/invoiceOut.js | 1 - .../invoiceOut/front/descriptor/index.html | 18 +++- modules/invoiceOut/front/descriptor/index.js | 10 ++ .../invoiceOut/front/descriptor/locale/es.yml | 4 +- .../ticket/back/methods/ticket/makeInvoice.js | 2 +- .../ticket/front/descriptor-menu/index.html | 14 +-- modules/ticket/front/descriptor-menu/index.js | 6 +- modules/ticket/front/descriptor/locale/es.yml | 8 +- 15 files changed, 115 insertions(+), 148 deletions(-) delete mode 100644 modules/invoiceOut/back/methods/invoiceOut/regenerate.js delete mode 100644 modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js diff --git a/back/methods/image/upload.js b/back/methods/image/upload.js index a93ead651..676a4b5fb 100644 --- a/back/methods/image/upload.js +++ b/back/methods/image/upload.js @@ -48,7 +48,7 @@ module.exports = Self => { throw new UserError(`You don't have enough privileges`); if (process.env.NODE_ENV == 'test') - throw new UserError(`You can't upload images on the test environment`); + throw new UserError(`Action not allowed on the test environment`); // Upload file to temporary path const tempContainer = await TempContainer.container(args.collection); diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 8d5156842..0a6c88f08 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -164,7 +164,7 @@ "Amount cannot be zero": "El importe no puede ser cero", "Company has to be official": "Empresa inválida", "You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria", - "You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas", + "Action not allowed on the test environment": "Esta acción no está permitida en el entorno de pruebas", "The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta", "Sorts whole route": "Reordena ruta entera", "New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día {{shipped}}, con una cantidad de {{quantity}} y un precio de {{price}} €", diff --git a/modules/claim/front/summary/index.html b/modules/claim/front/summary/index.html index e9ec1e765..0d52c7f47 100644 --- a/modules/claim/front/summary/index.html +++ b/modules/claim/front/summary/index.html @@ -199,7 +199,7 @@ - {{::action.sale.ticket.id | zeroFill:6}} + {{::action.sale.ticket.id}} {{::action.claimBeggining.description}} diff --git a/modules/invoiceOut/back/methods/invoiceOut/book.js b/modules/invoiceOut/back/methods/invoiceOut/book.js index 358de8fd5..af495c1f0 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/book.js +++ b/modules/invoiceOut/back/methods/invoiceOut/book.js @@ -21,10 +21,20 @@ module.exports = Self => { }); Self.book = async ref => { - let ticketAddress = await Self.app.models.Ticket.findOne({where: {invoiceOut: ref}}); - let invoiceCompany = await Self.app.models.InvoiceOut.findOne({where: {ref: ref}}); - let [taxArea] = await Self.rawSql(`Select vn.addressTaxArea(?, ?) AS code`, [ticketAddress.address, invoiceCompany.company]); + const models = Self.app.models; + const ticketAddress = await models.Ticket.findOne({ + where: {invoiceOut: ref} + }); + const invoiceCompany = await models.InvoiceOut.findOne({ + where: {ref: ref} + }); + let query = 'SELECT vn.addressTaxArea(?, ?) AS code'; + const [taxArea] = await Self.rawSql(query, [ + ticketAddress.address, + invoiceCompany.company + ]); - return Self.rawSql(`CALL vn.invoiceOutAgain(?, ?)`, [ref, taxArea.code]); + query = 'CALL vn.invoiceOutAgain(?, ?)'; + return Self.rawSql(query, [ref, taxArea.code]); }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index c423d2cdb..9bf4e93a3 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -5,37 +5,26 @@ const path = require('path'); module.exports = Self => { Self.remoteMethodCtx('createPdf', { description: 'Creates an invoice PDF', - accessType: 'READ', + accessType: 'WRITE', accepts: [ { arg: 'id', - type: 'String', + type: 'number', description: 'The invoice id', http: {source: 'path'} } ], - returns: [ - { - arg: 'body', - type: 'file', - root: true - }, { - arg: 'Content-Type', - type: 'String', - http: {target: 'header'} - }, { - arg: 'Content-Disposition', - type: 'String', - http: {target: 'header'} - } - ], + returns: { + type: 'object', + root: true + }, http: { path: `/:id/createPdf`, - verb: 'GET' + verb: 'POST' } }); - Self.createPdf = async function(ctx, id, options = {}) { + Self.createPdf = async function(ctx, id, options) { const models = Self.app.models; const headers = ctx.req.headers; const origin = headers.origin; @@ -44,31 +33,54 @@ module.exports = Self => { if (process.env.NODE_ENV == 'test') throw new UserError(`Action not allowed on the test environment`); - const invoiceOut = await Self.findById(id); - await invoiceOut.updateAttributes({ - hasPdf: true - }); + let tx; + let newOptions = {}; - const response = got.stream(`${origin}/api/report/invoice`, { - query: { - authorization: authorization, - invoiceId: id - } - }); + if (typeof options == 'object') + Object.assign(newOptions, options); - const invoiceYear = invoiceOut.created.getFullYear().toString(); - const container = await models.InvoiceContainer.container(invoiceYear); - const rootPath = container.client.root; - const fileName = `${invoiceOut.ref}.pdf`; - const fileSrc = path.join(rootPath, invoiceYear, fileName); + if (!newOptions.transaction) { + tx = await Self.beginTransaction({}); + newOptions.transaction = tx; + } - const writeStream = fs.createWriteStream(fileSrc); - writeStream.on('open', () => { - response.pipe(writeStream); - }); + let fileSrc; + try { + const invoiceOut = await Self.findById(id, null, newOptions); + await invoiceOut.updateAttributes({ + hasPdf: true + }, newOptions); - writeStream.on('finish', async function() { - writeStream.end(); - }); + const response = got.stream(`${origin}/api/report/invoice`, { + query: { + authorization: authorization, + invoiceId: id + } + }); + + const invoiceYear = invoiceOut.created.getFullYear().toString(); + const container = await models.InvoiceContainer.container(invoiceYear); + const rootPath = container.client.root; + const fileName = `${invoiceOut.ref}.pdf`; + fileSrc = path.join(rootPath, invoiceYear, fileName); + + const writeStream = fs.createWriteStream(fileSrc); + writeStream.on('open', () => { + response.pipe(writeStream); + }); + + writeStream.on('finish', async function() { + writeStream.end(); + }); + + if (tx) await tx.commit(); + + return invoiceOut; + } catch (e) { + if (tx) await tx.rollback(); + if (fs.existsSync(fileSrc)) + await fs.unlink(fileSrc); + throw e; + } }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js deleted file mode 100644 index 828db4c98..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js +++ /dev/null @@ -1,46 +0,0 @@ -module.exports = Self => { - Self.remoteMethodCtx('regenerate', { - description: 'Sends an invoice to a regeneration queue', - accessType: 'WRITE', - accepts: [{ - arg: 'id', - type: 'number', - required: true, - description: 'The invoiceOut id', - http: {source: 'path'} - }], - returns: { - type: 'object', - root: true - }, - http: { - path: '/:id/regenerate', - verb: 'POST' - } - }); - - Self.regenerate = async(ctx, id) => { - const models = Self.app.models; - const tx = await Self.beginTransaction({}); - - try { - let options = {transaction: tx}; - - // Remove all invoice references from tickets - const invoiceOut = await models.InvoiceOut.findById(id, null, options); - await invoiceOut.updateAttributes({ - hasPdf: false - }); - - // Create invoice PDF - await models.InvoiceOut.createPdf(ctx, id); - - await tx.commit(); - - return invoiceOut; - } catch (e) { - await tx.rollback(); - throw e; - } - }; -}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js deleted file mode 100644 index 2d495ea0e..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -const app = require('vn-loopback/server/server'); - -describe('invoiceOut regenerate()', () => { - const invoiceReportFk = 30; - const invoiceOutId = 1; - - it('should check that the invoice has a PDF and is not in print generation queue', async() => { - const invoiceOut = await app.models.InvoiceOut.findById(invoiceOutId); - const [queue] = await app.models.InvoiceOut.rawSql(` - SELECT COUNT(*) AS total - FROM vn.printServerQueue - WHERE reportFk = ?`, [invoiceReportFk]); - - expect(invoiceOut.hasPdf).toBeTruthy(); - expect(queue.total).toEqual(0); - }); - - it(`should mark the invoice as doesn't have PDF and add it to a print queue`, async() => { - const ctx = {req: {accessToken: {userId: 5}}}; - const invoiceOut = await app.models.InvoiceOut.regenerate(ctx, invoiceOutId); - const [queue] = await app.models.InvoiceOut.rawSql(` - SELECT COUNT(*) AS total - FROM vn.printServerQueue - WHERE reportFk = ?`, [invoiceReportFk]); - - expect(invoiceOut.hasPdf).toBeFalsy(); - expect(queue.total).toEqual(1); - - // restores - const invoiceOutToRestore = await app.models.InvoiceOut.findById(invoiceOutId); - await invoiceOutToRestore.updateAttributes({hasPdf: true}); - await app.models.InvoiceOut.rawSql(` - DELETE FROM vn.printServerQueue - WHERE reportFk = ?`, [invoiceReportFk]); - }); -}); diff --git a/modules/invoiceOut/back/models/invoiceOut.js b/modules/invoiceOut/back/models/invoiceOut.js index 10c36b37c..e84a0495e 100644 --- a/modules/invoiceOut/back/models/invoiceOut.js +++ b/modules/invoiceOut/back/models/invoiceOut.js @@ -2,7 +2,6 @@ module.exports = Self => { require('../methods/invoiceOut/filter')(Self); require('../methods/invoiceOut/summary')(Self); require('../methods/invoiceOut/download')(Self); - require('../methods/invoiceOut/regenerate')(Self); require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/createPdf')(Self); diff --git a/modules/invoiceOut/front/descriptor/index.html b/modules/invoiceOut/front/descriptor/index.html index fe22e4dd8..b4c76d808 100644 --- a/modules/invoiceOut/front/descriptor/index.html +++ b/modules/invoiceOut/front/descriptor/index.html @@ -25,6 +25,14 @@ translate> Book invoice + + Regenerate invoice PDF +
@@ -81,4 +89,12 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js index cb4b131ac..3e859478d 100644 --- a/modules/invoiceOut/front/descriptor/index.js +++ b/modules/invoiceOut/front/descriptor/index.js @@ -22,6 +22,16 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked'))); } + createInvoicePdf() { + const invoiceId = this.invoiceOut.id; + return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) + .then(() => { + const snackbarMessage = this.$t( + `The invoice PDF document has been regenerated`); + this.vnApp.showSuccess(snackbarMessage); + }); + } + get filter() { if (this.invoiceOut) return JSON.stringify({refFk: this.invoiceOut.ref}); diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml index e85be96bf..dd67660ee 100644 --- a/modules/invoiceOut/front/descriptor/locale/es.yml +++ b/modules/invoiceOut/front/descriptor/locale/es.yml @@ -8,4 +8,6 @@ InvoiceOut deleted: Factura eliminada Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? Book invoice: Asentar factura InvoiceOut booked: Factura asentada -Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? \ No newline at end of file +Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? +Regenerate invoice PDF: Regenerar PDF factura +The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado \ No newline at end of file diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js index 9500ab2a2..a44c41e16 100644 --- a/modules/ticket/back/methods/ticket/makeInvoice.js +++ b/modules/ticket/back/methods/ticket/makeInvoice.js @@ -67,7 +67,7 @@ module.exports = function(Self) { if (serial != 'R' && invoiceId) { await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options); - await models.InvoiceOut.createPdf(ctx, invoiceId); + await models.InvoiceOut.createPdf(ctx, invoiceId, options); } await tx.commit(); diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html index 80ad71d5f..390d9daf7 100644 --- a/modules/ticket/front/descriptor-menu/index.html +++ b/modules/ticket/front/descriptor-menu/index.html @@ -80,13 +80,13 @@ Make invoice - Regenerate invoice + Regenerate invoice PDF - + + vn-id="createInvoicePdfConfirmation" + on-accept="$ctrl.createInvoicePdf()" + question="Are you sure you want to regenerate the invoice PDF document?" + message="You are going to regenerate the invoice PDF document"> diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index d2dea6c0a..09783ec20 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -219,12 +219,12 @@ class Controller extends Section { .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); } - regenerateInvoice() { + createInvoicePdf() { const invoiceId = this.ticket.invoiceOut.id; - return this.$http.post(`InvoiceOuts/${invoiceId}/regenerate`) + return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) .then(() => { const snackbarMessage = this.$t( - `Invoice sent for a regeneration, will be available in a few minutes`); + `The invoice PDF document has been regenerated`); this.vnApp.showSuccess(snackbarMessage); }); } diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index 6524df353..c2b181c97 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -17,12 +17,12 @@ Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPo Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales." Ticket invoiced: Ticket facturado Make invoice: Crear factura -Regenerate invoice: Regenerar factura +Regenerate invoice PDF: Regenerar PDF factura +The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado You are going to invoice this ticket: Vas a facturar este ticket Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket? -You are going to regenerate the invoice: Vas a regenerar la factura -Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura? -Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos +You are going to regenerate the invoice PDF document: Vas a regenerar el documento PDF de la factura +Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de la factura? Shipped hour updated: Hora de envio modificada Deleted ticket: Ticket eliminado Recalculate components: Recalcular componentes From 565162740ab66aaf98d857680e01bdca9d733ed8 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 15 Mar 2021 16:35:55 +0100 Subject: [PATCH 09/21] Updated unit tests --- .../02-client/04_edit_billing_data.spec.js | 2 +- .../invoiceOut/specs/createPdf.spec.js | 26 +++++++++++++++++++ .../invoiceOut/front/descriptor/index.spec.js | 15 +++++++++++ .../methods/ticket/specs/makeInvoice.spec.js | 4 +++ .../front/descriptor-menu/index.spec.js | 6 ++--- 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js diff --git a/e2e/paths/02-client/04_edit_billing_data.spec.js b/e2e/paths/02-client/04_edit_billing_data.spec.js index da5e6232e..24ee3c29a 100644 --- a/e2e/paths/02-client/04_edit_billing_data.spec.js +++ b/e2e/paths/02-client/04_edit_billing_data.spec.js @@ -1,7 +1,7 @@ import selectors from '../../helpers/selectors'; import getBrowser from '../../helpers/puppeteer'; -fdescribe('Client Edit billing data path', () => { +describe('Client Edit billing data path', () => { let browser; let page; beforeAll(async() => { diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js new file mode 100644 index 000000000..3372411c1 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js @@ -0,0 +1,26 @@ +const app = require('vn-loopback/server/server'); +const got = require('got'); + +describe('InvoiceOut createPdf()', () => { + const userId = 1; + const ctx = { + req: { + + accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, + } + }; + + it('should create a new PDF file and set true the hasPdf property', async() => { + const invoiceId = 1; + const response = { + pipe: () => {}, + on: () => {}, + }; + spyOn(got, 'stream').and.returnValue(response); + + let result = await app.models.InvoiceOut.createPdf(ctx, invoiceId); + + expect(result.hasPdf).toBe(true); + }); +}); diff --git a/modules/invoiceOut/front/descriptor/index.spec.js b/modules/invoiceOut/front/descriptor/index.spec.js index 987763b0a..c16900a0a 100644 --- a/modules/invoiceOut/front/descriptor/index.spec.js +++ b/modules/invoiceOut/front/descriptor/index.spec.js @@ -3,6 +3,7 @@ import './index'; describe('vnInvoiceOutDescriptor', () => { let controller; let $httpBackend; + const invoiceOut = {id: 1}; beforeEach(ngModule('invoiceOut')); @@ -11,6 +12,20 @@ describe('vnInvoiceOutDescriptor', () => { controller = $componentController('vnInvoiceOutDescriptor', {$element: null}); })); + describe('createInvoicePdf()', () => { + it('should make a query and show a success snackbar', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.invoiceOut = invoiceOut; + + $httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond(); + controller.createInvoicePdf(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + describe('loadData()', () => { it(`should perform a get query to store the invoice in data into the controller`, () => { const id = 1; diff --git a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js index 79bd59848..e20d9d8a2 100644 --- a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js +++ b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js @@ -5,6 +5,7 @@ describe('ticket makeInvoice()', () => { const userId = 19; const activeCtx = { accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, }; const ctx = {req: activeCtx}; @@ -43,6 +44,9 @@ describe('ticket makeInvoice()', () => { }); it('should invoice a ticket, then try again to fail', async() => { + const invoiceOutModel = app.models.InvoiceOut; + spyOn(invoiceOutModel, 'createPdf'); + invoice = await app.models.Ticket.makeInvoice(ctx, ticketId); expect(invoice.invoiceFk).toBeDefined(); diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js index 3cd08fc38..6a3009daf 100644 --- a/modules/ticket/front/descriptor-menu/index.spec.js +++ b/modules/ticket/front/descriptor-menu/index.spec.js @@ -148,12 +148,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => { }); }); - describe('regenerateInvoice()', () => { + describe('createInvoicePdf()', () => { it('should make a query and show a success snackbar', () => { jest.spyOn(controller.vnApp, 'showSuccess'); - $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/regenerate`).respond(); - controller.regenerateInvoice(); + $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond(); + controller.createInvoicePdf(); $httpBackend.flush(); expect(controller.vnApp.showSuccess).toHaveBeenCalled(); From db9d1c84e058f2ca1e3e043f05f0d27cb9d5c838 Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 16 Mar 2021 13:37:59 +0100 Subject: [PATCH 10/21] Requested changes --- print/core/mixins/db-helper.js | 2 +- .../reports/invoice/sql/intrastat.sql | 76 +------------------ 2 files changed, 2 insertions(+), 76 deletions(-) diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js index add0ed96c..1de27e6cd 100644 --- a/print/core/mixins/db-helper.js +++ b/print/core/mixins/db-helper.js @@ -82,7 +82,7 @@ const dbHelper = { return db.getSqlFromDef(absolutePath); }, }, - props: ['tplPath', 'name'] + props: ['tplPath'] }; Vue.mixin(dbHelper); diff --git a/print/templates/reports/invoice/sql/intrastat.sql b/print/templates/reports/invoice/sql/intrastat.sql index ca2ed3f33..e391056ec 100644 --- a/print/templates/reports/invoice/sql/intrastat.sql +++ b/print/templates/reports/invoice/sql/intrastat.sql @@ -11,78 +11,4 @@ SELECT LEFT JOIN vn.item i ON i.id = s.itemFk JOIN vn.intrastat ir ON ir.id = i.intrastatFk WHERE io.id = ? - GROUP BY i.intrastatFk; - - -/* SELECT io.issued, - c.socialName, - c.street postalAddress, - IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, - io.clientFk, - c.postcode, - c.city, - io.companyFk, - io.ref, - tc.code, - s.concept, - s.quantity, - s.price, - s.discount, - s.ticketFk, - t.shipped, - t.refFk, - a.nickname, - s.itemFk, - s.id saleFk, - pm.name AS pmname, - sa.iban, - c.phone, - MAX(t.packages) packages, - a.incotermsFk, - ic.name incotermsName , - sub.description weight, - t.observations, - ca.fiscalName customsAgentName, - ca.street customsAgentStreet, - ca.nif customsAgentNif, - ca.phone customsAgentPhone, - ca.email customsAgentEmail, - CAST(sub2.volume AS DECIMAL (10,2)) volume, - sub3.intrastat - FROM vn.invoiceOut io - JOIN vn.supplier su ON su.id = io.companyFk - JOIN vn.client c ON c.id = io.clientFk - LEFT JOIN vn.province p ON p.id = c.provinceFk - JOIN vn.ticket t ON t.refFk = io.ref - LEFT JOIN (SELECT tob.ticketFk,tob.description - FROM vn.ticketObservation tob - LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk - WHERE ot.description = "Peso Aduana" - )sub ON sub.ticketFk = t.id - JOIN vn.address a ON a.id = t.addressFk - LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk - LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk - JOIN vn.sale s ON s.ticketFk = t.id - JOIN (SELECT SUM(volume) volume - FROM vn.invoiceOut io - JOIN vn.ticket t ON t.refFk = io.ref - JOIN vn.saleVolume sv ON sv.ticketFk = t.id - WHERE io.id = :invoiceId - )sub2 ON TRUE - JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk - JOIN vn.taxClass tc ON tc.id = itc.taxClassFk - LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE' - JOIN vn.country cty ON cty.id = c.countryFk - JOIN vn.payMethod pm ON pm.id = c .payMethodFk - JOIN vn.company co ON co.id=io.companyFk - JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk - LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat - FROM vn.ticket t - JOIN vn.invoiceOut io ON io.ref = t.refFk - JOIN vn.sale s ON t.id = s.ticketFk - JOIN vn.item i ON i.id = s.itemFk - JOIN vn.intrastat ir ON ir.id = i.intrastatFk - WHERE io.id = :invoiceId - )sub3 ON TRUE - WHERE io.id = :invoiceId - */ \ No newline at end of file + GROUP BY i.intrastatFk; \ No newline at end of file From a2165b6bfc53063c35f0cf27bdd9c85c0e0fe811 Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 16 Mar 2021 13:59:39 +0100 Subject: [PATCH 11/21] Updated E2E --- db/changes/10291-invoiceIn/00-ACL.sql | 3 ++- e2e/paths/05-ticket/12_descriptor.spec.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/db/changes/10291-invoiceIn/00-ACL.sql b/db/changes/10291-invoiceIn/00-ACL.sql index b4067d1a3..5a1cf6843 100644 --- a/db/changes/10291-invoiceIn/00-ACL.sql +++ b/db/changes/10291-invoiceIn/00-ACL.sql @@ -1,4 +1,5 @@ INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('Genus', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'), - ('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'); \ No newline at end of file + ('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'), + ('InvoiceOut', 'createPdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'); diff --git a/e2e/paths/05-ticket/12_descriptor.spec.js b/e2e/paths/05-ticket/12_descriptor.spec.js index 471d7a536..d81c1c3ed 100644 --- a/e2e/paths/05-ticket/12_descriptor.spec.js +++ b/e2e/paths/05-ticket/12_descriptor.spec.js @@ -162,7 +162,7 @@ describe('Ticket descriptor path', () => { }); it(`should regenerate the invoice using the descriptor menu`, async() => { - const expectedMessage = 'Invoice sent for a regeneration, will be available in a few minutes'; + const expectedMessage = 'The invoice PDF document has been regenerated'; await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitForContentLoaded(); From 41ce83426412afd21dae6895a3b83adae63af72a Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 16 Mar 2021 14:00:15 +0100 Subject: [PATCH 12/21] Removed unused require --- print/templates/reports/invoice-incoterms/invoice-incoterms.js | 1 - 1 file changed, 1 deletion(-) diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.js b/print/templates/reports/invoice-incoterms/invoice-incoterms.js index cdf08bed0..cfe0a21cb 100755 --- a/print/templates/reports/invoice-incoterms/invoice-incoterms.js +++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.js @@ -1,7 +1,6 @@ const Component = require(`${appPath}/core/component`); const reportHeader = new Component('report-header'); const reportFooter = new Component('report-footer'); -const db = require(`${appPath}/core/database`); module.exports = { name: 'invoice-incoterms', From 4ba4aba45a4868469c067dfa5df56de5061cec67 Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 16 Mar 2021 14:45:46 +0100 Subject: [PATCH 13/21] Added new pdf folder --- .gitignore | 1 - storage/pdfs/invoice/.keep | 0 2 files changed, 1 deletion(-) create mode 100644 storage/pdfs/invoice/.keep diff --git a/.gitignore b/.gitignore index 04a977352..f38e335c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ coverage node_modules dist -storage npm-debug.log .eslintcache datasources.*.json diff --git a/storage/pdfs/invoice/.keep b/storage/pdfs/invoice/.keep new file mode 100644 index 000000000..e69de29bb From 54bc1aef9696ebca57272bb3f1d2367ff80e06c2 Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 16 Mar 2021 14:46:12 +0100 Subject: [PATCH 14/21] Reenabled .gitignore storage folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f38e335c7..04a977352 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ coverage node_modules dist +storage npm-debug.log .eslintcache datasources.*.json From 6210e60896d87310ae46e6f222aeca6677375de9 Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 19 Mar 2021 19:20:19 +0100 Subject: [PATCH 15/21] 2669 - Changes to the translate values function --- loopback/common/models/loggable.js | 109 ++++-------------- loopback/util/log.js | 22 +++- .../importToNewRefundTicket.js | 45 +++++--- .../importToNewRefundTicket.spec.js | 42 +++---- .../methods/entry/specs/importBuys.spec.js | 43 +++---- .../methods/ticket-tracking/changeState.js | 2 +- .../ticket-tracking/specs/changeState.spec.js | 2 +- .../specs/setDelivered.spec.js | 14 ++- modules/ticket/back/models/ticket.json | 3 +- 9 files changed, 123 insertions(+), 159 deletions(-) diff --git a/loopback/common/models/loggable.js b/loopback/common/models/loggable.js index 258fff4ff..c26e66dad 100644 --- a/loopback/common/models/loggable.js +++ b/loopback/common/models/loggable.js @@ -1,5 +1,5 @@ -const pick = require('object.pick'); const LoopBackContext = require('loopback-context'); +const log = require('vn-loopback/util/log'); module.exports = function(Self) { Self.setup = function() { @@ -14,6 +14,8 @@ module.exports = function(Self) { Self.observe('before save', async function(ctx) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; + const modelName = definition.name; + const model = appModels[modelName]; const options = {}; // Check for transactions @@ -24,13 +26,12 @@ module.exports = function(Self) { let newInstance; if (ctx.data) { - const changes = pick(ctx.currentInstance, Object.keys(ctx.data)); - newInstance = await fkToValue(ctx.data, ctx); - oldInstance = await fkToValue(changes, ctx); + const changes = log.getChanges(ctx.currentInstance, ctx.data); + oldInstance = await log.translateValues(model, changes.old, options); + newInstance = await log.translateValues(model, changes.new, options); if (ctx.where && !ctx.currentInstance) { const fields = Object.keys(ctx.data); - const modelName = definition.name; ctx.oldInstances = await appModels[modelName].find({ where: ctx.where, @@ -41,40 +42,42 @@ module.exports = function(Self) { // Get changes from created instance if (ctx.isNewInstance) - newInstance = await fkToValue(ctx.instance.__data, ctx); + newInstance = await log.translateValues(model, ctx.instance.__data, options); ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = newInstance; }); Self.observe('before delete', async function(ctx) { - const appModels = ctx.Model.app.models; + const models = ctx.Model.app.models; const definition = ctx.Model.definition; const relations = ctx.Model.relations; - let options = {}; + const options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; if (ctx.where) { - let affectedModel = definition.name; - let deletedInstances = await appModels[affectedModel].find({ + const modelName = definition.name; + const model = models[modelName]; + const deletedRows = await model.find({ where: ctx.where }, options); - let relation = definition.settings.log.relation; + const relation = definition.settings.log.relation; if (relation) { - let primaryKey = relations[relation].keyFrom; + const primaryKey = relations[relation].keyFrom; - let arrangedDeletedInstances = []; - for (let i = 0; i < deletedInstances.length; i++) { + const instances = []; + for (let instance of deletedRows) { + const translatedValues = await log.translateValues(model, instance, options); if (primaryKey) - deletedInstances[i].originFk = deletedInstances[i][primaryKey]; - let arrangedInstance = await fkToValue(deletedInstances[i], ctx); - arrangedDeletedInstances[i] = arrangedInstance; + translatedValues.originFk = instance[primaryKey]; + instances.push(translatedValues); } - ctx.hookState.oldInstance = arrangedDeletedInstances; + + ctx.hookState.oldInstance = instances; } } }); @@ -116,72 +119,6 @@ module.exports = function(Self) { }); } - // Get log values from a foreign key - async function fkToValue(instance, ctx) { - const appModels = ctx.Model.app.models; - const relations = ctx.Model.relations; - let options = {}; - - // Check for transactions - if (ctx.options && ctx.options.transaction) - options.transaction = ctx.options.transaction; - - const instanceCopy = JSON.parse(JSON.stringify(instance)); - const result = {}; - for (const key in instanceCopy) { - let value = instanceCopy[key]; - - if (value instanceof Object) - continue; - - if (value === undefined || value === null) continue; - - for (let relationName in relations) { - const relation = relations[relationName]; - if (relation.keyFrom == key && key != 'id') { - const model = relation.modelTo; - const modelName = relation.modelTo.modelName; - const properties = model && model.definition.properties; - const settings = model && model.definition.settings; - - const recordSet = await appModels[modelName].findById(value, null, options); - - const hasShowField = settings.log && settings.log.showField; - let showField = hasShowField && recordSet - && recordSet[settings.log.showField]; - - if (!showField) { - const showFieldNames = [ - 'name', - 'description', - 'code', - 'nickname' - ]; - for (field of showFieldNames) { - const propField = properties && properties[field]; - const recordField = recordSet && recordSet[field]; - - if (propField && recordField) { - showField = field; - break; - } - } - } - - if (showField && recordSet && recordSet[showField]) { - value = recordSet[showField]; - break; - } - - value = recordSet && recordSet.id || value; - break; - } - } - result[key] = value; - } - return result; - } - async function logInModel(ctx, loopBackContext) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; @@ -325,8 +262,8 @@ module.exports = function(Self) { } function setActionType(ctx) { - let oldInstance = ctx.hookState.oldInstance; - let newInstance = ctx.hookState.newInstance; + const oldInstance = ctx.hookState.oldInstance; + const newInstance = ctx.hookState.newInstance; if (oldInstance && newInstance) return 'update'; diff --git a/loopback/util/log.js b/loopback/util/log.js index b491b97d0..067e458ec 100644 --- a/loopback/util/log.js +++ b/loopback/util/log.js @@ -3,7 +3,7 @@ * @param {Object} instance - The model or context instance * @param {Object} changes - Object containing changes */ -exports.translateValues = async(instance, changes) => { +exports.translateValues = async(instance, changes, options = {}) => { const models = instance.app.models; function getRelation(instance, property) { const relations = instance.definition.settings.relations; @@ -38,12 +38,19 @@ exports.translateValues = async(instance, changes) => { const properties = Object.assign({}, changes); for (let property in properties) { + const firstChar = property.substring(0, 1); + const isPrivate = firstChar == '$'; + if (isPrivate) { + delete properties[property]; + continue; + } + const relation = getRelation(instance, property); const value = properties[property]; - let finalValue = value; + let finalValue = value; if (relation) { - let fieldsToShow = ['alias', 'name', 'code', 'description']; + let fieldsToShow = ['nickname', 'name', 'code', 'description']; const modelName = relation.model; const model = models[modelName]; const log = model.definition.settings.log; @@ -51,9 +58,12 @@ exports.translateValues = async(instance, changes) => { if (log && log.showField) fieldsToShow = [log.showField]; + console.log('Falla aqui? ', value); + console.log('property: ', property); + console.log('changes: ', changes); const row = await model.findById(value, { fields: fieldsToShow - }); + }, options); const newValue = getValue(row); if (newValue) finalValue = newValue; } @@ -77,7 +87,9 @@ exports.getChanges = (original, changes) => { const oldChanges = {}; const newChanges = {}; for (let property in changes) { - if (changes[property] != original[property]) { + const firstChar = property.substring(0, 1); + const isPrivate = firstChar == '$'; + if (changes[property] != original[property] && !isPrivate) { newChanges[property] = changes[property]; if (original[property] != undefined) diff --git a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js index c2ab2001e..293aae012 100644 --- a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js +++ b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js @@ -19,11 +19,10 @@ module.exports = Self => { } }); - Self.importToNewRefundTicket = async(ctx, id) => { + Self.importToNewRefundTicket = async(ctx, id, options) => { const models = Self.app.models; const token = ctx.req.accessToken; const userId = token.userId; - const tx = await Self.beginTransaction({}); const filter = { where: {id: id}, include: [ @@ -63,29 +62,39 @@ module.exports = Self => { ] }; + let tx; + let myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + try { - let options = {transaction: tx}; const worker = await models.Worker.findOne({ where: {userFk: userId} - }, options); + }, myOptions); const obsevationType = await models.ObservationType.findOne({ where: {description: 'comercial'} - }, options); + }, myOptions); const agencyMode = await models.AgencyMode.findOne({ where: {code: 'refund'} - }, options); + }, myOptions); const state = await models.State.findOne({ where: {code: 'DELIVERED'} - }, options); + }, myOptions); const zone = await models.Zone.findOne({ where: {agencyModeFk: agencyMode.id} - }, options); + }, myOptions); - const claim = await models.Claim.findOne(filter, options); + const claim = await models.Claim.findOne(filter, myOptions); const today = new Date(); const newRefundTicket = await models.Ticket.create({ @@ -98,33 +107,33 @@ module.exports = Self => { addressFk: claim.ticket().addressFk, agencyModeFk: agencyMode.id, zoneFk: zone.id - }, options); + }, myOptions); await saveObservation({ description: `Reclama ticket: ${claim.ticketFk}`, ticketFk: newRefundTicket.id, observationTypeFk: obsevationType.id - }, options); + }, myOptions); await models.TicketTracking.create({ ticketFk: newRefundTicket.id, stateFk: state.id, workerFk: worker.id - }, options); + }, myOptions); - const salesToRefund = await models.ClaimBeginning.find(salesFilter, options); - const createdSales = await addSalesToTicket(salesToRefund, newRefundTicket.id, options); - await insertIntoClaimEnd(createdSales, id, worker.id, options); + const salesToRefund = await models.ClaimBeginning.find(salesFilter, myOptions); + const createdSales = await addSalesToTicket(salesToRefund, newRefundTicket.id, myOptions); + await insertIntoClaimEnd(createdSales, id, worker.id, myOptions); await Self.rawSql('CALL vn.ticketCalculateClon(?, ?)', [ newRefundTicket.id, claim.ticketFk - ], options); + ], myOptions); - await tx.commit(); + if (tx) await tx.commit(); return newRefundTicket; } catch (e) { - await tx.rollback(); + if (tx) await tx.rollback(); throw e; } }; diff --git a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js index 8c013c172..1e98bf634 100644 --- a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js +++ b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js @@ -1,42 +1,42 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); +const models = app.models; describe('claimBeginning', () => { const claimManagerId = 72; - let ticket; - let refundTicketSales; - let salesInsertedInClaimEnd; - const activeCtx = { accessToken: {userId: claimManagerId}, }; const ctx = {req: activeCtx}; - afterAll(async done => { - try { - await app.models.Ticket.destroyById(ticket.id); - await app.models.Ticket.rawSql(`DELETE FROM vn.orderTicket WHERE ticketFk ='${ticket.id}';`); - } catch (error) { - console.error(error); - } - - done(); - }); - describe('importToNewRefundTicket()', () => { it('should create a new ticket with negative sales and insert the negative sales into claimEnd', async() => { spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ active: activeCtx }); let claimId = 1; - ticket = await app.models.ClaimBeginning.importToNewRefundTicket(ctx, claimId); - refundTicketSales = await app.models.Sale.find({where: {ticketFk: ticket.id}}); - salesInsertedInClaimEnd = await app.models.ClaimEnd.find({where: {claimFk: claimId}}); + const tx = await models.Entry.beginTransaction({}); + try { + const options = {transaction: tx}; - expect(refundTicketSales.length).toEqual(1); - expect(refundTicketSales[0].quantity).toEqual(-5); - expect(salesInsertedInClaimEnd[0].saleFk).toEqual(refundTicketSales[0].id); + const ticket = await models.ClaimBeginning.importToNewRefundTicket(ctx, claimId, options); + + const refundTicketSales = await models.Sale.find({ + where: {ticketFk: ticket.id} + }, options); + const salesInsertedInClaimEnd = await models.ClaimEnd.find({ + where: {claimFk: claimId} + }, options); + + expect(refundTicketSales.length).toEqual(1); + expect(refundTicketSales[0].quantity).toEqual(-5); + expect(salesInsertedInClaimEnd[0].saleFk).toEqual(refundTicketSales[0].id); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + } }); }); }); diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js index d0793a2f6..dbe5ce201 100644 --- a/modules/entry/back/methods/entry/specs/importBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js @@ -2,7 +2,6 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); describe('entry import()', () => { - let newEntry; const buyerId = 35; const companyId = 442; const travelId = 1; @@ -52,29 +51,31 @@ describe('entry import()', () => { } }; const tx = await app.models.Entry.beginTransaction({}); - const options = {transaction: tx}; + try { + const options = {transaction: tx}; + const newEntry = await app.models.Entry.create({ + dated: new Date(), + supplierFk: supplierId, + travelFk: travelId, + companyFk: companyId, + observation: 'The entry', + ref: 'Entry ref' + }, options); - newEntry = await app.models.Entry.create({ - dated: new Date(), - supplierFk: supplierId, - travelFk: travelId, - companyFk: companyId, - observation: 'The entry', - ref: 'Entry ref' - }, options); + await app.models.Entry.importBuys(ctx, newEntry.id, options); - await app.models.Entry.importBuys(ctx, newEntry.id, options); + const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options); + const entryBuys = await app.models.Buy.find({ + where: {entryFk: newEntry.id} + }, options); - const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options); - const entryBuys = await app.models.Buy.find({ - where: {entryFk: newEntry.id} - }, options); + expect(updatedEntry.observation).toEqual(expectedObservation); + expect(updatedEntry.ref).toEqual(expectedRef); + expect(entryBuys.length).toEqual(2); - expect(updatedEntry.observation).toEqual(expectedObservation); - expect(updatedEntry.ref).toEqual(expectedRef); - expect(entryBuys.length).toEqual(2); - - // Restores - await tx.rollback(); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + } }); }); diff --git a/modules/ticket/back/methods/ticket-tracking/changeState.js b/modules/ticket/back/methods/ticket-tracking/changeState.js index f7baeecfd..5d574527c 100644 --- a/modules/ticket/back/methods/ticket-tracking/changeState.js +++ b/modules/ticket/back/methods/ticket-tracking/changeState.js @@ -23,7 +23,7 @@ module.exports = Self => { } }); - Self.changeState = async(ctx, params) => { + Self.changeState = async(ctx, params, options) => { let userId = ctx.req.accessToken.userId; let models = Self.app.models; diff --git a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js b/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js index 9b10f18f0..cc622ea1a 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js +++ b/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -describe('ticket changeState()', () => { +xdescribe('ticket changeState()', () => { const salesPersonId = 18; const employeeId = 1; const productionId = 49; diff --git a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js index 042ae0a1f..0d443fd80 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js +++ b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -describe('ticket setDelivered()', () => { +fdescribe('ticket setDelivered()', () => { const userId = 50; const activeCtx = { accessToken: {userId: userId}, @@ -18,11 +18,13 @@ describe('ticket setDelivered()', () => { let originalTicketOne = await app.models.Ticket.findById(8); let originalTicketTwo = await app.models.Ticket.findById(10); - originalTicketOne.id = null; - originalTicketTwo.id = null; + originalTicketOne.id = undefined; + originalTicketTwo.id = undefined; ticketOne = await app.models.Ticket.create(originalTicketOne); ticketTwo = await app.models.Ticket.create(originalTicketTwo); + console.log(ticketOne); + console.log(ticketTwo); } catch (error) { console.error(error); } @@ -32,8 +34,10 @@ describe('ticket setDelivered()', () => { afterAll(async done => { try { - await app.models.Ticket.destroyById(ticketOne.id); - await app.models.Ticket.destroyById(ticketTwo.id); + console.log(ticketOne); + console.log(ticketTwo); + // await app.models.Ticket.destroyById(ticketOne.id); + // await app.models.Ticket.destroyById(ticketTwo.id); } catch (error) { console.error(error); } diff --git a/modules/ticket/back/models/ticket.json b/modules/ticket/back/models/ticket.json index 8f91ee689..65127a78c 100644 --- a/modules/ticket/back/models/ticket.json +++ b/modules/ticket/back/models/ticket.json @@ -2,7 +2,8 @@ "name": "Ticket", "base": "Loggable", "log": { - "model":"TicketLog" + "model":"TicketLog", + "showField": "id" }, "options": { "mysql": { From 7ac4d67f9714ca44caf22e71e08fc0d3952c4516 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 09:08:27 +0100 Subject: [PATCH 16/21] Empty value not being logged in --- loopback/common/models/loggable.js | 111 ++++++++++++++---- loopback/util/log.js | 11 +- .../specs/setDelivered.spec.js | 10 +- 3 files changed, 97 insertions(+), 35 deletions(-) diff --git a/loopback/common/models/loggable.js b/loopback/common/models/loggable.js index c26e66dad..557aad66a 100644 --- a/loopback/common/models/loggable.js +++ b/loopback/common/models/loggable.js @@ -1,5 +1,5 @@ +const pick = require('object.pick'); const LoopBackContext = require('loopback-context'); -const log = require('vn-loopback/util/log'); module.exports = function(Self) { Self.setup = function() { @@ -14,8 +14,6 @@ module.exports = function(Self) { Self.observe('before save', async function(ctx) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; - const modelName = definition.name; - const model = appModels[modelName]; const options = {}; // Check for transactions @@ -26,12 +24,13 @@ module.exports = function(Self) { let newInstance; if (ctx.data) { - const changes = log.getChanges(ctx.currentInstance, ctx.data); - oldInstance = await log.translateValues(model, changes.old, options); - newInstance = await log.translateValues(model, changes.new, options); + const changes = pick(ctx.currentInstance, Object.keys(ctx.data)); + newInstance = await fkToValue(ctx.data, ctx); + oldInstance = await fkToValue(changes, ctx); if (ctx.where && !ctx.currentInstance) { const fields = Object.keys(ctx.data); + const modelName = definition.name; ctx.oldInstances = await appModels[modelName].find({ where: ctx.where, @@ -42,42 +41,40 @@ module.exports = function(Self) { // Get changes from created instance if (ctx.isNewInstance) - newInstance = await log.translateValues(model, ctx.instance.__data, options); + newInstance = await fkToValue(ctx.instance.__data, ctx); ctx.hookState.oldInstance = oldInstance; ctx.hookState.newInstance = newInstance; }); Self.observe('before delete', async function(ctx) { - const models = ctx.Model.app.models; + const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; const relations = ctx.Model.relations; - const options = {}; + let options = {}; if (ctx.options && ctx.options.transaction) options.transaction = ctx.options.transaction; if (ctx.where) { - const modelName = definition.name; - const model = models[modelName]; - const deletedRows = await model.find({ + let affectedModel = definition.name; + let deletedInstances = await appModels[affectedModel].find({ where: ctx.where }, options); - const relation = definition.settings.log.relation; + let relation = definition.settings.log.relation; if (relation) { - const primaryKey = relations[relation].keyFrom; + let primaryKey = relations[relation].keyFrom; - const instances = []; - for (let instance of deletedRows) { - const translatedValues = await log.translateValues(model, instance, options); + let arrangedDeletedInstances = []; + for (let i = 0; i < deletedInstances.length; i++) { if (primaryKey) - translatedValues.originFk = instance[primaryKey]; - instances.push(translatedValues); + deletedInstances[i].originFk = deletedInstances[i][primaryKey]; + let arrangedInstance = await fkToValue(deletedInstances[i], ctx); + arrangedDeletedInstances[i] = arrangedInstance; } - - ctx.hookState.oldInstance = instances; + ctx.hookState.oldInstance = arrangedDeletedInstances; } } }); @@ -119,6 +116,74 @@ module.exports = function(Self) { }); } + // Get log values from a foreign key + async function fkToValue(instance, ctx) { + const appModels = ctx.Model.app.models; + const relations = ctx.Model.relations; + let options = {}; + + // Check for transactions + if (ctx.options && ctx.options.transaction) + options.transaction = ctx.options.transaction; + + const instanceCopy = JSON.parse(JSON.stringify(instance)); + const result = {}; + for (const key in instanceCopy) { + let value = instanceCopy[key]; + + if (value instanceof Object) + continue; + + if (value === undefined) continue; + + if (value) { + for (let relationName in relations) { + const relation = relations[relationName]; + if (relation.keyFrom == key && key != 'id') { + const model = relation.modelTo; + const modelName = relation.modelTo.modelName; + const properties = model && model.definition.properties; + const settings = model && model.definition.settings; + + const recordSet = await appModels[modelName].findById(value, null, options); + + const hasShowField = settings.log && settings.log.showField; + let showField = hasShowField && recordSet + && recordSet[settings.log.showField]; + + if (!showField) { + const showFieldNames = [ + 'name', + 'description', + 'code', + 'nickname' + ]; + for (field of showFieldNames) { + const propField = properties && properties[field]; + const recordField = recordSet && recordSet[field]; + + if (propField && recordField) { + showField = field; + break; + } + } + } + + if (showField && recordSet && recordSet[showField]) { + value = recordSet[showField]; + break; + } + + value = recordSet && recordSet.id || value; + break; + } + } + } + result[key] = value; + } + return result; + } + async function logInModel(ctx, loopBackContext) { const appModels = ctx.Model.app.models; const definition = ctx.Model.definition; @@ -262,8 +327,8 @@ module.exports = function(Self) { } function setActionType(ctx) { - const oldInstance = ctx.hookState.oldInstance; - const newInstance = ctx.hookState.newInstance; + let oldInstance = ctx.hookState.oldInstance; + let newInstance = ctx.hookState.newInstance; if (oldInstance && newInstance) return 'update'; diff --git a/loopback/util/log.js b/loopback/util/log.js index 067e458ec..9832a018a 100644 --- a/loopback/util/log.js +++ b/loopback/util/log.js @@ -47,9 +47,10 @@ exports.translateValues = async(instance, changes, options = {}) => { const relation = getRelation(instance, property); const value = properties[property]; + const hasValue = value != null && value != undefined; let finalValue = value; - if (relation) { + if (relation && hasValue) { let fieldsToShow = ['nickname', 'name', 'code', 'description']; const modelName = relation.model; const model = models[modelName]; @@ -58,9 +59,6 @@ exports.translateValues = async(instance, changes, options = {}) => { if (log && log.showField) fieldsToShow = [log.showField]; - console.log('Falla aqui? ', value); - console.log('property: ', property); - console.log('changes: ', changes); const row = await model.findById(value, { fields: fieldsToShow }, options); @@ -86,10 +84,13 @@ exports.translateValues = async(instance, changes, options = {}) => { exports.getChanges = (original, changes) => { const oldChanges = {}; const newChanges = {}; + for (let property in changes) { const firstChar = property.substring(0, 1); const isPrivate = firstChar == '$'; - if (changes[property] != original[property] && !isPrivate) { + if (isPrivate) return; + + if (changes[property] != original[property]) { newChanges[property] = changes[property]; if (original[property] != undefined) diff --git a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js index 0d443fd80..694cbf8c6 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js +++ b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -fdescribe('ticket setDelivered()', () => { +describe('ticket setDelivered()', () => { const userId = 50; const activeCtx = { accessToken: {userId: userId}, @@ -23,8 +23,6 @@ fdescribe('ticket setDelivered()', () => { ticketOne = await app.models.Ticket.create(originalTicketOne); ticketTwo = await app.models.Ticket.create(originalTicketTwo); - console.log(ticketOne); - console.log(ticketTwo); } catch (error) { console.error(error); } @@ -34,10 +32,8 @@ fdescribe('ticket setDelivered()', () => { afterAll(async done => { try { - console.log(ticketOne); - console.log(ticketTwo); - // await app.models.Ticket.destroyById(ticketOne.id); - // await app.models.Ticket.destroyById(ticketTwo.id); + await app.models.Ticket.destroyById(ticketOne.id); + await app.models.Ticket.destroyById(ticketTwo.id); } catch (error) { console.error(error); } From 29979d536765183157a6f92d7f469d5e6116b61c Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 09:10:12 +0100 Subject: [PATCH 17/21] Rollback --- .../back/methods/ticket-tracking/specs/setDelivered.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js index 694cbf8c6..042ae0a1f 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js +++ b/modules/ticket/back/methods/ticket-tracking/specs/setDelivered.spec.js @@ -18,8 +18,8 @@ describe('ticket setDelivered()', () => { let originalTicketOne = await app.models.Ticket.findById(8); let originalTicketTwo = await app.models.Ticket.findById(10); - originalTicketOne.id = undefined; - originalTicketTwo.id = undefined; + originalTicketOne.id = null; + originalTicketTwo.id = null; ticketOne = await app.models.Ticket.create(originalTicketOne); ticketTwo = await app.models.Ticket.create(originalTicketTwo); From 59bf458ca578f398cace1c304592cec84cdf6c78 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 09:10:45 +0100 Subject: [PATCH 18/21] Removed xdescribe --- .../back/methods/ticket-tracking/specs/changeState.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js b/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js index cc622ea1a..9b10f18f0 100644 --- a/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js +++ b/modules/ticket/back/methods/ticket-tracking/specs/changeState.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -xdescribe('ticket changeState()', () => { +describe('ticket changeState()', () => { const salesPersonId = 18; const employeeId = 1; const productionId = 49; From 985bcc61b16bad673b690bdd36ac4db4baba9747 Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 09:11:02 +0100 Subject: [PATCH 19/21] Rollback --- modules/ticket/back/methods/ticket-tracking/changeState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ticket/back/methods/ticket-tracking/changeState.js b/modules/ticket/back/methods/ticket-tracking/changeState.js index 5d574527c..f7baeecfd 100644 --- a/modules/ticket/back/methods/ticket-tracking/changeState.js +++ b/modules/ticket/back/methods/ticket-tracking/changeState.js @@ -23,7 +23,7 @@ module.exports = Self => { } }); - Self.changeState = async(ctx, params, options) => { + Self.changeState = async(ctx, params) => { let userId = ctx.req.accessToken.userId; let models = Self.app.models; From 6da84e46c190db3b1d9061bc7fb31df96675e01e Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 10:20:04 +0100 Subject: [PATCH 20/21] Added throw error --- .../methods/claim-beginning/importToNewRefundTicket.spec.js | 1 + modules/entry/back/methods/entry/specs/importBuys.spec.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js index 1e98bf634..b05b2ac15 100644 --- a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js +++ b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js @@ -36,6 +36,7 @@ describe('claimBeginning', () => { await tx.rollback(); } catch (e) { await tx.rollback(); + throw e; } }); }); diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js index dbe5ce201..b0a55e3a1 100644 --- a/modules/entry/back/methods/entry/specs/importBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -describe('entry import()', () => { +fdescribe('entry import()', () => { const buyerId = 35; const companyId = 442; const travelId = 1; @@ -64,7 +64,7 @@ describe('entry import()', () => { await app.models.Entry.importBuys(ctx, newEntry.id, options); - const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options); + const updatedEntry = await app.models.Entry.findById(null, null, options); const entryBuys = await app.models.Buy.find({ where: {entryFk: newEntry.id} }, options); @@ -76,6 +76,7 @@ describe('entry import()', () => { await tx.rollback(); } catch (e) { await tx.rollback(); + throw e; } }); }); From eb12126603a2cba5b931cb88c1bc9c7a7cde495b Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 22 Mar 2021 10:27:07 +0100 Subject: [PATCH 21/21] Rollback test changes --- modules/entry/back/methods/entry/specs/importBuys.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js index b0a55e3a1..942ce0a0b 100644 --- a/modules/entry/back/methods/entry/specs/importBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js @@ -1,7 +1,7 @@ const app = require('vn-loopback/server/server'); const LoopBackContext = require('loopback-context'); -fdescribe('entry import()', () => { +describe('entry import()', () => { const buyerId = 35; const companyId = 442; const travelId = 1; @@ -64,7 +64,7 @@ fdescribe('entry import()', () => { await app.models.Entry.importBuys(ctx, newEntry.id, options); - const updatedEntry = await app.models.Entry.findById(null, null, options); + const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options); const entryBuys = await app.models.Buy.find({ where: {entryFk: newEntry.id} }, options);