diff --git a/back/models/sage-withholding.json b/back/models/sage-withholding.json index 8d93daeae6..dddbcfd747 100644 --- a/back/models/sage-withholding.json +++ b/back/models/sage-withholding.json @@ -6,6 +6,9 @@ "table": "sage.TiposRetencion" } }, + "log": { + "showField": "withholding" + }, "properties": { "id": { "type": "Number", diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index e1aaa2cd6d..c441913813 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2339,6 +2339,14 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`) (9, 9, 9), (10, 10, 10); +INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxCodeFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`, `created`) + VALUES + (1, 4, 99.99, '2000000000', null, null, null, CURDATE()), + (2, 4, 999.99, '2000000000', null, null, null, CURDATE()), + (3, 4, 1000.50, '2000000000', null, null, null, CURDATE()), + (4, 4, 0.50, '2000000000', null, null, null, CURDATE()), + (5, 4, 150.50, '2000000000', null, null, null, CURDATE()); + INSERT INTO `vn`.`ticketRecalc`(`ticketFk`) SELECT `id` FROM `vn`.`ticket` t diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index d81bcb3ba7..a561a08cf8 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -464,8 +464,8 @@ export default { itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor', itemDescriptorPopoverItemDiaryButton: 'vn-item-descriptor a[href="#!/item/2/diary?warehouseFk=5&lineFk=16"]', popoverDiaryButton: '.vn-popover.shown vn-item-descriptor vn-icon[icon="icon-transaction"]', - firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(4)', - firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(7)', + firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(5)', + firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(8)', invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span', setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button', descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div' @@ -568,18 +568,18 @@ export default { moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input', transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text', transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable', - firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(5) > span', + firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > span', firstSaleClaimIcon: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) vn-icon[icon="icon-claims"]', firstSaleDescriptorImage: '.vn-popover.shown vn-item-descriptor img', firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img', firstSaleZoomedImage: 'body > div > div > img', firstSaleQuantity: 'vn-ticket-sale [ng-model="sale.quantity"]', - firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(6)', - firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(8) > span', + firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(7)', + firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span', firstSalePriceInput: '.vn-popover.shown input[ng-model="$ctrl.field"]', - firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span', + firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(10) > span', firstSaleDiscountInput: '.vn-popover.shown [ng-model="$ctrl.field"]', - firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(10)', + firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(11)', firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)', firstSaleColour: 'vn-ticket-sale vn-tr:nth-child(1) vn-fetched-tags section', firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[ng-model="sale.checked"]', @@ -587,8 +587,8 @@ export default { secondSaleId: 'vn-ticket-sale:nth-child(2) vn-td-editable:nth-child(4) text > span', secondSaleIdAutocomplete: 'vn-ticket-sale vn-tr:nth-child(2) vn-autocomplete[ng-model="sale.itemFk"]', secondSaleQuantity: 'vn-ticket-sale vn-table vn-tr:nth-child(2) vn-input-number', - secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(6)', - secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(7)', + secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(7)', + secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(8)', secondSaleConceptInput: 'vn-ticket-sale vn-tbody > :nth-child(2) > vn-td-editable.ng-isolate-scope.selected vn-textfield', totalImport: 'vn-ticket-sale vn-one.taxes > p:nth-child(3) > strong', selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check', @@ -938,7 +938,8 @@ export default { invoiceInDescriptor: { moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]', moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]', - acceptDeleteButton: '.vn-confirm.shown button[response="accept"]' + moreMenuCloneInvoiceIn: '.vn-menu [name="cloneInvoice"]', + acceptButton: '.vn-confirm.shown button[response="accept"]' }, invoiceInBasicData: { issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]', diff --git a/e2e/paths/09-invoice-in/02_descriptor.spec.js b/e2e/paths/09-invoice-in/02_descriptor.spec.js index 2386dada4f..02bbce7ac1 100644 --- a/e2e/paths/09-invoice-in/02_descriptor.spec.js +++ b/e2e/paths/09-invoice-in/02_descriptor.spec.js @@ -10,16 +10,30 @@ describe('InvoiceIn descriptor path', () => { page = browser.page; await page.loginAndModule('administrative', 'invoiceIn'); await page.accessToSearchResult('10'); + await page.accessToSection('invoiceIn.card.basicData'); }); afterAll(async() => { await browser.close(); }); - it('should delete the invoiceIn using the descriptor more menu', async() => { + it('should clone the invoiceIn using the descriptor more menu', async() => { + await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); + await page.waitToClick(selectors.invoiceInDescriptor.moreMenuCloneInvoiceIn); + await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('InvoiceIn cloned'); + }); + + it('should have been redirected to the created invoiceIn summary', async() => { + await page.waitForState('invoiceIn.card.summary'); + }); + + it('should delete the cloned invoiceIn using the descriptor more menu', async() => { await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn); - await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton); + await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); const message = await page.waitForSnackbar(); expect(message.text).toContain('InvoiceIn deleted'); diff --git a/modules/invoiceIn/back/methods/invoice-in/clone.js b/modules/invoiceIn/back/methods/invoice-in/clone.js new file mode 100644 index 0000000000..c1bf0f3ac2 --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/clone.js @@ -0,0 +1,120 @@ + +const loggable = require('vn-loopback/util/log'); + +module.exports = Self => { + Self.remoteMethodCtx('clone', { + description: 'Clone the invoiceIn and as many invoiceInTax and invoiceInDueDay referencing it', + accessType: 'WRITE', + accepts: { + arg: 'id', + type: 'number', + required: true, + description: 'The invoiceIn id', + http: {source: 'path'} + }, + returns: { + type: 'object', + root: true + }, + http: { + path: '/:id/clone', + verb: 'POST' + } + }); + + Self.clone = async(ctx, id, options) => { + const userId = ctx.req.accessToken.userId; + const models = Self.app.models; + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const sourceInvoiceIn = await Self.findById(id, { + fields: [ + 'id', + 'serial', + 'supplierRef', + 'supplierFk', + 'issued', + 'currencyFk', + 'companyFk', + 'isVatDeductible', + 'withholdingSageFk', + 'deductibleExpenseFk', + ] + }, myOptions); + const sourceInvoiceInTax = await models.InvoiceInTax.find({where: {invoiceInFk: id}}, myOptions); + const sourceInvoiceInDueDay = await models.InvoiceInDueDay.find({where: {invoiceInFk: id}}, myOptions); + + const issued = new Date(sourceInvoiceIn.issued); + issued.setMonth(issued.getMonth() + 1); + + const clone = await models.InvoiceIn.create({ + serial: sourceInvoiceIn.serial, + supplierRef: sourceInvoiceIn.supplierRef, + supplierFk: sourceInvoiceIn.supplierFk, + issued: issued, + currencyFk: sourceInvoiceIn.currencyFk, + companyFk: sourceInvoiceIn.companyFk, + isVatDeductible: sourceInvoiceIn.isVatDeductible, + withholdingSageFk: sourceInvoiceIn.withholdingSageFk, + deductibleExpenseFk: sourceInvoiceIn.deductibleExpenseFk, + }, myOptions); + + const oldProperties = await loggable.translateValues(Self, sourceInvoiceIn); + const newProperties = await loggable.translateValues(Self, clone); + await models.InvoiceInLog.create({ + originFk: clone.id, + userFk: userId, + action: 'insert', + changedModel: 'InvoiceIn', + changedModelId: clone.id, + oldInstance: oldProperties, + newInstance: newProperties + }, myOptions); + + const promises = []; + + for (let tax of sourceInvoiceInTax) { + promises.push(models.InvoiceInTax.create({ + invoiceInFk: clone.id, + taxableBase: tax.taxableBase, + expenceFk: tax.expenceFk, + foreignValue: tax.foreignValue, + taxTypeSageFk: tax.taxTypeSageFk, + transactionTypeSageFk: tax.transactionTypeSageFk + }, myOptions)); + } + + for (let dueDay of sourceInvoiceInDueDay) { + const dueDated = dueDay.dueDated; + dueDated.setMonth(dueDated.getMonth() + 1); + + promises.push(models.InvoiceInDueDay.create({ + invoiceInFk: clone.id, + dueDated: dueDated, + bankFk: dueDay.bankFk, + amount: dueDay.amount, + foreignValue: dueDated.foreignValue, + }, myOptions)); + } + + await Promise.all(promises); + + if (tx) await tx.commit(); + + return clone; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js b/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js new file mode 100644 index 0000000000..09b7e60192 --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js @@ -0,0 +1,36 @@ +const models = require('vn-loopback/server/server').models; + +describe('invoiceIn clone()', () => { + it('should return the cloned invoiceIn and also clone invoiceInDueDays and invoiceInTaxes if there are any referencing the invoiceIn', async() => { + const userId = 1; + const ctx = { + req: { + + accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, + } + }; + + const tx = await models.InvoiceIn.beginTransaction({}); + const options = {transaction: tx}; + + try { + const clone = await models.InvoiceIn.clone(ctx, 1, options); + + expect(clone.supplierRef).toEqual('1234'); + + const invoiceInTaxes = await models.InvoiceInTax.find({where: {invoiceInFk: clone.id}}, options); + + expect(invoiceInTaxes.length).toEqual(1); + + const invoiceInDueDays = await models.InvoiceInDueDay.find({where: {invoiceInFk: clone.id}}, options); + + expect(invoiceInDueDays.length).toEqual(2); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceIn/back/model-config.json b/modules/invoiceIn/back/model-config.json index 467d6f7f93..f0745f53b2 100644 --- a/modules/invoiceIn/back/model-config.json +++ b/modules/invoiceIn/back/model-config.json @@ -2,6 +2,9 @@ "InvoiceIn": { "dataSource": "vn" }, + "InvoiceInTax": { + "dataSource": "vn" + }, "InvoiceInDueDay": { "dataSource": "vn" }, diff --git a/modules/invoiceIn/back/models/invoice-in-due-day.json b/modules/invoiceIn/back/models/invoice-in-due-day.json index 6c27dcd6c4..5a66ecd8b0 100644 --- a/modules/invoiceIn/back/models/invoice-in-due-day.json +++ b/modules/invoiceIn/back/models/invoice-in-due-day.json @@ -24,6 +24,9 @@ "amount": { "type": "number" }, + "foreignValue": { + "type": "number" + }, "created": { "type": "date" } diff --git a/modules/invoiceIn/back/models/invoice-in-tax.json b/modules/invoiceIn/back/models/invoice-in-tax.json new file mode 100644 index 0000000000..abc137ecd3 --- /dev/null +++ b/modules/invoiceIn/back/models/invoice-in-tax.json @@ -0,0 +1,42 @@ +{ + "name": "InvoiceInTax", + "base": "VnModel", + "options": { + "mysql": { + "table": "invoiceInTax" + } + }, + "properties": { + "id": { + "id": true, + "type": "number", + "description": "Identifier" + }, + "invoiceInFk": { + "type": "number" + }, + "taxCodeFk": { + "type": "number" + }, + "taxableBase": { + "type": "number" + }, + "expenceFk": { + "type": "string" + }, + "foreignValue": { + "type": "number" + }, + "taxTypeSageFk": { + "type": "number" + }, + "transactionTypeSageFk": { + "type": "number" + }, + "created": { + "type": "date" + } + } +} + + diff --git a/modules/invoiceIn/back/models/invoice-in.js b/modules/invoiceIn/back/models/invoice-in.js index 7c5b163586..7754890cad 100644 --- a/modules/invoiceIn/back/models/invoice-in.js +++ b/modules/invoiceIn/back/models/invoice-in.js @@ -1,4 +1,5 @@ module.exports = Self => { require('../methods/invoice-in/filter')(Self); require('../methods/invoice-in/summary')(Self); + require('../methods/invoice-in/clone')(Self); }; diff --git a/modules/invoiceIn/back/models/invoice-in.json b/modules/invoiceIn/back/models/invoice-in.json index 468972523f..9ec08c7598 100644 --- a/modules/invoiceIn/back/models/invoice-in.json +++ b/modules/invoiceIn/back/models/invoice-in.json @@ -12,7 +12,7 @@ "properties": { "id": { "id": true, - "type": "Number", + "type": "number", "description": "Identifier" }, "serialNumber": { @@ -36,6 +36,9 @@ "booked": { "type": "date" }, + "isVatDeductible": { + "type": "boolean" + }, "operated": { "type": "date" }, diff --git a/modules/invoiceIn/front/descriptor/index.html b/modules/invoiceIn/front/descriptor/index.html index 42a9469136..6829a0daf4 100644 --- a/modules/invoiceIn/front/descriptor/index.html +++ b/modules/invoiceIn/front/descriptor/index.html @@ -8,6 +8,13 @@ translate> Delete Invoice + + Clone Invoice +
@@ -42,8 +49,16 @@
- - + + + diff --git a/modules/invoiceIn/front/descriptor/index.js b/modules/invoiceIn/front/descriptor/index.js index be507e0d4c..a767f4b5c5 100644 --- a/modules/invoiceIn/front/descriptor/index.js +++ b/modules/invoiceIn/front/descriptor/index.js @@ -30,6 +30,12 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('InvoiceIn deleted'))); } + cloneInvoiceIn() { + return this.$http.post(`InvoiceIns/${this.id}/clone`) + .then(res => this.$state.go('invoiceIn.card.summary', {id: res.data.id})) + .then(() => this.vnApp.showSuccess(this.$t('InvoiceIn cloned'))); + } + loadData() { const filter = { include: [ diff --git a/modules/invoiceIn/front/locale/es.yml b/modules/invoiceIn/front/locale/es.yml index ac432aba8b..dee0d2bc16 100644 --- a/modules/invoiceIn/front/locale/es.yml +++ b/modules/invoiceIn/front/locale/es.yml @@ -2,4 +2,5 @@ InvoiceIn: Facturas recibidas Search invoices in by reference: Buscar facturas recibidas por referencia Entries list: Listado de entradas Invoice list: Listado de facturas recibidas -InvoiceIn deleted: Factura eliminada \ No newline at end of file +InvoiceIn deleted: Factura eliminada +InvoiceIn cloned: Factura clonada \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml index ae4396a01b..ec9cd33100 100644 --- a/modules/invoiceOut/front/descriptor/locale/es.yml +++ b/modules/invoiceOut/front/descriptor/locale/es.yml @@ -5,8 +5,10 @@ Invoice ticket list: Listado de tickets de la factura Show invoice PDF: Ver factura en PDF Send invoice PDF: Enviar factura en PDF Delete Invoice: Eliminar factura +Clone Invoice: Clonar factura InvoiceOut deleted: Factura eliminada Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? +Are you sure you want to clone this invoice?: Estas seguro de clonar 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?