From ff71e6f2c7e3596db04a51d79e828f365864d142 Mon Sep 17 00:00:00 2001 From: joan Date: Wed, 14 Jul 2021 11:14:53 +0200 Subject: [PATCH 01/14] 2879 - Autocomplete item field from imported buys --- .../10340-summer/00-buy_importReference.sql | 14 +++++++ .../entry/back/methods/entry/importBuys.js | 7 ++++ .../back/methods/entry/importBuysPreview.js | 16 ++++++- modules/entry/back/model-config.json | 3 ++ .../back/models/buy-import-reference.json | 32 ++++++++++++++ modules/entry/front/buy/import/index.html | 42 +++++++------------ modules/entry/front/buy/import/index.js | 4 +- 7 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 db/changes/10340-summer/00-buy_importReference.sql create mode 100644 modules/entry/back/models/buy-import-reference.json diff --git a/db/changes/10340-summer/00-buy_importReference.sql b/db/changes/10340-summer/00-buy_importReference.sql new file mode 100644 index 000000000..4046b498b --- /dev/null +++ b/db/changes/10340-summer/00-buy_importReference.sql @@ -0,0 +1,14 @@ +create table `vn`.`buyImportReference` +( + itemFk int not null, + name varchar(80) not null, + company varchar(80) not null, + size int not null, + constraint buyImportReference_pk + primary key (itemFk, name, company, size), + constraint itemFk___fk + foreign key (itemFk) references item (id) + on update cascade on delete cascade +) +comment 'Referencias de compras importadas'; + diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js index 325fe4d22..d8db328a3 100644 --- a/modules/entry/back/methods/entry/importBuys.js +++ b/modules/entry/back/methods/entry/importBuys.js @@ -71,6 +71,13 @@ module.exports = Self => { buyingValue: buy.buyingValue, packageFk: buy.packageFk }); + + await models.BuyImportReference.upsert({ + itemFk: buy.itemFk, + name: buy.description, + company: buy.companyName, + size: buy.size + }, options); } const createdBuys = await models.Buy.create(buys, options); diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js index 9ba2b58ed..6b03579c7 100644 --- a/modules/entry/back/methods/entry/importBuysPreview.js +++ b/modules/entry/back/methods/entry/importBuysPreview.js @@ -37,7 +37,21 @@ module.exports = Self => { where: {volume: {gte: buy.volume}}, order: 'volume ASC' }, myOptions); - buy.packageFk = packaging.id; + + if (packaging) + buy.packageFk = packaging.id; + + const reference = await models.BuyImportReference.findOne({ + fields: ['itemFk'], + where: { + name: buy.description, + company: buy.companyName, + size: buy.size + } + }, myOptions); + + if (reference) + buy.itemFk = reference.itemFk; } return buys; diff --git a/modules/entry/back/model-config.json b/modules/entry/back/model-config.json index eddef9c41..b4c50c3aa 100644 --- a/modules/entry/back/model-config.json +++ b/modules/entry/back/model-config.json @@ -5,6 +5,9 @@ "Buy": { "dataSource": "vn" }, + "BuyImportReference": { + "dataSource": "vn" + }, "EntryLog": { "dataSource": "vn" }, diff --git a/modules/entry/back/models/buy-import-reference.json b/modules/entry/back/models/buy-import-reference.json new file mode 100644 index 000000000..ffe524858 --- /dev/null +++ b/modules/entry/back/models/buy-import-reference.json @@ -0,0 +1,32 @@ +{ + "name": "BuyImportReference", + "base": "VnModel", + "options": { + "mysql": { + "table": "buyImportReference" + } + }, + "properties": { + "itemFk": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "name": { + "type": "string" + }, + "company": { + "type": "string" + }, + "size": { + "type": "string" + } + }, + "relations": { + "item": { + "type": "belongsTo", + "model": "Item", + "foreignKey": "itemFk" + } + } +} \ No newline at end of file diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html index 74b6c708a..742c156be 100644 --- a/modules/entry/front/buy/import/index.html +++ b/modules/entry/front/buy/import/index.html @@ -9,20 +9,6 @@ class="vn-ma-md">
- - - - - - - - + + + + + + + + @@ -51,7 +51,6 @@ - @@ -74,16 +73,8 @@ - - + -
Grouping Buying value BoxVolume
{{::buy.description | dashIfEmpty}} {{::buy.size | dashIfEmpty}} - - {{::buy.packing | dashIfEmpty}} - - - - {{::buy.grouping | dashIfEmpty}} - - + {{::buy.packing | dashIfEmpty}}{{::buy.grouping | dashIfEmpty}} {{::buy.buyingValue | currency: 'EUR':2}} {{::buy.volume | number}}
diff --git a/modules/entry/front/buy/import/index.js b/modules/entry/front/buy/import/index.js index b5ff92a89..b802f36c4 100644 --- a/modules/entry/front/buy/import/index.js +++ b/modules/entry/front/buy/import/index.js @@ -29,6 +29,7 @@ class Controller extends Section { this.$.$applyAsync(() => { this.import.observation = invoice.tx_awb; + const companyName = invoice.tx_company; const boxes = invoice.boxes; const buys = []; for (let box of boxes) { @@ -37,11 +38,12 @@ class Controller extends Section { const packing = product.nu_stems_bunch * product.nu_bunches; buys.push({ description: product.nm_product, + companyName: companyName, size: product.nu_length, packing: packing, grouping: product.nu_stems_bunch, buyingValue: parseFloat(product.mny_rate_stem), - volume: boxVolume + volume: boxVolume, }); } } From 94b668c01e84f4a5064ed4224ca6aa11d88597e5 Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 15 Jul 2021 13:41:29 +0200 Subject: [PATCH 02/14] Filter item from a dialog --- modules/entry/front/buy/import/index.html | 97 ++++++++++++++++++- modules/entry/front/buy/import/index.js | 53 ++++++++++ modules/entry/front/buy/import/style.scss | 6 ++ modules/entry/front/buy/locale/es.yml | 4 +- modules/item/front/search-panel/locale/es.yml | 2 +- 5 files changed, 159 insertions(+), 3 deletions(-) diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html index 742c156be..179657dae 100644 --- a/modules/entry/front/buy/import/index.html +++ b/modules/entry/front/buy/import/index.html @@ -69,6 +69,13 @@ {{::id}} - {{::name}} + + + + {{::buy.description | dashIfEmpty}} @@ -100,7 +107,95 @@ label="Cancel" ui-sref="entry.card.buy.index"> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + ID + Item + Size + Producer + Color + + + + + + + {{::item.id}} + + + {{::item.name}} + {{::item.size}} + {{::item.producer.name}} + {{::item.ink.name}} + + + + + + + + \ No newline at end of file diff --git a/modules/entry/front/buy/import/index.js b/modules/entry/front/buy/import/index.js index b802f36c4..2f13b2746 100644 --- a/modules/entry/front/buy/import/index.js +++ b/modules/entry/front/buy/import/index.js @@ -88,6 +88,59 @@ class Controller extends Section { ? {id: $search} : {name: {like: '%' + $search + '%'}}; } + + showFilterDialog(buy) { + this.activeBuy = buy; + this.itemFilterParams = {}; + this.itemFilter = { + include: [ + { + relation: 'producer', + scope: { + fields: ['name'] + } + }, + { + relation: 'ink', + scope: { + fields: ['name'] + } + } + ] + }; + + this.$.filterDialog.show(); + } + + selectItem(id) { + this.activeBuy['itemFk'] = id; + this.$.filterDialog.hide(); + } + + filter() { + const filter = this.itemFilter; + const params = this.itemFilterParams; + const where = {}; + + for (let key in params) { + const value = params[key]; + if (!value) continue; + + switch (key) { + case 'name': + where[key] = {like: `%${value}%`}; + break; + case 'producerFk': + case 'typeFk': + case 'size': + case 'ink': + where[key] = value; + } + } + + filter.where = where; + this.$.itemsModel.applyFilter(filter); + } } Controller.$inject = ['$element', '$scope']; diff --git a/modules/entry/front/buy/import/style.scss b/modules/entry/front/buy/import/style.scss index dba069616..8426d4169 100644 --- a/modules/entry/front/buy/import/style.scss +++ b/modules/entry/front/buy/import/style.scss @@ -2,4 +2,10 @@ vn-entry-buy-import { .vn-table > tbody td:nth-child(1) { width: 250px } +} + +.itemFilter { + vn-table.scrollable { + height: 500px + } } \ No newline at end of file diff --git a/modules/entry/front/buy/locale/es.yml b/modules/entry/front/buy/locale/es.yml index c77587758..55828a3c6 100644 --- a/modules/entry/front/buy/locale/es.yml +++ b/modules/entry/front/buy/locale/es.yml @@ -3,4 +3,6 @@ Observation: Observación Box: Embalaje Import buys: Importar compras Some of the imported buys doesn't have an item: Algunas de las compras importadas no tienen un artículo -JSON files only: Solo ficheros JSON \ No newline at end of file +JSON files only: Solo ficheros JSON +Filter item: Filtrar artículo +Filter...: Filtrar... \ No newline at end of file diff --git a/modules/item/front/search-panel/locale/es.yml b/modules/item/front/search-panel/locale/es.yml index 197da0695..67a5200d7 100644 --- a/modules/item/front/search-panel/locale/es.yml +++ b/modules/item/front/search-panel/locale/es.yml @@ -1,6 +1,6 @@ Ink: Tinta Origin: Origen -Producer: Productor. +Producer: Productor With visible: Con visible Field: Campo More fields: Más campos From 531937ebbcc9a80db37734d264e807192aa8620d Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 27 Jul 2021 09:07:04 +0200 Subject: [PATCH 03/14] 2985 - Create a manual invoice from a ticket or client --- db/changes/10340-summer/00-ACL.sql | 3 + .../10340-summer/00-invoiceFromClient.sql | 21 ++ .../00-invoiceOut_newFromClient.sql | 45 +++++ .../00-invoiceOut_newFromTicket.sql | 38 ++++ loopback/locale/en.json | 4 +- loopback/locale/es.json | 10 +- .../methods/invoiceOut/createManualInvoice.js | 188 ++++++++++++++++++ .../back/methods/invoiceOut/createPdf.js | 13 +- .../back/methods/invoiceOut/delete.js | 7 +- .../back/methods/invoiceOut/download.js | 42 ++-- modules/invoiceOut/back/model-config.json | 18 ++ .../back/models/invoice-out-serial.json | 38 ++++ .../models/{invoiceOut.js => invoice-out.js} | 1 + .../{invoiceOut.json => invoice-out.json} | 0 modules/invoiceOut/back/models/tax-area.json | 22 ++ .../back/models/tax-class-code.json | 0 .../back/models/tax-class.json | 0 .../back/models/tax-code.json | 0 .../back/models/tax-type.json | 0 modules/invoiceOut/front/index.js | 1 + modules/invoiceOut/front/index/index.html | 23 +++ .../invoiceOut/front/index/manual/index.html | 75 +++++++ .../invoiceOut/front/index/manual/index.js | 47 +++++ .../front/index/manual/index.spec.js | 66 ++++++ .../front/index/manual/locale/es.yml | 4 + .../invoiceOut/front/index/manual/style.scss | 5 + modules/item/back/model-config.json | 12 -- 27 files changed, 645 insertions(+), 38 deletions(-) create mode 100644 db/changes/10340-summer/00-invoiceFromClient.sql create mode 100644 db/changes/10340-summer/00-invoiceOut_newFromClient.sql create mode 100644 db/changes/10340-summer/00-invoiceOut_newFromTicket.sql create mode 100644 modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js create mode 100644 modules/invoiceOut/back/models/invoice-out-serial.json rename modules/invoiceOut/back/models/{invoiceOut.js => invoice-out.js} (86%) rename modules/invoiceOut/back/models/{invoiceOut.json => invoice-out.json} (100%) create mode 100644 modules/invoiceOut/back/models/tax-area.json rename modules/{item => invoiceOut}/back/models/tax-class-code.json (100%) rename modules/{item => invoiceOut}/back/models/tax-class.json (100%) rename modules/{item => invoiceOut}/back/models/tax-code.json (100%) rename modules/{item => invoiceOut}/back/models/tax-type.json (100%) create mode 100644 modules/invoiceOut/front/index/manual/index.html create mode 100644 modules/invoiceOut/front/index/manual/index.js create mode 100644 modules/invoiceOut/front/index/manual/index.spec.js create mode 100644 modules/invoiceOut/front/index/manual/locale/es.yml create mode 100644 modules/invoiceOut/front/index/manual/style.scss diff --git a/db/changes/10340-summer/00-ACL.sql b/db/changes/10340-summer/00-ACL.sql index a9a5778e5..fd92b3c1c 100644 --- a/db/changes/10340-summer/00-ACL.sql +++ b/db/changes/10340-summer/00-ACL.sql @@ -2,3 +2,6 @@ DELETE FROM `salix`.`ACL` WHERE id = 189; DELETE FROM `salix`.`ACL` WHERE id = 188; UPDATE `salix`.`ACL` tdms SET tdms.accessType = '*' WHERE tdms.id = 165; +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) + VALUES ('InvoiceOut', 'createManualInvoice', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'); + diff --git a/db/changes/10340-summer/00-invoiceFromClient.sql b/db/changes/10340-summer/00-invoiceFromClient.sql new file mode 100644 index 000000000..d198e873d --- /dev/null +++ b/db/changes/10340-summer/00-invoiceFromClient.sql @@ -0,0 +1,21 @@ +drop procedure `vn`.`invoiceFromClient`; + +DELIMITER $$ +$$ +create + definer = root@`%` procedure `vn`.`invoiceFromClient`(IN vMaxTicketDate datetime, IN vClientFk INT, IN vCompanyFk INT) +BEGIN + DECLARE vMinTicketDate DATE DEFAULT TIMESTAMPADD(YEAR, -3, CURDATE()); + SET vMaxTicketDate = util.dayend(vMaxTicketDate); + + DROP TEMPORARY TABLE IF EXISTS `ticketToInvoice`; + CREATE TEMPORARY TABLE `ticketToInvoice` + (PRIMARY KEY (`id`)) + ENGINE = MEMORY + SELECT id FROM ticket t + WHERE t.clientFk = vClientFk + AND t.refFk IS NULL + AND t.companyFk = vCompanyFk + AND (t.shipped BETWEEN vMinTicketDate AND vMaxTicketDate); +END;;$$ +DELIMITER ; diff --git a/db/changes/10340-summer/00-invoiceOut_newFromClient.sql b/db/changes/10340-summer/00-invoiceOut_newFromClient.sql new file mode 100644 index 000000000..b4d4f916b --- /dev/null +++ b/db/changes/10340-summer/00-invoiceOut_newFromClient.sql @@ -0,0 +1,45 @@ +drop procedure `vn`.`invoiceOut_newFromClient`; + +DELIMITER $$ +$$ +create + definer = root@`%` procedure `vn`.`invoiceOut_newFromClient`(IN vClientFk int, IN vSerial char(2), IN vMaxShipped date, + IN vCompanyFk int, IN vTaxArea varchar(25), + IN vRef varchar(25), OUT vInvoiceId int) +BEGIN +/** + * Factura los tickets de un cliente hasta una fecha dada + * @param vClientFk Id del cliente a facturar + * @param vSerial Serie de factura + * @param vMaxShipped Fecha hasta la cual cogera tickets para facturar + * @param vCompanyFk Id de la empresa desde la que se factura + * @param vTaxArea Tipo de iva en relacion a la empresa y al cliente, NULL por defecto + * @param vRef Referencia de la factura en caso que se quiera forzar, NULL por defecto + * @return vInvoiceId factura + */ + + DECLARE vIsRefEditable BOOLEAN; + + IF vRef IS NOT NULL THEN + SELECT isRefEditable INTO vIsRefEditable + FROM invoiceOutSerial + WHERE code = vSerial; + + IF NOT vIsRefEditable THEN + CALL util.throw('serial non editable'); + END IF; + END IF; + + CALL invoiceFromClient(vMaxShipped, vClientFk, vCompanyFk); + CALL invoiceOut_new(vSerial, CURDATE(), vTaxArea, vInvoiceId); + + UPDATE invoiceOut + SET `ref` = vRef + WHERE id = vInvoiceId + AND vRef IS NOT NULL; + + IF vSerial <> 'R' AND NOT ISNULL(vInvoiceId) AND vInvoiceId <> 0 THEN + CALL invoiceOutBooking(vInvoiceId); + END IF; +END;;$$ +DELIMITER ; diff --git a/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql b/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql new file mode 100644 index 000000000..15be3d83c --- /dev/null +++ b/db/changes/10340-summer/00-invoiceOut_newFromTicket.sql @@ -0,0 +1,38 @@ +drop procedure `vn`.`invoiceOut_newFromTicket`; + +DELIMITER $$ +$$ +create + definer = root@`%` procedure `vn`.`invoiceOut_newFromTicket`(IN vTicketFk int, IN vSerial char(2), IN vTaxArea varchar(25), + IN vRef varchar(25), OUT vInvoiceId int) +BEGIN +/** + * Factura un ticket + * @param vTicketFk Id del ticket + * @param vSerial Serie de factura + * @param vTaxArea Area de la factura en caso de querer forzarlo, + * en la mayoria de los casos poner NULL + * @return vInvoiceId + */ + DECLARE vIsRefEditable BOOLEAN; + CALL invoiceFromTicket(vTicketFk); + CALL invoiceOut_new(vSerial, CURDATE(), vTaxArea, vInvoiceId); + + IF vRef IS NOT NULL THEN + SELECT isRefEditable INTO vIsRefEditable + FROM invoiceOutSerial + WHERE code = vSerial; + IF NOT vIsRefEditable THEN + CALL util.throw('serial non editable'); + END IF; + + UPDATE invoiceOut + SET `ref` = vRef + WHERE id = vInvoiceId; + END IF; + + IF vSerial <> 'R' AND NOT ISNULL(vInvoiceId) AND vInvoiceId <> 0 THEN + CALL invoiceOutBooking(vInvoiceId); + END IF; +END;;$$ +DELIMITER ; diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 75804ba21..2484990ed 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -98,6 +98,6 @@ "Client assignment has changed": "I did change the salesperson ~*\"<{{previousWorkerName}}>\"*~ by *\"<{{currentWorkerName}}>\"* from the client [{{clientName}} ({{clientId}})]({{{url}}})", "None": "None", "error densidad = 0": "error densidad = 0", - "nickname": "nickname", - "This document already exists on this ticket": "This document already exists on this ticket" + "This document already exists on this ticket": "This document already exists on this ticket", + "serial non editable": "This serial doesn't allow to set a reference" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index ff30a61ff..1d73cbbf3 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -184,5 +184,13 @@ "The contract was not active during the selected date": "El contrato no estaba activo durante la fecha seleccionada", "This document already exists on this ticket": "Este documento ya existe en el ticket", "Some of the selected tickets are not billable": "Algunos de los tickets seleccionados no son facturables", - "You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes" + "You can't invoice tickets from multiple clients": "No puedes facturar tickets de multiples clientes", + "This client is not invoiceable": "Este cliente no es facturable", + "serial non editable": "Esta serie no permite asignar la referencia", + "Max shipped required": "La fecha límite es requerida", + "Can't invoice to future": "No se puede facturar a futuro", + "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" } \ No newline at end of file diff --git a/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js new file mode 100644 index 000000000..44d52b38f --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js @@ -0,0 +1,188 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('createManualInvoice', { + description: 'Make a manual invoice', + accessType: 'WRITE', + accepts: [ + { + arg: 'clientFk', + type: 'any', + description: 'The invoiceable client id' + }, + { + arg: 'ticketFk', + type: 'any', + description: 'The invoiceable ticket id' + }, + { + arg: 'maxShipped', + type: 'date', + description: 'The maximum shipped date' + }, + { + arg: 'serial', + type: 'string', + description: 'The invoice serial' + }, + { + arg: 'taxArea', + type: 'string', + description: 'The invoice tax area' + }, + { + arg: 'reference', + type: 'string', + description: 'The invoice reference' + } + ], + returns: { + type: 'object', + root: true + }, + http: { + path: '/createManualInvoice', + verb: 'POST' + } + }); + + Self.createManualInvoice = async(ctx, options) => { + const models = Self.app.models; + const args = ctx.args; + + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + const ticketId = args.ticketFk; + let clientId = args.clientFk; + let maxShipped = args.maxShipped; + let companyId; + let query; + try { + if (ticketId) { + const ticket = await models.Ticket.findById(ticketId, null, myOptions); + const company = await models.Company.findById(ticket.companyFk, null, myOptions); + + clientId = ticket.clientFk; + maxShipped = ticket.shipped; + companyId = ticket.companyFk; + + // Validates invoiced ticket + if (ticket.refFk) + throw new UserError('This ticket is already invoiced'); + + // Validates ticket amount + if (ticket.totalWithVat == 0) { + // Change state to delivered + throw new UserError(`A ticket with an amount of zero can't be invoiced`); + } + + // Validates ticket nagative base + const hasNegativeBase = await getNegativeBase(ticketId, myOptions); + + if (hasNegativeBase && company.code == 'VNL') + throw new UserError(`A ticket with a negative base can't be invoiced`); + } else { + if (!maxShipped) + throw new UserError(`Max shipped required`); + + const company = await models.Ticket.findOne({ + fields: ['companyFk'], + where: { + clientFk: clientId, + shipped: {lte: maxShipped} + } + }, myOptions); + companyId = company.companyFk; + } + + // Set shipped at night + maxShipped.setHours(23, 59, 59, 59); + + // Validate invoiceable client + const isClientInvoiceable = await isInvoiceable(clientId, myOptions); + if (!isClientInvoiceable) + throw new UserError(`This client is not invoiceable`); + + // Can't invoice tickets into future + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + if (maxShipped >= tomorrow) + throw new UserError(`Can't invoice to future`); + + const maxInvoiceDate = await getMaxIssued(args.serial, companyId, myOptions); + if (new Date() < maxInvoiceDate) + throw new UserError(`Can't invoice to past`); + + if (ticketId) { + query = `CALL invoiceOut_newFromTicket(?, ?, ?, ?, @newInvoiceId)`; + await Self.rawSql(query, [ + ticketId, + args.serial, + args.taxArea, + args.reference + ], myOptions); + } else { + query = `CALL invoiceOut_newFromClient(?, ?, ?, ?, ?, ?, @newInvoiceId)`; + await Self.rawSql(query, [ + clientId, + args.serial, + maxShipped, + companyId, + args.taxArea, + args.reference + ], myOptions); + } + + const [newInvoice] = await Self.rawSql(`SELECT @newInvoiceId id`, null, myOptions); + if (newInvoice.id) + await Self.createPdf(ctx, newInvoice.id, myOptions); + + if (tx) await tx.commit(); + + return newInvoice; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; + + async function isInvoiceable(clientId, options) { + const models = Self.app.models; + const query = `SELECT (hasToInvoice AND isTaxDataChecked) AS invoiceable + FROM client + WHERE id = ?`; + const [result] = await models.InvoiceOut.rawSql(query, [clientId], options); + + return result.invoiceable; + } + + async function getNegativeBase(ticketId, options) { + const models = Self.app.models; + const query = 'SELECT vn.hasSomeNegativeBase(?) AS base'; + const [result] = await models.InvoiceOut.rawSql(query, [ticketId], options); + + return result.base; + } + + async function getMaxIssued(serial, companyId, options) { + const models = Self.app.models; + const query = `SELECT MAX(issued) AS issued + FROM invoiceOut + WHERE serial = ? AND companyFk = ?`; + const [maxIssued] = await models.InvoiceOut.rawSql(query, + [serial, companyId], options); + const maxInvoiceDate = maxIssued && maxIssued.issued || new Date(); + + return maxInvoiceDate; + } +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js index 6e596db62..5f43e4a32 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -58,11 +58,18 @@ module.exports = Self => { } }); - const invoiceYear = invoiceOut.created.getFullYear().toString(); - const container = await models.InvoiceContainer.container(invoiceYear); + const created = invoiceOut.created; + const year = created.getFullYear().toString(); + const month = created.getMonth().toString(); + const day = created.getDate().toString(); + + const container = await models.InvoiceContainer.container(year); const rootPath = container.client.root; const fileName = `${invoiceOut.ref}.pdf`; - fileSrc = path.join(rootPath, invoiceYear, fileName); + const src = path.join(rootPath, year, month, day); + fileSrc = path.join(src, fileName); + + await fs.mkdir(src, {recursive: true}); const writeStream = fs.createWriteStream(fileSrc); writeStream.on('open', () => { diff --git a/modules/invoiceOut/back/methods/invoiceOut/delete.js b/modules/invoiceOut/back/methods/invoiceOut/delete.js index 96b5e652e..d8b9d309b 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/delete.js +++ b/modules/invoiceOut/back/methods/invoiceOut/delete.js @@ -34,13 +34,14 @@ module.exports = Self => { try { const invoiceOut = await Self.findById(id, {}, myOptions); - const tickets = await Self.app.models.Ticket.find({where: {refFk: invoiceOut.ref}}, myOptions); + const tickets = await Self.app.models.Ticket.find({ + where: {refFk: invoiceOut.ref} + }, myOptions); const promises = []; - tickets.forEach(ticket => { + for (let ticket of tickets) promises.push(ticket.updateAttribute('refFk', null, myOptions)); - }); await Promise.all(promises); diff --git a/modules/invoiceOut/back/methods/invoiceOut/download.js b/modules/invoiceOut/back/methods/invoiceOut/download.js index 3c9924fc3..983647982 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/download.js +++ b/modules/invoiceOut/back/methods/invoiceOut/download.js @@ -1,4 +1,5 @@ const fs = require('fs-extra'); +const path = require('path'); module.exports = Self => { Self.remoteMethod('download', { @@ -33,24 +34,31 @@ module.exports = Self => { } }); - Self.download = async function(id) { - let file; - let env = process.env.NODE_ENV; - let [invoice] = await Self.rawSql(`SELECT invoiceOut_getPath(?) path`, [id]); + Self.download = async function(id, options) { + const models = Self.app.models; + const myOptions = {}; - if (env && env != 'development') { - file = { - path: `/var/lib/salix/pdfs/${invoice.path}`, - contentType: 'application/pdf', - name: `${id}.pdf` - }; - } else { - file = { - path: `${process.cwd()}/README.md`, - contentType: 'text/plain', - name: `README.md` - }; - } + if (typeof options == 'object') + Object.assign(myOptions, options); + + const invoiceOut = await models.InvoiceOut.findById(id, null, myOptions); + + const created = invoiceOut.created; + const year = created.getFullYear().toString(); + const month = created.getMonth().toString(); + const day = created.getDate().toString(); + + const container = await models.InvoiceContainer.container(year); + const rootPath = container.client.root; + const src = path.join(rootPath, year, month, day); + const fileName = `${invoiceOut.ref}.pdf`; + const fileSrc = path.join(src, fileName); + + const file = { + path: fileSrc, + contentType: 'application/pdf', + name: `${id}.pdf` + }; await fs.access(file.path); let stream = fs.createReadStream(file.path); diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index e144ce80e..d52f79477 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -2,7 +2,25 @@ "InvoiceOut": { "dataSource": "vn" }, + "InvoiceOutSerial": { + "dataSource": "vn" + }, "InvoiceContainer": { "dataSource": "invoiceStorage" + }, + "TaxArea": { + "dataSource": "vn" + }, + "TaxClass": { + "dataSource": "vn" + }, + "TaxClassCode": { + "dataSource": "vn" + }, + "TaxCode": { + "dataSource": "vn" + }, + "TaxType": { + "dataSource": "vn" } } diff --git a/modules/invoiceOut/back/models/invoice-out-serial.json b/modules/invoiceOut/back/models/invoice-out-serial.json new file mode 100644 index 000000000..912269fd7 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-out-serial.json @@ -0,0 +1,38 @@ +{ + "name": "InvoiceOutSerial", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceOutSerial" + } + }, + "properties": { + "code": { + "type": "string", + "id": true, + "description": "Identifier" + }, + "description": { + "type": "string" + }, + "isTaxed": { + "type": "boolean" + }, + "isCEE": { + "type": "boolean" + } + }, + "relations": { + "taxArea": { + "type": "belongsTo", + "model": "TaxArea", + "foreignKey": "taxAreaFk" + } + }, + "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/invoice-out.js similarity index 86% rename from modules/invoiceOut/back/models/invoiceOut.js rename to modules/invoiceOut/back/models/invoice-out.js index 7c6503d8e..8a1dda41f 100644 --- a/modules/invoiceOut/back/models/invoiceOut.js +++ b/modules/invoiceOut/back/models/invoice-out.js @@ -6,4 +6,5 @@ module.exports = Self => { require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/createPdf')(Self); + require('../methods/invoiceOut/createManualInvoice')(Self); }; diff --git a/modules/invoiceOut/back/models/invoiceOut.json b/modules/invoiceOut/back/models/invoice-out.json similarity index 100% rename from modules/invoiceOut/back/models/invoiceOut.json rename to modules/invoiceOut/back/models/invoice-out.json diff --git a/modules/invoiceOut/back/models/tax-area.json b/modules/invoiceOut/back/models/tax-area.json new file mode 100644 index 000000000..0aa00d194 --- /dev/null +++ b/modules/invoiceOut/back/models/tax-area.json @@ -0,0 +1,22 @@ +{ + "name": "TaxArea", + "base": "VnModel", + "options": { + "mysql": { + "table": "taxArea" + } + }, + "properties": { + "code": { + "type": "string", + "id": true, + "description": "Identifier" + } + }, + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/item/back/models/tax-class-code.json b/modules/invoiceOut/back/models/tax-class-code.json similarity index 100% rename from modules/item/back/models/tax-class-code.json rename to modules/invoiceOut/back/models/tax-class-code.json diff --git a/modules/item/back/models/tax-class.json b/modules/invoiceOut/back/models/tax-class.json similarity index 100% rename from modules/item/back/models/tax-class.json rename to modules/invoiceOut/back/models/tax-class.json diff --git a/modules/item/back/models/tax-code.json b/modules/invoiceOut/back/models/tax-code.json similarity index 100% rename from modules/item/back/models/tax-code.json rename to modules/invoiceOut/back/models/tax-code.json diff --git a/modules/item/back/models/tax-type.json b/modules/invoiceOut/back/models/tax-type.json similarity index 100% rename from modules/item/back/models/tax-type.json rename to modules/invoiceOut/back/models/tax-type.json diff --git a/modules/invoiceOut/front/index.js b/modules/invoiceOut/front/index.js index 9843e188b..bdb87f9a9 100644 --- a/modules/invoiceOut/front/index.js +++ b/modules/invoiceOut/front/index.js @@ -7,3 +7,4 @@ import './summary'; import './card'; import './descriptor'; import './descriptor-popover'; +import './index/manual'; diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html index 8685990a4..9d0cc4337 100644 --- a/modules/invoiceOut/front/index/index.html +++ b/modules/invoiceOut/front/index/index.html @@ -57,6 +57,26 @@ +
+ + + + + + + Manual invoicing + + + +
@@ -65,3 +85,6 @@ + + \ No newline at end of file diff --git a/modules/invoiceOut/front/index/manual/index.html b/modules/invoiceOut/front/index/manual/index.html new file mode 100644 index 000000000..f2fd10d6f --- /dev/null +++ b/modules/invoiceOut/front/index/manual/index.html @@ -0,0 +1,75 @@ + + Create manual invoice + + + + + + + + + + {{::id}} - {{::nickname}} + + + Or + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/index/manual/index.js b/modules/invoiceOut/front/index/manual/index.js new file mode 100644 index 000000000..77dd93de7 --- /dev/null +++ b/modules/invoiceOut/front/index/manual/index.js @@ -0,0 +1,47 @@ +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() + }; + } + + 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/createManualInvoice`, this.invoice) + .then(res => { + this.$state.go('invoiceOut.card.summary', {id: res.data.id}); + 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('vnInvoiceOutManual', { + slotTemplate: require('./index.html'), + controller: Controller, + bindings: { + ticketFk: ' { + 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/manual/locale/es.yml b/modules/invoiceOut/front/index/manual/locale/es.yml new file mode 100644 index 000000000..839b212f6 --- /dev/null +++ b/modules/invoiceOut/front/index/manual/locale/es.yml @@ -0,0 +1,4 @@ +Create manual invoice: Crear factura manual +Some fields are required: Algunos campos son obligatorios +Client and max shipped fields should be filled: Los campos de cliente y fecha límite deben rellenarse +Max date: Fecha límite \ No newline at end of file diff --git a/modules/invoiceOut/front/index/manual/style.scss b/modules/invoiceOut/front/index/manual/style.scss new file mode 100644 index 000000000..998472157 --- /dev/null +++ b/modules/invoiceOut/front/index/manual/style.scss @@ -0,0 +1,5 @@ +.vn-invoice-out-manual { + tpl-body { + width: 500px + } +} \ No newline at end of file diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json index 9f101f9c7..ab680b696 100644 --- a/modules/item/back/model-config.json +++ b/modules/item/back/model-config.json @@ -65,18 +65,6 @@ "Tag": { "dataSource": "vn" }, - "TaxClass": { - "dataSource": "vn" - }, - "TaxClassCode": { - "dataSource": "vn" - }, - "TaxCode": { - "dataSource": "vn" - }, - "TaxType": { - "dataSource": "vn" - }, "FixedPrice": { "dataSource": "vn" } From 8b005ae012fafa7be3afe7750006760b4c5a744d Mon Sep 17 00:00:00 2001 From: joan Date: Tue, 27 Jul 2021 12:24:45 +0200 Subject: [PATCH 04/14] Updated unit tests --- .../specs/createManualInvoice.spec.js | 145 ++++++++++++++++++ .../invoiceOut/specs/createPdf.spec.js | 14 +- .../methods/invoiceOut/specs/download.spec.js | 4 +- 3 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js new file mode 100644 index 000000000..e5c3c0512 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createManualInvoice.spec.js @@ -0,0 +1,145 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('InvoiceOut createManualInvoice()', () => { + const userId = 1; + const ticketId = 16; + const clientId = 1106; + const activeCtx = { + accessToken: {userId: userId}, + }; + const ctx = {req: activeCtx}; + + it('should throw an error trying to invoice again', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + let error; + try { + ctx.args = { + ticketFk: ticketId, + serial: 'T', + taxArea: 'CEE' + }; + await models.InvoiceOut.createManualInvoice(ctx, options); + await models.InvoiceOut.createManualInvoice(ctx, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain('This ticket is already invoiced'); + }); + + it('should throw an error for a ticket with an amount of zero', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true); + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + let error; + try { + const ticket = await models.Ticket.findById(ticketId, null, options); + await ticket.updateAttributes({ + totalWithVat: 0 + }, options); + + ctx.args = { + ticketFk: ticketId, + serial: 'T', + taxArea: 'CEE' + }; + await models.InvoiceOut.createManualInvoice(ctx, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`A ticket with an amount of zero can't be invoiced`); + }); + + it('should throw an error when the clientFk property is set without the max shipped date', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + let error; + try { + ctx.args = { + clientFk: clientId, + serial: 'T', + taxArea: 'CEE' + }; + await models.InvoiceOut.createManualInvoice(ctx, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`Max shipped required`); + }); + + it('should throw an error for a non-invoiceable client', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + let error; + try { + const client = await models.Client.findById(clientId, null, options); + await client.updateAttributes({ + isTaxDataChecked: false + }, options); + + ctx.args = { + ticketFk: ticketId, + serial: 'T', + taxArea: 'CEE' + }; + await models.InvoiceOut.createManualInvoice(ctx, options); + + await tx.rollback(); + } catch (e) { + error = e; + await tx.rollback(); + } + + expect(error.message).toContain(`This client is not invoiceable`); + }); + + it('should create a manual invoice', async() => { + spyOn(models.InvoiceOut, 'createPdf').and.returnValue(true); + + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; + + try { + ctx.args = { + ticketFk: ticketId, + serial: 'T', + taxArea: 'CEE' + }; + const result = await models.InvoiceOut.createManualInvoice(ctx, options); + + expect(result.id).toEqual(jasmine.any(Number)); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js index 60dd5576d..0ed0b35eb 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js @@ -19,8 +19,18 @@ describe('InvoiceOut createPdf()', () => { }; spyOn(got, 'stream').and.returnValue(response); - const result = await models.InvoiceOut.createPdf(ctx, invoiceId); + const tx = await models.InvoiceOut.beginTransaction({}); + const options = {transaction: tx}; - expect(result.hasPdf).toBe(true); + try { + const result = await models.InvoiceOut.createPdf(ctx, invoiceId, options); + + expect(result.hasPdf).toBe(true); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } }); }); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js index 2d9056708..3ad4c2f11 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/download.spec.js @@ -4,7 +4,7 @@ describe('InvoiceOut download()', () => { it('should return the downloaded fine name', async() => { const result = await models.InvoiceOut.download(1); - expect(result[1]).toEqual('text/plain'); - expect(result[2]).toEqual('filename="README.md"'); + expect(result[1]).toEqual('application/pdf'); + expect(result[2]).toEqual('filename="1.pdf"'); }); }); From 82259eee162d942d0b2fbed9940fda26b34b98db Mon Sep 17 00:00:00 2001 From: carlosjr Date: Wed, 4 Aug 2021 11:48:21 +0200 Subject: [PATCH 05/14] added catalog calculate join to show only available item types --- .../methods/order/getItemTypeAvailable.js | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/modules/order/back/methods/order/getItemTypeAvailable.js b/modules/order/back/methods/order/getItemTypeAvailable.js index 56f6a8c0e..52a2db5f5 100644 --- a/modules/order/back/methods/order/getItemTypeAvailable.js +++ b/modules/order/back/methods/order/getItemTypeAvailable.js @@ -31,13 +31,36 @@ module.exports = Self => { let stmts = []; let stmt; + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item'); + + stmt = new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.item + (PRIMARY KEY (itemFk)) ENGINE = MEMORY + SELECT DISTINCT + i.id AS itemFk, + i.typeFk, + it.categoryFk + FROM vn.item i + JOIN vn.itemType it ON it.id = i.typeFk + JOIN vn.itemCategory ic ON ic.id = it.categoryFk`); + stmts.push(stmt); + let order = await app.models.Order.findById(orderId); stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [ order.landed, order.addressFk, order.agencyModeFk ]); + + stmts.push(new ParameterizedSQL( + 'CALL vn.catalog_calculate(?, ?, ?)', [ + order.landed, + order.addressFk, + order.agencyModeFk, + ] + )); stmts.push(stmt); + stmt = new ParameterizedSQL(` SELECT it.id, it.name, ic.name categoryName FROM tmp.availableCalc ac @@ -45,6 +68,7 @@ module.exports = Self => { JOIN item i ON i.id = a.item_id JOIN itemType it ON it.id = i.typeFk JOIN itemCategory ic ON ic.id = it.categoryFk + JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.id WHERE it.categoryFk = ? GROUP BY it.id`, [itemCategoryId] ); From d4b1105cdaab3332388008aa31a036598704714b Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 5 Aug 2021 13:39:11 +0200 Subject: [PATCH 06/14] Ammends --- .../back/methods/invoiceOut/createManualInvoice.js | 8 +------- modules/invoiceOut/front/index/index.html | 2 +- modules/invoiceOut/front/index/locale/es.yml | 3 ++- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js index 44d52b38f..854c1e2f9 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js +++ b/modules/invoiceOut/back/methods/invoiceOut/createManualInvoice.js @@ -80,14 +80,11 @@ module.exports = Self => { throw new UserError('This ticket is already invoiced'); // Validates ticket amount - if (ticket.totalWithVat == 0) { - // Change state to delivered + if (ticket.totalWithVat == 0) throw new UserError(`A ticket with an amount of zero can't be invoiced`); - } // Validates ticket nagative base const hasNegativeBase = await getNegativeBase(ticketId, myOptions); - if (hasNegativeBase && company.code == 'VNL') throw new UserError(`A ticket with a negative base can't be invoiced`); } else { @@ -104,9 +101,6 @@ module.exports = Self => { companyId = company.companyFk; } - // Set shipped at night - maxShipped.setHours(23, 59, 59, 59); - // Validate invoiceable client const isClientInvoiceable = await isInvoiceable(clientId, myOptions); if (!isClientInvoiceable) diff --git a/modules/invoiceOut/front/index/index.html b/modules/invoiceOut/front/index/index.html index 9d0cc4337..2ebd7b421 100644 --- a/modules/invoiceOut/front/index/index.html +++ b/modules/invoiceOut/front/index/index.html @@ -60,7 +60,7 @@
Date: Thu, 5 Aug 2021 13:42:19 +0200 Subject: [PATCH 07/14] Ammends --- db/changes/10340-summer/00-buy_importReference.sql | 10 +++++----- modules/entry/back/methods/entry/importBuys.js | 2 +- modules/entry/back/methods/entry/importBuysPreview.js | 2 +- modules/entry/back/model-config.json | 2 +- modules/entry/back/models/buy-import-reference.json | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/db/changes/10340-summer/00-buy_importReference.sql b/db/changes/10340-summer/00-buy_importReference.sql index 4046b498b..f6bdc059c 100644 --- a/db/changes/10340-summer/00-buy_importReference.sql +++ b/db/changes/10340-summer/00-buy_importReference.sql @@ -1,14 +1,14 @@ -create table `vn`.`buyImportReference` +create table `vn`.`itemMatchProperties` ( itemFk int not null, name varchar(80) not null, - company varchar(80) not null, + producer varchar(80) not null, size int not null, - constraint buyImportReference_pk - primary key (itemFk, name, company, size), + constraint itemMatchProperties_pk + primary key (itemFk, name, producer, size), constraint itemFk___fk foreign key (itemFk) references item (id) on update cascade on delete cascade ) -comment 'Referencias de compras importadas'; +comment 'Propiedades para encontrar articulos equivalentes en verdnatura'; diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js index d8db328a3..5425bdd5d 100644 --- a/modules/entry/back/methods/entry/importBuys.js +++ b/modules/entry/back/methods/entry/importBuys.js @@ -72,7 +72,7 @@ module.exports = Self => { packageFk: buy.packageFk }); - await models.BuyImportReference.upsert({ + await models.ItemMatchProperties.upsert({ itemFk: buy.itemFk, name: buy.description, company: buy.companyName, diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js index 6b03579c7..b3ef92ee5 100644 --- a/modules/entry/back/methods/entry/importBuysPreview.js +++ b/modules/entry/back/methods/entry/importBuysPreview.js @@ -41,7 +41,7 @@ module.exports = Self => { if (packaging) buy.packageFk = packaging.id; - const reference = await models.BuyImportReference.findOne({ + const reference = await models.ItemMatchProperties.findOne({ fields: ['itemFk'], where: { name: buy.description, diff --git a/modules/entry/back/model-config.json b/modules/entry/back/model-config.json index b4c50c3aa..ad5a9063e 100644 --- a/modules/entry/back/model-config.json +++ b/modules/entry/back/model-config.json @@ -5,7 +5,7 @@ "Buy": { "dataSource": "vn" }, - "BuyImportReference": { + "ItemMatchProperties": { "dataSource": "vn" }, "EntryLog": { diff --git a/modules/entry/back/models/buy-import-reference.json b/modules/entry/back/models/buy-import-reference.json index ffe524858..bb9c559a2 100644 --- a/modules/entry/back/models/buy-import-reference.json +++ b/modules/entry/back/models/buy-import-reference.json @@ -1,9 +1,9 @@ { - "name": "BuyImportReference", + "name": "ItemMatchProperties", "base": "VnModel", "options": { "mysql": { - "table": "buyImportReference" + "table": "itemMatchProperties" } }, "properties": { From eba39cc706f8f9897bedab9804e6475ac670d58c Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 5 Aug 2021 13:44:31 +0200 Subject: [PATCH 08/14] Ammends --- modules/entry/back/methods/entry/importBuys.js | 2 +- modules/entry/back/methods/entry/importBuysPreview.js | 2 +- modules/entry/back/models/buy-import-reference.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js index 5425bdd5d..febddc9e7 100644 --- a/modules/entry/back/methods/entry/importBuys.js +++ b/modules/entry/back/methods/entry/importBuys.js @@ -75,7 +75,7 @@ module.exports = Self => { await models.ItemMatchProperties.upsert({ itemFk: buy.itemFk, name: buy.description, - company: buy.companyName, + producer: buy.companyName, size: buy.size }, options); } diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js index b3ef92ee5..790d33364 100644 --- a/modules/entry/back/methods/entry/importBuysPreview.js +++ b/modules/entry/back/methods/entry/importBuysPreview.js @@ -45,7 +45,7 @@ module.exports = Self => { fields: ['itemFk'], where: { name: buy.description, - company: buy.companyName, + producer: buy.companyName, size: buy.size } }, myOptions); diff --git a/modules/entry/back/models/buy-import-reference.json b/modules/entry/back/models/buy-import-reference.json index bb9c559a2..ab64dad73 100644 --- a/modules/entry/back/models/buy-import-reference.json +++ b/modules/entry/back/models/buy-import-reference.json @@ -15,7 +15,7 @@ "name": { "type": "string" }, - "company": { + "producer": { "type": "string" }, "size": { From 0c45aa64990c24d4b08296668f07454ddd32d6e2 Mon Sep 17 00:00:00 2001 From: joan Date: Thu, 5 Aug 2021 15:08:53 +0200 Subject: [PATCH 09/14] Updated e2e --- e2e/paths/12-entry/07_buys.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js index e5617b8bd..17a3dd8d6 100644 --- a/e2e/paths/12-entry/07_buys.spec.js +++ b/e2e/paths/12-entry/07_buys.spec.js @@ -1,7 +1,7 @@ import selectors from '../../helpers/selectors.js'; import getBrowser from '../../helpers/puppeteer'; -describe('Entry import, create and edit buys path', () => { +fdescribe('Entry import, create and edit buys path', () => { let browser; let page; @@ -29,9 +29,6 @@ describe('Entry import, create and edit buys path', () => { }); it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { - await page.write(selectors.entryBuys.ref, 'a reference'); - await page.write(selectors.entryBuys.observation, 'an observation'); - let currentDir = process.cwd(); let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; @@ -41,6 +38,9 @@ describe('Entry import, create and edit buys path', () => { ]); await fileChooser.accept([filePath]); + await page.waitForTextInField(selectors.entryBuys.ref, '200573095, 200573106, 200573117, 200573506'); + await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846'); + await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); From 3432fab6af11079901509c1b52963f6823aea073 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Fri, 6 Aug 2021 11:53:38 +0200 Subject: [PATCH 10/14] now shows only the itemType of available items --- .../methods/order/getItemTypeAvailable.js | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/modules/order/back/methods/order/getItemTypeAvailable.js b/modules/order/back/methods/order/getItemTypeAvailable.js index 52a2db5f5..906095f41 100644 --- a/modules/order/back/methods/order/getItemTypeAvailable.js +++ b/modules/order/back/methods/order/getItemTypeAvailable.js @@ -28,9 +28,17 @@ module.exports = Self => { }); Self.getItemTypeAvailable = async(orderId, itemCategoryId) => { - let stmts = []; + const stmts = []; let stmt; + const order = await app.models.Order.findById(orderId); + stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [ + order.landed, + order.addressFk, + order.agencyModeFk + ]); + stmts.push(stmt); + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item'); stmt = new ParameterizedSQL( @@ -38,44 +46,39 @@ module.exports = Self => { (PRIMARY KEY (itemFk)) ENGINE = MEMORY SELECT DISTINCT i.id AS itemFk, - i.typeFk, - it.categoryFk - FROM vn.item i + it.id AS typeFk, + it.name, + ic.name AS categoryName + FROM tmp.availableCalc ac + JOIN cache.available a ON a.calc_id = ac.calcFk + JOIN vn.item i ON i.id = a.item_id JOIN vn.itemType it ON it.id = i.typeFk - JOIN vn.itemCategory ic ON ic.id = it.categoryFk`); + JOIN vn.itemCategory ic ON ic.id = it.categoryFk + WHERE it.categoryFk = ?`, [itemCategoryId] + ); stmts.push(stmt); - let order = await app.models.Order.findById(orderId); - stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [ - order.landed, - order.addressFk, - order.agencyModeFk - ]); - - stmts.push(new ParameterizedSQL( + stmt = new ParameterizedSQL( 'CALL vn.catalog_calculate(?, ?, ?)', [ order.landed, order.addressFk, order.agencyModeFk, ] - )); + ); stmts.push(stmt); stmt = new ParameterizedSQL(` - SELECT it.id, it.name, ic.name categoryName - FROM tmp.availableCalc ac - JOIN cache.available a ON a.calc_id = ac.calcFk - JOIN item i ON i.id = a.item_id - JOIN itemType it ON it.id = i.typeFk - JOIN itemCategory ic ON ic.id = it.categoryFk - JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.id - WHERE it.categoryFk = ? - GROUP BY it.id`, [itemCategoryId] + SELECT i.typeFk AS id, i.name, i.categoryName + FROM tmp.item i + JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.itemFk + GROUP BY i.typeFk` ); - let categoriesIndex = stmts.push(stmt) - 1; + const categoriesIndex = stmts.push(stmt) - 1; - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await Self.rawStmt(sql); + stmts.push('DROP TEMPORARY TABLE tmp.item'); + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await Self.rawStmt(sql); return result[categoriesIndex]; }; From ce37f982c4d3c16f4c6e47bd7459016fd592f438 Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 6 Aug 2021 12:03:39 +0200 Subject: [PATCH 11/14] Removed focus --- e2e/paths/12-entry/07_buys.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js index 17a3dd8d6..4042c99b6 100644 --- a/e2e/paths/12-entry/07_buys.spec.js +++ b/e2e/paths/12-entry/07_buys.spec.js @@ -1,7 +1,7 @@ import selectors from '../../helpers/selectors.js'; import getBrowser from '../../helpers/puppeteer'; -fdescribe('Entry import, create and edit buys path', () => { +describe('Entry import, create and edit buys path', () => { let browser; let page; From 9dc1168ad2a7e399879904c501b664bd597b9ba6 Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 6 Aug 2021 12:24:36 +0200 Subject: [PATCH 12/14] Transaction refactor --- .../entry/back/methods/entry/importBuys.js | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/modules/entry/back/methods/entry/importBuys.js b/modules/entry/back/methods/entry/importBuys.js index febddc9e7..3ed8ac1c7 100644 --- a/modules/entry/back/methods/entry/importBuys.js +++ b/modules/entry/back/methods/entry/importBuys.js @@ -11,11 +11,6 @@ module.exports = Self => { description: 'The entry id', http: {source: 'path'} }, - { - arg: 'options', - type: 'object', - description: 'Callback options', - }, { arg: 'ref', type: 'string', @@ -28,11 +23,11 @@ module.exports = Self => { }, { arg: 'buys', - type: ['Object'], + type: ['object'], description: 'The buys', }], returns: { - type: ['Object'], + type: ['object'], root: true }, http: { @@ -41,23 +36,27 @@ module.exports = Self => { } }); - Self.importBuys = async(ctx, id, options = {}) => { + Self.importBuys = async(ctx, id, options) => { const conn = Self.dataSource.connector; const args = ctx.args; const models = Self.app.models; let tx; + const myOptions = {}; - if (!options.transaction) { + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { tx = await Self.beginTransaction({}); - options.transaction = tx; + myOptions.transaction = tx; } try { - const entry = await models.Entry.findById(id, null, options); + const entry = await models.Entry.findById(id, null, myOptions); await entry.updateAttributes({ observation: args.observation, ref: args.ref - }, options); + }, myOptions); const buys = []; for (let buy of args.buys) { @@ -77,10 +76,10 @@ module.exports = Self => { name: buy.description, producer: buy.companyName, size: buy.size - }, options); + }, myOptions); } - const createdBuys = await models.Buy.create(buys, options); + const createdBuys = await models.Buy.create(buys, myOptions); const buyIds = createdBuys.map(buy => buy.id); const stmts = []; @@ -97,7 +96,7 @@ module.exports = Self => { stmts.push('CALL buy_recalcPrices()'); const sql = ParameterizedSQL.join(stmts, ';'); - await conn.executeStmt(sql, options); + await conn.executeStmt(sql, myOptions); if (tx) await tx.commit(); } catch (e) { if (tx) await tx.rollback(); From 9048ce238eb2aa3c1b89c123a46c2d2154cc590b Mon Sep 17 00:00:00 2001 From: joan Date: Fri, 6 Aug 2021 12:47:46 +0200 Subject: [PATCH 13/14] Added translation --- modules/invoiceOut/front/index/manual/locale/es.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/invoiceOut/front/index/manual/locale/es.yml b/modules/invoiceOut/front/index/manual/locale/es.yml index 839b212f6..826057c8d 100644 --- a/modules/invoiceOut/front/index/manual/locale/es.yml +++ b/modules/invoiceOut/front/index/manual/locale/es.yml @@ -1,4 +1,5 @@ Create manual invoice: Crear factura manual Some fields are required: Algunos campos son obligatorios Client and max shipped fields should be filled: Los campos de cliente y fecha límite deben rellenarse -Max date: Fecha límite \ No newline at end of file +Max date: Fecha límite +Serial: Serie \ No newline at end of file From e7a8bc0b6d7e54da77f423633ad19f8eaff8e9a6 Mon Sep 17 00:00:00 2001 From: carlosjr Date: Mon, 9 Aug 2021 18:24:53 +0200 Subject: [PATCH 14/14] e2e for manual invoice --- e2e/helpers/selectors.js | 9 +++ .../09-invoice-out/03_manualInvoice.spec.js | 65 +++++++++++++++++++ loopback/locale/en.json | 4 +- 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 e2e/paths/09-invoice-out/03_manualInvoice.spec.js diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index a561a08cf..40b2ae68a 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -915,6 +915,15 @@ export default { invoiceOutIndex: { topbarSearch: 'vn-searchbar', searchResult: 'vn-invoice-out-index vn-card > vn-table > div > vn-tbody > a.vn-tr', + createInvoice: 'vn-invoice-out-index > div > vn-vertical > vn-button > button vn-icon[icon="add"]', + createManualInvoice: 'vn-item[name="manualInvoice"]', + manualInvoiceForm: '.vn-invoice-out-manual', + manualInvoiceTicket: 'vn-autocomplete[ng-model="$ctrl.invoice.ticketFk"]', + manualInvoiceClient: 'vn-autocomplete[ng-model="$ctrl.invoice.clientFk"]', + manualInvoiceSerial: 'vn-autocomplete[ng-model="$ctrl.invoice.serial"]', + manualInvoiceTaxArea: 'vn-autocomplete[ng-model="$ctrl.invoice.taxArea"]', + saveManualInvoice: 'button[response="accept"]' + }, invoiceOutDescriptor: { moreMenu: 'vn-invoice-out-descriptor vn-icon-button[icon=more_vert]', diff --git a/e2e/paths/09-invoice-out/03_manualInvoice.spec.js b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js new file mode 100644 index 000000000..aa8cc7454 --- /dev/null +++ b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js @@ -0,0 +1,65 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('InvoiceOut manual invoice path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('administrative', 'invoiceOut'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should open the manual invoice form', async() => { + await page.waitToClick(selectors.invoiceOutIndex.createInvoice); + await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice); + await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm); + }); + + it('should create an invoice from a ticket', async() => { + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTicket, '7'); + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceSerial, 'Global nacional'); + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTaxArea, 'national'); + await page.waitToClick(selectors.invoiceOutIndex.saveManualInvoice); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should have been redirected to the created invoice summary`, async() => { + await page.waitForState('invoiceOut.card.summary'); + }); + + it(`should navigate back to the invoiceOut index`, async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.invoiceOutButton); + await page.waitForSelector(selectors.invoiceOutIndex.topbarSearch); + await page.waitForState('invoiceOut.index'); + }); + + it('should now open the manual invoice form', async() => { + await page.waitToClick(selectors.invoiceOutIndex.createInvoice); + await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice); + await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm); + }); + + it('should create an invoice from a client', async() => { + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceClient, 'Charles Xavier'); + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceSerial, 'Global nacional'); + await page.autocompleteSearch(selectors.invoiceOutIndex.manualInvoiceTaxArea, 'national'); + await page.waitToClick(selectors.invoiceOutIndex.saveManualInvoice); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should have been redirected to the created invoice summary`, async() => { + await page.waitForState('invoiceOut.card.summary'); + }); +}); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index d77b0c26d..b71603f35 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -109,5 +109,7 @@ "This document already exists on this ticket": "This document already exists on this ticket", "State": "State", "regular": "regular", - "reserved": "reserved" + "reserved": "reserved", + "A ticket with a negative base can't be invoiced": "A ticket with a negative base can't be invoiced", + "This client is not invoiceable": "This client is not invoiceable" } \ No newline at end of file