3014-invoiceIn_clone_from_descriptor #701

Merged
joan merged 6 commits from 3014-invoiceIn_clone_from_descriptor into dev 2021-07-23 13:30:51 +00:00
15 changed files with 274 additions and 16 deletions

View File

@ -6,6 +6,9 @@
"table": "sage.TiposRetencion"
}
},
"log": {
"showField": "withholding"
},
"properties": {
"id": {
"type": "Number",

View File

@ -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

View File

@ -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"]',

View File

@ -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');

View File

@ -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;
}
};
};

View File

@ -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;
}
});
});

View File

@ -2,6 +2,9 @@
"InvoiceIn": {
"dataSource": "vn"
},
"InvoiceInTax": {
"dataSource": "vn"
},
"InvoiceInDueDay": {
"dataSource": "vn"
},

View File

@ -24,6 +24,9 @@
"amount": {
"type": "number"
},
"foreignValue": {
"type": "number"
},
"created": {
"type": "date"
}

View File

@ -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"
}
}
}

View File

@ -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);
};

View File

@ -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"
},

View File

@ -8,6 +8,13 @@
translate>
Delete Invoice
</vn-item>
<vn-item
ng-click="cloneConfirmation.show()"
vn-acl="invoicing"
name="cloneInvoice"
translate>
Clone Invoice
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -42,8 +49,16 @@
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm vn-id="deleteConfirmation" on-accept="$ctrl.deleteInvoiceIn()"
<vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteInvoiceIn()"
question="Are you sure you want to delete this invoice?">
</vn-confirm>
<vn-supplier-descriptor-popover vn-id="supplierDescriptor">
<vn-confirm
vn-id="cloneConfirmation"
on-accept="$ctrl.cloneInvoiceIn()"
question="Are you sure you want to clone this invoice?">
</vn-confirm>
<vn-supplier-descriptor-popover
vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover>

View File

@ -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: [

View File

@ -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
InvoiceIn deleted: Factura eliminada
InvoiceIn cloned: Factura clonada

View File

@ -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?