refs #5914 fix: ticket canBeInvoiced and optimized invoiceTickets
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Alex Moreno 2023-12-14 15:56:49 +01:00
parent 1ce1af82fe
commit 21a1dbeae7
13 changed files with 64 additions and 51 deletions

View File

@ -26,3 +26,4 @@ BEGIN
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -720,7 +720,7 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`) INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`)
VALUES VALUES
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1), (1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2), (2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 1, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL), (3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL), (4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL), (5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL),

View File

@ -35,7 +35,7 @@ describe('Ticket index payout path', () => {
await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton); await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton);
await page.write(selectors.ticketsIndex.advancedSearchClient, '1101'); await page.write(selectors.ticketsIndex.advancedSearchClient, '1101');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 9); await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 10);
await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox); await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox); await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox);

View File

@ -28,7 +28,6 @@ describe('InvoiceOut summary path', () => {
it('should contain the tax breakdown', async() => { it('should contain the tax breakdown', async() => {
const firstTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxOne, 'innerText'); const firstTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxOne, 'innerText');
const secondTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxTwo, 'innerText'); const secondTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxTwo, 'innerText');
expect(firstTax).toContain('10%'); expect(firstTax).toContain('10%');
@ -37,10 +36,9 @@ describe('InvoiceOut summary path', () => {
it('should contain the tickets info', async() => { it('should contain the tickets info', async() => {
const firstTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketOne, 'innerText'); const firstTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketOne, 'innerText');
const secondTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketTwo, 'innerText'); const secondTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketTwo, 'innerText');
expect(firstTicket).toContain('Bat cave'); expect(firstTicket).toContain('Bat cave');
expect(secondTicket).toContain('Stark tower'); expect(secondTicket).toContain('Bat cave');
}); });
}); });

View File

@ -45,6 +45,7 @@
"Extension format is invalid": "Extension format is invalid", "Extension format is invalid": "Extension format is invalid",
"NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS", "NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS",
"This client can't be invoiced": "This client can't be invoiced", "This client can't be invoiced": "This client can't be invoiced",
"You must provide the correction information to generate a corrective invoice": "You must provide the correction information to generate a corrective invoice",
"The introduced hour already exists": "The introduced hour already exists", "The introduced hour already exists": "The introduced hour already exists",
"Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket", "Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket",
"Concept cannot be blank": "Concept cannot be blank", "Concept cannot be blank": "Concept cannot be blank",
@ -178,7 +179,8 @@
"The renew period has not been exceeded": "The renew period has not been exceeded", "The renew period has not been exceeded": "The renew period has not been exceeded",
"You can not use the same password": "You can not use the same password", "You can not use the same password": "You can not use the same password",
"Valid priorities": "Valid priorities: %d", "Valid priorities": "Valid priorities: %d",
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}", "hasAnyNegativeBase": "Negative basis of tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Positive basis of tickets: {{ticketsIds}}",
"This ticket cannot be left empty.": "This ticket cannot be left empty. %s", "This ticket cannot be left empty.": "This ticket cannot be left empty. %s",
"Social name should be uppercase": "Social name should be uppercase", "Social name should be uppercase": "Social name should be uppercase",
"Street should be uppercase": "Street should be uppercase", "Street should be uppercase": "Street should be uppercase",
@ -201,4 +203,4 @@
"keepPrice": "keepPrice", "keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries", "Cannot past travels with entries": "Cannot past travels with entries",
"It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}" "It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}"
} }

View File

@ -72,6 +72,7 @@
"The secret can't be blank": "La contraseña no puede estar en blanco", "The secret can't be blank": "La contraseña no puede estar en blanco",
"We weren't able to send this SMS": "No hemos podido enviar el SMS", "We weren't able to send this SMS": "No hemos podido enviar el SMS",
"This client can't be invoiced": "Este cliente no puede ser facturado", "This client can't be invoiced": "Este cliente no puede ser facturado",
"You must provide the correction information to generate a corrective invoice": "Debes informar la información de corrección para generar una factura rectificativa",
"This ticket can't be invoiced": "Este ticket no puede ser facturado", "This ticket can't be invoiced": "Este ticket no puede ser facturado",
"You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado", "You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado",
"This ticket can not be modified": "Este ticket no puede ser modificado", "This ticket can not be modified": "Este ticket no puede ser modificado",
@ -305,7 +306,8 @@
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico", "Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado", "The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Valid priorities": "Prioridades válidas: %d", "Valid priorities": "Prioridades válidas: %d",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}", "hasAnyNegativeBase": "Base negativa para los tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Base positivas para los tickets: {{ticketsIds}}",
"You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado", "You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado",
"This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s", "This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s",
"The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias", "The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias",
@ -330,5 +332,8 @@
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima", "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas", "Cannot past travels with entries": "No se pueden pasar envíos con entradas",
"No tickets to invoice": "No hay tickets para facturar", "No tickets to invoice": "No hay tickets para facturar",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}" "It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}",
"Base negativa para los tickets: 39": "Base negativa para los tickets: 39",
"Base negativa para los tickets: 41": "Base negativa para los tickets: 41",
"keepPrice": "keepPrice"
} }

View File

@ -16,7 +16,7 @@ describe('client consumption() filter', () => {
}; };
const result = await models.Client.consumption(ctx, filter, options); const result = await models.Client.consumption(ctx, filter, options);
expect(result.length).toEqual(10); expect(result.length).toEqual(11);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -49,7 +49,7 @@ describe('client consumption() filter', () => {
const thirdRow = result[2]; const thirdRow = result[2];
expect(result.length).toEqual(3); expect(result.length).toEqual(3);
expect(firstRow.quantity).toEqual(10); expect(firstRow.quantity).toEqual(11);
expect(secondRow.quantity).toEqual(15); expect(secondRow.quantity).toEqual(15);
expect(thirdRow.quantity).toEqual(20); expect(thirdRow.quantity).toEqual(20);

View File

@ -2,7 +2,7 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('InvoiceOut tranferInvoice()', () => { describe('InvoiceOut transferInvoice()', () => {
const activeCtx = { const activeCtx = {
accessToken: {userId: 5}, accessToken: {userId: 5},
http: { http: {
@ -69,9 +69,33 @@ describe('InvoiceOut tranferInvoice()', () => {
await models.InvoiceOut.transferInvoice( await models.InvoiceOut.transferInvoice(
ctx, ctx,
options); options);
await tx.rollback();
} catch (e) { } catch (e) {
expect(e.message).toBe(`Select a different client`); expect(e.message).toBe(`Select a different client`);
await tx.rollback(); await tx.rollback();
} }
}); });
it('should throw an UserError when it is refund', async() => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
refFk: 'T1111111',
newClientFk: 1102,
cplusRectificationTypeFk: 1,
siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeFk: 1
};
ctx.args = args;
try {
await models.InvoiceOut.transferInvoice(
ctx,
options);
await tx.rollback();
} catch (e) {
expect(e.message).toContain(`This ticket is already a refund`);
await tx.rollback();
}
});
}); });

View File

@ -227,7 +227,7 @@
vn-one vn-one
vn-id="siiTypeInvoiceOut" vn-id="siiTypeInvoiceOut"
data="siiTypeInvoiceOuts" data="siiTypeInvoiceOuts"
show-field="code" show-field="description"
value-field="id" value-field="id"
fields="['id','code','description']" fields="['id','code','description']"
required="true" required="true"

View File

@ -6,6 +6,7 @@
auto-load="true" auto-load="true"
url="InvoiceOutSerials" url="InvoiceOutSerials"
data="invoiceOutSerials" data="invoiceOutSerials"
where="{code: {neq: 'R'}}"
order="code"> order="code">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model

View File

@ -39,27 +39,14 @@ module.exports = function(Self) {
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, },
fields: ['id', 'refFk', 'shipped', 'totalWithVat', 'companyFk'] fields: ['id', 'refFk', 'shipped', 'totalWithVat']
}, myOptions); }, myOptions);
const [firstTicket] = tickets;
const companyFk = firstTicket.companyFk;
const query =
`SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await Self.rawSql(query, [companyFk], options);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
const taxBaseFunction = isRectificative ? 'hasAnyPositiveBase' : 'hasAnyNegativeBase'; const taxBaseFunction = isRectificative ? 'hasAnyPositiveBase' : 'hasAnyNegativeBase';
const [result] = const [hasAnyIncorrectBase] =
await Self.rawSql(`SELECT ?() AS hasBasesProblem`, [taxBaseFunction], options); await Self.rawSql(`SELECT ${taxBaseFunction}() AS hasBasesProblem`, null, options);
const hasAnyIncorrectBase = result?.hasBasesProblem && isSpanishCompany; if (hasAnyIncorrectBase?.hasBasesProblem)
if (hasAnyIncorrectBase) throw new UserError($t(taxBaseFunction, {ticketsIds: ticketsIds}));
throw new UserError($t('Negative basis of tickets', {ticketsIds: ticketsIds}));
const today = Date.vnNew(); const today = Date.vnNew();
tickets.some(ticket => { tickets.some(ticket => {

View File

@ -47,10 +47,10 @@ module.exports = function(Self) {
let invoicesIds = []; let invoicesIds = [];
try { try {
const tickets = await models.Ticket.find({ const tickets = await models.Ticket.find({
fields: ['id', 'clientFk', 'companyFk', 'addressFk'],
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, }
fields: ['id', 'clientFk', 'companyFk']
}, myOptions); }, myOptions);
const [firstTicket] = tickets; const [firstTicket] = tickets;
@ -65,18 +65,17 @@ module.exports = function(Self) {
fields: ['id', 'hasToInvoiceByAddress'] fields: ['id', 'hasToInvoiceByAddress']
}, myOptions); }, myOptions);
let ticketsByAddress = {[firstTicket.addressFk]: ticketsIds};
if (client.hasToInvoiceByAddress) { if (client.hasToInvoiceByAddress) {
const query = ` ticketsByAddress = tickets.reduce((group, ticket) => {
SELECT DISTINCT addressFk const {addressFk} = ticket;
FROM ticket t group[addressFk] = group[addressFk] ?? [];
WHERE id IN (?)`; group[addressFk].push(ticket.id);
const result = await Self.rawSql(query, [ticketsIds], myOptions); return group;
}, {});
const addressIds = result.map(address => address.addressFk); }
for (const address of addressIds) for (const ticketIds of Object.values(ticketsByAddress))
await createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, invoiceCorrection, myOptions); invoicesIds.push(await createInvoice(ctx, companyId, ticketIds, invoiceCorrection, myOptions));
} else
await createInvoice(ctx, companyId, ticketsIds, null, invoicesIds, invoiceCorrection, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
} catch (e) { } catch (e) {
@ -91,7 +90,7 @@ module.exports = function(Self) {
return invoicesIds; return invoicesIds;
}; };
async function createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, invoiceCorrection, myOptions) { async function createInvoice(ctx, companyId, ticketsIds, invoiceCorrection, myOptions) {
const models = Self.app.models; const models = Self.app.models;
await models.Ticket.rawSql(` await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
@ -100,12 +99,8 @@ module.exports = function(Self) {
SELECT id SELECT id
FROM vn.ticket FROM vn.ticket
WHERE id IN (?) WHERE id IN (?)
${address ? `AND addressFk = ${address}` : ''}
`, [ticketsIds], myOptions); `, [ticketsIds], myOptions);
return models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), invoiceCorrection, myOptions);
const invoiceId =
await models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), invoiceCorrection, myOptions);
invoicesIds.push(invoiceId);
} }
}; };

View File

@ -106,7 +106,7 @@ module.exports = function(Self) {
); );
} }
if (resultInvoice.id) // serial != 'R' && if (resultInvoice.id)
await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions); await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();