From 41a1b3d9098895f38855d9f4ef9860290c1b642c Mon Sep 17 00:00:00 2001 From: joan Date: Mon, 2 Aug 2021 13:35:38 +0200 Subject: [PATCH] 2984 - Global invoicing endpoint --- db/dump/fixtures.sql | 11 +- loopback/locale/es.json | 4 +- .../back/methods/invoiceOut/createPdf.js | 14 +- .../methods/invoiceOut/globalInvoicing.js | 246 ++++++++++++++++++ modules/invoiceOut/back/models/invoice-out.js | 1 + modules/invoiceOut/front/index.js | 1 + .../front/index/global-invoicing/index.html | 62 +++++ .../front/index/global-invoicing/index.js | 77 ++++++ .../index/global-invoicing/index.spec.js | 66 +++++ .../index/global-invoicing/locale/es.yml | 3 + .../front/index/global-invoicing/style.scss | 5 + modules/invoiceOut/front/index/index.html | 11 +- modules/invoiceOut/front/index/locale/es.yml | 3 +- .../ticket/back/methods/ticket/makeInvoice.js | 7 +- 14 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js create mode 100644 modules/invoiceOut/front/index/global-invoicing/index.html create mode 100644 modules/invoiceOut/front/index/global-invoicing/index.js create mode 100644 modules/invoiceOut/front/index/global-invoicing/index.spec.js create mode 100644 modules/invoiceOut/front/index/global-invoicing/locale/es.yml create mode 100644 modules/invoiceOut/front/index/global-invoicing/style.scss diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index c44191381..cbc467f6a 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -127,11 +127,12 @@ INSERT INTO `vn`.`warehouseAlias`(`id`, `name`) INSERT INTO `vn`.`warehouse`(`id`, `name`, `code`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`, `hasComission`, `aliasFk`, `countryFk`) VALUES - (1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1), - (2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13), - (3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), - (4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), - (5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1); + (1, 'Warehouse One', 'ALG', 1, 1, 1, 1, 1, 1, 1, 2, 1), + (2, 'Warehouse Two', NULL, 1, 1, 1, 1, 0, 0, 1, 2, 13), + (3, 'Warehouse Three', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), + (4, 'Warehouse Four', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), + (5, 'Warehouse Five', NULL, 1, 1, 1, 1, 0, 0, 0, 2, 1), + (13, 'Inventory', NULL, 1, 1, 1, 0, 0, 0, 0, 2, 1); INSERT INTO `vn`.`sector`(`id`, `description`, `warehouseFk`, `isPreviousPreparedByPacking`, `code`, `pickingPlacement`, `path`) VALUES diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 1d73cbbf3..12003cf08 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -192,5 +192,7 @@ "Can't invoice to past": "No se puede facturar a pasado", "This ticket is already invoiced": "Este ticket ya está facturado", "A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero", - "A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa" + "A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa", + "Not invoiceable": "Not invoiceable", + "Not invoiceable 1101": "Not invoiceable 1101" } \ No newline at end of file diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index 5f43e4a32..d8962e430 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -71,18 +71,22 @@ module.exports = Self => { await fs.mkdir(src, {recursive: true}); + if (tx) await tx.commit(); + const writeStream = fs.createWriteStream(fileSrc); writeStream.on('open', () => { response.pipe(writeStream); }); - writeStream.on('finish', async function() { - writeStream.end(); + return await new Promise(resolve => { + writeStream.on('finish', () => { + writeStream.end(); + + resolve(invoiceOut); + }); }); - if (tx) await tx.commit(); - - return invoiceOut; + // return invoiceOut; } catch (e) { if (tx) await tx.rollback(); if (fs.existsSync(fileSrc)) diff --git a/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js new file mode 100644 index 000000000..df79a6c09 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/globalInvoicing.js @@ -0,0 +1,246 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('globalInvoicing', { + description: 'Make a global invoice', + accessType: 'WRITE', + accepts: [ + { + arg: 'invoiceDate', + type: 'date', + description: 'The invoice date' + }, + { + arg: 'maxShipped', + type: 'date', + description: 'The maximum shipped date' + }, + { + arg: 'fromClientId', + type: 'number', + description: 'The minimum client id' + }, + { + arg: 'toClientId', + type: 'number', + description: 'The maximum client id' + }, + { + arg: 'companyFk', + type: 'number', + description: 'The company id to invoice' + } + ], + returns: { + type: 'object', + root: true + }, + http: { + path: '/globalInvoicing', + verb: 'POST' + } + }); + + Self.globalInvoicing = async(ctx, options) => { + const models = Self.app.models; + const args = ctx.args; + const invoicesIds = []; + + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + let query; + try { + query = ` + SELECT MAX(issued) issued + FROM vn.invoiceOut io + JOIN vn.time t ON t.dated = io.issued + WHERE io.serial = 'A' + AND t.year = YEAR(?) + AND io.companyFk = ?`; + const [maxIssued] = await Self.rawSql(query, [ + args.invoiceDate, + args.companyFk + ], myOptions); + + const maxSerialDate = maxIssued.issued || args.invoiceDate; + if (args.invoiceDate < maxSerialDate) + args.invoiceDate = maxSerialDate; + + if (args.invoiceDate < args.maxShipped) + args.maxShipped = args.invoiceDate; + + const minShipped = new Date(); + minShipped.setFullYear(minShipped.getFullYear() - 1); + + // Liquidacion de cubos y carros + const vIsAllInvoiceable = false; + const clientsWithPackaging = await getClientsWithPackaging(ctx, myOptions); + for (let client of clientsWithPackaging) { + await Self.rawSql('CALL packageInvoicing(?, ?, ?, ?, @newTicket)', [ + client.id, + args.invoiceDate, + args.companyFk, + vIsAllInvoiceable + ], myOptions); + } + + const company = await models.Company.findById(args.companyFk, null, myOptions); + const invoiceableClients = await getInvoiceableClients(ctx, myOptions); + + if (!invoiceableClients.length) return; + + for (let client of invoiceableClients) { + // esto es para los que no tienen rol de invoicing?? + /* const [clientTax] = await Self.rawSql('SELECT vn.clientTaxArea(?, ?) AS taxArea', [ + client.id, + args.companyFk + ], myOptions); + const clientTaxArea = clientTax.taxArea; + if (clientTaxArea != 'WORLD' && company.code === 'VNL' && hasRole('invoicing')) { + // Exit process?? + console.log(clientTaxArea); + throw new UserError('Not invoiceable ' + client.id); + } + */ + if (client.hasToInvoiceByAddress) { + await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [ + minShipped, + args.maxShipped, + client.addressFk, + args.companyFk + ], myOptions); + } else { + await Self.rawSql('CALL invoiceFromClient(?, ?, ?)', [ + args.maxShipped, + client.id, + args.companyFk + ], myOptions); + } + + // Make invoice + + const isSpanishCompany = await getIsSpanishCompany(args.companyFk, myOptions); + + // Validates ticket nagative base + const hasAnyNegativeBase = await getNegativeBase(myOptions); + if (hasAnyNegativeBase && isSpanishCompany) + continue; + + query = `SELECT invoiceSerial(?, ?, ?) AS serial`; + const [invoiceSerial] = await Self.rawSql(query, [ + client.id, + args.companyFk, + 'G' + ], myOptions); + const serialLetter = invoiceSerial.serial; + + query = `CALL invoiceOut_new(?, ?, NULL, @invoiceId)`; + await Self.rawSql(query, [ + serialLetter, + args.invoiceDate + ], myOptions); + + const [newInvoice] = await Self.rawSql(`SELECT @invoiceId id`, null, myOptions); + if (newInvoice.id) { + await Self.rawSql('CALL invoiceOutBooking(?)', [newInvoice.id], myOptions); + + invoicesIds.push(newInvoice.id); + } + + // IMPRIMIR PDF ID 3? + } + + // Print invoice to network printer + + if (tx) await tx.commit(); + + // Print invoices + for (let invoiceId of invoicesIds) + await Self.createPdf(ctx, invoiceId); + + return {}; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; + + async function getNegativeBase(options) { + const models = Self.app.models; + const query = 'SELECT hasAnyNegativeBase() AS base'; + const [result] = await models.InvoiceOut.rawSql(query, null, options); + + return result && result.base; + } + + async function getIsSpanishCompany(companyId, options) { + const models = Self.app.models; + const query = `SELECT COUNT(*) AS total + FROM supplier s + JOIN country c ON c.id = s.countryFk + AND c.code = 'ES' + WHERE s.id = ?`; + const [supplierCompany] = await models.InvoiceOut.rawSql(query, [ + companyId + ], options); + + return supplierCompany && supplierCompany.total; + } + + async function getClientsWithPackaging(ctx, options) { + const models = Self.app.models; + const args = ctx.args; + const query = `SELECT DISTINCT clientFk AS id + FROM ticket t + JOIN ticketPackaging tp ON t.id = tp.ticketFk + WHERE t.shipped BETWEEN '2017-11-21' AND ? + AND t.clientFk BETWEEN ? AND ?`; + return models.InvoiceOut.rawSql(query, [ + args.maxShipped, + args.fromClientId, + args.toClientId + ], options); + } + + async function getInvoiceableClients(ctx, options) { + const models = Self.app.models; + const args = ctx.args; + const minShipped = new Date(); + minShipped.setFullYear(minShipped.getFullYear() - 1); + + const query = `SELECT + c.id, + SUM(IFNULL(s.quantity * s.price * (100-s.discount)/100, 0) + IFNULL(ts.quantity * ts.price,0)) AS sumAmount, + c.hasToInvoiceByAddress, + c.email, + c.isToBeMailed, + a.id addressFk + FROM ticket t + LEFT JOIN sale s ON s.ticketFk = t.id + LEFT JOIN ticketService ts ON ts.ticketFk = t.id + JOIN address a ON a.id = t.addressFk + JOIN client c ON c.id = t.clientFk + WHERE ISNULL(t.refFk) AND c.id BETWEEN ? AND ? + AND t.shipped BETWEEN ? AND util.dayEnd(?) + AND t.companyFk = ? AND c.hasToInvoice + AND c.isTaxDataChecked + GROUP BY c.id, IF(c.hasToInvoiceByAddress,a.id,TRUE) HAVING sumAmount > 0`; + + return models.InvoiceOut.rawSql(query, [ + args.fromClientId, + args.toClientId, + minShipped, + args.maxShipped, + args.companyFk + ], options); + } +}; diff --git a/modules/invoiceOut/back/models/invoice-out.js b/modules/invoiceOut/back/models/invoice-out.js index 8a1dda41f..3b2822ada 100644 --- a/modules/invoiceOut/back/models/invoice-out.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -7,4 +7,5 @@ module.exports = Self => { require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/createPdf')(Self); require('../methods/invoiceOut/createManualInvoice')(Self); + require('../methods/invoiceOut/globalInvoicing')(Self); }; diff --git a/modules/invoiceOut/front/index.js b/modules/invoiceOut/front/index.js index bdb87f9a9..3bb6d54d2 100644 --- a/modules/invoiceOut/front/index.js +++ b/modules/invoiceOut/front/index.js @@ -8,3 +8,4 @@ import './card'; import './descriptor'; import './descriptor-popover'; import './index/manual'; +import './index/global-invoicing'; diff --git a/modules/invoiceOut/front/index/global-invoicing/index.html b/modules/invoiceOut/front/index/global-invoicing/index.html new file mode 100644 index 000000000..fd26afab9 --- /dev/null +++ b/modules/invoiceOut/front/index/global-invoicing/index.html @@ -0,0 +1,62 @@ + + Create global invoice + + + + + + + + + + + + + + + {{::id}} - {{::name}} + + + {{::id}} - {{::name}} + + + + + + + + + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/index/global-invoicing/index.js b/modules/invoiceOut/front/index/global-invoicing/index.js new file mode 100644 index 000000000..1dd943758 --- /dev/null +++ b/modules/invoiceOut/front/index/global-invoicing/index.js @@ -0,0 +1,77 @@ +import ngModule from '../../module'; +import Dialog from 'core/components/dialog'; +import './style.scss'; + +class Controller extends Dialog { + constructor($element, $, $transclude) { + super($element, $, $transclude); + + this.invoice = { + maxShipped: new Date() + }; + + this.getMinClientId(); + this.getMaxClientId(); + } + + getMinClientId() { + this.getClientId('min').then(res => { + this.invoice.fromClientId = res.data.id; + }); + } + + getMaxClientId() { + this.getClientId('max').then(res => { + this.invoice.toClientId = res.data.id; + }); + } + + getClientId(func) { + const order = func == 'min' ? 'ASC' : 'DESC'; + const params = { + filter: { + order: 'id ' + order, + limit: 1 + } + }; + return this.$http.get('Clients/findOne', {params}); + } + + get companyFk() { + return this.invoice.companyFk; + } + + set companyFk(value) { + this.invoice.companyFk = value; + } + + responseHandler(response) { + try { + if (response !== 'accept') + return super.responseHandler(response); + + /* if (this.invoice.clientFk && !this.invoice.maxShipped) + throw new Error('Client and the max shipped should be filled'); + + if (!this.invoice.serial || !this.invoice.taxArea) + throw new Error('Some fields are required'); */ + + return this.$http.post(`InvoiceOuts/globalInvoicing`, this.invoice) + .then(() => super.responseHandler(response)) + .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))); + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + } +} + +Controller.$inject = ['$element', '$scope', '$transclude']; + +ngModule.vnComponent('vnInvoiceOutGlobalInvoicing', { + slotTemplate: require('./index.html'), + controller: Controller, + bindings: { + companyFk: ' { + describe('Component vnInvoiceOutManual', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('invoiceOut')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + let $scope = $rootScope.$new(); + const $element = angular.element(''); + const $transclude = { + $$boundTransclude: { + $$slots: [] + } + }; + controller = $componentController('vnInvoiceOutManual', {$element, $scope, $transclude}); + })); + + describe('responseHandler()', () => { + it('should throw an error when clientFk property is set and the maxShipped is not filled', () => { + jest.spyOn(controller.vnApp, 'showError'); + + controller.invoice = { + clientFk: 1101, + serial: 'T', + taxArea: 'B' + }; + + controller.responseHandler('accept'); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`Client and the max shipped should be filled`); + }); + + it('should throw an error when some required fields are not filled in', () => { + jest.spyOn(controller.vnApp, 'showError'); + + controller.invoice = { + ticketFk: 1101 + }; + + controller.responseHandler('accept'); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`Some fields are required`); + }); + + it('should make an http POST query and then call to the parent showSuccess() method', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.invoice = { + ticketFk: 1101, + serial: 'T', + taxArea: 'B' + }; + + $httpBackend.expect('POST', `InvoiceOuts/createManualInvoice`).respond({id: 1}); + controller.responseHandler('accept'); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/modules/invoiceOut/front/index/global-invoicing/locale/es.yml b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml new file mode 100644 index 000000000..b4b13945c --- /dev/null +++ b/modules/invoiceOut/front/index/global-invoicing/locale/es.yml @@ -0,0 +1,3 @@ +Create global invoice: Crear factura global +Some fields are required: Algunos campos son obligatorios +Max date: Fecha límite \ No newline at end of file diff --git a/modules/invoiceOut/front/index/global-invoicing/style.scss b/modules/invoiceOut/front/index/global-invoicing/style.scss new file mode 100644 index 000000000..4e2435c21 --- /dev/null +++ b/modules/invoiceOut/front/index/global-invoicing/style.scss @@ -0,0 +1,5 @@ +.vn-invoice-out-global-invoicing { + tpl-body { + width: 500px + } +} \ No newline at end of file diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html index 9d0cc4337..999fafe95 100644 --- a/modules/invoiceOut/front/index/index.html +++ b/modules/invoiceOut/front/index/index.html @@ -74,6 +74,11 @@ ng-click="manualInvoicing.show()"> Manual invoicing + + Global invoicing + @@ -87,4 +92,8 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/index/locale/es.yml b/modules/invoiceOut/front/index/locale/es.yml index eb22e1779..560666e9c 100644 --- a/modules/invoiceOut/front/index/locale/es.yml +++ b/modules/invoiceOut/front/index/locale/es.yml @@ -3,4 +3,5 @@ Issued: Fecha factura Due date: Fecha vencimiento Has PDF: PDF disponible Minimum: Minimo -Maximum: Máximo \ No newline at end of file +Maximum: Máximo +Global invoicing: Facturación global \ 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 d8c2dc5c9..ce7380568 100644 --- a/modules/ticket/back/methods/ticket/makeInvoice.js +++ b/modules/ticket/back/methods/ticket/makeInvoice.js @@ -100,13 +100,14 @@ module.exports = function(Self) { }, myOptions); } - if (serial != 'R' && invoiceId) { + if (serial != 'R' && invoiceId) await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], myOptions); - await models.InvoiceOut.createPdf(ctx, invoiceId, myOptions); - } if (tx) await tx.commit(); + if (serial != 'R' && invoiceId) + await models.InvoiceOut.createPdf(ctx, invoiceId); + return {invoiceFk: invoiceId, serial: serial}; } catch (e) { if (tx) await tx.rollback();