#7346 - manualInvoice #3060

Merged
jgallego merged 2 commits from 7346-manualInvoice into dev 2024-10-03 05:45:10 +00:00
3 changed files with 209 additions and 148 deletions

View File

@ -0,0 +1,56 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`invoiceOut_newFromAddress`(
IN vAddressFk INT,
IN vSerial CHAR(2),
IN vMaxShipped DATE,
IN vCompanyFk INT,
IN vTaxArea VARCHAR(25),
IN vRef VARCHAR(25),
OUT vInvoiceId INT)
BEGIN
/**
* Factura los tickets de un consignatario hasta una fecha dada
* @param vAddressFk Id del consignatario a facturar
* @param vSerial Serie de factura
* @param vMaxShipped Fecha hasta la cual cogerá tickets para facturar
* @param vCompanyFk Id de la empresa desde la que se factura
* @param vTaxArea Tipo de iva en relacion a la empresa y al cliente, NULL por defecto
* @param vRef Referencia de la factura en caso que se quiera forzar, NULL por defecto
* @return vInvoiceId factura
*/
DECLARE vIsRefEditable BOOLEAN;
IF vRef IS NOT NULL AND vSerial IS NOT NULL THEN
SELECT isRefEditable INTO vIsRefEditable
FROM invoiceOutSerial
WHERE code = vSerial;
IF NOT vIsRefEditable THEN
CALL util.throw('serial non editable');
END IF;
END IF;
DROP TEMPORARY TABLE IF EXISTS `tmp`.`ticketToInvoice`;
CREATE TEMPORARY TABLE `tmp`.`ticketToInvoice`
(PRIMARY KEY (`id`))
ENGINE = MEMORY
SELECT id FROM ticket t
WHERE t.addressFk = vAddressFk
AND t.refFk IS NULL
AND t.companyFk = vCompanyFk
AND t.shipped BETWEEN
util.firstDayOfYear(vMaxShipped - INTERVAL 1 YEAR)
AND util.dayend(vMaxShipped);
CALL invoiceOut_new(vSerial, util.VN_CURDATE(), vTaxArea, vInvoiceId);
UPDATE invoiceOut
SET `ref` = vRef
WHERE id = vInvoiceId
AND vRef IS NOT NULL;
IF vSerial <> 'R' AND NOT ISNULL(vInvoiceId) AND vInvoiceId <> 0 THEN
CALL invoiceOutBooking(vInvoiceId);
END IF;
END$$
DELIMITER ;

View File

@ -10,6 +10,11 @@ module.exports = Self => {
type: 'any', type: 'any',
description: 'The invoiceable client id' description: 'The invoiceable client id'
}, },
{
arg: 'addressFk',
type: 'any',
description: 'The address id'
},
{ {
arg: 'ticketFk', arg: 'ticketFk',
type: 'any', type: 'any',
@ -23,7 +28,8 @@ module.exports = Self => {
{ {
arg: 'serial', arg: 'serial',
type: 'string', type: 'string',
description: 'The invoice serial' description: 'The invoice serial',
required: true
}, },
{ {
arg: 'taxArea', arg: 'taxArea',
@ -46,108 +52,126 @@ module.exports = Self => {
} }
}); });
Self.createManualInvoice = async(ctx, clientFk, ticketFk, maxShipped, serial, taxArea, reference, options) => { Self.createManualInvoice =
if (!clientFk && !ticketFk) throw new UserError(`Select ticket or client`); async(ctx, clientFk, addressFk, ticketFk, maxShipped, serial, taxArea, reference, options) => {
const models = Self.app.models; if (!clientFk && !ticketFk) throw new UserError(`Select ticket or client`);
Review

Se li pot pasar address sense clientFk?

Se li pot pasar address sense clientFk?
Review

No, pero tant si es pasa buit com si es pasa un que no correspon te una validació posterior.

No, pero tant si es pasa buit com si es pasa un que no correspon te una validació posterior.
const myOptions = {userId: ctx.req.accessToken.userId}; const models = Self.app.models;
let tx; const myOptions = {userId: ctx.req.accessToken.userId};
let tx;
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
if (!myOptions.transaction) { if (!myOptions.transaction) {
tx = await Self.beginTransaction({}); tx = await Self.beginTransaction({});
myOptions.transaction = tx; myOptions.transaction = tx;
} }
let companyId; let companyFk;
let newInvoice; let newInvoice;
let query; let query;
try { try {
if (ticketFk) { if (ticketFk) {
const ticket = await models.Ticket.findById(ticketFk, null, myOptions); const ticket = await models.Ticket.findById(ticketFk, {
const company = await models.Company.findById(ticket.companyFk, null, myOptions); fields: ['clientFk', 'companyFk', 'shipped', 'refFk', 'totalWithVat']
}, myOptions);
const company = await models.Company.findById(ticket.companyFk, {
fields: ['code']
}, myOptions);
clientFk = ticket.clientFk; clientFk = ticket.clientFk;
maxShipped = ticket.shipped; maxShipped = ticket.shipped;
companyId = ticket.companyFk; companyFk = ticket.companyFk;
// Validates invoiced ticket if (ticket.refFk)
if (ticket.refFk) throw new UserError('This ticket is already invoiced');
throw new UserError('This ticket is already invoiced');
// Validates ticket amount if (ticket.totalWithVat == 0)
if (ticket.totalWithVat == 0) throw new UserError(`A ticket with an amount of zero can't be invoiced`);
throw new UserError(`A ticket with an amount of zero can't be invoiced`);
// Validates ticket nagative base const hasNegativeBase = await getNegativeBase(maxShipped, clientFk, companyFk, myOptions);
const hasNegativeBase = await getNegativeBase(maxShipped, clientFk, companyId, myOptions); if (hasNegativeBase && company.code == 'VNL')
if (hasNegativeBase && company.code == 'VNL') throw new UserError(`A ticket with a negative base can't be invoiced`);
throw new UserError(`A ticket with a negative base can't be invoiced`); } else {
} else { if (!maxShipped)
if (!maxShipped) throw new UserError(`Max shipped required`);
throw new UserError(`Max shipped required`);
const company = await models.Ticket.findOne({ if (addressFk) {
fields: ['companyFk'], const address = await models.Address.findById(addressFk, {
where: { fields: ['clientFk']
clientFk: clientFk, }, myOptions);
shipped: {lte: maxShipped}
if (clientFk && clientFk !== address.clientFk)
throw new UserError('The provided clientFk does not match');
} }
}, myOptions); const company = await models.Ticket.findOne({
companyId = company.companyFk; fields: ['companyFk'],
where: {
clientFk: clientFk,
shipped: {lte: maxShipped}
}
}, myOptions);
companyFk = company.companyFk;
}
const isClientInvoiceable = await isInvoiceable(clientFk, myOptions);
if (!isClientInvoiceable)
throw new UserError(`This client is not invoiceable`);
const tomorrow = Date.vnNew();
tomorrow.setDate(tomorrow.getDate() + 1);
if (maxShipped >= tomorrow)
throw new UserError(`Can't invoice to future`);
const maxInvoiceDate = await getMaxIssued(serial, companyFk, myOptions);
if (Date.vnNew() < maxInvoiceDate)
throw new UserError(`Can't invoice to past`);
if (ticketFk) {
query = `CALL invoiceOut_newFromTicket(?, ?, ?, ?, @newInvoiceId)`;
await Self.rawSql(query, [
ticketFk,
serial,
taxArea,
reference
], myOptions);
} else if (addressFk) {
query = `CALL invoiceOut_newFromAddress(?, ?, ?, ?, ?, ?, @newInvoiceId)`;
await Self.rawSql(query, [
addressFk,
serial,
maxShipped,
companyFk,
taxArea,
reference
], myOptions);
} else {
query = `CALL invoiceOut_newFromClient(?, ?, ?, ?, ?, ?, @newInvoiceId)`;
await Self.rawSql(query, [
clientFk,
serial,
maxShipped,
companyFk,
taxArea,
reference
], myOptions);
}
[newInvoice] = await Self.rawSql(`SELECT @newInvoiceId id`, null, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
} }
// Validate invoiceable client if (!newInvoice.id) throw new UserError('It was not able to create the invoice');
const isClientInvoiceable = await isInvoiceable(clientFk, myOptions);
if (!isClientInvoiceable)
throw new UserError(`This client is not invoiceable`);
// Can't invoice tickets into future await Self.createPdf(ctx, newInvoice.id);
const tomorrow = Date.vnNew();
tomorrow.setDate(tomorrow.getDate() + 1);
if (maxShipped >= tomorrow) return newInvoice;
throw new UserError(`Can't invoice to future`); };
const maxInvoiceDate = await getMaxIssued(serial, companyId, myOptions);
if (Date.vnNew() < maxInvoiceDate)
throw new UserError(`Can't invoice to past`);
if (ticketFk) {
query = `CALL invoiceOut_newFromTicket(?, ?, ?, ?, @newInvoiceId)`;
await Self.rawSql(query, [
ticketFk,
serial,
taxArea,
reference
], myOptions);
} else {
query = `CALL invoiceOut_newFromClient(?, ?, ?, ?, ?, ?, @newInvoiceId)`;
await Self.rawSql(query, [
clientFk,
serial,
maxShipped,
companyId,
taxArea,
reference
], myOptions);
}
[newInvoice] = await Self.rawSql(`SELECT @newInvoiceId id`, null, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
if (!newInvoice.id) throw new UserError('It was not able to create the invoice');
await Self.createPdf(ctx, newInvoice.id);
return newInvoice;
};
async function isInvoiceable(clientFk, options) { async function isInvoiceable(clientFk, options) {
const models = Self.app.models; const models = Self.app.models;
@ -159,10 +183,10 @@ module.exports = Self => {
return result.invoiceable; return result.invoiceable;
} }
async function getNegativeBase(maxShipped, clientFk, companyId, options) { async function getNegativeBase(maxShipped, clientFk, companyFk, options) {
const models = Self.app.models; const models = Self.app.models;
await models.InvoiceOut.rawSql('CALL invoiceOut_exportationFromClient(?,?,?)', await models.InvoiceOut.rawSql('CALL invoiceOut_exportationFromClient(?,?,?)',
[maxShipped, clientFk, companyId], options [maxShipped, clientFk, companyFk], options
); );
const query = 'SELECT vn.hasAnyNegativeBase() AS base'; const query = 'SELECT vn.hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, [], options); const [result] = await models.InvoiceOut.rawSql(query, [], options);
@ -170,14 +194,14 @@ module.exports = Self => {
return result.base; return result.base;
} }
async function getMaxIssued(serial, companyId, options) { async function getMaxIssued(serial, companyFk, options) {
const models = Self.app.models; const models = Self.app.models;
const query = `SELECT MAX(issued) AS issued const query = `SELECT MAX(issued) AS issued
FROM invoiceOut FROM invoiceOut
WHERE serial = ? AND companyFk = ?`; WHERE serial = ? AND companyFk = ?`;
const [maxIssued] = await models.InvoiceOut.rawSql(query, const [maxIssued] = await models.InvoiceOut.rawSql(query,
[serial, companyId], options); [serial, companyFk], options);
const maxInvoiceDate = maxIssued && maxIssued.issued || Date.vnNew(); const maxInvoiceDate = maxIssued?.issued || Date.vnNew();
return maxInvoiceDate; return maxInvoiceDate;
} }

View File

@ -6,110 +6,90 @@ describe('InvoiceOut createManualInvoice()', () => {
const clientId = 1106; const clientId = 1106;
const activeCtx = {accessToken: {userId: 1}}; const activeCtx = {accessToken: {userId: 1}};
const ctx = {req: activeCtx}; const ctx = {req: activeCtx};
let tx; let options;
beforeEach(async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(Promise.resolve(true));
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
tx = await models.InvoiceOut.beginTransaction({});
options = {transaction: tx};
});
afterEach(async() => {
await tx.rollback();
});
it('should throw an error trying to invoice again', async() => { it('should throw an error trying to invoice again', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error; let error;
try { try {
await createInvoice(ctx, options, undefined, ticketId); await createInvoice(ctx, options, undefined, undefined, ticketId);
await createInvoice(ctx, options, undefined, ticketId); await createInvoice(ctx, options, undefined, undefined, ticketId);
await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
await tx.rollback();
} }
expect(error.message).toContain('This ticket is already invoiced'); expect(error.message).toContain('This ticket is already invoiced');
}); });
it('should throw an error for a ticket with an amount of zero', async() => { it('should throw an error for a ticket with an amount of zero', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error; let error;
try { try {
const ticket = await models.Ticket.findById(ticketId, null, options); const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttributes({totalWithVat: 0}, options); await ticket.updateAttributes({totalWithVat: 0}, options);
await createInvoice(ctx, options, undefined, ticketId); await createInvoice(ctx, options, undefined, undefined, ticketId);
await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
await tx.rollback();
} }
expect(error.message).toContain(`A ticket with an amount of zero can't be invoiced`); expect(error.message).toContain(`A ticket with an amount of zero can't be invoiced`);
}); });
it('should throw an error when the clientFk property is set without the max shipped date', async() => { it('should throw an error when the clientFk property is set without the max shipped date', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error; let error;
try { try {
await createInvoice(ctx, options, clientId); await createInvoice(ctx, options, clientId);
await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
await tx.rollback();
} }
expect(error.message).toContain(`Max shipped required`); expect(error.message).toContain(`Max shipped required`);
}); });
it('should throw an error for a non-invoiceable client', async() => { it('should throw an error for a non-invoiceable client', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error; let error;
try { try {
const client = await models.Client.findById(clientId, null, options); const client = await models.Client.findById(clientId, null, options);
await client.updateAttributes({isTaxDataChecked: false}, options); await client.updateAttributes({isTaxDataChecked: false}, options);
await createInvoice(ctx, options, undefined, ticketId); await createInvoice(ctx, options, undefined, undefined, ticketId);
await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
await tx.rollback();
} }
expect(error.message).toContain(`This client is not invoiceable`); expect(error.message).toContain(`This client is not invoiceable`);
}); });
it('should create a manual invoice', async() => { it('should create a manual invoice with ticket', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true))); const result = await createInvoice(ctx, options, undefined, undefined, ticketId);
const tx = await models.InvoiceOut.beginTransaction({}); expect(result.id).toEqual(jasmine.any(Number));
const options = {transaction: tx}; });
try { it('should create a manual invoice with client', async() => {
const result = await createInvoice(ctx, options, undefined, ticketId); const result = await createInvoice(ctx, options, clientId, undefined, undefined, Date.vnNew());
expect(result.id).toEqual(jasmine.any(Number)); expect(result.id).toEqual(jasmine.any(Number));
});
await tx.rollback(); it('should create a manual invoice with address', async() => {
} catch (e) { const addressFk = 126;
await tx.rollback(); const result = await createInvoice(ctx, options, clientId, addressFk, undefined, Date.vnNew());
throw e;
} expect(result.id).toEqual(jasmine.any(Number));
}); });
}); });
@ -117,6 +97,7 @@ function createInvoice(
ctx, ctx,
options, options,
clientFk = undefined, clientFk = undefined,
addressFk = undefined,
ticketFk = undefined, ticketFk = undefined,
maxShipped = undefined, maxShipped = undefined,
serial = 'T', serial = 'T',
@ -124,6 +105,6 @@ function createInvoice(
reference = undefined reference = undefined
) { ) {
return models.InvoiceOut.createManualInvoice( return models.InvoiceOut.createManualInvoice(
ctx, clientFk, ticketFk, maxShipped, serial, taxArea, reference, options ctx, clientFk, addressFk, ticketFk, maxShipped, serial, taxArea, reference, options
); );
} }