Merge branch 'dev' into 7567-movingCleanFix
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Guillermo Bonet 2024-08-23 07:09:31 +00:00
commit 3974728658
19 changed files with 258 additions and 88 deletions

View File

@ -632,7 +632,7 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
('A', 'Global nacional', 1, 'NATIONAL', 0, 'global'),
('T', 'Española rapida', 1, 'NATIONAL', 0, 'quick'),
('V', 'Intracomunitaria global', 0, 'CEE', 1, 'global'),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0, 'quick'),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0, 'multiple'),
('R', 'Rectificativa', 1, 'NATIONAL', 0, NULL),
('E', 'Exportación rápida', 0, 'WORLD', 0, 'quick');

View File

@ -1,26 +1,32 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`invoiceSerial`(vClientFk INT, vCompanyFk INT, vType CHAR(1))
RETURNS char(1) CHARSET utf8mb3 COLLATE utf8mb3_general_ci
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`invoiceSerial`(vClientFk INT, vCompanyFk INT, vType CHAR(15))
RETURNS char(2) CHARSET utf8mb3 COLLATE utf8mb3_general_ci
DETERMINISTIC
BEGIN
/**
* Obtiene la serie de de una factura
* Obtiene la serie de una factura
* dependiendo del area del cliente.
*
*
* @param vClientFk Id del cliente
* @param vCompanyFk Id de la empresa
* @param vType Tipo de factura ["R", "M", "G"]
* @return Serie de la factura
* @param vType Tipo de factura ['global','multiple','quick']
* @return vSerie de la factura
*/
DECLARE vTaxArea VARCHAR(25);
DECLARE vSerie CHAR(1);
DECLARE vTaxArea VARCHAR(25) COLLATE utf8mb3_general_ci;
DECLARE vSerie CHAR(2);
IF (SELECT hasInvoiceSimplified FROM client WHERE id = vClientFk) THEN
RETURN 'S';
END IF;
SELECT clientTaxArea(vClientFk, vCompanyFk) INTO vTaxArea;
SELECT invoiceSerialArea(vType,vTaxArea) INTO vSerie;
SELECT addressTaxArea(defaultAddressFk, vCompanyFk) INTO vTaxArea
FROM client
WHERE id = vClientFk;
SELECT code INTO vSerie
FROM invoiceOutSerial
WHERE `type` = vType AND taxAreaFk = vTaxArea;
RETURN vSerie;
END$$
DELIMITER ;

View File

@ -1,34 +0,0 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`invoiceSerialArea`(vType CHAR(1), vTaxArea VARCHAR(25))
RETURNS char(1) CHARSET utf8mb3 COLLATE utf8mb3_unicode_ci
DETERMINISTIC
BEGIN
DECLARE vSerie CHAR(1);
IF vType = 'R' THEN
SELECT
CASE vTaxArea
WHEN 'CEE' THEN 'H'
WHEN 'WORLD' THEN 'E'
ELSE 'T'
END INTO vSerie;
-- Factura multiple
ELSEIF vType = 'M' THEN
SELECT
CASE vTaxArea
WHEN 'CEE' THEN 'H'
WHEN 'WORLD' THEN 'E'
ELSE 'M'
END INTO vSerie;
-- Factura global
ELSEIF vType = 'G' THEN
SELECT
CASE vTaxArea
WHEN 'CEE' THEN 'V'
WHEN 'WORLD' THEN 'X'
ELSE 'A'
END INTO vSerie;
END IF;
RETURN vSerie;
END$$
DELIMITER ;

View File

@ -97,7 +97,7 @@ BEGIN
AND (vCorrectingSerial = vSerial OR NOT hasAnyNegativeBase())
THEN
-- el trigger añade el siguiente Id_Factura correspondiente a la vSerial
-- el trigger añade el siguiente ref correspondiente a la vSerial
INSERT INTO invoiceOut(
ref,
serial,

View File

@ -2,7 +2,7 @@ DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_close`()
BEGIN
/**
* Realiza el cierre de todos los
* Realiza el cierre de todos los
* tickets de la tabla tmp.ticket_close.
*
* @table tmp.ticket_close(ticketFk) Identificadores de los tickets a cerrar
@ -20,7 +20,7 @@ BEGIN
DECLARE cur CURSOR FOR
SELECT ticketFk FROM tmp.ticket_close;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN
RESIGNAL;
@ -30,7 +30,7 @@ BEGIN
proc: LOOP
SET vDone = FALSE;
FETCH cur INTO vCurTicketFk;
IF vDone THEN
@ -47,12 +47,12 @@ BEGIN
c.hasToInvoice
INTO vClientFk,
vIsTaxDataChecked,
vCompanyFk,
vCompanyFk,
vShipped,
vHasDailyInvoice,
vWithPackage,
vHasToInvoice
FROM ticket t
FROM ticket t
JOIN `client` c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
LEFT JOIN autonomy a ON a.id = p.autonomyFk
@ -62,7 +62,7 @@ BEGIN
INSERT INTO ticketPackaging (ticketFk, packagingFk, quantity)
(SELECT vCurTicketFk, p.id, COUNT(*)
FROM expedition e
FROM expedition e
JOIN packaging p ON p.id = e.packagingFk
JOIN ticket t ON t.id = e.ticketFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
@ -73,15 +73,15 @@ BEGIN
GROUP BY p.itemFk);
-- No retornables o no catalogados
INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, isPriceFixed)
INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, isPriceFixed)
(SELECT e.freightItemFk, vCurTicketFk, i.name, COUNT(*) AS amount, getSpecialPrice(e.freightItemFk, vClientFk), 1
FROM expedition e
FROM expedition e
JOIN item i ON i.id = e.freightItemFk
LEFT JOIN packaging p ON p.itemFk = i.id
WHERE e.ticketFk = vCurTicketFk AND IFNULL(p.isPackageReturnable, 0) = 0
AND getSpecialPrice(e.freightItemFk, vClientFk) > 0
GROUP BY e.freightItemFk);
IF(vHasDailyInvoice) AND vHasToInvoice THEN
-- Facturacion rapida
@ -89,10 +89,10 @@ BEGIN
-- Facturar si está contabilizado
IF vIsTaxDataChecked THEN
CALL invoiceOut_newFromClient(
vClientFk,
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'M')),
vShipped,
vCompanyFk,
vClientFk,
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'multiple')),
vShipped,
vCompanyFk,
NULL,
NULL,
vNewInvoiceId);

View File

@ -0,0 +1,12 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`roadmap_beforeInsert`
BEFORE INSERT ON `roadmap`
FOR EACH ROW
BEGIN
IF NEW.driver1Fk IS NOT NULL THEN
SET NEW.driverName = (SELECT firstName FROM worker WHERE id = NEW.driver1Fk);
ELSE
SET NEW.driverName = NULL;
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,12 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `vn`.`roadmap_beforeUpdate`
BEFORE UPDATE ON `roadmap`
FOR EACH ROW
BEGIN
IF NEW.driver1Fk IS NOT NULL THEN
SET NEW.driverName = (SELECT firstName FROM worker WHERE id = NEW.driver1Fk);
ELSE
SET NEW.driverName = NULL;
END IF;
END$$
DELIMITER ;

View File

@ -0,0 +1,4 @@
ALTER TABLE vn.invoiceOutSerial
MODIFY COLUMN `type` enum('global','quick','multiple') CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;
CREATE UNIQUE INDEX invoiceOutSerial_taxAreaFk_IDX USING BTREE ON vn.invoiceOutSerial (taxAreaFk,`type`);

View File

@ -0,0 +1,3 @@
UPDATE vn.invoiceOutSerial
SET `type`='multiple'
WHERE `description` LIKE '%Múltiple%';

View File

@ -0,0 +1,6 @@
ALTER TABLE vn.roadmap
ADD COLUMN m3 INT UNSIGNED NULL,
ADD COLUMN driver2Fk INT UNSIGNED NULL,
ADD COLUMN driver1Fk INT UNSIGNED NULL,
ADD CONSTRAINT roadmap_worker_FK FOREIGN KEY (driver1Fk) REFERENCES vn.worker(id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT roadmap_worker_FK_2 FOREIGN KEY (driver2Fk) REFERENCES vn.worker(id) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -46,7 +46,7 @@ module.exports = Self => {
}
});
filter = mergeFilters(args.filter, {where});
const filter = mergeFilters(args.filter, {where});
const stmt = new ParameterizedSQL(
`SELECT i.serial, SUM(IF(i.isBooked, 0,1)) pending, COUNT(*) total

View File

@ -75,7 +75,7 @@ module.exports = Self => {
AND c.isTaxDataChecked
AND c.isActive
AND NOT t.isDeleted
GROUP BY c.id, IF(c.hasToInvoiceByAddress, a.id, TRUE)
GROUP BY IF(c.hasToInvoiceByAddress, a.id, c.id)
HAVING SUM(t.totalWithVat) > 0;`;
const addresses = await Self.rawSql(query, [

View File

@ -28,6 +28,11 @@ module.exports = Self => {
type: 'number',
description: 'The company id to invoice',
required: true
}, {
arg: 'serialType',
type: 'string',
description: 'Invoice serial number type (see vn.invoiceOutSerial.type enum)',
required: true
}
],
returns: {
@ -39,12 +44,10 @@ module.exports = Self => {
verb: 'POST'
}
});
Self.invoiceClient = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
options = typeof options == 'object'
? Object.assign({}, options) : {};
options = typeof options === 'object' ? {...options} : {};
options.userId = ctx.req.accessToken.userId;
let tx;
@ -74,10 +77,9 @@ module.exports = Self => {
], options);
}
const invoiceType = 'G';
const invoiceId = await models.Ticket.makeInvoice(
ctx,
invoiceType,
args.serialType,
args.companyFk,
args.invoiceDate,
null,

View File

@ -0,0 +1,75 @@
const models = require('vn-loopback/server/server').models;
describe('InvoiceOut clientsToInvoice()', () => {
const userId = 1;
const clientId = 1101;
const companyFk = 442;
const maxShipped = new Date();
maxShipped.setMonth(11);
maxShipped.setDate(31);
maxShipped.setHours(23, 59, 59, 999);
const invoiceDate = new Date();
const activeCtx = {
getLocale: () => {
return 'en';
},
accessToken: {userId: userId},
__: value => {
return value;
},
headers: {origin: 'http://localhost'}
};
const ctx = {req: activeCtx};
it('should return a list of clients to invoice', async() => {
spyOn(models.InvoiceOut, 'rawSql').and.callFake(query => {
if (query.includes('ticketPackaging_add'))
return Promise.resolve(true);
else if (query.includes('SELECT c.id clientId')) {
return Promise.resolve([
{
clientId: clientId,
clientName: 'Test Client',
id: 1,
nickname: 'Address 1'
}
]);
}
});
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
const addresses = await models.InvoiceOut.clientsToInvoice(
ctx, clientId, invoiceDate, maxShipped, companyFk, options);
expect(addresses.length).toBeGreaterThan(0);
expect(addresses[0].clientId).toBe(clientId);
expect(addresses[0].clientName).toBe('Test Client');
expect(addresses[0].id).toBe(1);
expect(addresses[0].nickname).toBe('Address 1');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should handle errors and rollback transaction', async() => {
spyOn(models.InvoiceOut, 'rawSql').and.callFake(() => {
return Promise.reject(new Error('Test Error'));
});
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
await models.InvoiceOut.clientsToInvoice(ctx, clientId, invoiceDate, maxShipped, companyFk, options);
} catch (e) {
expect(e.message).toBe('Test Error');
await tx.rollback();
}
});
});

View File

@ -1,16 +1,16 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('InvoiceOut invoiceClient()', () => {
const userId = 1;
const clientId = 1101;
const addressId = 121;
const addressFk = 121;
const companyFk = 442;
const minShipped = Date.vnNew();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
const invoiceSerial = 'A';
const activeCtx = {
getLocale: () => {
return 'en';
@ -23,9 +23,14 @@ describe('InvoiceOut invoiceClient()', () => {
};
const ctx = {req: activeCtx};
beforeAll(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should make a global invoicing', async() => {
spyOn(models.InvoiceOut, 'makePdf').and.returnValue(new Promise(resolve => resolve(true)));
it('should make a global invoicing by address and verify billing status', async() => {
spyOn(models.InvoiceOut, 'makePdf').and.returnValue(Promise.resolve(true));
spyOn(models.InvoiceOut, 'invoiceEmail');
const tx = await models.InvoiceOut.beginTransaction({});
@ -34,20 +39,96 @@ describe('InvoiceOut invoiceClient()', () => {
try {
ctx.args = {
clientId: clientId,
addressId: addressId,
addressId: addressFk,
invoiceDate: Date.vnNew(),
maxShipped: Date.vnNew(),
companyFk: companyFk,
minShipped: minShipped
serialType: 'global'
};
const invoiceOutId = await models.InvoiceOut.invoiceClient(ctx, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceOutId, null, options);
const [firstTicket] = await models.Ticket.find({
expect(invoiceOutId).toBeGreaterThan(0);
const allClientTickets = await models.Ticket.find({
where: {
clientFk: clientId,
or: [
{refFk: null},
{refFk: invoiceOut.ref}
]
}
}, options);
const billedTickets = await models.Ticket.find({
where: {refFk: invoiceOut.ref}
}, options);
const allBilledTicketsHaveCorrectAddress = billedTickets.every(ticket => ticket.addressFk === addressFk);
expect(allBilledTicketsHaveCorrectAddress).toBe(true);
const addressTickets = allClientTickets.filter(ticket => ticket.addressFk === addressFk);
const allAddressTicketsBilled = addressTickets.every(ticket => ticket.refFk === invoiceOut.ref);
expect(allAddressTicketsBilled).toBe(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should invoice all tickets regardless of address when hasToInvoiceByAddress is false', async() => {
spyOn(models.InvoiceOut, 'makePdf').and.returnValue(Promise.resolve(true));
spyOn(models.InvoiceOut, 'invoiceEmail');
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('hasToInvoiceByAddress', false, options);
ctx.args = {
clientId: clientId,
invoiceDate: Date.vnNew(),
maxShipped: Date.vnNew(),
companyFk: companyFk,
serialType: 'global'
};
const invoiceOutId = await models.InvoiceOut.invoiceClient(ctx, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceOutId, null, options);
expect(invoiceOutId).toBeGreaterThan(0);
expect(firstTicket.refFk).toContain(invoiceSerial);
const allClientTickets = await models.Ticket.find({
where: {
clientFk: clientId,
or: [
{refFk: null},
{refFk: invoiceOut.ref}
]
}
}, options);
const billedTickets = await models.Ticket.find({
where: {refFk: invoiceOut.ref}
}, options);
const allTicketsBilled = allClientTickets.every(ticket => ticket.refFk === invoiceOut.ref);
expect(allTicketsBilled).toBe(true);
const billedAddresses = new Set(billedTickets.map(ticket => ticket.addressFk));
expect(billedAddresses.size).toBeGreaterThan(1);
await tx.rollback();
} catch (e) {

View File

@ -20,6 +20,9 @@
},
"isCEE": {
"type": "boolean"
},
"type": {
"type": "string"
}
},
"relations": {
@ -35,4 +38,4 @@
"principalId": "$everyone",
"permission": "ALLOW"
}]
}
}

View File

@ -93,8 +93,8 @@ module.exports = Self => {
SUM(t.isDeleted) hasErrorDeleted,
SUM(itc.id IS NULL) hasErrorItemTaxCountry,
SUM(a.id IS NULL) hasErrorAddress,
SUM(ios.code IS NOT NULL
AND(ad.customsAgentFk IS NULL
SUM(ios.code IS NOT NULL
AND(ad.customsAgentFk IS NULL
OR ad.incotermsFk IS NULL)) hasErrorInfoTaxAreaWorld,
t.clientFk clientFk
FROM ticket t
@ -113,24 +113,24 @@ module.exports = Self => {
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
LEFT JOIN itemTaxCountry itc ON itc.itemFk = i.id
AND itc.countryFk = su.countryFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.taxAreaFk = 'WORLD'
AND ios.code = invoiceSerial(t.clientFk, t.companyFk, 'M')
LEFT JOIN vn.invoiceOutSerial ios ON ios.taxAreaFk = 'WORLD'
AND ios.code = invoiceSerial(t.clientFk, t.companyFk, 'multiple')
WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered'))
AND DATE(t.shipped) BETWEEN ? - INTERVAL 2 DAY AND util.dayEnd(?)
AND t.refFk IS NULL
AND IFNULL(a.hasDailyInvoice, co.hasDailyInvoice)
GROUP BY ticketFk
HAVING hasErrorToInvoice
OR hasErrorTaxDataChecked
OR hasErrorDeleted
OR hasErrorItemTaxCountry
OR hasErrorAddress
GROUP BY ticketFk
HAVING hasErrorToInvoice
OR hasErrorTaxDataChecked
OR hasErrorDeleted
OR hasErrorItemTaxCountry
OR hasErrorAddress
OR hasErrorInfoTaxAreaWorld
)sub
)sub2
) SELECT IF(errors = '{"tickets": null}',
'No errors',
util.notification_send('invoice-ticket-closure', errors, NULL))
util.notification_send('invoice-ticket-closure', errors, NULL))
FROM ticketNotInvoiceable`, [toDate, toDate]);
await closure(ctx, Self, tickets);

View File

@ -95,7 +95,7 @@ module.exports = function(Self) {
FROM vn.ticket
WHERE id IN (?)
`, [ticketsIds], myOptions);
return models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), invoiceCorrection, myOptions);
return models.Ticket.makeInvoice(ctx, 'quick', companyId, Date.vnNew(), invoiceCorrection, myOptions);
}
};

View File

@ -3,7 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('ticket makeInvoice()', () => {
const userId = 19;
const invoiceType = 'R';
const invoiceType = 'quick';
const companyFk = 442;
const invoiceDate = Date.vnNew();
const activeCtx = {