Merge pull request '#7346 - manualInvoice' (!3060) from 7346-manualInvoice into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #3060
Reviewed-by: Alex Moreno <alexm@verdnatura.es>
This commit is contained in:
Javi Gallego 2024-10-03 05:45:09 +00:00
commit 91d9a6e417
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',
description: 'The invoiceable client id'
},
{
arg: 'addressFk',
type: 'any',
description: 'The address id'
},
{
arg: 'ticketFk',
type: 'any',
@ -23,7 +28,8 @@ module.exports = Self => {
{
arg: 'serial',
type: 'string',
description: 'The invoice serial'
description: 'The invoice serial',
required: true
},
{
arg: 'taxArea',
@ -46,7 +52,8 @@ module.exports = Self => {
}
});
Self.createManualInvoice = async(ctx, clientFk, ticketFk, maxShipped, serial, taxArea, reference, options) => {
Self.createManualInvoice =
async(ctx, clientFk, addressFk, ticketFk, maxShipped, serial, taxArea, reference, options) => {
if (!clientFk && !ticketFk) throw new UserError(`Select ticket or client`);
const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId};
@ -60,34 +67,43 @@ module.exports = Self => {
myOptions.transaction = tx;
}
let companyId;
let companyFk;
let newInvoice;
let query;
try {
if (ticketFk) {
const ticket = await models.Ticket.findById(ticketFk, null, myOptions);
const company = await models.Company.findById(ticket.companyFk, null, myOptions);
const ticket = await models.Ticket.findById(ticketFk, {
fields: ['clientFk', 'companyFk', 'shipped', 'refFk', 'totalWithVat']
}, myOptions);
const company = await models.Company.findById(ticket.companyFk, {
fields: ['code']
}, myOptions);
clientFk = ticket.clientFk;
maxShipped = ticket.shipped;
companyId = ticket.companyFk;
companyFk = ticket.companyFk;
// Validates invoiced ticket
if (ticket.refFk)
throw new UserError('This ticket is already invoiced');
// Validates ticket amount
if (ticket.totalWithVat == 0)
throw new UserError(`A ticket with an amount of zero can't be invoiced`);
// Validates ticket nagative base
const hasNegativeBase = await getNegativeBase(maxShipped, clientFk, companyId, myOptions);
const hasNegativeBase = await getNegativeBase(maxShipped, clientFk, companyFk, myOptions);
if (hasNegativeBase && company.code == 'VNL')
throw new UserError(`A ticket with a negative base can't be invoiced`);
} else {
if (!maxShipped)
throw new UserError(`Max shipped required`);
if (addressFk) {
const address = await models.Address.findById(addressFk, {
fields: ['clientFk']
}, myOptions);
if (clientFk && clientFk !== address.clientFk)
throw new UserError('The provided clientFk does not match');
}
const company = await models.Ticket.findOne({
fields: ['companyFk'],
where: {
@ -95,22 +111,20 @@ module.exports = Self => {
shipped: {lte: maxShipped}
}
}, myOptions);
companyId = company.companyFk;
companyFk = company.companyFk;
}
// Validate invoiceable client
const isClientInvoiceable = await isInvoiceable(clientFk, myOptions);
if (!isClientInvoiceable)
throw new UserError(`This client is not invoiceable`);
// Can't invoice tickets into future
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, companyId, myOptions);
const maxInvoiceDate = await getMaxIssued(serial, companyFk, myOptions);
if (Date.vnNew() < maxInvoiceDate)
throw new UserError(`Can't invoice to past`);
@ -122,13 +136,23 @@ module.exports = Self => {
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,
companyId,
companyFk,
taxArea,
reference
], myOptions);
@ -159,10 +183,10 @@ module.exports = Self => {
return result.invoiceable;
}
async function getNegativeBase(maxShipped, clientFk, companyId, options) {
async function getNegativeBase(maxShipped, clientFk, companyFk, options) {
const models = Self.app.models;
await models.InvoiceOut.rawSql('CALL invoiceOut_exportationFromClient(?,?,?)',
[maxShipped, clientFk, companyId], options
[maxShipped, clientFk, companyFk], options
);
const query = 'SELECT vn.hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, [], options);
@ -170,14 +194,14 @@ module.exports = Self => {
return result.base;
}
async function getMaxIssued(serial, companyId, options) {
async function getMaxIssued(serial, companyFk, options) {
const models = Self.app.models;
const query = `SELECT MAX(issued) AS issued
FROM invoiceOut
WHERE serial = ? AND companyFk = ?`;
const [maxIssued] = await models.InvoiceOut.rawSql(query,
[serial, companyId], options);
const maxInvoiceDate = maxIssued && maxIssued.issued || Date.vnNew();
[serial, companyFk], options);
const maxInvoiceDate = maxIssued?.issued || Date.vnNew();
return maxInvoiceDate;
}

View File

@ -6,110 +6,90 @@ describe('InvoiceOut createManualInvoice()', () => {
const clientId = 1106;
const activeCtx = {accessToken: {userId: 1}};
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() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error;
try {
await createInvoice(ctx, options, undefined, ticketId);
await createInvoice(ctx, options, undefined, ticketId);
await tx.rollback();
await createInvoice(ctx, options, undefined, undefined, ticketId);
await createInvoice(ctx, options, undefined, undefined, ticketId);
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain('This ticket is already invoiced');
});
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;
try {
const ticket = await models.Ticket.findById(ticketId, null, options);
await ticket.updateAttributes({totalWithVat: 0}, options);
await createInvoice(ctx, options, undefined, ticketId);
await tx.rollback();
await createInvoice(ctx, options, undefined, undefined, ticketId);
} catch (e) {
error = e;
await tx.rollback();
}
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() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
let error;
try {
await createInvoice(ctx, options, clientId);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`Max shipped required`);
});
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;
try {
const client = await models.Client.findById(clientId, null, options);
await client.updateAttributes({isTaxDataChecked: false}, options);
await createInvoice(ctx, options, undefined, ticketId);
await tx.rollback();
await createInvoice(ctx, options, undefined, undefined, ticketId);
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toContain(`This client is not invoiceable`);
});
it('should create a manual invoice', async() => {
spyOn(models.InvoiceOut, 'createPdf').and.returnValue(new Promise(resolve => resolve(true)));
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
try {
const result = await createInvoice(ctx, options, undefined, ticketId);
it('should create a manual invoice with ticket', async() => {
const result = await createInvoice(ctx, options, undefined, undefined, ticketId);
expect(result.id).toEqual(jasmine.any(Number));
});
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
it('should create a manual invoice with client', async() => {
const result = await createInvoice(ctx, options, clientId, undefined, undefined, Date.vnNew());
expect(result.id).toEqual(jasmine.any(Number));
});
it('should create a manual invoice with address', async() => {
const addressFk = 126;
const result = await createInvoice(ctx, options, clientId, addressFk, undefined, Date.vnNew());
expect(result.id).toEqual(jasmine.any(Number));
});
});
@ -117,6 +97,7 @@ function createInvoice(
ctx,
options,
clientFk = undefined,
addressFk = undefined,
ticketFk = undefined,
maxShipped = undefined,
serial = 'T',
@ -124,6 +105,6 @@ function createInvoice(
reference = undefined
) {
return models.InvoiceOut.createManualInvoice(
ctx, clientFk, ticketFk, maxShipped, serial, taxArea, reference, options
ctx, clientFk, addressFk, ticketFk, maxShipped, serial, taxArea, reference, options
);
}