#7896 - 24.36 Dev To Test #2884
|
@ -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');
|
||||
|
||||
|
|
|
@ -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 ;
|
||||
|
|
|
@ -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 ;
|
|
@ -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,
|
||||
|
|
|
@ -90,7 +90,7 @@ BEGIN
|
|||
IF vIsTaxDataChecked THEN
|
||||
CALL invoiceOut_newFromClient(
|
||||
vClientFk,
|
||||
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'M')),
|
||||
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'multiple')),
|
||||
vShipped,
|
||||
vCompanyFk,
|
||||
NULL,
|
||||
|
|
|
@ -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 ;
|
|
@ -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 ;
|
|
@ -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`);
|
|
@ -0,0 +1,3 @@
|
|||
UPDATE vn.invoiceOutSerial
|
||||
SET `type`='multiple'
|
||||
WHERE `description` LIKE '%Múltiple%';
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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) {
|
||||
|
|
|
@ -20,6 +20,9 @@
|
|||
},
|
||||
"isCEE": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
|
|
|
@ -114,7 +114,7 @@ module.exports = Self => {
|
|||
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')
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue