3430-ticket_step-two ticket without negatives #823

Merged
joan merged 31 commits from 3430-ticket_step-two into dev 2022-02-01 08:34:41 +00:00
134 changed files with 3819 additions and 824 deletions
Showing only changes of commit d0c4099255 - Show all commits

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES('InvoiceInDueDay', '*', '*', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1,248 @@
DROP PROCEDURE IF EXISTS vn.invoiceInBookingMain;
DELIMITER $$
$$
CREATE DEFINER=`root`@`%` PROCEDURE `vn`.`invoiceInBookingMain`(vInvoiceInId INT)
BEGIN
DECLARE vTotalAmount,vTotalAmountDivisa DECIMAL(10,2);
DECLARE vBookNumber,vSerialNumber INT;
DECLARE vRate DECIMAL(10,4);
CALL invoiceInBookingCommon(vInvoiceInId,vSerialNumber);
SELECT SUM(iit.taxableBase * IF( i.serial= 'R' AND ti.Iva <> 'HP DEVENGADO 21 ISP', 1 +(ti.PorcentajeIva/100),1)),
SUM(iit.foreignValue * IF( i.serial= 'R', 1 + (ti.PorcentajeIva/100),1)),
iit.taxableBase/iit.foreignValue
INTO vTotalAmount, vTotalAmountDivisa, vRate
FROM newInvoiceIn i
JOIN invoiceInTax iit ON iit.invoiceInFk = i.id
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk;
CALL vn.ledger_next(vBookNumber);
-- Apunte del proveedor
INSERT INTO XDiario(ASIEN,
FECHA,
SUBCTA,
EUROHABER,
CONCEPTO,
CAMBIO,
HABERME,
NFACTICK,
CLAVE,
empresa_id
)
SELECT
vBookNumber,
n.bookEntried,
s.supplierAccount,
vTotalAmount EUROHABER,
n.conceptWithSupplier,
vRate,
vTotalAmountDivisa,
n.invoicesCount,
vInvoiceInId,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s;
-- Línea de Gastos
INSERT INTO XDiario ( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EURODEBE,
EUROHABER,
CONCEPTO,
CAMBIO,
DEBEME,
HABERME,
NFACTICK,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(e.isWithheld , LPAD(RIGHT(s.supplierAccount,5),10,iit.expenceFk),iit.expenceFk) SUBCTA,
s.supplierAccount CONTRA,
IF(e.isWithheld AND iit.taxableBase < 0, NULL, ROUND(SUM(iit.taxableBase),2)) EURODEBE,
IF(e.isWithheld AND iit.taxableBase < 0,ROUND(SUM(-iit.taxableBase),2),NULL) EUROHABER,
n.conceptWithSupplier CONCEPTO,
vRate,
IF(e.isWithheld,NULL,ABS(ROUND(SUM(iit.foreignValue),2))) DEBEME,
IF(e.isWithheld,ABS(ROUND(SUM(iit.foreignValue),2)),NULL) HABERME,
n.invoicesCount NFACTICK,
n.companyFk empresa_id
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax iit ON iit.invoiceInFk = n.id
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = iit.expenceFk
WHERE e.name != 'Suplidos Transitarios nacionales'
GROUP BY iit.expenceFk;
-- Líneas de IVA
INSERT INTO XDiario( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EURODEBE,
BASEEURO,
CONCEPTO,
FACTURA,
IVA,
AUXILIAR,
SERIE,
TIPOOPE,
FECHA_EX,
FECHA_OP,
NFACTICK,
FACTURAEX,
L340,
LRECT349,
TIPOCLAVE,
TIPOEXENCI,
TIPONOSUJE,
TIPOFACT,
TIPORECTIF,
TERIDNIF,
TERNIF,
TERNOM,
FECREGCON,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
IF(n.expenceFkDeductible>0, n.expenceFkDeductible, ti.CuentaIvaSoportado) SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100 /* + 0.0001*/ , 2)) EURODEBE,
SUM(it.taxableBase) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
IF(isUeeMember AND eWithheld.id IS NULL,'','*') AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
TRUE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
n.cplusTrascendency472Fk TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.booked FECREGCON,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
LEFT JOIN (
SELECT eWithheld.id
FROM invoiceInTax hold
JOIN expence eWithheld ON eWithheld.id = hold.expenceFk AND eWithheld.isWithheld
WHERE hold.invoiceInFk = vInvoiceInId LIMIT 1
) eWithheld ON TRUE
WHERE it.taxTypeSageFk IS NOT NULL
AND it.taxTypeSageFk NOT IN (22, 90)
GROUP BY ti.PorcentajeIva, e.id;
-- Línea iva inversor sujeto pasivo
INSERT INTO XDiario( ASIEN,
FECHA,
SUBCTA,
CONTRA,
EUROHABER,
BASEEURO,
CONCEPTO,
FACTURA,
IVA,
AUXILIAR,
SERIE,
TIPOOPE,
FECHA_EX,
FECHA_OP,
NFACTICK,
FACTURAEX,
L340,
LRECT349,
TIPOCLAVE,
TIPOEXENCI,
TIPONOSUJE,
TIPOFACT,
TIPORECTIF,
TERIDNIF,
TERNIF,
TERNOM,
empresa_id
)
SELECT vBookNumber ASIEN,
n.bookEntried FECHA,
ti.CuentaIvaRepercutido SUBCTA,
s.supplierAccount CONTRA,
SUM(ROUND(ti.PorcentajeIva * it.taxableBase / 100,2)) EUROHABER,
ROUND(SUM(it.taxableBase),2) BASEEURO,
GROUP_CONCAT(DISTINCT e.`name` SEPARATOR ', ') CONCEPTO,
vSerialNumber FACTURA,
ti.PorcentajeIva IVA,
'*' AUXILIAR,
n.serial SERIE,
ttr.ClaveOperacionDefecto,
n.issued FECHA_EX,
n.operated FECHA_OP,
n.invoicesCount NFACTICK,
n.supplierRef FACTURAEX,
FALSE L340,
(isSameCountry OR NOT isUeeMember) LRECT349,
1 TIPOCLAVE,
n.cplusTaxBreakFk TIPOEXENCI,
n.cplusSubjectOpFk TIPONOSUJE,
n.cplusInvoiceType472Fk TIPOFACT,
n.cplusRectificationTypeFk TIPORECTIF,
iis.cplusTerIdNifFk TERIDNIF,
s.nif AS TERNIF,
s.name AS TERNOM,
n.companyFk
FROM newInvoiceIn n
JOIN newSupplier s
JOIN invoiceInTax it ON n.id = it.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = it.taxTypeSageFk
JOIN sage.TiposTransacciones ttr ON ttr.CodigoTransaccion = it.transactionTypeSageFk
JOIN invoiceInSerial iis ON iis.code = n.serial
JOIN (SELECT * FROM expence e GROUP BY e.id)e ON e.id = it.expenceFk
WHERE ti.Iva = 'HP DEVENGADO 21 ISP' OR MID(s.account, 4, 1) = '1'
GROUP BY ti.PorcentajeIva, e.id;
-- Actualización del registro original
UPDATE invoiceIn ii
JOIN newInvoiceIn ni ON ii.id = ni.id
SET ii.serialNumber = vSerialNumber,
ii.isBooked = TRUE;
-- Problemas derivados de la precisión en los decimales al calcular los impuestos
UPDATE XDiario
SET EURODEBE = EURODEBE -
(SELECT IF(ABS(sub.difference) = 0.01, sub.difference, 0)
FROM(
SELECT SUM(IFNULL(ROUND(EURODEBE, 2),0)) - SUM(IFNULL(ROUND(EUROHABER, 2), 0)) difference
FROM XDiario
WHERE ASIEN = vBookNumber
)sub
)
WHERE ASIEN = vBookNumber
AND EURODEBE <> 0
ORDER BY id DESC
LIMIT 1;
END$$
DELIMITER ;

View File

@ -137,7 +137,8 @@ module.exports = class Docker {
user: this.dbConf.username, user: this.dbConf.username,
password: this.dbConf.password, password: this.dbConf.password,
host: this.dbConf.host, host: this.dbConf.host,
port: this.dbConf.port port: this.dbConf.port,
connectTimeout: maxInterval
}; };
log('Waiting for MySQL init process...'); log('Waiting for MySQL init process...');

View File

@ -8,7 +8,7 @@ ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1;
INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`) INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES VALUES
('TOTALLY_SECURE_TOKEN', '1209600', CURDATE(), 66); ('DEFAULT_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`) INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
@ -104,17 +104,17 @@ INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`)
(3, 'GBP', 'Libra', 1), (3, 'GBP', 'Libra', 1),
(4, 'JPY', 'Yen Japones', 1); (4, 'JPY', 'Yen Japones', 1);
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`) INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES VALUES
(1, 'España', 1, 'ES', 1, 24, 4), (1, 'España', 1, 'ES', 1, 24, 4, 0, 1),
(2, 'Italia', 1, 'IT', 1, 27, 4), (2, 'Italia', 1, 'IT', 1, 27, 4, 0, 1),
(3, 'Alemania', 1, 'DE', 1, 22, 4), (3, 'Alemania', 1, 'DE', 1, 22, 4, 0, 1),
(4, 'Rumania', 1, 'RO', 1, 24, 4), (4, 'Rumania', 1, 'RO', 1, 24, 4, 0, 1),
(5, 'Holanda', 1, 'NL', 1, 18, 4), (5, 'Holanda', 1, 'NL', 1, 18, 4, 0, 1),
(8, 'Portugal', 1, 'PT', 1, 27, 4), (8, 'Portugal', 1, 'PT', 1, 27, 4, 0, 1),
(13,'Ecuador', 0, 'EC', 1, 24, 2), (13,'Ecuador', 0, 'EC', 1, 24, 2, 1, 2),
(19,'Francia', 1, 'FR', 1, 27, 4), (19,'Francia', 1, 'FR', 1, 27, 4, 0, 1),
(30,'Canarias', 1, 'IC', 1, 24, 4); (30,'Canarias', 1, 'IC', 1, 24, 4, 1, 2);
INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`) INSERT INTO `hedera`.`language` (`code`, `name`, `orgName`, `isActive`)
VALUES VALUES
@ -243,7 +243,7 @@ INSERT INTO `vn`.`province`(`id`, `name`, `countryFk`, `autonomyFk`, `warehouseF
VALUES VALUES
(1, 'Province one', 1, 1, NULL), (1, 'Province one', 1, 1, NULL),
(2, 'Province two', 1, 1, NULL), (2, 'Province two', 1, 1, NULL),
(3, 'Province three', 1, 2, NULL), (3, 'Province three', 30, 2, NULL),
(4, 'Province four', 2, 3, NULL), (4, 'Province four', 2, 3, NULL),
(5, 'Province five', 13, 4, NULL); (5, 'Province five', 13, 4, NULL);
@ -455,7 +455,8 @@ INSERT INTO `vn`.`creditInsurance`(`id`, `creditClassification`, `credit`, `crea
INSERT INTO `vn`.`companyGroup`(`id`, `code`) INSERT INTO `vn`.`companyGroup`(`id`, `code`)
VALUES VALUES
(1, 'Wayne Industries'); (1, 'wayneIndustries'),
(2, 'Verdnatura');
INSERT INTO `vn`.`bankEntity`(`id`, `countryFk`, `name`, `bic`) INSERT INTO `vn`.`bankEntity`(`id`, `countryFk`, `name`, `bic`)
VALUES VALUES
@ -466,13 +467,13 @@ INSERT INTO `vn`.`supplierAccount`(`id`, `supplierFk`, `iban`, `bankEntityFk`)
VALUES VALUES
(241, 442, 'ES111122333344111122221111', 128); (241, 442, 'ES111122333344111122221111', 128);
INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `phytosanitary`) INSERT INTO `vn`.`company`(`id`, `code`, `supplierAccountFk`, `workerManagerFk`, `companyCode`, `sage200Company`, `expired`, `companyGroupFk`, `phytosanitary`)
VALUES VALUES
(69 , 'CCs', NULL, 30, NULL, 0, NULL, NULL), (69 , 'CCs', NULL, 30, NULL, 0, NULL, 1, NULL),
(442 , 'VNL', 241, 30, 2 , 1, NULL, 'VNL Company - Plant passport'), (442 , 'VNL', 241, 30, 2 , 1, NULL, 2, 'VNL Company - Plant passport'),
(567 , 'VNH', NULL, 30, NULL, 4, NULL, 'VNH Company - Plant passport'), (567 , 'VNH', NULL, 30, NULL, 4, NULL, 1, 'VNH Company - Plant passport'),
(791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', NULL), (791 , 'FTH', NULL, 30, NULL, 3, '2015-11-30', 1, NULL),
(1381, 'ORN', NULL, 30, NULL, 7, NULL, 'ORN Company - Plant passport'); (1381, 'ORN', NULL, 30, NULL, 7, NULL, 1, 'ORN Company - Plant passport');
INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`) INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`)
VALUES VALUES
@ -486,7 +487,9 @@ INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaF
('A', 'Global nacional', 1, 'NATIONAL', 0), ('A', 'Global nacional', 1, 'NATIONAL', 0),
('T', 'Española rapida', 1, 'NATIONAL', 0), ('T', 'Española rapida', 1, 'NATIONAL', 0),
('V', 'Intracomunitaria global', 0, 'CEE', 1), ('V', 'Intracomunitaria global', 0, 'CEE', 1),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0); ('M', 'Múltiple nacional', 1, 'NATIONAL', 0),
('E', 'Exportación rápida', 0, 'WORLD', 0);
;
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`) INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES VALUES
@ -2352,7 +2355,7 @@ REPLACE INTO `vn`.`invoiceIn`(`id`, `serialNumber`,`serial`, `supplierFk`, `issu
INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`) INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`)
VALUES VALUES
(1, CURDATE(), 1, 237), (1, CURDATE(), 1, 336.99),
(1, CURDATE(), 1, 15.25), (1, CURDATE(), 1, 15.25),
(2, CURDATE(), 1, 168), (2, CURDATE(), 1, 168),
(2, CURDATE(), 1, 55.17), (2, CURDATE(), 1, 55.17),

View File

@ -206,6 +206,12 @@ describe('Ticket Edit sale path', () => {
expect(message.text).toContain('Data saved!'); expect(message.text).toContain('Data saved!');
}); });
it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
});
it('should select the third sale and create a pay back', async() => { it('should select the third sale and create a pay back', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);

View File

@ -43,4 +43,10 @@
&.disabled.checked > .btn { &.disabled.checked > .btn {
background-color: $color-font-secondary; background-color: $color-font-secondary;
} }
&[triple-state]:not(.indeterminate):not(.checked) {
.btn {
background-color: lighten($color-alert, 5%);
}
}
} }

View File

@ -171,9 +171,10 @@ export default class SmartTable extends Component {
if (field.length === 2) if (field.length === 2)
sortType = field[1]; sortType = field[1];
const priority = this.sortCriteria.length + 1;
const column = this.columns.find(column => column.field == fieldName); const column = this.columns.find(column => column.field == fieldName);
if (column) { if (column) {
this.sortCriteria.push({field: fieldName, sortType: sortType}); this.sortCriteria.push({field: fieldName, sortType: sortType, priority: priority});
const isASC = sortType == 'ASC'; const isASC = sortType == 'ASC';
const isDESC = sortType == 'DESC'; const isDESC = sortType == 'DESC';
@ -187,6 +188,8 @@ export default class SmartTable extends Component {
column.element.classList.remove('desc'); column.element.classList.remove('desc');
column.element.classList.add('asc'); column.element.classList.add('asc');
} }
this.setPriority(column.element, priority);
} }
} }
} }
@ -241,9 +244,13 @@ export default class SmartTable extends Component {
const isDESC = existingCriteria && existingCriteria.sortType == 'DESC'; const isDESC = existingCriteria && existingCriteria.sortType == 'DESC';
if (!existingCriteria) { if (!existingCriteria) {
this.sortCriteria.push({field: field, sortType: 'ASC'}); const priority = this.sortCriteria.length + 1;
this.sortCriteria.push({field: field, sortType: 'ASC', priority: priority});
element.classList.remove('desc'); element.classList.remove('desc');
element.classList.add('asc'); element.classList.add('asc');
this.setPriority(element, priority);
} }
if (isDESC) { if (isDESC) {
@ -252,6 +259,8 @@ export default class SmartTable extends Component {
}), 1); }), 1);
element.classList.remove('desc'); element.classList.remove('desc');
element.classList.remove('asc'); element.classList.remove('asc');
element.querySelector('sort-priority').remove();
} }
if (isASC) { if (isASC) {
@ -260,9 +269,29 @@ export default class SmartTable extends Component {
element.classList.add('desc'); element.classList.add('desc');
} }
let priority = 0;
for (const criteria of this.sortCriteria) {
const column = this.columns.find(column => column.field == criteria.field);
if (column) {
criteria.priority = priority;
priority++;
column.element.querySelector('sort-priority').remove();
this.setPriority(column.element, priority);
}
}
this.applySort(); this.applySort();
} }
setPriority(column, priority) {
const sortPriority = document.createElement('sort-priority');
sortPriority.setAttribute('class', 'sort-priority');
sortPriority.innerHTML = priority;
column.appendChild(sortPriority);
}
displaySearch() { displaySearch() {
const header = this.element.querySelector('thead > tr'); const header = this.element.querySelector('thead > tr');
if (!header) return; if (!header) return;

View File

@ -96,9 +96,10 @@ describe('Component smartTable', () => {
expect(firstSortCriteria.field).toEqual('id'); expect(firstSortCriteria.field).toEqual('id');
expect(firstSortCriteria.sortType).toEqual('ASC'); expect(firstSortCriteria.sortType).toEqual('ASC');
expect(firstSortCriteria.priority).toEqual(1);
}); });
it('should insert two new objects to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => { it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
const element = document.createElement('div'); const element = document.createElement('div');
controller.model = {order: 'test1, id DESC'}; controller.model = {order: 'test1, id DESC'};
controller.columns = [ controller.columns = [
@ -114,8 +115,11 @@ describe('Component smartTable', () => {
expect(firstSortCriteria.field).toEqual('test1'); expect(firstSortCriteria.field).toEqual('test1');
expect(firstSortCriteria.sortType).toEqual('ASC'); expect(firstSortCriteria.sortType).toEqual('ASC');
expect(firstSortCriteria.priority).toEqual(1);
expect(secondSortCriteria.field).toEqual('id'); expect(secondSortCriteria.field).toEqual('id');
expect(secondSortCriteria.sortType).toEqual('DESC'); expect(secondSortCriteria.sortType).toEqual('DESC');
expect(secondSortCriteria.priority).toEqual(2);
}); });
}); });

View File

@ -9,7 +9,7 @@ smart-table {
} }
th[field][number] { th[field][number] {
& > :before { & > span:before {
vertical-align: middle; vertical-align: middle;
font-family: 'Material Icons'; font-family: 'Material Icons';
content: 'arrow_downward'; content: 'arrow_downward';
@ -19,26 +19,26 @@ smart-table {
} }
&.asc > :before, &.desc > :before { &.asc > span:before, &.desc > span:before {
color: $color-font; color: $color-font;
opacity: 1; opacity: 1;
} }
&.asc > :before { &.asc > span:before {
content: 'arrow_upward'; content: 'arrow_upward';
} }
&.desc > :before { &.desc > span:before {
content: 'arrow_downward'; content: 'arrow_downward';
} }
&:hover > :before { &:hover > span:before {
opacity: 1; opacity: 1;
} }
} }
th[field]:not([number]) { th[field]:not([number]) {
& > :after { & > span:after {
vertical-align: middle; vertical-align: middle;
font-family: 'Material Icons'; font-family: 'Material Icons';
content: 'arrow_downward'; content: 'arrow_downward';
@ -48,20 +48,20 @@ smart-table {
} }
&.asc > :after, &.desc > :after { &.asc > span:after, &.desc > span:after {
color: $color-font; color: $color-font;
opacity: 1; opacity: 1;
} }
&.asc > :after { &.asc > span:after {
content: 'arrow_upward'; content: 'arrow_upward';
} }
&.desc > :after { &.desc > span:after {
content: 'arrow_downward'; content: 'arrow_downward';
} }
&:hover > :after { &:hover > span:after {
opacity: 1; opacity: 1;
} }
} }
@ -144,3 +144,15 @@ smart-table {
width: 33% width: 33%
} }
} }
.sort-priority {
background-color: $color-font-bg-marginal;
border-radius: 50%;
padding: 2px 5px;
display: inline-block;
text-align: center;
width: 7px;
height: 13px;
font-size: 10px;
color: $color-font-bg
}

133
front/package-lock.json generated Normal file
View File

@ -0,0 +1,133 @@
{
"name": "salix-front",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@uirouter/angularjs": {
"version": "1.0.29",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.29.tgz",
"integrity": "sha512-RImWnBarNixkMto0o8stEaGwZmvhv5cnuOLXyMU2pY8MP2rgEF74ZNJTLeJCW14LR7XDUxVH8Mk8bPI6lxedmQ==",
"requires": {
"@uirouter/core": "6.0.7"
}
},
"@uirouter/core": {
"version": "6.0.7",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.7.tgz",
"integrity": "sha512-KUTJxL+6q0PiBnFx4/Z+Hsyg0pSGiaW5yZQeJmUxknecjpTbnXkLU8H2EqRn9N2B+qDRa7Jg8RcgeNDPY72O1w=="
},
"angular": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.2.tgz",
"integrity": "sha512-IauMOej2xEe7/7Ennahkbb5qd/HFADiNuLSESz9Q27inmi32zB0lnAsFeLEWcox3Gd1F6YhNd1CP7/9IukJ0Gw=="
},
"angular-animate": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
},
"angular-moment": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": {
"moment": ">=2.8.0 <3.0.0"
}
},
"angular-translate": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.18.4.tgz",
"integrity": "sha512-KohNrkH6J9PK+VW0L/nsRTcg5Fw70Ajwwe3Jbfm54Pf9u9Fd+wuingoKv+h45mKf38eT+Ouu51FPua8VmZNoCw==",
"requires": {
"angular": "^1.8.0"
}
},
"angular-translate-loader-partial": {
"version": "2.18.4",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.18.4.tgz",
"integrity": "sha512-bsjR+FbB0sdA2528E/ugwKdlPPQhA1looxLxI3otayBTFXBpED33besfSZhYAISLgNMSL038vSssfRUen9qD8w==",
"requires": {
"angular-translate": "~2.18.4"
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"croppie": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
},
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"mg-crud": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha1-p6AWGzWSPK7/8ZpIBpS2V1vDggw=",
"requires": {
"angular": "^1.6.1"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"oclazyload": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha1-Kjirv/QJDAihEBZxkZRbWfLoJ5w="
},
"require-yaml": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha1-LhsY2RPDuqcqWk03O28Tjd0sMr0=",
"requires": {
"js-yaml": "^4.1.0"
},
"dependencies": {
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
}
}
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"validator": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha1-R84j7Y1Ord+p1LjvAHG2zxB418g="
}
}
}

View File

@ -24,5 +24,28 @@
"shipped": "shipped", "shipped": "shipped",
"landed": "landed", "landed": "landed",
"isWithoutNegatives": "isWithoutNegatives", "isWithoutNegatives": "isWithoutNegatives",
"Changed this data from the ticket": "Changed this data from the ticket" "Changed this data from the ticket": "Changed this data from the ticket",
"INACTIVE_PROVIDER": "Proveedor inactivo",
"This client is not invoiceable": "Este cliente no es facturable",
"serial non editable": "Esta serie no permite asignar la referencia",
"Max shipped required": "La fecha límite es requerida",
"Can't invoice to future": "No se puede facturar a futuro",
"Can't invoice to past": "No se puede facturar a pasado",
"This ticket is already invoiced": "Este ticket ya está facturado",
"A ticket with an amount of zero can't be invoiced": "No se puede facturar un ticket con importe cero",
"A ticket with a negative base can't be invoiced": "No se puede facturar un ticket con una base negativa",
"Global invoicing failed": "[Facturación global] No se han podido facturar algunos clientes",
"Wasn't able to invoice the following clients": "No se han podido facturar los siguientes clientes",
"Can't verify data unless the client has a business type": "No se puede verificar datos de un cliente que no tiene tipo de negocio",
"You don't have enough privileges to set this credit amount": "No tienes suficientes privilegios para establecer esta cantidad de crédito",
"You can't change the credit set to zero from a manager": "No puedes cambiar el cŕedito establecido a cero por un gerente",
"Amounts do not match": "Las cantidades no coinciden",
"The PDF document does not exists": "El documento PDF no existe. Prueba a regenerarlo desde la opción 'Regenerar PDF factura'",
"The type of business must be filled in basic data": "El tipo de negocio debe estar rellenado en datos básicos",
"You can't create a claim from a ticket delivered more than seven days ago": "No puedes crear una reclamación de un ticket entregado hace más de siete días",
"The worker has hours recorded that day": "El trabajador tiene horas fichadas ese día",
"The worker has a marked absence that day": "El trabajador tiene marcada una ausencia ese día",
"You can not modify is pay method checked": "No se puede modificar el campo método de pago validado",
"Can't transfer claimed sales": "No puedes transferir lineas reclamadas",
"You don't have privileges to create pay back": "No tienes permisos para crear un abono"
} }

View File

@ -20,6 +20,9 @@
"MailForward": { "MailForward": {
"dataSource": "vn" "dataSource": "vn"
}, },
"RoleConfig": {
"dataSource": "vn"
},
"RoleInherit": { "RoleInherit": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -114,17 +114,22 @@ module.exports = Self => {
'bcryptPassword', 'bcryptPassword',
'updated' 'updated'
], ],
include: { include: [
relation: 'roles', {
scope: { relation: 'roles',
include: { scope: {
relation: 'inherits', include: {
scope: { relation: 'inherits',
fields: ['name'] scope: {
fields: ['name']
}
} }
} }
}, {
relation: 'role',
fields: ['name']
} }
} ]
}); });
let info = { let info = {

View File

@ -0,0 +1,103 @@
module.exports = Self => {
Self.getSynchronizer = async function() {
return await Self.findOne({fields: ['id']});
};
Object.assign(Self.prototype, {
async init() {
const [row] = await Self.rawSql('SELECT VERSION() AS `version`');
if (row.version.includes('MariaDB'))
this.dbType = 'MariaDB';
else
this.dbType = 'MySQL';
},
async syncUser(userName, info, password) {
const mysqlHost = '%';
let mysqlUser = userName;
if (this.dbType == 'MySQL') mysqlUser = `!${mysqlUser}`;
const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows
FROM mysql.user
WHERE User = ?
AND Host = ?`,
[mysqlUser, mysqlHost]
);
let userExists = row.nRows > 0;
let isUpdatable = true;
if (this.dbType == 'MariaDB') {
const [row] = await Self.rawSql(
`SELECT Priv AS priv
FROM mysql.global_priv
WHERE User = ?
AND Host = ?`,
[mysqlUser, mysqlHost]
);
const priv = row && JSON.parse(row.priv);
const role = priv && priv.default_role;
isUpdatable = !row || (role && role.startsWith('z-'));
}
if (!isUpdatable) {
console.warn(`RoleConfig.syncUser(): User '${userName}' cannot be updated, not managed by me`);
return;
}
if (info.hasAccount) {
if (password) {
if (!userExists) {
await Self.rawSql('CREATE USER ?@? IDENTIFIED BY ?',
[mysqlUser, mysqlHost, password]
);
userExists = true;
} else {
switch (this.dbType) {
case 'MariaDB':
await Self.rawSql('ALTER USER ?@? IDENTIFIED BY ?',
[mysqlUser, mysqlHost, password]
);
break;
default:
await Self.rawSql('SET PASSWORD FOR ?@? = PASSWORD(?)',
[mysqlUser, mysqlHost, password]
);
}
}
}
if (userExists && this.dbType == 'MariaDB') {
let role = `z-${info.user.role().name}`;
try {
await Self.rawSql('REVOKE ALL, GRANT OPTION FROM ?@?',
[mysqlUser, mysqlHost]
);
} catch (err) {
if (err.code == 'ER_REVOKE_GRANTS')
console.warn(`${err.code}: ${err.sqlMessage}: ${err.sql}`);
else
throw err;
}
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, mysqlHost]
);
if (role) {
await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?',
[role, mysqlUser, mysqlHost]
);
} else {
await Self.rawSql('SET DEFAULT ROLE NONE FOR ?@?',
[mysqlUser, mysqlHost]
);
}
}
} else if (userExists)
await Self.rawSql('DROP USER ?@?', [mysqlUser, mysqlHost]);
}
});
};

View File

@ -0,0 +1,21 @@
{
"name": "RoleConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.roleConfig"
}
},
"mixins": {
"AccountSynchronizer": {}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"mysqlPassword": {
"type": "string"
}
}
}

View File

@ -52,12 +52,13 @@ module.exports = Self => {
let response; let response;
try { try {
if (process.env.NODE_ENV !== 'production') if (process.env.NODE_ENV !== 'production')
params.fake = 1; response = {result: [{status: 'ok'}]};
else {
const jsonTest = { const jsonTest = {
json: params json: params
}; };
response = await got.post(smsConfig.uri, jsonTest).json(); response = await got.post(smsConfig.uri, jsonTest).json();
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@ -35,6 +35,13 @@
{{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) &#x2192; {{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) &#x2192;
{{::warehouseOutName}} ({{::landed | date: 'dd/MM/yyyy'}}) {{::warehouseOutName}} ({{::landed | date: 'dd/MM/yyyy'}})
</tpl-item> </tpl-item>
<append>
<vn-icon-button
icon="filter_alt"
vn-click-stop="$ctrl.showFilterDialog($ctrl.entry.travelFk)"
vn-tooltip="Filter...">
</vn-icon-button>
</append>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
@ -122,3 +129,93 @@
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>
<!-- Filter travel dialog -->
<vn-dialog
vn-id="filterDialog"
message="Filter travel">
<tpl-body class="travelFilter">
<vn-horizontal>
<vn-autocomplete
label="Agency"
ng-model="$ctrl.travelFilterParams.agencyFk"
url="AgencyModes"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse Out"
ng-model="$ctrl.travelFilterParams.warehouseOutFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete
label="Warehouse In"
ng-model="$ctrl.travelFilterParams.warehouseInFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-date-picker
label="Shipped"
ng-model="$ctrl.travelFilterParams.shipped">
</vn-date-picker>
<vn-date-picker
label="Landed"
ng-model="$ctrl.travelFilterParams.landed">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-mb-md">
<vn-button vn-none
label="Search"
ng-click="$ctrl.filter()">
</vn-button>
</vn-horizontal>
<vn-crud-model
vn-id="travelsModel"
url="Travels"
filter="$ctrl.travelFilter"
data="travels"
limit="10">
</vn-crud-model>
<vn-data-viewer
model="travelsModel"
class="vn-w-lg">
<vn-table class="scrollable">
<vn-thead>
<vn-tr>
<vn-th shrink>ID</vn-th>
<vn-th expand>Agency</vn-th>
<vn-th expand>Warehouse Out</vn-th>
<vn-th expand>Warehouse In</vn-th>
<vn-th expand>Shipped</vn-th>
<vn-th expand>Landed</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="travel in travels"
class="clickable vn-tr search-result"
ng-click="$ctrl.selectTravel(travel.id)">
<vn-td shrink>
<span
vn-click-stop="travelDescriptor.show($event, travel.id)"
class="link">
{{::travel.id}}
</span>
</vn-td>
<vn-td expand>{{::travel.agency.name}}</vn-td>
<vn-td expand>{{::travel.warehouseOut.name}}</vn-td>
<vn-td expand>{{::travel.warehouseIn.name}}</vn-td>
<vn-td expand>{{::travel.shipped | date: 'dd/MM/yyyy'}}</vn-td>
<vn-td expand>{{::travel.landed | date: 'dd/MM/yyyy'}}</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-data-viewer>
<vn-travel-descriptor-popover
vn-id="travel-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-travel-descriptor-popover>
</tpl-body>
</vn-dialog>

View File

@ -1,10 +1,68 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
showFilterDialog(travel) {
this.activeTravel = travel;
this.travelFilterParams = {};
this.travelFilter = {
include: [
{
relation: 'agency',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseIn',
scope: {
fields: ['name']
}
},
{
relation: 'warehouseOut',
scope: {
fields: ['name']
}
}
]
};
this.$.filterDialog.show();
}
selectTravel(id) {
this.entry.travelFk = id;
this.$.filterDialog.hide();
}
filter() {
const filter = this.travelFilter;
const params = this.travelFilterParams;
const where = {};
for (let key in params) {
const value = params[key];
if (!value) continue;
switch (key) {
case 'agencyFk':
case 'warehouseInFk':
case 'warehouseOutFk':
case 'shipped':
case 'landed':
where[key] = value;
}
}
filter.where = where;
this.$.travelsModel.applyFilter(filter);
}
}
ngModule.vnComponent('vnEntryBasicData', { ngModule.vnComponent('vnEntryBasicData', {
template: require('./index.html'), template: require('./index.html'),
controller: Section,
bindings: { bindings: {
entry: '<' entry: '<'
} },
controller: Controller
}); });

View File

@ -0,0 +1,3 @@
.travelFilter{
width: 950px;
}

View File

@ -0,0 +1,43 @@
module.exports = Self => {
Self.remoteMethod('getTotals', {
description: 'Return totals for an invoiceIn',
accessType: 'READ',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'invoiceIn id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/getTotals',
verb: 'GET'
}
});
Self.getTotals = async(id, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const [result] = await Self.rawSql(`
SELECT iit.*,
SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat
FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
WHERE
ii.id = ?`, [id, id]);
return result;
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn getTotals()', () => {
it('should check that returns invoiceIn totals', async() => {
const invoiceInId = 1;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
const invoiceIntotals = await models.InvoiceIn.getTotals(invoiceInId, options);
expect(typeof invoiceIntotals.totalTaxableBase).toBe('number');
expect(invoiceIntotals.totalTaxableBase).toEqual(invoiceIntotals.totalDueDay);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,34 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn toBook()', () => {
it('should check that invoiceIn is booked', async() => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const invoiceInId = 1;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
const invoiceInBefore = await models.InvoiceIn.findById(invoiceInId, null, options);
expect(invoiceInBefore.isBooked).toEqual(false);
await models.InvoiceIn.toBook(ctx, invoiceInId, options);
const invoiceIn = await models.InvoiceIn.findById(invoiceInId, null, options);
expect(invoiceIn.isBooked).toEqual(true);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -20,6 +20,7 @@ module.exports = Self => {
}); });
Self.summary = async(id, options) => { Self.summary = async(id, options) => {
const models = Self.app.models;
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
@ -85,25 +86,9 @@ module.exports = Self => {
} }
] ]
}; };
let summaryObj = await models.InvoiceIn.findById(id, filter, myOptions);
let summaryObj = await Self.app.models.InvoiceIn.findById(id, filter, myOptions); summaryObj.totals = await models.InvoiceIn.getTotals(id, myOptions);
summaryObj.totals = await getTotals(id);
return summaryObj; return summaryObj;
}; };
async function getTotals(invoiceInFk) {
return (await Self.rawSql(`
SELECT iit.*,
SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) totalVat
FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
WHERE
ii.id = ?`, [invoiceInFk, invoiceInFk]))[0];
}
}; };

View File

@ -0,0 +1,42 @@
module.exports = Self => {
Self.remoteMethodCtx('toBook', {
description: 'To book the invoiceIn',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/toBook',
verb: 'POST'
}
});
Self.toBook = async(ctx, id, options) => {
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
await Self.rawSql(`CALL vn.invoiceInBookingMain(?)`, [id], myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -2,4 +2,6 @@ module.exports = Self => {
require('../methods/invoice-in/filter')(Self); require('../methods/invoice-in/filter')(Self);
require('../methods/invoice-in/summary')(Self); require('../methods/invoice-in/summary')(Self);
require('../methods/invoice-in/clone')(Self); require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self);
}; };

View File

@ -1,8 +1,16 @@
<vn-descriptor-content module="invoiceIn" description="$ctrl.invoiceIn.supplierRef"> <vn-descriptor-content module="invoiceIn" description="$ctrl.invoiceIn.supplierRef">
<slot-menu> <slot-menu>
<vn-item
ng-click="$ctrl.checkToBook()"
vn-acl="administrative"
ng-hide="$ctrl.invoiceIn.isBooked == true"
translate>
To book
</vn-item>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
vn-acl="invoicing" vn-acl="administrative"
vn-acl-action="remove" vn-acl-action="remove"
name="deleteInvoice" name="deleteInvoice"
translate> translate>
@ -10,7 +18,7 @@
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="cloneConfirmation.show()" ng-click="cloneConfirmation.show()"
vn-acl="invoicing" vn-acl="administrative"
name="cloneInvoice" name="cloneInvoice"
translate> translate>
Clone Invoice Clone Invoice
@ -46,7 +54,9 @@
icon="icon-invoice-in"> icon="icon-invoice-in">
</vn-quick-link> </vn-quick-link>
</div> </div>
</div> </div>
</slot-body> </slot-body>
</vn-descriptor-content> </vn-descriptor-content>
<vn-confirm <vn-confirm
@ -62,3 +72,8 @@
<vn-supplier-descriptor-popover <vn-supplier-descriptor-popover
vn-id="supplierDescriptor"> vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover> </vn-supplier-descriptor-popover>
<vn-confirm
vn-id="confirm-toBookAnyway"
message="Are you sure you want to book this invoice?"
on-accept="$ctrl.onAcceptToBook()">
</vn-confirm>

View File

@ -51,6 +51,43 @@ class Controller extends Descriptor {
return this.getData(`InvoiceIns/${this.id}`, {filter}) return this.getData(`InvoiceIns/${this.id}`, {filter})
.then(res => this.entity = res.data); .then(res => this.entity = res.data);
} }
checkToBook() {
let message = '';
const id = this.invoiceIn.id;
this.$q.all([
this.$http.get(`InvoiceIns/${this.id}/getTotals`)
.then(res => {
const taxableBaseNotEqualDueDay = res.data.totalDueDay != res.data.totalTaxableBase;
const vatNotEqualDueDay = res.data.totalDueDay != res.data.totalVat;
if (taxableBaseNotEqualDueDay && vatNotEqualDueDay)
message += 'amountsDoNotMatch';
}),
this.$http.get('InvoiceInDueDays/count', {
filter: {
where: {
invoiceInFk: id,
dueDated: {gte: new Date()}
}
}})
.then(res => {
if (res.data)
message += 'future payments';
})
]).finally(() => {
if (message.length)
this.$.confirmToBookAnyway.show();
else
this.onAcceptToBook();
});
}
onAcceptToBook() {
this.$http.post(`InvoiceIns/${this.id}/toBook`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
}
} }
ngModule.vnComponent('vnInvoiceInDescriptor', { ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -8,19 +8,70 @@ describe('vnInvoiceInDescriptor', () => {
beforeEach(inject(($componentController, _$httpBackend_) => { beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
controller = $componentController('vnInvoiceInDescriptor', {$element: null}); const $element = angular.element('<vn-invoice-in-descriptor></vn-invoice-in-descriptor>');
controller = $componentController('vnInvoiceInDescriptor', {$element});
controller.invoiceIn = {id: 1};
$httpBackend.when('GET', `InvoiceIns/${controller.invoiceIn.id}`).respond({id: 1});
})); }));
describe('loadData()', () => { describe('loadData()', () => {
it(`should perform a get query to store the invoice in data into the controller`, () => { it(`should perform a get query to store the invoice in data into the controller`, () => {
const id = 1; expect(controller.invoiceIn).toEqual({id: 1});
const response = {id: 1}; });
});
$httpBackend.expectGET(`InvoiceIns/${id}`).respond(response); describe('onAcceptToBook()', () => {
controller.id = id; it(`should perform a post query to book the invoice`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.reload = jest.fn();
const id = 1;
$httpBackend.expectPOST(`InvoiceIns/${id}/toBook`).respond();
controller.onAcceptToBook();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.invoiceIn).toEqual(response); expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('InvoiceIn booked');
});
});
describe('checkToBook()', () => {
it(`should show a warning before book`, () => {
controller.$.confirmToBookAnyway = {show: () => {}};
jest.spyOn(controller.$.confirmToBookAnyway, 'show');
const invoceInId = 1;
const data = {
totalDueDay: 'an amount',
totalTaxableBase: 'distinct amount'
};
$httpBackend.expectGET(`InvoiceIns/${invoceInId}/getTotals`).respond(data);
$httpBackend.expectGET(`InvoiceInDueDays/count`).respond();
controller.checkToBook();
$httpBackend.flush();
expect(controller.$.confirmToBookAnyway.show).toHaveBeenCalledWith();
});
it(`should call onAcceptToBook`, () => {
controller.onAcceptToBook = jest.fn();
const invoceInId = 1;
const data = {
totalDueDay: 'same amount',
totalTaxableBase: 'same amount'
};
$httpBackend.expectGET(`InvoiceIns/${invoceInId}/getTotals`).respond(data);
$httpBackend.expectGET(`InvoiceInDueDays/count`).respond();
controller.checkToBook();
$httpBackend.flush();
expect(controller.onAcceptToBook).toHaveBeenCalledWith();
}); });
}); });
}); });

View File

@ -1,13 +1,16 @@
InvoiceIn: Facturas recibidas
Search invoices in by reference: Buscar facturas recibidas por referencia
Entries list: Listado de entradas
InvoiceIn deleted: Factura eliminada
Remove tax: Quitar iva
Add tax: Añadir iva Add tax: Añadir iva
Amounts do not match: La BI no coincide con el vencimiento ni con el total
Due day: Vencimiento
Entries list: Listado de entradas
Foreign value: Divisa
InvoiceIn: Facturas recibidas
InvoiceIn cloned: Factura clonada
InvoiceIn deleted: Factura eliminada
Invoice list: Listado de facturas recibidas
InvoiceIn booked: Factura contabilizada
Remove tax: Quitar iva
Sage tax: Sage iva Sage tax: Sage iva
Sage transaction: Sage transaccion Sage transaction: Sage transaccion
Foreign value: Divisa Search invoices in by reference: Buscar facturas recibidas por referencia
Due day: Vencimiento To book: Contabilizar
Invoice list: Listado de facturas recibidas
InvoiceIn cloned: Factura clonada

View File

@ -21,6 +21,20 @@
ng-model="filter.fi"> ng-model="filter.fi">
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one ng-model="filter.supplierFk"
url="Suppliers"
show-field="nickname"
search-function="{or: [{id: $search}, {nickname: {like: '%'+ $search +'%'}}]}"
value-field="id"
order="nickname"
label="Supplier">
<tpl-item>
{{::id}} - {{::nickname}}
</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield
vn-one vn-one

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra'); const fs = require('fs-extra');
const got = require('got');
const path = require('path'); const path = require('path');
const axios = require('axios');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('createPdf', { Self.remoteMethodCtx('createPdf', {
@ -57,39 +57,37 @@ module.exports = Self => {
hasPdf: true hasPdf: true
}, myOptions); }, myOptions);
const response = got.stream(`${origin}/api/report/invoice`, { return axios.get(`${origin}/api/report/invoice`, {
searchParams: { responseType: 'stream',
params: {
authorization: auth.id, authorization: auth.id,
invoiceId: id invoiceId: id
} }
}); }).then(async response => {
const issued = invoiceOut.issued;
const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString();
const issued = invoiceOut.issued; const container = await models.InvoiceContainer.container(year);
const year = issued.getFullYear().toString(); const rootPath = container.client.root;
const month = (issued.getMonth() + 1).toString(); const fileName = `${year}${invoiceOut.ref}.pdf`;
const day = issued.getDate().toString(); const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
const container = await models.InvoiceContainer.container(year); await fs.mkdir(src, {recursive: true});
const rootPath = container.client.root;
const fileName = `${year}${invoiceOut.ref}.pdf`;
const src = path.join(rootPath, year, month, day);
fileSrc = path.join(src, fileName);
await fs.mkdir(src, {recursive: true}); if (tx) await tx.commit();
if (tx) await tx.commit(); response.data.pipe(fs.createWriteStream(fileSrc));
}).catch(async e => {
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
const writeStream = fs.createWriteStream(fileSrc); throw e;
writeStream.on('open', () => response.pipe(writeStream));
writeStream.on('finish', () => writeStream.end());
return new Promise(resolve => {
writeStream.on('close', () => resolve(invoiceOut));
}); });
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
throw e; throw e;
} }
}; };

View File

@ -1,24 +1,28 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const got = require('got'); const LoopBackContext = require('loopback-context');
const fs = require('fs-extra'); const fs = require('fs-extra');
const axios = require('axios');
describe('InvoiceOut createPdf()', () => { describe('InvoiceOut createPdf()', () => {
const userId = 1; const userId = 1;
const ctx = { const activeCtx = {
req: { accessToken: {userId: userId, id: 'DEFAULT_TOKEN'},
headers: {origin: 'http://localhost:5000'}
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
}; };
const ctx = {req: activeCtx};
it('should create a new PDF file and set true the hasPdf property', async() => { it('should create a new PDF file and set true the hasPdf property', async() => {
const invoiceId = 1; const invoiceId = 1;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
const response = { const response = {
pipe: () => {}, data: {
on: () => {}, pipe: () => {},
on: () => {},
}
}; };
spyOn(got, 'stream').and.returnValue(response); spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(models.InvoiceContainer, 'container').and.returnValue({ spyOn(models.InvoiceContainer, 'container').and.returnValue({
client: {root: '/path'} client: {root: '/path'}
}); });
@ -32,9 +36,10 @@ describe('InvoiceOut createPdf()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
try { try {
const result = await models.InvoiceOut.createPdf(ctx, invoiceId, options); await models.InvoiceOut.createPdf(ctx, invoiceId, options);
const invoiceOut = await models.InvoiceOut.findById(invoiceId, null, options);
expect(result.hasPdf).toBe(true); expect(invoiceOut.hasPdf).toBe(true);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -19,6 +19,10 @@ class Controller extends Section {
this.id = value.id; this.id = value.id;
} }
get hasInvoicing() {
return this.aclService.hasAny(['invoicing']);
}
loadData() { loadData() {
const filter = { const filter = {
include: [ include: [
@ -51,8 +55,13 @@ class Controller extends Section {
deleteInvoiceOut() { deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.id}/delete`) return this.$http.post(`InvoiceOuts/${this.invoiceOut.id}/delete`)
.then(() => this.$state.go('invoiceOut.index')) .then(() => {
.then(() => this.$state.reload()) const isInsideInvoiceOut = this.$state.current.name.startsWith('invoiceOut');
if (isInsideInvoiceOut)
this.$state.go('invoiceOut.index');
else
this.$state.reload();
})
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted'))); .then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
} }

View File

@ -50,6 +50,35 @@ describe('vnInvoiceOutDescriptorMenu', () => {
}); });
}); });
describe('deleteInvoiceOut()', () => {
it(`should make a query and call showSuccess()`, () => {
controller.invoiceOut = invoiceOut;
controller.$state.reload = jest.fn();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't in invoiceOut module`, () => {
controller.invoiceOut = invoiceOut;
jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'invoiceOut.card.something';
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/delete`).respond();
controller.deleteInvoiceOut();
$httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('invoiceOut.index');
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('sendPdfInvoice()', () => { describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => { it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage'); jest.spyOn(controller.vnApp, 'showMessage');

View File

@ -1,8 +1,8 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="SalesMonitors/salesFilter" url="SalesMonitors/salesFilter"
limit="20" auto-load="false"
order="shipped DESC, theoreticalHour, id"> limit="20">
</vn-crud-model> </vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar <vn-searchbar

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('payBack', { Self.remoteMethodCtx('payBack', {
description: 'Create ticket with the selected lines changing the sign to the quantites', description: 'Create ticket with the selected lines changing the sign to the quantites',
@ -39,6 +41,15 @@ module.exports = Self => {
try { try {
const salesIds = []; const salesIds = [];
const params = []; const params = [];
const userId = ctx.req.accessToken.userId;
const isClaimManager = await Self.app.models.Account.hasRole(userId, 'claimManager');
const isSalesAssistant = await Self.app.models.Account.hasRole(userId, 'salesAssistant');
const hasValidRole = isClaimManager || isSalesAssistant;
if (!hasValidRole)
throw new UserError(`You don't have privileges to create pay back`);
sales.forEach(sale => { sales.forEach(sale => {
salesIds.push(sale.id); salesIds.push(sale.id);
params.push('?'); params.push('?');

View File

@ -3,15 +3,17 @@ const models = require('vn-loopback/server/server').models;
describe('sale payBack()', () => { describe('sale payBack()', () => {
it('should create ticket with the selected lines changing the sign to the quantites', async() => { it('should create ticket with the selected lines changing the sign to the quantites', async() => {
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});
const ctx = {req: {accessToken: {userId: 9}}};
const ticketId = 11; const ticketId = 11;
const sales = [ const sales = [
{id: 7, ticketFk: 11}, {id: 7, ticketFk: 11},
{id: 8, ticketFk: 11} {id: 8, ticketFk: 11}
]; ];
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}};
const response = await models.Sale.payBack(ctx, sales, ticketId, options); const response = await models.Sale.payBack(ctx, sales, ticketId, options);
const [newTicketId] = await models.Sale.rawSql('SELECT MAX(t.id) id FROM vn.ticket t;', null, options); const [newTicketId] = await models.Sale.rawSql('SELECT MAX(t.id) id FROM vn.ticket t;', null, options);
@ -23,4 +25,30 @@ describe('sale payBack()', () => {
throw e; throw e;
} }
}); });
it(`should throw an error if the user doesn't have privileges to create a pay back`, async() => {
const tx = await models.Sale.beginTransaction({});
const ctx = {req: {accessToken: {userId: 1}}};
const ticketId = 11;
const sales = [
{id: 7, ticketFk: 11}
];
let error;
try {
const options = {transaction: tx};
await models.Sale.payBack(ctx, sales, ticketId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeDefined();
expect(error.message).toEqual(`You don't have privileges to create pay back`);
});
}); });

View File

@ -149,7 +149,10 @@ class Controller extends Section {
return this.$http.post(`Tickets/${this.id}/setDeleted`) return this.$http.post(`Tickets/${this.id}/setDeleted`)
.then(() => this.reload()) .then(() => this.reload())
.then(() => { .then(() => {
this.$state.go('ticket.index'); const isInsideTicket = this.$state.current.name.startsWith('ticket');
if (isInsideTicket)
this.$state.go('ticket.index');
this.vnApp.showSuccess(this.$t('Ticket deleted. You can undo this action within the first hour')); this.vnApp.showSuccess(this.$t('Ticket deleted. You can undo this action within the first hour'));
}); });
} }

View File

@ -78,9 +78,22 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
describe('deleteTicket()', () => { describe('deleteTicket()', () => {
it('should make a query and call vnApp.showSuccess()', () => { it('should make a query and call vnApp.showSuccess()', () => {
jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket();
$httpBackend.flush();
expect(controller.reload).toHaveBeenCalled();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
it(`should make a query and call showSuccess() after state.go if the state wasn't inside ticket module`, () => {
jest.spyOn(controller, 'reload').mockReturnThis(); jest.spyOn(controller, 'reload').mockReturnThis();
jest.spyOn(controller.$state, 'go').mockReturnValue('ok'); jest.spyOn(controller.$state, 'go').mockReturnValue('ok');
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
controller.$state.current.name = 'ticket.card.something';
$httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond(); $httpBackend.expectPOST(`Tickets/${ticket.id}/setDeleted`).respond();
controller.deleteTicket(); controller.deleteTicket();

View File

@ -492,7 +492,9 @@
</vn-item> </vn-item>
<vn-item translate <vn-item translate
name="payBack" name="payBack"
ng-click="$ctrl.createPayBack()"> ng-click="$ctrl.createPayBack()"
vn-acl="claimManager, salesAssistant"
vn-acl-action="remove">
Pay Back Pay Back
</vn-item> </vn-item>
</vn-menu> </vn-menu>

View File

@ -12,6 +12,7 @@
"node": ">=14" "node": ">=14"
}, },
"dependencies": { "dependencies": {
"axios": "^0.25.0",
"bmp-js": "^0.1.0", "bmp-js": "^0.1.0",
"compression": "^1.7.3", "compression": "^1.7.3",
"fs-extra": "^5.0.0", "fs-extra": "^5.0.0",
@ -42,6 +43,7 @@
"strong-error-handler": "^2.3.2", "strong-error-handler": "^2.3.2",
"uuid": "^3.3.3", "uuid": "^3.3.3",
"vn-loopback": "file:./loopback", "vn-loopback": "file:./loopback",
"vn-print": "file:./print",
"xml2js": "^0.4.23" "xml2js": "^0.4.23"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,11 +1,9 @@
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const puppeteer = require('puppeteer');
const templatesPath = path.resolve(__dirname, './templates'); const templatesPath = path.resolve(__dirname, './templates');
const componentsPath = path.resolve(__dirname, './core/components'); const componentsPath = path.resolve(__dirname, './core/components');
const config = require('./core/config');
module.exports = async app => { module.exports = async app => {
global.appPath = __dirname; global.appPath = __dirname;
@ -53,21 +51,4 @@ module.exports = async app => {
app.use(`/api/${templateName}/assets`, express.static(assetsDir)); app.use(`/api/${templateName}/assets`, express.static(assetsDir));
}); });
}); });
// Instantiate Puppeteer browser
async function launchBrowser() {
config.browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--no-zygote'
]
});
config.browser.on('disconnected', launchBrowser);
}
launchBrowser();
}; };

View File

@ -6,6 +6,8 @@
.grid { .grid {
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
font-size: 16px !important; font-size: 16px !important;
max-width: 1200px;
margin: 0 auto;
width: 100% width: 100%
} }

View File

@ -48,6 +48,12 @@
"pool": true "pool": true
}, },
"storage": { "storage": {
"root": "./storage/dms" "root": "./storage/dms",
"invoice": {
"root": "./storage/pdfs/invoice"
},
"signature": {
"root": "./storage/signatures"
}
} }
} }

View File

@ -27,29 +27,50 @@ class Component {
get locale() { get locale() {
if (!this._locale) if (!this._locale)
this.getLocale(); this._locale = this.getLocales();
return this._locale; return this._locale;
} }
getLocale() { getLocales() {
const mergedLocale = {messages: {}}; const mergedLocales = {messages: {}};
const localePath = path.resolve(__dirname, `${this.path}/locale`); const localePath = path.resolve(__dirname, `${this.path}/locale`);
if (!fs.existsSync(localePath)) if (!fs.existsSync(localePath))
return mergedLocale; return mergedLocales;
const localeDir = fs.readdirSync(localePath); const localeDir = fs.readdirSync(localePath);
localeDir.forEach(locale => { for (const locale of localeDir) {
const fullPath = path.join(localePath, '/', locale); const fullPath = path.join(localePath, '/', locale);
const yamlLocale = fs.readFileSync(fullPath, 'utf8'); const yamlLocale = fs.readFileSync(fullPath, 'utf8');
const jsonLocale = yaml.safeLoad(yamlLocale); const jsonLocale = yaml.safeLoad(yamlLocale);
const localeName = locale.replace('.yml', ''); const localeName = locale.replace('.yml', '');
mergedLocale.messages[localeName] = jsonLocale; mergedLocales.messages[localeName] = jsonLocale;
}); }
this._locale = mergedLocale; return mergedLocales;
}
async getUserLocale() {
let locale = this.args.auth.locale;
// Fetches user locale from mixing method getLocale()
if (this.args.recipientId) {
const component = await this.component();
locale = await component.getLocale(this.args.recipientId);
}
const messages = this.locale.messages;
const userTranslations = messages[locale];
if (!userTranslations) {
const fallbackLocale = config.i18n.fallbackLocale;
return messages[fallbackLocale];
}
return userTranslations;
} }
get stylesheet() { get stylesheet() {
@ -75,7 +96,7 @@ class Component {
build() { build() {
const fullPath = path.resolve(__dirname, this.path); const fullPath = path.resolve(__dirname, this.path);
if (!fs.existsSync(fullPath)) if (!fs.existsSync(fullPath))
throw new Error(`Sample "${this.name}" not found`); throw new Error(`Template "${this.name}" not found`);
const component = require(`${this.path}/${this.name}`); const component = require(`${this.path}/${this.name}`);
component.i18n = this.locale; component.i18n = this.locale;

View File

@ -1,5 +1,9 @@
<header> <header>
<img v-bind:src="getReportSrc('report-logo.svg')" alt="Verdnatura"/> <img
v-if="companyGroup == 'verdnatura'"
v-bind:src="getReportSrc('report-logo.svg')"
alt="Verdnatura"
/>
<section> <section>
{{companyName}}. {{company.street}}. {{companyName}}. {{company.street}}.
{{company.postCode}} {{company.city}}. {{company.postCode}} {{company.city}}.

View File

@ -10,9 +10,16 @@ module.exports = {
}, },
computed: { computed: {
companyName() { companyName() {
if (!this.company.name) return; if (this.company.name)
return this.company.name.toUpperCase();
return this.company.name.toUpperCase(); return;
},
companyGroup() {
if (this.company.groupName)
return this.company.groupName.toLowerCase();
return;
}, },
companyPhone() { companyPhone() {
if (!this.company.phone) return; if (!this.company.phone) return;
@ -30,8 +37,15 @@ module.exports = {
methods: { methods: {
getCompany(code) { getCompany(code) {
return db.findOne(` return db.findOne(`
SELECT s.name, s.street, s.postCode, s.city, s.phone SELECT
s.name,
s.street,
s.postCode,
s.city,
s.phone,
cg.code AS groupName
FROM company c FROM company c
JOIN companyGroup cg ON cg.id = c.companyGroupFk
JOIN supplier s ON s.id = c.id JOIN supplier s ON s.id = c.id
WHERE c.code = ?`, [code]); WHERE c.code = ?`, [code]);
}, },

View File

@ -19,22 +19,7 @@ class Email extends Component {
} }
async getSubject() { async getSubject() {
const component = await this.component(); return (await this.getUserLocale())['subject'];
let locale = this.args.auth.locale;
if (this.args.recipientId)
locale = await component.getLocale(this.args.recipientId);
const messages = this.locale.messages;
const userTranslations = messages[locale];
if (!userTranslations) {
const fallbackLocale = config.i18n.fallbackLocale;
return messages[fallbackLocale].subject;
}
return userTranslations.subject;
} }
/** /**
@ -63,6 +48,7 @@ class Email extends Component {
const reportName = fileName.replace('.pdf', ''); const reportName = fileName.replace('.pdf', '');
const report = new Report(reportName, this.args); const report = new Report(reportName, this.args);
fileCopy.content = await report.toPdfStream(); fileCopy.content = await report.toPdfStream();
fileCopy.filename = await report.getFileName();
} }
attachments.push(fileCopy); attachments.push(fileCopy);

View File

@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const config = require('./config'); const config = require('./config');
const Component = require('./component'); const Component = require('./component');
const puppeteer = require('puppeteer');
if (!process.env.OPENSSL_CONF) if (!process.env.OPENSSL_CONF)
process.env.OPENSSL_CONF = '/etc/ssl/'; process.env.OPENSSL_CONF = '/etc/ssl/';
@ -17,6 +18,10 @@ class Report extends Component {
return `../templates/reports/${this.name}`; return `../templates/reports/${this.name}`;
} }
async getName() {
return (await this.getUserLocale())['reportName'];
}
async toPdfStream() { async toPdfStream() {
const template = await this.render(); const template = await this.render();
const defaultOptions = Object.assign({}, config.pdf); const defaultOptions = Object.assign({}, config.pdf);
@ -27,7 +32,17 @@ class Report extends Component {
if (fs.existsSync(fullPath)) if (fs.existsSync(fullPath))
options = require(optionsPath); options = require(optionsPath);
const page = (await config.browser.pages())[0]; const browser = await puppeteer.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--no-zygote'
]
});
const page = (await browser.pages())[0];
await page.emulateMedia('screen'); await page.emulateMedia('screen');
await page.setContent(template); await page.setContent(template);
@ -47,8 +62,43 @@ class Report extends Component {
const buffer = await page.pdf(options); const buffer = await page.pdf(options);
await browser.close();
return buffer; return buffer;
} }
/**
* Returns all the params that ends with id
*
* @return {array} List of identifiers
*/
getIdentifiers() {
const identifiers = [];
const args = this.args;
const keys = Object.keys(args);
for (let arg of keys) {
if (arg.endsWith('Id'))
identifiers.push(arg);
}
return identifiers;
}
async getFileName() {
const args = this.args;
const identifiers = this.getIdentifiers(args);
const name = await this.getName();
const params = [];
params.push(name);
for (let id of identifiers)
params.push(args[id]);
const fileName = params.join('_');
return `${fileName}.pdf`;
}
} }
module.exports = Report; module.exports = Report;

View File

@ -1,43 +1,30 @@
const path = require('path');
const fs = require('fs');
const db = require('./database'); const db = require('./database');
module.exports = app => { module.exports = app => {
const methodsPath = path.resolve(__dirname, '../methods'); const routes = require('../methods/routes');
const methodsDir = fs.readdirSync(methodsPath);
const methods = [];
// Get all methods const paths = routes.map(route => route.url);
for (let method of methodsDir) {
if (method.includes('.js'))
methods.push(method.replace('.js', ''));
}
// Auth middleware
const paths = [];
for (let method of methods)
paths.push(`/api/${method}/*`);
app.use(paths, async function(req, res, next) {
const token = getToken(req);
const query = `SELECT at.id, at.userId, eu.email, u.lang, at.ttl, at.created
FROM salix.AccessToken at
JOIN account.user u ON u.id = at.userid
JOIN account.emailUser eu ON eu.userFk = u.id
WHERE at.id = ?`;
app.use(paths, async function(request, response, next) {
try { try {
const token = getToken(request);
const query = `SELECT at.id, at.userId, eu.email, u.lang, at.ttl, at.created
FROM salix.AccessToken at
JOIN account.user u ON u.id = at.userid
JOIN account.emailUser eu ON eu.userFk = u.id
WHERE at.id = ?`;
const auth = await db.findOne(query, [token]); const auth = await db.findOne(query, [token]);
if (!auth || isTokenExpired(auth.created, auth.ttl)) if (!auth || isTokenExpired(auth.created, auth.ttl))
throw new Error('Invalid authorization token'); throw new Error('Invalid authorization token');
const args = Object.assign({}, req.query); const args = Object.assign({}, request.query);
const props = Object.assign(args, req.body); const props = Object.assign(args, request.body);
props.authorization = auth.id; props.authorization = auth.id;
req.args = props; response.locals = props;
req.args.auth = { response.locals.auth = {
userId: auth.userId, userId: auth.userId,
token: auth.id, token: auth.id,
email: auth.email, email: auth.email,
@ -50,6 +37,10 @@ module.exports = app => {
} }
}); });
// Register routes
for (let route of routes)
app.use(route.url, route.cb);
function getToken(request) { function getToken(request) {
const headers = request.headers; const headers = request.headers;
const queryParams = request.query; const queryParams = request.query;
@ -68,8 +59,4 @@ module.exports = app => {
return false; return false;
} }
// Mount methods
for (let method of methods)
require(`../methods/${method}`)(app);
}; };

View File

@ -25,14 +25,17 @@ module.exports = {
throw err; throw err;
}).finally(async() => { }).finally(async() => {
const attachments = []; const attachments = [];
for (let attachment of options.attachments) { if (options.attachments) {
const fileName = attachment.filename; for (let attachment of options.attachments) {
const filePath = attachment.path; const fileName = attachment.filename;
if (fileName.includes('.png')) return; const filePath = attachment.path;
if (fileName.includes('.png')) return;
if (fileName || filePath) if (fileName || filePath)
attachments.push(filePath ? filePath : fileName); attachments.push(filePath ? filePath : fileName);
}
} }
const fileNames = attachments.join(',\n'); const fileNames = attachments.join(',\n');
await db.rawSql(` await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status) INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)

28
print/core/storage.js Normal file
View File

@ -0,0 +1,28 @@
const config = require('./config.js');
const path = require('path');
const fs = require('fs-extra');
module.exports = {
async write(stream, options) {
const storage = config.storage[options.type];
if (!storage) return;
const src = path.join(storage.root, options.path);
const fileSrc = path.join(src, options.fileName);
await fs.mkdir(src, {recursive: true});
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => writeStream.write(stream));
writeStream.on('finish', () => writeStream.end());
return new Promise(resolve => {
writeStream.on('close', () => resolve());
});
},
load(type, data) {
}
};

View File

@ -7,9 +7,8 @@ class Stylesheet {
} }
mergeStyles() { mergeStyles() {
this.files.forEach(file => { for (const file of this.files)
this.css.push(fs.readFileSync(file)); this.css.push(fs.readFileSync(file));
});
return this.css.join('\n'); return this.css.join('\n');
} }

View File

@ -1,311 +0,0 @@
const db = require('../core/database');
const Email = require('../core/email');
const Report = require('../core/report');
const smtp = require('../core/smtp');
const config = require('../core/config');
module.exports = app => {
app.get('/api/closure/all', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.to)
throw new Error('The argument to is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.to, reqArgs.to]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, req.args);
await db.rawSql(`
UPDATE ticket t
JOIN ticketState ts ON t.id = ts.ticketFk
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND al.code NOT IN('DELIVERED','PACKED')
AND t.routeFk
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-ticket', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.id = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-agency', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.agencyModeId)
throw new Error('The argument agencyModeId is required');
if (!reqArgs.warehouseId)
throw new Error('The argument warehouseId is required');
if (!reqArgs.to)
throw new Error('The argument to is required');
res.status(200).json({
message: 'Task executed successfully'
});
const agenciesId = reqArgs.agencyModeId.split(',');
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [
agenciesId,
reqArgs.warehouseId,
reqArgs.to,
reqArgs.to
]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
} catch (error) {
next(error);
}
});
app.get('/api/closure/by-route', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.routeId)
throw new Error('The argument routeId is required');
res.status(200).json({
message: 'Task executed successfully'
});
const tickets = await db.rawSql(`
SELECT
t.id
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.routeFk = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
// Send route report to the agency
const agencyMail = await db.findValue(`
SELECT am.reportMail
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id = ?`, [reqArgs.routeId]);
if (agencyMail) {
const args = Object.assign({
routeId: reqArgs.routeId,
recipient: agencyMail
}, reqArgs);
const email = new Email('driver-route', args);
await email.send();
}
} catch (error) {
next(error);
}
});
async function closeAll(ticketIds, reqArgs) {
const failedtickets = [];
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM ticket t
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE t.id IN(?)`, [ticketIds]);
for (const ticket of tickets) {
try {
await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]);
if (!ticket.salesPersonFk || !ticket.isToBeMailed) continue;
if (!ticket.recipient) {
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk}</strong> porque no tiene un email especificado.<br/><br/>
Para dejar de recibir esta notificación, asígnale un email o desactiva
la notificación por email para este cliente.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
continue;
}
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
if (hasToInvoice) {
const invoice = await db.findOne(`
SELECT io.id, io.ref, io.serial, cny.code companyCode
FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ?
`, [ticket.id]);
const args = Object.assign({
invoiceId: invoice.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
let mailOptions = {};
if (invoice.serial == 'E' && invoice.companyCode == 'VNL') {
const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream();
const fileName = `exportation-${invoice.ref}.pdf`;
mailOptions = {
overrideAttachments: false,
attachments: [{
filename: fileName,
content: stream
}]
};
}
const email = new Email('invoice', args);
await email.send(mailOptions);
} else {
const args = Object.assign({
ticketId: ticket.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const email = new Email('delivery-note-link', args);
await email.send();
}
} catch (error) {
// Domain not found
if (error.responseCode == 450)
return invalidEmail(ticket);
// Save tickets on a list of failed ids
failedtickets.push({
id: ticket.id,
stacktrace: error
});
}
}
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>';
for (ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
smtp.send({
to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report',
html: body
});
}
}
async function invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk
]);
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
]);
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};

View File

@ -0,0 +1,58 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.to)
throw new Error('The argument to is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM ticket t
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY t.id`, [reqArgs.to, reqArgs.to]);
await closure.start(tickets, response.locals);
await db.rawSql(`
UPDATE ticket t
JOIN ticketState ts ON t.id = ts.ticketFk
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND al.code NOT IN('DELIVERED','PACKED')
AND t.routeFk
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,58 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.agencyModeId)
throw new Error('The argument agencyModeId is required');
if (!reqArgs.warehouseId)
throw new Error('The argument warehouseId is required');
if (!reqArgs.to)
throw new Error('The argument to is required');
response.status(200).json({
message: 'Success'
});
const agencyIds = reqArgs.agencyModeId.split(',');
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [
agencyIds,
reqArgs.warehouseId,
reqArgs.to,
reqArgs.to
]);
await closure.start(tickets, response.locals);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,61 @@
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.routeId)
throw new Error('The argument routeId is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.routeFk = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.routeId]);
await closure.start(tickets, response.locals);
// Send route report to the agency
const agencyMail = await db.findValue(`
SELECT am.reportMail
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id = ?`, [reqArgs.routeId]);
if (agencyMail) {
const args = Object.assign({
routeId: Number.parseInt(reqArgs.routeId),
recipient: agencyMail
}, response.locals);
const email = new Email('driver-route', args);
await email.send();
}
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,43 @@
const db = require('vn-print/core/database');
const closure = require('./closure');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
response.status(200).json({
message: 'Success'
});
const tickets = await db.rawSql(`
SELECT
t.id,
t.clientFk,
c.name clientName,
c.email recipient,
c.salesPersonFk,
c.isToBeMailed,
c.hasToInvoice,
co.hasDailyInvoice,
eu.email salesPersonEmail
FROM expedition e
JOIN ticket t ON t.id = e.ticketFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE al.code = 'PACKED'
AND t.id = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
await closure.start(tickets, response.locals);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,153 @@
const db = require('vn-print/core/database');
const Report = require('vn-print/core/report');
const Email = require('vn-print/core/email');
const smtp = require('vn-print/core/smtp');
const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage');
module.exports = {
async start(tickets, reqArgs) {
if (tickets.length == 0) return;
const failedtickets = [];
for (const ticket of tickets) {
try {
await db.rawSql('START TRANSACTION');
await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]);
const invoiceOut = await db.findOne(`
SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ?
`, [ticket.id]);
const mailOptions = {
overrideAttachments: true,
attachments: []
};
const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
if (invoiceOut) {
const args = Object.assign({
invoiceId: invoiceOut.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const invoiceReport = new Report('invoice', args);
const stream = await invoiceReport.toPdfStream();
const issued = invoiceOut.issued;
const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString();
const fileName = `${year}${invoiceOut.ref}.pdf`;
// Store invoice
storage.write(stream, {
type: 'invoice',
path: `${year}/${month}/${day}`,
fileName: fileName
});
await db.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id]);
if (isToBeMailed) {
const invoiceAttachment = {
filename: fileName,
content: stream
};
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({
filename: fileName,
content: stream
});
}
mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args);
await email.send(mailOptions);
}
} else if (isToBeMailed) {
const args = Object.assign({
ticketId: ticket.id,
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const email = new Email('delivery-note-link', args);
await email.send();
}
await db.rawSql('COMMIT');
} catch (error) {
await db.rawSql('ROLLBACK');
// Domain not found
if (error.responseCode == 450)
return invalidEmail(ticket);
// Save tickets on a list of failed ids
failedtickets.push({
id: ticket.id,
stacktrace: error
});
}
}
// Send email with failed tickets
if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>';
for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
}
smtp.send({
to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report',
html: body
});
}
},
async invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk
]);
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
]);
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`;
smtp.send({
to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán',
html: body
});
}
};

View File

@ -0,0 +1,9 @@
const express = require('express');
const router = new express.Router();
router.get('/all', require('./closeAll'));
router.get('/by-ticket', require('./closeByTicket'));
router.get('/by-agency', require('./closeByAgency'));
router.get('/by-route', require('./closeByRoute'));
module.exports = router;

View File

@ -1,31 +0,0 @@
module.exports = app => {
app.use('/api/csv/delivery-note', require('./csv/delivery-note')(app));
app.use('/api/csv/invoice', require('./csv/invoice')(app));
app.toCSV = function toCSV(rows) {
const [columns] = rows;
let content = Object.keys(columns).join('\t');
for (let row of rows) {
const values = Object.values(row);
const finalValues = values.map(value => {
if (value instanceof Date) return formatDate(value);
if (value === null) return '';
return value;
});
content += '\n';
content += finalValues.join('\t');
}
return content;
};
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
};

31
print/methods/csv/csv.js Normal file
View File

@ -0,0 +1,31 @@
function toCSV(rows) {
const [columns] = rows;
let content = Object.keys(columns).join('\t');
for (let row of rows) {
const values = Object.values(row);
const finalValues = values.map(value => {
if (value instanceof Date) return formatDate(value);
if (value === null) return '';
return value;
});
content += '\n';
content += finalValues.join('\t');
}
return content;
}
function formatDate(date) {
return new Intl.DateTimeFormat('es', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(date);
}
module.exports = {
toCSV,
formatDate
};

View File

@ -0,0 +1,24 @@
const path = require('path');
const db = require('vn-print/core/database');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
response.setHeader('Content-type', 'text/csv');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(content);
} catch (error) {
next(error);
}
};

View File

@ -1,82 +0,0 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const args = Object.assign({
ticketId: (String(ticket.id)),
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
const email = new Email('delivery-note', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,40 @@
const path = require('path');
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.ticketId)
throw new Error('The argument ticketId is required');
const ticketId = reqArgs.ticketId;
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
const args = Object.assign({
ticketId: (String(ticket.id)),
recipientId: ticket.clientFk,
recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail
}, response.locals);
const content = toCSV(sales);
const fileName = `ticket_${ticketId}.csv`;
const email = new Email('delivery-note', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
response.status(200).json({message: 'Success'});
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,9 @@
const express = require('express');
const router = new express.Router();
router.get('/delivery-note/download', require('./delivery-note/download'));
router.get('/delivery-note/send', require('./delivery-note/send'));
router.get('/invoice/download', require('./invoice/download'));
router.get('/invoice/send', require('./invoice/send'));
module.exports = router;

View File

@ -0,0 +1,24 @@
const path = require('path');
const db = require('vn-print/core/database');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
response.setHeader('Content-type', 'text/csv');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(content);
} catch (error) {
next(error);
}
};

View File

@ -1,82 +0,0 @@
const express = require('express');
const router = new express.Router();
const path = require('path');
const db = require('../../../core/database');
const sqlPath = path.join(__dirname, 'sql');
module.exports = app => {
router.get('/preview', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'application/json; charset=utf-8');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
router.get('/download', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
res.setHeader('Content-type', 'text/csv');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(content);
} catch (error) {
next(error);
}
});
const Email = require('../../../core/email');
router.get('/send', async function(req, res, next) {
try {
const reqArgs = req.args;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const args = Object.assign({
invoiceId: (String(invoice.id)),
recipientId: invoice.clientFk,
recipient: invoice.recipient,
replyTo: invoice.salesPersonEmail
}, reqArgs);
const content = app.toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
const email = new Email('invoice', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
res.status(200).json({message: 'ok'});
} catch (error) {
next(error);
}
});
return router;
};

View File

@ -0,0 +1,40 @@
const path = require('path');
const db = require('vn-print/core/database');
const Email = require('vn-print/core/email');
const {toCSV} = require('../csv');
const sqlPath = path.join(__dirname, 'sql');
module.exports = async function(request, response, next) {
try {
const reqArgs = request.query;
if (!reqArgs.invoiceId)
throw new Error('The argument invoiceId is required');
const invoiceId = reqArgs.invoiceId;
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
const args = Object.assign({
invoiceId: (String(invoice.id)),
recipientId: invoice.clientFk,
recipient: invoice.recipient,
replyTo: invoice.salesPersonEmail
}, response.locals);
const content = toCSV(sales);
const fileName = `invoice_${invoiceId}.csv`;
const email = new Email('invoice', args);
await email.send({
overrideAttachments: true,
attachments: [{
filename: fileName,
content: content
}]
});
response.status(200).json({message: 'Success'});
} catch (error) {
next(error);
}
};

View File

@ -1,33 +0,0 @@
const Email = require('../core/email');
module.exports = app => {
app.get(`/api/email/:name`, async(req, res, next) => {
try {
const reportName = req.params.name;
const email = new Email(reportName, req.args);
await email.send();
res.status(200).json({
message: 'Sent'
});
} catch (e) {
next(e);
}
});
app.get(`/api/email/:name/preview`, async(req, res, next) => {
try {
const reportName = req.params.name;
const args = req.args;
args.isPreview = true;
const email = new Email(reportName, args);
const rendered = await email.render();
res.send(rendered);
} catch (e) {
next(e);
}
});
};

View File

@ -0,0 +1,16 @@
const Email = require('vn-print/core/email');
module.exports = async function(request, response, next) {
try {
const templateName = request.params.name;
const args = response.locals;
const email = new Email(templateName, args);
await email.send();
response.status(200).json({
message: 'Sent'
});
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,7 @@
const express = require('express');
const router = new express.Router();
router.get('/:name', require('./email'));
router.get('/:name/preview', require('./preview'));
module.exports = router;

View File

@ -0,0 +1,14 @@
const Email = require('vn-print/core/email');
module.exports = async function(request, response, next) {
try {
const templateName = request.params.name;
const args = Object.assign({isPreview: true}, response.locals);
const email = new Email(templateName, args);
const template = await email.render();
response.send(template);
} catch (error) {
next(error);
}
};

View File

@ -1,54 +0,0 @@
const Report = require('../core/report');
module.exports = app => {
app.get(`/api/report/:name`, async(req, res, next) => {
try {
const reportName = req.params.name;
const fileName = getFileName(reportName, req.args);
const report = new Report(reportName, req.args);
if (req.args.preview) {
const template = await report.render();
res.send(template);
} else {
const stream = await report.toPdfStream();
res.setHeader('Content-type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(stream);
}
} catch (error) {
next(error);
}
});
/**
* Returns all the params that ends with id
* @param {object} args - Params object
*
* @return {array} List of identifiers
*/
function getIdentifiers(args) {
const identifiers = [];
const keys = Object.keys(args);
for (let arg of keys) {
if (arg.endsWith('Id'))
identifiers.push(arg);
}
return identifiers;
}
function getFileName(name, args) {
const identifiers = getIdentifiers(args);
const params = [];
params.push(name);
for (let id of identifiers)
params.push(args[id]);
const fileName = params.join('_');
return `${fileName}.pdf`;
}
};

View File

@ -0,0 +1,17 @@
const Report = require('vn-print/core/report');
module.exports = async function(request, response, next) {
try {
const reportName = request.params.name;
const args = response.locals;
const report = new Report(reportName, args);
const stream = await report.toPdfStream();
const fileName = await report.getFileName();
response.setHeader('Content-type', 'application/pdf');
response.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
response.end(stream);
} catch (error) {
next(error);
}
};

View File

@ -0,0 +1,7 @@
const express = require('express');
const router = new express.Router();
router.get('/:name', require('./document'));
router.get('/:name/preview', require('./preview'));
module.exports = router;

View File

@ -0,0 +1,13 @@
const Report = require('vn-print/core/report');
module.exports = async function(request, response, next) {
try {
const reportName = request.params.name;
const report = new Report(reportName, request.query);
const template = await report.render();
response.send(template);
} catch (error) {
next(error);
}
};

18
print/methods/routes.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = [
{
url: '/api/report',
cb: require('./report')
},
{
url: '/api/email',
cb: require('./email')
},
{
url: '/api/csv',
cb: require('./csv')
},
{
url: '/api/closure',
cb: require('./closure')
},
];

1676
print/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
from: { from: {

View File

@ -10,6 +10,7 @@ module.exports = {
}, },
props: { props: {
claimId: { claimId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -16,6 +16,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
from: { from: {

View File

@ -18,6 +18,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -10,6 +10,7 @@ module.exports = {
}, },
props: { props: {
ticketId: { ticketId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -10,7 +10,7 @@ module.exports = {
}, },
props: { props: {
ticketId: { ticketId: {
type: String, type: [Number, String],
required: true required: true
} }
} }

View File

@ -10,7 +10,7 @@ module.exports = {
}, },
props: { props: {
routeId: { routeId: {
type: String, type: [Number, String],
required: true required: true
} }
} }

View File

@ -18,7 +18,7 @@ module.exports = {
}, },
props: { props: {
invoiceId: { invoiceId: {
type: String, type: [Number, String],
required: true required: true
} }
} }

View File

@ -30,9 +30,11 @@ module.exports = {
required: true required: true
}, },
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
companyId: { companyId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -1,5 +1,4 @@
const Component = require(`${appPath}/core/component`); const Component = require(`${appPath}/core/component`);
const db = require(`${appPath}/core/database`);
const emailHeader = new Component('email-header'); const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer'); const emailFooter = new Component('email-footer');
const attachment = new Component('attachment'); const attachment = new Component('attachment');
@ -28,9 +27,11 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
companyId: { companyId: {
type: [Number, String],
required: true required: true
}, },
} }

View File

@ -26,6 +26,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -24,6 +24,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -16,9 +16,11 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
companyId: { companyId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -21,6 +21,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
from: { from: {

View File

@ -25,6 +25,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
from: { from: {

View File

@ -1,3 +1,4 @@
reportName: consumo-cliente
title: Consumo title: Consumo
Client: Cliente Client: Cliente
clientData: Datos del cliente clientData: Datos del cliente

View File

@ -32,6 +32,7 @@ module.exports = {
}, },
props: { props: {
claimId: { claimId: {
type: [Number, String],
required: true required: true
} }
} }

View File

@ -1,3 +1,4 @@
reportName: orden-de-recogida
title: Ord. recogida title: Ord. recogida
claimId: Reclamación claimId: Reclamación
clientId: Cliente clientId: Cliente

View File

@ -69,6 +69,7 @@ module.exports = {
}, },
props: { props: {
recipientId: { recipientId: {
type: [Number, String],
required: true required: true
}, },
from: { from: {

View File

@ -1,3 +1,4 @@
reportName: extracto-cliente
title: Extracto title: Extracto
clientId: Cliente clientId: Cliente
clientData: Datos del cliente clientData: Datos del cliente

View File

@ -1,3 +1,4 @@
reportName: releve-de-compte
title: Relevé de compte title: Relevé de compte
clientId: Client clientId: Client
clientData: Données client clientData: Données client

Some files were not shown because too many files have changed in this diff Show More