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?