refs #5000 Invoicing RC
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Carlos Andrés 2023-02-23 09:25:23 +01:00
parent 01ade6eb03
commit 4cc6485cbd
16 changed files with 838 additions and 439 deletions

View File

@ -4,5 +4,6 @@
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"search.useIgnoreFiles": false
}

View File

@ -0,0 +1,6 @@
ALTER TABLE vn.invoiceOutSerial
ADD `type` ENUM('global', 'quick') DEFAULT NULL NULL;
UPDATE vn.invoiceOutSerial
SET type = 'global'
WHERE code IN ('A','V');

View File

@ -0,0 +1,34 @@
DROP FUNCTION IF EXISTS `vn`.`invoiceOut_getMaxIssued`;
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`invoiceOut_getMaxIssued`(
vSerial VARCHAR(2),
vCompanyFk INT,
vYear INT
) RETURNS DATE
READS SQL DATA
BEGIN
/**
* Retorna la fecha a partir de la cual es válido emitir una factura
*
* @param vSerial Serie de facturación
* @param vCompanyFk Empresa factura emitida
* @param vYear Año contable
* @return vInvoiceOutIssued fecha factura válida
*/
DECLARE vInvoiceOutIssued DATE;
DECLARE vFirstDayOfYear DATE;
SET vFirstDayOfYear := MAKEDATE(vYear, 1);
SELECT IFNULL(MAX(io.issued), vFirstDayOfYear) INTO vInvoiceOutIssued
FROM invoiceOut io
WHERE io.serial = vSerial
AND io.companyFk = vCompanyFk
AND io.issued BETWEEN vFirstDayOfYear
AND util.lastDayOfYear(vFirstDayOfYear);
RETURN vInvoiceOutIssued;
END$$
DELIMITER ;

View File

@ -0,0 +1,258 @@
DROP PROCEDURE IF EXISTS `vn`.`invoiceOut_new`;
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`invoiceOut_new`(
vSerial VARCHAR(255),
vInvoiceDate DATE,
vTaxArea VARCHAR(25),
OUT vNewInvoiceId INT)
BEGIN
/**
* Creación de facturas emitidas.
* requiere previamente tabla ticketToInvoice(id).
*
* @param vSerial serie a la cual se hace la factura
* @param vInvoiceDate fecha de la factura
* @param vTaxArea tipo de iva en relacion a la empresa y al cliente
* @param vNewInvoiceId id de la factura que se acaba de generar
* @return vNewInvoiceId
*/
DECLARE vIsAnySaleToInvoice BOOL;
DECLARE vIsAnyServiceToInvoice BOOL;
DECLARE vNewRef VARCHAR(255);
DECLARE vWorker INT DEFAULT account.myUser_getId();
DECLARE vCompanyFk INT;
DECLARE vInterCompanyFk INT;
DECLARE vClientFk INT;
DECLARE vCplusStandardInvoiceTypeFk INT DEFAULT 1;
DECLARE vCplusCorrectingInvoiceTypeFk INT DEFAULT 6;
DECLARE vCplusSimplifiedInvoiceTypeFk INT DEFAULT 2;
DECLARE vCorrectingSerial VARCHAR(1) DEFAULT 'R';
DECLARE vSimplifiedSerial VARCHAR(1) DEFAULT 'S';
DECLARE vNewInvoiceInFk INT;
DECLARE vIsInterCompany BOOL DEFAULT FALSE;
DECLARE vIsCEESerial BOOL DEFAULT FALSE;
DECLARE vIsCorrectInvoiceDate BOOL;
DECLARE vMaxShipped DATE;
SET vInvoiceDate = IFNULL(vInvoiceDate, util.CURDATE());
SELECT t.clientFk,
t.companyFk,
MAX(DATE(t.shipped)),
DATE(vInvoiceDate) >= invoiceOut_getMaxIssued(
vSerial,
t.companyFk,
YEAR(vInvoiceDate))
INTO vClientFk,
vCompanyFk,
vMaxShipped,
vIsCorrectInvoiceDate
FROM ticketToInvoice tt
JOIN ticket t ON t.id = tt.id;
IF(vMaxShipped > vInvoiceDate) THEN
CALL util.throw("Invoice date can't be less than max date");
END IF;
IF NOT vIsCorrectInvoiceDate THEN
CALL util.throw('Exists an invoice with a previous date');
END IF;
-- Eliminem de ticketToInvoice els tickets que no han de ser facturats
DELETE ti.*
FROM ticketToInvoice ti
JOIN ticket t ON t.id = ti.id
JOIN sale s ON s.ticketFk = t.id
JOIN item i ON i.id = s.itemFk
JOIN supplier su ON su.id = t.companyFk
JOIN client c ON c.id = t.clientFk
LEFT JOIN itemTaxCountry itc ON itc.itemFk = i.id AND itc.countryFk = su.countryFk
WHERE YEAR(t.shipped) < 2001
OR c.isTaxDataChecked = FALSE
OR t.isDeleted
OR c.hasToInvoice = FALSE
OR itc.id IS NULL;
SELECT SUM(s.quantity * s.price * (100 - s.discount)/100) <> 0
INTO vIsAnySaleToInvoice
FROM ticketToInvoice t
JOIN sale s ON s.ticketFk = t.id;
SELECT COUNT(*) > 0 INTO vIsAnyServiceToInvoice
FROM ticketToInvoice t
JOIN ticketService ts ON ts.ticketFk = t.id;
IF (vIsAnySaleToInvoice OR vIsAnyServiceToInvoice)
AND (vCorrectingSerial = vSerial OR NOT hasAnyNegativeBase())
THEN
-- el trigger añade el siguiente Id_Factura correspondiente a la vSerial
INSERT INTO invoiceOut(
ref,
serial,
issued,
clientFk,
dued,
companyFk,
cplusInvoiceType477Fk
)
SELECT
1,
vSerial,
vInvoiceDate,
vClientFk,
getDueDate(vInvoiceDate, dueDay),
vCompanyFk,
IF(vSerial = vCorrectingSerial,
vCplusCorrectingInvoiceTypeFk,
IF(vSerial = vSimplifiedSerial,
vCplusSimplifiedInvoiceTypeFk,
vCplusStandardInvoiceTypeFk))
FROM client
WHERE id = vClientFk;
SET vNewInvoiceId = LAST_INSERT_ID();
SELECT `ref`
INTO vNewRef
FROM invoiceOut
WHERE id = vNewInvoiceId;
UPDATE ticket t
JOIN ticketToInvoice ti ON ti.id = t.id
SET t.refFk = vNewRef;
DROP TEMPORARY TABLE IF EXISTS tmp.updateInter;
CREATE TEMPORARY TABLE tmp.updateInter ENGINE = MEMORY
SELECT s.id,ti.id ticket_id,vWorker Id_Trabajador
FROM ticketToInvoice ti
LEFT JOIN ticketState ts ON ti.id = ts.ticket
JOIN state s
WHERE IFNULL(ts.alertLevel,0) < 3 and s.`code` = getAlert3State(ti.id);
INSERT INTO ticketTracking(stateFk,ticketFk,workerFk)
SELECT * FROM tmp.updateInter;
INSERT INTO ticketLog (action, userFk, originFk, description)
SELECT 'UPDATE', account.myUser_getId(), ti.id, CONCAT('Crea factura ', vNewRef)
FROM ticketToInvoice ti;
CALL invoiceExpenceMake(vNewInvoiceId);
CALL invoiceTaxMake(vNewInvoiceId,vTaxArea);
UPDATE invoiceOut io
JOIN (
SELECT SUM(amount) total
FROM invoiceOutExpence
WHERE invoiceOutFk = vNewInvoiceId
) base
JOIN (
SELECT SUM(vat) total
FROM invoiceOutTax
WHERE invoiceOutFk = vNewInvoiceId
) vat
SET io.amount = base.total + vat.total
WHERE io.id = vNewInvoiceId;
DROP TEMPORARY TABLE tmp.updateInter;
SELECT COUNT(*), id
INTO vIsInterCompany, vInterCompanyFk
FROM company
WHERE clientFk = vClientFk;
IF (vIsInterCompany) THEN
INSERT INTO invoiceIn(supplierFk, supplierRef, issued, companyFk)
SELECT vCompanyFk, vNewRef, vInvoiceDate, vInterCompanyFk;
SET vNewInvoiceInFk = LAST_INSERT_ID();
DROP TEMPORARY TABLE IF EXISTS tmp.ticket;
CREATE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticketToInvoice;
CALL `ticket_getTax`('NATIONAL');
SET @vTaxableBaseServices := 0.00;
SET @vTaxCodeGeneral := NULL;
INSERT INTO invoiceInTax(invoiceInFk, taxableBase, expenceFk, taxTypeSageFk, transactionTypeSageFk)
SELECT vNewInvoiceInFk,
@vTaxableBaseServices,
sub.expenceFk,
sub.taxTypeSageFk,
sub.transactionTypeSageFk
FROM (
SELECT @vTaxableBaseServices := SUM(tst.taxableBase) taxableBase,
i.expenceFk,
i.taxTypeSageFk,
i.transactionTypeSageFk,
@vTaxCodeGeneral := i.taxClassCodeFk
FROM tmp.ticketServiceTax tst
JOIN invoiceOutTaxConfig i ON i.taxClassCodeFk = tst.code
WHERE i.isService
HAVING taxableBase
) sub;
INSERT INTO invoiceInTax(invoiceInFk, taxableBase, expenceFk, taxTypeSageFk, transactionTypeSageFk)
SELECT vNewInvoiceInFk,
SUM(tt.taxableBase) - IF(tt.code = @vTaxCodeGeneral,
@vTaxableBaseServices, 0) taxableBase,
i.expenceFk,
i.taxTypeSageFk ,
i.transactionTypeSageFk
FROM tmp.ticketTax tt
JOIN invoiceOutTaxConfig i ON i.taxClassCodeFk = tt.code
WHERE !i.isService
GROUP BY tt.pgcFk
HAVING taxableBase
ORDER BY tt.priority;
CALL invoiceInDueDay_calculate(vNewInvoiceInFk);
SELECT COUNT(*) INTO vIsCEESerial
FROM invoiceOutSerial
WHERE code = vSerial;
IF vIsCEESerial THEN
INSERT INTO invoiceInIntrastat (
invoiceInFk,
intrastatFk,
amount,
stems,
countryFk,
net)
SELECT
vNewInvoiceInFk,
i.intrastatFk,
SUM(CAST((s.quantity * s.price * (100 - s.discount) / 100 ) AS DECIMAL(10, 2))),
SUM(CAST(IFNULL(i.stems, 1) * s.quantity AS DECIMAL(10, 2))),
su.countryFk,
CAST(SUM(IFNULL(i.stems, 1)
* s.quantity
* IF(ic.grams, ic.grams, IFNULL(i.weightByPiece, 0)) / 1000) AS DECIMAL(10, 2))
FROM sale s
JOIN ticket t ON s.ticketFk = t.id
JOIN supplier su ON su.id = t.companyFk
JOIN item i ON i.id = s.itemFk
LEFT JOIN itemCost ic ON ic.itemFk = i.id AND ic.warehouseFk = t.warehouseFk
WHERE t.refFk = vNewRef
GROUP BY i.intrastatFk;
END IF;
DROP TEMPORARY TABLE tmp.ticket;
DROP TEMPORARY TABLE tmp.ticketAmount;
DROP TEMPORARY TABLE tmp.ticketTax;
DROP TEMPORARY TABLE tmp.ticketServiceTax;
END IF;
END IF;
DROP TEMPORARY TABLE `ticketToInvoice`;
END$$
DELIMITER ;

View File

@ -0,0 +1,141 @@
DROP PROCEDURE IF EXISTS `vn`.`ticketPackaging_add`;
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticketPackaging_add`(
vClientFk INT,
vDated DATE,
vCompanyFk INT,
vWithoutPeriodGrace BOOLEAN)
BEGIN
/**
* Genera nuevos tickets de embalajes para los clientes no han los han retornado
* y actualiza los valores para la tabla ticketPackaging
*
* @param vClientFk Cliente en caso de NULL todos los clientes
* @param vDated Fecha hasta la cual se revisan los embalajes
* @param vCompanyFk Empresa de la cual se comprobaran sus clientes
* @param vWithoutPeriodGrace si no se aplica el periodo de gracia de un mes
*/
DECLARE vNewTicket INT;
DECLARE vDateStart DATE;
DECLARE vDateEnd DATE;
DECLARE vGraceDate DATE DEFAULT vDated;
DECLARE vWarehouseInventory INT;
DECLARE vComponentCost INT;
DECLARE vDone INT DEFAULT FALSE;
DECLARE vClientId INT;
DECLARE vCursor CURSOR FOR
SELECT DISTINCT clientFk
FROM (
SELECT clientFk, SUM(quantity) totalQuantity
FROM tmp.packagingToInvoice
GROUP BY itemFk, clientFk
HAVING totalQuantity > 0)sub;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
SELECT id INTO vWarehouseInventory
FROM warehouse
WHERE `code`= 'inv';
SELECT id INTO vComponentCost
FROM component
WHERE `code`= 'purchaseValue';
SELECT packagingInvoicingDated INTO vDateStart
FROM ticketConfig;
IF vWarehouseInventory IS NULL THEN
CALL util.throw('Warehouse inventory not seted');
END IF;
IF vComponentCost IS NULL THEN
CALL util.throw('Component cost not seted');
END IF;
SET vDateEnd = vDated + INTERVAL 1 DAY;
IF NOT vWithoutPeriodGrace THEN
SET vGraceDate = vGraceDate -INTERVAL 1 MONTH;
END IF;
DROP TEMPORARY TABLE IF EXISTS tmp.packagingToInvoice;
CREATE TEMPORARY TABLE tmp.packagingToInvoice
(INDEX (clientFk))
ENGINE = MEMORY
SELECT p.itemFk,
tp.packagingFk,
tp.quantity,
tp.ticketFk,
p.price,
t.clientFk
FROM ticketPackaging tp
JOIN packaging p ON p.id = tp.packagingFk
JOIN ticket t ON t.id = tp.ticketFk
JOIN client c ON c.id = t.clientFk
WHERE c.isActive
AND (vClientFk IS NULL OR t.clientFk = vClientFk)
AND t.shipped BETWEEN vDateStart AND vDateEnd
AND (tp.quantity < 0 OR (tp.quantity > 0 AND t.shipped < vGraceDate))
AND tp.quantity
AND p.itemFk;
OPEN vCursor;
l: LOOP
FETCH vCursor INTO vClientId;
IF vDone THEN
LEAVE l;
END IF;
START TRANSACTION;
CALL ticket_add(
vClientId,
vDateEnd,
vWarehouseInventory,
vCompanyFk,
NULL,
NULL,
NULL,
vDateEnd,
account.myUser_getId(),
TRUE,
vNewTicket);
INSERT INTO ticketPackaging(ticketFk, packagingFk, quantity, pvp)
SELECT vNewTicket, packagingFk, - SUM(quantity) totalQuantity, price
FROM tmp.packagingToInvoice
WHERE clientFk = vClientId
GROUP BY packagingFk
HAVING IF(vWithoutPeriodGrace, totalQuantity <> 0, totalQuantity < 0);
INSERT INTO sale(ticketFk, itemFk, concept, quantity, price)
SELECT vNewTicket, pti.itemFk, i.name, SUM(pti.quantity) totalQuantity, pti.price
FROM tmp.packagingToInvoice pti
JOIN item i ON i.id = pti.itemFk
WHERE pti.clientFk = vClientId
GROUP BY pti.itemFk
HAVING IF(vWithoutPeriodGrace, totalQuantity <> 0, totalQuantity > 0);
INSERT INTO saleComponent(saleFk, componentFk, value)
SELECT id, vComponentCost, price
FROM sale
WHERE ticketFk = vNewTicket;
COMMIT;
END LOOP;
CLOSE vCursor;
DROP TEMPORARY TABLE tmp.packagingToInvoice;
END$$
DELIMITER ;

View File

@ -572,14 +572,13 @@ INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`
('NATIONAL', 0, 1),
('WORLD', 2, 15);
INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaFk`, `isCEE`)
INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaFk`, `isCEE`, `type`)
VALUES
('A', 'Global nacional', 1, 'NATIONAL', 0),
('T', 'Española rapida', 1, 'NATIONAL', 0),
('V', 'Intracomunitaria global', 0, 'CEE', 1),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0),
('E', 'Exportación rápida', 0, 'WORLD', 0);
;
('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'),
('E', 'Exportación rápida', 0, 'WORLD', 0, 'quick');
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES

View File

@ -260,10 +260,11 @@
"Aplicación bloqueada por el usuario 9": "Aplicación bloqueada por el usuario 9",
"Failed to upload file": "Error al subir archivo",
"The DOCUWARE PDF document does not exists": "El documento PDF Docuware no existe",
"It is not possible to modify tracked sales": "No es posible modificar líneas de pedido que se hayan empezado a preparar",
"It is not possible to modify sales that their articles are from Floramondo": "No es posible modificar líneas de pedido cuyos artículos sean de Floramondo",
"It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas",
"It is not possible to modify tracked sales": "No es posible modificar líneas de pedido que se hayan empezado a preparar",
"It is not possible to modify sales that their articles are from Floramondo": "No es posible modificar líneas de pedido cuyos artículos sean de Floramondo",
"It is not possible to modify cloned sales": "No es posible modificar líneas de pedido clonadas",
"A supplier with the same name already exists. Change the country.": "Un proveedor con el mismo nombre ya existe. Cambie el país.",
"There is no assigned email for this client": "No hay correo asignado para este cliente"
}
"There is no assigned email for this client": "No hay correo asignado para este cliente",
"Exists an invoice with a previous date": "Existe una factura con fecha anterior",
"Invoice date can't be less than max date": "La fecha de factura no puede ser inferior a la fecha límite"
}

View File

@ -4,47 +4,37 @@ module.exports = Self => {
accessType: 'WRITE',
accepts: [
{
arg: 'clientId',
type: 'number',
description: 'The client id'
}, {
arg: 'invoiceDate',
type: 'date',
description: 'The invoice date'
},
{
description: 'The invoice date',
required: true
}, {
arg: 'maxShipped',
type: 'date',
description: 'The maximum shipped date'
},
{
arg: 'fromClientId',
type: 'number',
description: 'The minimum client id'
},
{
arg: 'toClientId',
type: 'number',
description: 'The maximum client id'
},
{
description: 'The maximum shipped date',
required: true
}, {
arg: 'companyFk',
type: 'number',
description: 'The company id to invoice'
}
description: 'The company id to invoice',
required: true
},
],
returns: [{
arg: 'clientsAndAddresses',
type: ['object']
returns: {
type: 'Object',
root: true
},
{
arg: 'invoice',
type: 'object'
}],
http: {
path: '/clientsToInvoice',
verb: 'POST'
}
});
Self.clientsToInvoice = async(ctx, options) => {
const args = ctx.args;
Self.clientsToInvoice = async(ctx, clientId, invoiceDate, maxShipped, companyFk, options) => {
let tx;
const myOptions = {};
@ -56,121 +46,52 @@ module.exports = Self => {
myOptions.transaction = tx;
}
let query;
try {
query = `
SELECT MAX(issued) issued
FROM vn.invoiceOut io
JOIN vn.time t ON t.dated = io.issued
WHERE io.serial = 'A'
AND t.year = YEAR(?)
AND io.companyFk = ?`;
const [maxIssued] = await Self.rawSql(query, [
args.invoiceDate,
args.companyFk
], myOptions);
const maxSerialDate = maxIssued.issued || args.invoiceDate;
if (args.invoiceDate < maxSerialDate)
args.invoiceDate = maxSerialDate;
if (args.invoiceDate < args.maxShipped)
args.maxShipped = args.invoiceDate;
const minShipped = Date.vnNew();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
// Packaging liquidation
const vIsAllInvoiceable = false;
const clientsWithPackaging = await getClientsWithPackaging(ctx, myOptions);
for (let client of clientsWithPackaging) {
await Self.rawSql('CALL packageInvoicing(?, ?, ?, ?, @newTicket)', [
client.id,
args.invoiceDate,
args.companyFk,
vIsAllInvoiceable
], myOptions);
}
await Self.rawSql('CALL ticketPackaging_add(?, ?, ?, ?)', [
clientId,
invoiceDate,
companyFk,
vIsAllInvoiceable
], myOptions);
const invoiceableClients = await getInvoiceableClients(ctx, minShipped, myOptions);
const minShipped = Date.vnNew();
minShipped.setFullYear(maxShipped.getFullYear() - 1);
if (!invoiceableClients) return;
const query = `
SELECT c.id clientId,
c.name clientName,
a.id,
a.nickname
FROM ticket t
JOIN address a ON a.id = t.addressFk
JOIN client c ON c.id = t.clientFk
WHERE t.refFk IS NULL
AND t.shipped BETWEEN ? AND util.dayEnd(?)
AND (t.clientFk = ? OR ? IS NULL )
AND t.companyFk = ?
AND c.hasToInvoice
AND c.isTaxDataChecked
AND c.isActive
AND NOT t.isDeleted
GROUP BY c.id, IF(c.hasToInvoiceByAddress, a.id, TRUE)
HAVING SUM(t.totalWithVat) > 0;`;
const clientsAndAddresses = invoiceableClients.map(invoiceableClient => {
return {
clientId: invoiceableClient.id,
addressId: invoiceableClient.addressFk
};
}
);
const addresses = await Self.rawSql(query, [
minShipped,
maxShipped,
clientId,
clientId,
companyFk
], myOptions);
if (tx) await tx.commit();
return [
clientsAndAddresses,
{
invoiceDate: args.invoiceDate,
maxShipped: args.maxShipped,
fromClientId: args.fromClientId,
toClientId: args.toClientId,
companyFk: args.companyFk,
minShipped: minShipped
}
];
return addresses;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
async function getClientsWithPackaging(ctx, options) {
const models = Self.app.models;
const args = ctx.args;
const query = `SELECT DISTINCT clientFk AS id
FROM ticket t
JOIN ticketPackaging tp ON t.id = tp.ticketFk
JOIN client c ON c.id = t.clientFk
WHERE t.shipped BETWEEN '2017-11-21' AND ?
AND t.clientFk >= ?
AND (t.clientFk <= ? OR ? IS NULL)
AND c.isActive`;
return models.InvoiceOut.rawSql(query, [
args.maxShipped,
args.fromClientId,
args.toClientId,
args.toClientId
], options);
}
async function getInvoiceableClients(ctx, minShipped, options) {
const models = Self.app.models;
const args = ctx.args;
minShipped.setFullYear(minShipped.getFullYear() - 1);
const query = `SELECT c.id,
c.hasToInvoiceByAddress,
a.id addressFk,
sum(t.totalWithVat) totalAmount
FROM ticket t
JOIN address a ON a.id = t.addressFk
JOIN client c ON c.id = t.clientFk
WHERE t.refFk IS NULL
AND t.shipped BETWEEN ? AND util.dayEnd(?)
AND t.companyFk = ?
AND c.hasToInvoice
AND c.isTaxDataChecked
AND c.isActive
AND NOT t.isDeleted
GROUP BY c.id, IF(c.hasToInvoiceByAddress, a.id, TRUE)
HAVING totalAmount > 0;`;
return models.InvoiceOut.rawSql(query, [
minShipped,
args.maxShipped,
args.companyFk
], options);
}
};

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethod('getInvoiceDate', {
description: 'Returns default Invoice Date',
accessType: 'READ',
accepts: [
{
arg: 'year',
type: 'number',
required: true
}, {
arg: 'companyFk',
type: 'number',
required: true
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/getInvoiceDate`,
verb: 'GET'
}
});
Self.getInvoiceDate = async(year, companyFk) => {
const models = Self.app.models;
const [invoiceDate] = await models.InvoiceOut.rawSql(`
SELECT MAX(io.issued) issued
FROM invoiceOut io
JOIN invoiceOutSerial ios ON ios.code = io.serial
WHERE ios.type = 'global'
AND io.issued BETWEEN MAKEDATE(?, 1) AND
util.lastDayOfYear(MAKEDATE(?, 1))
AND io.companyFk = ?`,
[year, year, companyFk]);
return invoiceDate;
};
};

View File

@ -4,48 +4,39 @@ module.exports = Self => {
Self.remoteMethodCtx('invoiceClient', {
description: 'Make a invoice of a client',
accessType: 'WRITE',
accepts: [{
arg: 'clientId',
type: 'number',
description: 'The client id to invoice',
required: true
},
{
arg: 'addressId',
type: 'number',
description: 'The address id to invoice',
required: true
},
{
arg: 'invoiceDate',
type: 'date',
description: 'The invoice date',
required: true
},
{
arg: 'maxShipped',
type: 'date',
description: 'The maximum shipped date',
required: true
},
{
arg: 'companyFk',
type: 'number',
description: 'The company id to invoice',
required: true
},
{
arg: 'minShipped',
type: 'date',
description: 'The minium shupped date',
required: true
},
{
arg: 'printerFk',
type: 'number',
description: 'The printer to print',
required: true
}],
accepts: [
{
arg: 'clientId',
type: 'number',
description: 'The client id to invoice',
required: true
}, {
arg: 'addressId',
type: 'number',
description: 'The address id to invoice',
required: true
}, {
arg: 'invoiceDate',
type: 'date',
description: 'The invoice date',
required: true
}, {
arg: 'maxShipped',
type: 'date',
description: 'The maximum shipped date',
required: true
}, {
arg: 'companyFk',
type: 'number',
description: 'The company id to invoice',
required: true
}, {
arg: 'printerFk',
type: 'number',
description: 'The printer to print',
required: true
}
],
returns: {
type: 'object',
root: true
@ -70,6 +61,9 @@ module.exports = Self => {
myOptions.transaction = tx;
}
const minShipped = Date.vnNew();
minShipped.setFullYear(args.maxShipped.getFullYear() - 1);
let invoiceId;
let invoiceOut;
try {
@ -79,7 +73,7 @@ module.exports = Self => {
if (client.hasToInvoiceByAddress) {
await Self.rawSql('CALL ticketToInvoiceByAddress(?, ?, ?, ?)', [
args.minShipped,
minShipped,
args.maxShipped,
args.addressId,
args.companyFk
@ -133,26 +127,26 @@ module.exports = Self => {
throw e;
}
if (invoiceId && !invoiceOut.client().isToBeMailed) {
const query = `CALL vn.report_print(
'invoice',
?,
account.myUser_getId(),
JSON_OBJECT('refFk', ?),
'normal'
);`;
await models.InvoiceOut.rawSql(query, [args.printerFk, invoiceOut.ref]);
if (invoiceId) {
if (!invoiceOut.client().isToBeMailed) {
const query = `
CALL vn.report_print(
'invoice',
?,
account.myUser_getId(),
JSON_OBJECT('refFk', ?),
'normal'
);`;
await models.InvoiceOut.rawSql(query, [args.printerFk, invoiceOut.ref]);
} else {
ctx.args = {
reference: invoiceOut.ref,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.client().email
};
await models.InvoiceOut.invoiceEmail(ctx, invoiceOut.ref);
}
}
if (invoiceId && invoiceOut.client().isToBeMailed) {
ctx.args = {
reference: invoiceOut.ref,
recipientId: invoiceOut.clientFk,
recipient: invoiceOut.client().email
};
await models.InvoiceOut.invoiceEmail(ctx, invoiceOut.ref);
}
return invoiceId;
};
@ -160,13 +154,12 @@ module.exports = Self => {
const models = Self.app.models;
const query = 'SELECT hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, null, options);
return result && result.base;
}
async function getIsSpanishCompany(companyId, options) {
const models = Self.app.models;
const query = `SELECT COUNT(*) AS total
const query = `SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
@ -175,6 +168,6 @@ module.exports = Self => {
companyId
], options);
return supplierCompany && supplierCompany.total;
return supplierCompany && supplierCompany.isSpanishCompany;
}
};

View File

@ -16,4 +16,5 @@ module.exports = Self => {
require('../methods/invoiceOut/invoiceCsv')(Self);
require('../methods/invoiceOut/invoiceCsvEmail')(Self);
require('../methods/invoiceOut/invoiceOutPdf')(Self);
require('../methods/invoiceOut/getInvoiceDate')(Self);
};

View File

@ -1,59 +1,66 @@
<vn-card class="vn-w-lg vn-ma-md">
<h5 ng-if="$ctrl.packageInvoicing" translate text-center class="vn-pa-md">{{'Calculating packages to invoice...'}}</h5>
<vn-vertical ng-if="$ctrl.clients.length" text-center class="vn-pa-md">
<h5 translate ng-if="$ctrl.percentage != 100">Invoicing</h5>
<h5 translate ng-if="$ctrl.percentage == 100">Ended process</h5>
<div ng-if="$ctrl.percentage != 100">
{{'Current client id' | translate}}: {{$ctrl.currentClientId}}
</div>
<h6>
{{($ctrl.percentage / 100) | percentage: 0}} ({{$ctrl.currentClient}} {{'of' | translate}} {{$ctrl.clients.length}})
</h6>
</vn-vertical>
<vn-card
ng-if="$ctrl.status"
class="vn-w-lg vn-pa-md"
style="height: 80px; display: flex; align-items: center; justify-content: center; gap: 20px;">
<vn-spinner
enable="$ctrl.status != 'done'">
</vn-spinner>
<div>
<div ng-switch="$ctrl.status">
<span ng-switch-when="packageInvoicing" translate>
Build packaging tickets
</span>
<span ng-switch-when="invoicing">
{{'Invoicing client' | translate}} {{$ctrl.currentAddress.clientId}}
</span>
<span ng-switch-when="stopping" translate>
Stopping process
</span>
<span ng-switch-when="done" translate>
Ended process
</span>
</div>
<div ng-if="$ctrl.nAddresses" class="text-caption text-secondary">
{{$ctrl.percentage | percentage: 0}} ({{$ctrl.addressNumber}} {{'of' | translate}} {{$ctrl.nAddresses}})
</div>
</div>
</vn-card>
<vn-card class="vn-w-lg">
<vn-table ng-if="data.length">
<vn-card class="vn-w-lg vn-mt-md" ng-if="$ctrl.errors.length">
<vn-table>
<vn-thead>
<vn-tr>
<vn-th field="clientId" number>Client id</vn-th>
<vn-th field="nickname">Nickname</vn-th>
<vn-th field="addressId" number>Address id</vn-th>
<vn-th field="street" expand>Street</vn-th>
<vn-th field="error" expand>Error</vn-th>
<vn-th number>Id</vn-th>
<vn-th>Client</vn-th>
<vn-th number>Address id</vn-th>
<vn-th>Street</vn-th>
<vn-th>Error</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr
ng-repeat="client in data">
<vn-tr ng-repeat="error in $ctrl.errors">
<vn-td number>
<span
vn-click-stop="clientDescriptor.show($event, client.id)"
vn-click-stop="clientDescriptor.show($event, error.address.clientId)"
class="link">
{{::client.id}}
{{::error.address.clientId}}
</span>
</vn-td>
<vn-td expand>
{{::client.address.nickname}}
{{::error.address.clientName}}
</vn-td>
<vn-td number>
{{::client.address.id}}
{{::error.address.id}}
</vn-td>
<vn-td expand>
{{::client.address.street}}
{{::error.address.nickname}}
</vn-td>
<vn-td expand id="error">
<vn-spinner
ng-if="client.status == 'waiting'"
enable="true">
</vn-spinner>
{{::client.error}}
<vn-td expand>
<span class="chip alert">{{::error.message}}</span>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
<vn-side-menu side="right">
<vn-crud-model
auto-load="true"
@ -69,22 +76,46 @@
</vn-crud-model>
<form class="vn-pa-md">
<vn-vertical>
<vn-vertical class="vn-mb-sm">
<vn-radio
label="All clients"
val="all"
ng-model="$ctrl.clientsToInvoice">
</vn-radio>
<vn-radio
label="One client"
val="one"
ng-model="$ctrl.clientsToInvoice">
</vn-radio>
</vn-vertical>
<vn-autocomplete
url="Clients"
label="Client"
search-function="{or: [{id: $search}, {name: {like: '%'+$search+'%'}}]}"
order="id"
show-field="name"
value-field="id"
ng-model="$ctrl.clientId"
required="true"
ng-if="$ctrl.clientsToInvoice == 'one'">
<tpl-item>{{::id}} - {{::name}}</tpl-item>
</vn-autocomplete>
<vn-date-picker
vn-one
label="Invoice date"
ng-model="$ctrl.invoice.invoiceDate">
ng-model="$ctrl.invoiceDate">
</vn-date-picker>
<vn-date-picker
vn-one
label="Max date"
ng-model="$ctrl.invoice.maxShipped">
ng-model="$ctrl.maxShipped">
</vn-date-picker>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.invoice.companyFk">
ng-model="$ctrl.companyFk">
</vn-autocomplete>
<vn-autocomplete
url="Printers"
@ -92,9 +123,18 @@
show-field="name"
value-field="id"
where="{isLabeler: false}"
ng-model="$ctrl.invoice.printerFk">
ng-model="$ctrl.printerFk">
</vn-autocomplete>
<vn-submit vn-id="invoiceButton" ng-click="$ctrl.makeInvoice()" label="Invoice out"></vn-submit>
<vn-submit
ng-if="!$ctrl.invoicing"
ng-click="$ctrl.makeInvoice()"
label="Invoice out">
</vn-submit>
<vn-submit
ng-if="$ctrl.invoicing"
label="Stop"
ng-click="$ctrl.stopInvoicing()">
</vn-submit>
</vn-vertical>
</form>
</vn-side-menu>

View File

@ -4,128 +4,131 @@ import UserError from 'core/lib/user-error';
import './style.scss';
class Controller extends Section {
constructor($element, $, $transclude) {
super($element, $, $transclude);
this.invoice = {
maxShipped: Date.vnNew(),
companyFk: this.vnConfig.companyFk
};
}
$onInit() {
this.getMinClientId();
this.getMaxClientId();
}
const date = Date.vnNew();
Object.assign(this, {
maxShipped: new Date(date.getFullYear(), date.getMonth(), 0),
clientsToInvoice: 'all',
});
getMinClientId() {
this.getClientId('min')
.then(res => this.invoice.fromClientId = res.data.id);
}
getMaxClientId() {
this.getClientId('max')
.then(res => this.invoice.toClientId = res.data.id);
}
getClientId(func) {
const order = func == 'min' ? 'ASC' : 'DESC';
const params = {
filter: {
order: 'id ' + order,
limit: 1
}
};
return this.$http.get('Clients/findOne', {params});
}
getPercentage() {
this.percentage = ((this.currentClient - 1) * 100) / this.clients.length;
}
restartValues() {
this.$.invoiceButton.disabled = false;
this.packageInvoicing = false;
}
invoiceOut(invoice, clientsAndAddresses) {
const [clientAndAddress] = clientsAndAddresses;
if (!clientAndAddress) {
this.percentage = 100;
return;
}
this.currentClientId = clientAndAddress.clientId;
this.currentClient = ++this.currentClient;
this.getPercentage();
const params = {
clientId: clientAndAddress.clientId,
addressId: clientAndAddress.addressId,
invoiceDate: invoice.invoiceDate,
maxShipped: invoice.maxShipped,
companyFk: invoice.companyFk,
minShipped: invoice.minShipped,
printerFk: this.invoice.printerFk,
};
this.$http.get(`Addresses/${clientAndAddress.addressId}`)
this.$http.get('UserConfigs/getUserConfig')
.then(res => {
this.address = res.data;
return this.$http.post(`InvoiceOuts/invoiceClient`, params)
.catch(res => {
this.$.data.unshift({
id: clientAndAddress.clientId,
address: this.address,
status: 'error',
error: res.data.error.message
});
}).finally(() => {
clientsAndAddresses.shift();
return this.invoiceOut(invoice, clientsAndAddresses);
});
this.companyFk = res.data.companyFk;
const params = {
year: this.maxShipped.getFullYear(),
companyFk: this.companyFk
};
return this.$http.get('InvoiceOuts/getInvoiceDate', {params});
})
.then(res => {
this.minInvoicingDate = new Date(res.data.issued);
this.invoiceDate = this.minInvoicingDate;
});
}
stopInvoicing() {
this.status = 'stopping';
}
makeInvoice() {
this.invoicing = true;
this.status = 'packageInvoicing';
this.errors = [];
this.addresses = null;
try {
if (!this.invoice.invoiceDate || !this.invoice.maxShipped)
throw new Error('Invoice date and the max date should be filled');
if (this.clientsToInvoice == 'one' && !this.clientId)
throw new UserError('Choose a valid client');
if (!this.invoiceDate || !this.maxShipped)
throw new UserError('Invoice date and the max date should be filled');
if (this.invoiceDate < this.maxShipped)
throw new UserError('Invoice date can\'t be less than max date');
if (this.invoiceDate.getTime() < this.minInvoicingDate.getTime())
throw new UserError('Exists an invoice with a previous date');
if (!this.companyFk)
throw new UserError('Choose a valid company');
if (!this.printerFk)
throw new UserError('Choose a valid printer');
if (!this.invoice.companyFk)
throw new Error('Choose a valid company');
if (this.clientsToInvoice == 'all')
this.clientId = undefined;
if (!this.invoice.printerFk)
throw new Error('Choose a valid printer');
this.$.invoiceButton.disabled = true;
this.$.data = [];
this.packageInvoicing = true;
this.$http.post(`InvoiceOuts/clientsToInvoice`, this.invoice)
const params = {
invoiceDate: this.invoiceDate,
maxShipped: this.maxShipped,
clientId: this.clientId,
companyFk: this.companyFk
};
this.$http.post(`InvoiceOuts/clientsToInvoice`, params)
.then(res => {
this.packageInvoicing = false;
const invoice = res.data.invoice;
this.currentClient = 0;
console.log(res.data);
this.addresses = res.data;
console.log(this.address);
if (!this.addresses.length)
throw new UserError(`There aren't tickets to invoice`);
const clientsAndAddresses = res.data.clientsAndAddresses;
if (!clientsAndAddresses.length) throw new UserError(`There aren't clients to invoice`);
this.clients = [];
for (const clientAndAddress of clientsAndAddresses)
this.clients.push(clientAndAddress.clientId);
return this.invoiceOut(invoice, clientsAndAddresses);
this.addressIndex = 0;
return this.invoiceOut();
})
.finally(() => this.restartValues());
} catch (e) {
this.vnApp.showError(this.$t(e.message));
this.restartValues();
return false;
.catch(err => this.handleError(err));
} catch (err) {
this.handleError(err);
}
}
}
Controller.$inject = ['$element', '$scope', '$transclude'];
handleError(err) {
this.invoicing = false;
this.status = null;
throw err;
}
invoiceOut() {
if (this.addressIndex == this.addresses.length || this.status == 'stopping') {
this.invoicing = false;
this.status = 'done';
return;
}
this.status = 'invoicing';
const address = this.addresses[this.addressIndex];
this.currentAddress = address;
const params = {
clientId: address.clientId,
addressId: address.id,
invoiceDate: this.invoiceDate,
maxShipped: this.maxShipped,
companyFk: this.companyFk,
printerFk: this.printerFk,
};
this.$http.post(`InvoiceOuts/invoiceClient`, params)
.catch(res => {
this.errors.unshift({
address,
message: res.data.error.message
});
})
.finally(() => {
this.addressIndex++;
this.invoiceOut();
});
}
get nAddresses() {
if (!this.addresses) return 0;
return this.addresses.length;
}
get addressNumber() {
return Math.min(this.addressIndex + 1, this.nAddresses);
}
get percentage() {
const len = this.nAddresses;
return Math.min(this.addressIndex, len) / len;
}
}
ngModule.vnComponent('vnInvoiceOutGlobalInvoicing', {
template: require('./index.html'),

View File

@ -1,5 +1,5 @@
import './index';
const UserError = require('vn-loopback/util/user-error');
describe('InvoiceOut', () => {
describe('Component vnInvoiceOutGlobalInvoicing', () => {
let controller;
@ -22,99 +22,60 @@ describe('InvoiceOut', () => {
controller.$.invoiceButton = {disabled: false};
}));
describe('getMinClientId()', () => {
it('should set the invoice fromClientId property', () => {
const filter = {
order: 'id ASC',
limit: 1
};
const serializedParams = $httpParamSerializer({filter});
$httpBackend.expectGET(`Clients/findOne?${serializedParams}`).respond(200, {id: 1101});
controller.getMinClientId();
$httpBackend.flush();
expect(controller.invoice.fromClientId).toEqual(1101);
});
});
describe('getMaxClientId()', () => {
it('should set the invoice toClientId property', () => {
const filter = {
order: 'id DESC',
limit: 1
};
const serializedParams = $httpParamSerializer({filter});
$httpBackend.expectGET(`Clients/findOne?${serializedParams}`).respond(200, {id: 1112});
controller.getMaxClientId();
$httpBackend.flush();
expect(controller.invoice.toClientId).toEqual(1112);
});
});
describe('makeInvoice()', () => {
it('should throw an error when invoiceDate or maxShipped properties are not filled in', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.clientsToInvoice = 'all';
controller.invoice = {
fromClientId: 1101,
toClientId: 1101
};
controller.makeInvoice();
let error;
try {
controller.makeInvoice();
} catch (e) {
error = e.message;
}
const expectedError = 'Invoice date and the max date should be filled';
expect(controller.vnApp.showError).toHaveBeenCalledWith(expectedError);
expect(error).toBe(expectedError);
});
it('should throw an error when fromClientId or toClientId properties are not filled in', () => {
it('should throw an error when select one client and clientId is not filled in', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.clientsToInvoice = 'one';
controller.invoice = {
invoiceDate: Date.vnNew(),
maxShipped: Date.vnNew()
};
let error;
try {
controller.makeInvoice();
} catch (e) {
error = e.message;
}
controller.makeInvoice();
const expectedError = 'Choose a valid client';
expect(controller.vnApp.showError).toHaveBeenCalledWith(`Choose a valid clients range`);
expect(error).toBe(expectedError);
});
it('should make an http POST query and then call to the showSuccess() method', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const minShipped = Date.vnNew();
minShipped.setFullYear(minShipped.getFullYear() - 1);
minShipped.setMonth(1);
minShipped.setDate(1);
minShipped.setHours(0, 0, 0, 0);
controller.invoice = {
invoiceDate: Date.vnNew(),
maxShipped: Date.vnNew(),
fromClientId: 1101,
toClientId: 1101,
companyFk: 442,
minShipped: minShipped
};
const response = {
clientsAndAddresses: [{clientId: 1101, addressId: 121}],
invoice: controller.invoice
};
const address = {id: 121};
const date = Date.vnNew();
date.setDate(date.getDate() + 1);
controller.invoiceDate = date;
controller.maxShipped = date;
controller.minInvoicingDate = Date.vnNew();
controller.clientsToInvoice = 'one';
controller.clientId = 1101;
controller.companyFk = 442;
controller.printerFk = 1;
const response = [{
clientId: 1101,
id: 121
}];
$httpBackend.expect('POST', `InvoiceOuts/clientsToInvoice`).respond(response);
$httpBackend.expect('GET', `Addresses/${response.clientsAndAddresses[0].addressId}`).respond(address);
$httpBackend.expect('POST', `InvoiceOuts/invoiceClient`).respond({id: 1});
$httpBackend.expect('POST', `InvoiceOuts/invoiceClient`).respond(1);
controller.makeInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.status).toEqual('done');
});
});
});

View File

@ -1,21 +1,20 @@
There aren't clients to invoice: No existen clientes para facturar
There aren't tickets to invoice: No existen tickets para facturar
Max date: Fecha límite
Invoice date: Fecha de factura
Invoice date can't be less than max date: La fecha de factura no puede ser inferior a la fecha límite
Invoice date and the max date should be filled: La fecha de factura y la fecha límite deben rellenarse
Choose a valid clients range: Selecciona un rango válido de clientes
Choose a valid company: Selecciona un empresa válida
Choose a valid printer: Selecciona una impresora válida
Clients range: Rango de clientes
All clients: Todos los clientes
Calculating packages to invoice...: Calculando paquetes a facturar...
Clean: Limpiar
From client: Desde cliente
To client: Hasta cliente
Build packaging tickets: Generando tickets de embalajes
Address id: Id dirección
Printer: Impresora
of: de
Client: Cliente
Current client id: Id cliente actual
Invoicing: Facturando
Invoicing client: Facturando cliente
Ended process: Proceso finalizado
Invoice out: Facturar
One client: Un solo cliente
Choose a valid client: Selecciona un cliente válido
Stop: Parar

View File

@ -1,7 +1,9 @@
@import "variables";
.chip {
display: inline-block;
min-width: 15px;
min-height: 25px;
vn-sale-tracking {
.chip {
display: inline-block;
min-width: 15px;
min-height: 25px;
}
}