Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 7917-freelancerRoute2

This commit is contained in:
Carlos Satorres 2025-01-20 12:38:46 +01:00
commit a98967bac1
32 changed files with 428 additions and 109 deletions

View File

@ -158,13 +158,13 @@ INSERT INTO `account`.`mailForward`(`account`, `forwardTo`)
INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`) INSERT INTO `vn`.`currency`(`id`, `code`, `name`, `ratio`, `hasToDownloadRate`)
VALUES VALUES
(1, 'EUR', 'Euro', 1), (1, 'EUR', 'Euro', 1, FALSE),
(2, 'USD', 'Dollar USA', 1.4), (2, 'USD', 'Dollar USA', 1.4, TRUE),
(3, 'GBP', 'Libra', 1), (3, 'GBP', 'Libra', 1, TRUE),
(4, 'JPY', 'Yen Japones', 1), (4, 'JPY', 'Yen Japones', 1, FALSE),
(5, 'CNY', 'Yuan Chino', 1.2); (5, 'CNY', 'Yuan Chino', 1.2, TRUE);
INSERT INTO `vn`.`country`(`id`, `name`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`) INSERT INTO `vn`.`country`(`id`, `name`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES VALUES
@ -694,22 +694,22 @@ INSERT INTO `vn`.`invoiceOutExpense`(`id`, `invoiceOutFk`, `amount`, `expenseFk`
(6, 4, 8.07, 2000000000, util.VN_CURDATE()), (6, 4, 8.07, 2000000000, util.VN_CURDATE()),
(7, 5, 8.07, 2000000000, util.VN_CURDATE()); (7, 5, 8.07, 2000000000, util.VN_CURDATE());
INSERT INTO `vn`.`zone` (`id`, `name`, `hour`, `agencyModeFk`, `travelingDays`, `price`, `bonus`, `itemMaxSize`) INSERT INTO `vn`.`zone`
(`id`, `name`, `hour`, `agencyModeFk`, `travelingDays`, `price`, `bonus`, `itemMaxSize`, `priceOptimum`)
VALUES VALUES
(1, 'Zone pickup A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100), (1, 'Zone pickup A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100, 1),
(2, 'Zone pickup B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100), (2, 'Zone pickup B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100, 1),
(3, 'Zone 247 A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 7, 1, 2, 0, 100), (3, 'Zone 247 A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 7, 1, 2, 0, 100, 1),
(4, 'Zone 247 B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 7, 1, 2, 0, 100), (4, 'Zone 247 B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 7, 1, 2, 0, 100, 1),
(5, 'Zone expensive A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 8, 1, 1000, 0, 100), (5, 'Zone expensive A', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 8, 1, 1000, 0, 100, 500),
(6, 'Zone expensive B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 8, 1, 1000, 0, 100), (6, 'Zone expensive B', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 8, 1, 1000, 0, 100, 500),
(7, 'Zone refund', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 23, 0, 1, 0, 100), (7, 'Zone refund', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 23, 0, 1, 0, 100, 0.5),
(8, 'Zone others', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 10, 0, 1, 0, 100), (8, 'Zone others', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 10, 0, 1, 0, 100, 0.5),
(9, 'Zone superMan', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 2, 0, 1, 0, 100), (9, 'Zone superMan', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 2, 0, 1, 0, 100, 0.5),
(10, 'Zone teleportation', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 3, 0, 1, 0, 100), (10, 'Zone teleportation', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 3, 0, 1, 0, 100, 0.5),
(11, 'Zone pickup C', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100), (11, 'Zone pickup C', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 1, 0, 1, 0, 100, 0.5),
(12, 'Zone entanglement', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 4, 0, 1, 0, 100), (12, 'Zone entanglement', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 4, 0, 1, 0, 100, 0.5),
(13, 'Zone quantum break', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 5, 0, 1, 0, 100); (13, 'Zone quantum break', CONCAT(util.VN_CURDATE(), ' ', TIME('23:59')), 5, 0, 1, 0, 100, 0.5);
INSERT INTO `vn`.`zoneWarehouse` (`id`, `zoneFk`, `warehouseFk`) INSERT INTO `vn`.`zoneWarehouse` (`id`, `zoneFk`, `warehouseFk`)
VALUES VALUES

View File

@ -0,0 +1,8 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` EVENT `vn`.`client_setPackagesDiscountFactor`
ON SCHEDULE EVERY 1 DAY
STARTS '2024-10-18 03:00:00.000'
ON COMPLETION PRESERVE
ENABLE
DO CALL client_setPackagesDiscountFactor()$$
DELIMITER ;

View File

@ -231,7 +231,19 @@ BEGIN
SELECT tcc.warehouseFK, SELECT tcc.warehouseFK,
tcc.itemFk, tcc.itemFk,
c2.id, c2.id,
z.inflation * ROUND(ic.cm3delivery * (IFNULL(zo.price,5000) - IFNULL(zo.bonus,0)) / (1000 * vc.standardFlowerBox) , 4) cost z.inflation
* ROUND(
ic.cm3delivery
* (
(
zo.priceOptimum + (( zo.price - zo.priceOptimum) * 2 * ( 1 - c.packagesDiscountFactor))
)
- IFNULL(zo.bonus, 0)
)
/ (1000 * vc.standardFlowerBox),
4
) cost
FROM tmp.ticketComponentCalculate tcc FROM tmp.ticketComponentCalculate tcc
JOIN item i ON i.id = tcc.itemFk JOIN item i ON i.id = tcc.itemFk
JOIN tmp.zoneOption zo ON zo.zoneFk = vZoneFk JOIN tmp.zoneOption zo ON zo.zoneFk = vZoneFk
@ -239,6 +251,7 @@ BEGIN
JOIN agencyMode am ON am.id = z.agencyModeFk JOIN agencyMode am ON am.id = z.agencyModeFk
JOIN vn.volumeConfig vc JOIN vn.volumeConfig vc
JOIN vn.component c2 ON c2.code = 'delivery' JOIN vn.component c2 ON c2.code = 'delivery'
JOIN `client` c on c.id = vClientFk
LEFT JOIN itemCost ic ON ic.warehouseFk = tcc.warehouseFk LEFT JOIN itemCost ic ON ic.warehouseFk = tcc.warehouseFk
AND ic.itemFk = tcc.itemFk AND ic.itemFk = tcc.itemFk
HAVING cost <> 0; HAVING cost <> 0;

View File

@ -0,0 +1,25 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost`
PROCEDURE `vn`.`client_setPackagesDiscountFactor`()
BEGIN
/**
* Set the discount factor for the packages of the clients.
*/
UPDATE client c
JOIN (
SELECT t.clientFk,
LEAST((
SUM(t.packages) / COUNT(DISTINCT DATE(t.shipped))
) / cc.packagesOptimum, 1) discountFactor
FROM ticket t
JOIN clientConfig cc ON TRUE
WHERE t.shipped > util.VN_CURDATE() - INTERVAL cc.monthsToCalcOptimumPrice MONTH
AND t.packages
GROUP BY t.clientFk
) ca ON c.id = ca.clientFk
SET c.packagesDiscountFactor = ca.discountFactor;
END$$
DELIMITER ;

View File

@ -1,26 +1,27 @@
DELIMITER $$ DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`zone_getAddresses`( CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`zone_getAddresses`(
vSelf INT, vSelf INT,
vShipped DATE, vLanded DATE,
vDepartmentFk INT vDepartmentFk INT
) )
BEGIN BEGIN
/** /**
* Devuelve un listado de todos los clientes activos * Devuelve un listado de todos los clientes activos
* con consignatarios a los que se les puede * con consignatarios a los que se les puede
* vender producto para esa zona. * entregar producto para esa zona.
* *
* @param vSelf Id de zona * @param vSelf Id de zona
* @param vShipped Fecha de envio * @param vLanded Fecha de entrega
* @param vDepartmentFk Id de departamento * @param vDepartmentFk Id de departamento | NULL para mostrar todos
* @return Un select * @return Un select
*/ */
CALL zone_getPostalCode(vSelf); CALL zone_getPostalCode(vSelf);
WITH clientWithTicket AS ( WITH clientWithTicket AS (
SELECT clientFk SELECT DISTINCT clientFk
FROM vn.ticket FROM vn.ticket
WHERE shipped BETWEEN vShipped AND util.dayEnd(vShipped) WHERE landed BETWEEN vLanded AND util.dayEnd(vLanded)
AND NOT isDeleted
) )
SELECT c.id, SELECT c.id,
c.name, c.name,
@ -30,7 +31,7 @@ BEGIN
u.name username, u.name username,
aai.invoiced, aai.invoiced,
cnb.lastShipped, cnb.lastShipped,
cwt.clientFk IF(cwt.clientFk, TRUE, FALSE) hasTicket
FROM vn.client c FROM vn.client c
JOIN vn.worker w ON w.id = c.salesPersonFk JOIN vn.worker w ON w.id = c.salesPersonFk
JOIN vn.workerDepartment wd ON wd.workerFk = w.id JOIN vn.workerDepartment wd ON wd.workerFk = w.id
@ -50,7 +51,7 @@ BEGIN
AND c.isActive AND c.isActive
AND ct.code = 'normal' AND ct.code = 'normal'
AND bt.code <> 'worker' AND bt.code <> 'worker'
AND (d.id = vDepartmentFk OR NOT vDepartmentFk) AND (d.id = vDepartmentFk OR vDepartmentFk IS NULL)
GROUP BY c.id; GROUP BY c.id;
DROP TEMPORARY TABLE tmp.zoneNodes; DROP TEMPORARY TABLE tmp.zoneNodes;

View File

@ -30,6 +30,7 @@ BEGIN
TIME(IFNULL(e.`hour`, z.`hour`)) `hour`, TIME(IFNULL(e.`hour`, z.`hour`)) `hour`,
l.travelingDays, l.travelingDays,
IFNULL(e.price, z.price) price, IFNULL(e.price, z.price) price,
IFNULL(e.priceOptimum, z.priceOptimum) priceOptimum,
IFNULL(e.bonus, z.bonus) bonus, IFNULL(e.bonus, z.bonus) bonus,
l.landed, l.landed,
vShipped shipped vShipped shipped

View File

@ -0,0 +1,5 @@
ALTER TABLE `vn`.`zoneEvent`
ADD COLUMN `priceOptimum` DECIMAL(10,2) NULL COMMENT 'Precio mínimo que puede pagar un bulto'
AFTER `price`,
ADD CONSTRAINT `ck_zoneEvent_priceOptimum`
CHECK (priceOptimum <= price)

View File

@ -0,0 +1,5 @@
ALTER TABLE `vn`.`zone`
ADD COLUMN `priceOptimum` DECIMAL(10,2) NOT NULL COMMENT 'Precio mínimo que puede pagar un bulto'
AFTER `price`,
ADD CONSTRAINT `ck_zone_priceOptimum`
CHECK (priceOptimum <= price)

View File

@ -0,0 +1,2 @@
UPDATE `vn`.`zone`
SET `priceOptimum` = `price`;

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`client`
ADD COLUMN `packagesDiscountFactor` DECIMAL(4,3) NOT NULL DEFAULT 1.000
COMMENT 'Porcentaje de ajuste entre el numero de bultos medio del cliente, y el número medio óptimo para las zonas en las que compra';

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`clientConfig`
ADD COLUMN `packagesOptimum` INT UNSIGNED NOT NULL DEFAULT 20 COMMENT 'Numero de bultos por cliente/dia para conseguir el precio optimo',
ADD COLUMN `monthsToCalcOptimumPrice` TINYINT UNSIGNED NOT NULL DEFAULT 3 COMMENT 'Número de meses a usar para el cálculo de client.packagesDiscountFactor';

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`entry`
ADD COLUMN `initialTemperature` decimal(10,2) DEFAULT NULL COMMENT 'Temperatura de como lo recibimos del proveedor ej. en colombia',
ADD COLUMN `finalTemperature` decimal(10,2) DEFAULT NULL COMMENT 'Temperatura final de como llega a nuestras instalaciones';

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`currency`
ADD COLUMN `hasToDownloadRate` TINYINT(1) NOT NULL DEFAULT 0 comment 'Si se guarda el tipo de cambio diariamente en referenceRate';

View File

@ -0,0 +1,3 @@
UPDATE `vn`.`currency`
SET `hasToDownloadRate` = TRUE
WHERE `code` IN ('USD', 'CNY', 'GBP');

View File

@ -0,0 +1,2 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId)
VALUES ('VnUser','adminUser','WRITE','ALLOW','ROLE','sysadmin');

View File

@ -0,0 +1,2 @@
RENAME TABLE bi.f_tvc TO bi.f_tvc__;
ALTER TABLE bi.f_tvc__ COMMENT='@deprecated 2025-01-15';

View File

@ -0,0 +1 @@
CREATE INDEX ticket_landed_IDX USING BTREE ON vn.ticket (landed);

View File

@ -398,5 +398,6 @@
"Holidays to past days not available": "Las vacaciones a días pasados no están disponibles", "Holidays to past days not available": "Las vacaciones a días pasados no están disponibles",
"All tickets have a route order": "Todos los tickets tienen orden de ruta", "All tickets have a route order": "Todos los tickets tienen orden de ruta",
"Price cannot be blank": "Price cannot be blank", "Price cannot be blank": "Price cannot be blank",
"There are tickets to be invoiced": "La zona tiene tickets por facturar" "There are tickets to be invoiced": "La zona tiene tickets por facturar",
"Social name should be uppercase": "La razón social debe ir en mayúscula"
} }

View File

@ -52,6 +52,14 @@ module.exports = function(Self) {
arg: 'customsAgentFk', arg: 'customsAgentFk',
type: 'number' type: 'number'
}, },
{
arg: 'longitude',
type: 'number'
},
{
arg: 'latitude',
type: 'number'
},
{ {
arg: 'isActive', arg: 'isActive',
type: 'boolean' type: 'boolean'

View File

@ -94,7 +94,7 @@ module.exports = Self => {
AND r1.started = r2.maxStarted AND r1.started = r2.maxStarted
) r ON r.clientFk = c.id ) r ON r.clientFk = c.id
LEFT JOIN workerDepartment wd ON wd.workerFk = u.id LEFT JOIN workerDepartment wd ON wd.workerFk = u.id
JOIN department dp ON dp.id = wd.departmentFk LEFT JOIN department dp ON dp.id = wd.departmentFk
WHERE WHERE
d.created = ? d.created = ?
AND d.amount > 0 AND d.amount > 0

View File

@ -19,6 +19,9 @@
}, },
"created": { "created": {
"type": "date" "type": "date"
},
"workerFk": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -119,6 +119,16 @@ module.exports = Self => {
arg: 'invoiceAmount', arg: 'invoiceAmount',
type: 'number', type: 'number',
description: `The invoice amount` description: `The invoice amount`
},
{
arg: 'initialTemperature',
type: 'number',
description: 'Initial temperature value'
},
{
arg: 'finalTemperature',
type: 'number',
description: 'Final temperature value'
} }
], ],
returns: { returns: {
@ -170,6 +180,10 @@ module.exports = Self => {
case 'invoiceInFk': case 'invoiceInFk':
param = `e.${param}`; param = `e.${param}`;
return {[param]: value}; return {[param]: value};
case 'initialTemperature':
return {'e.initialTemperature': {lte: value}};
case 'finalTemperature':
return {'e.finalTemperature': {gte: value}};
} }
}); });
filter = mergeFilters(ctx.args.filter, {where}); filter = mergeFilters(ctx.args.filter, {where});
@ -204,6 +218,8 @@ module.exports = Self => {
e.gestDocFk, e.gestDocFk,
e.invoiceInFk, e.invoiceInFk,
e.invoiceAmount, e.invoiceAmount,
e.initialTemperature,
e.finalTemperature,
t.landed, t.landed,
s.name supplierName, s.name supplierName,
s.nickname supplierAlias, s.nickname supplierAlias,

View File

@ -68,6 +68,12 @@
}, },
"invoiceAmount": { "invoiceAmount": {
"type": "number" "type": "number"
},
"initialTemperature": {
"type": "number"
},
"finalTemperature": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -13,66 +13,114 @@ module.exports = Self => {
} }
}); });
Self.exchangeRateUpdate = async() => { Self.exchangeRateUpdate = async(options = {}) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const response = await axios.get('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml'); const response = await axios.get('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml');
const xmlData = response.data; const xmlData = response.data;
const doc = new DOMParser({errorHandler: {warning: () => {}}})?.parseFromString(xmlData, 'text/xml'); const doc = new DOMParser({errorHandler: {warning: () => {}}})
.parseFromString(xmlData, 'text/xml');
const cubes = doc?.getElementsByTagName('Cube'); const cubes = doc?.getElementsByTagName('Cube');
if (!cubes || cubes.length === 0) if (!cubes || cubes.length === 0)
throw new UserError('No cubes found. Exiting the method.'); throw new UserError('No cubes found. Exiting the method.');
const models = Self.app.models; const currencies = await models.Currency.find({where: {hasToDownloadRate: true}}, myOptions);
const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'}, myOptions);
const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'});
const maxDate = maxDateRecord?.dated ? new Date(maxDateRecord.dated) : null; const maxDate = maxDateRecord?.dated ? new Date(maxDateRecord.dated) : null;
let lastProcessedDate = maxDate;
for (const cube of Array.from(cubes)) { for (const cube of Array.from(cubes)) {
if (cube.nodeType === doc.ELEMENT_NODE && cube.attributes.getNamedItem('time')) { if (cube.nodeType === doc.ELEMENT_NODE && cube.attributes.getNamedItem('time')) {
const xmlDate = new Date(cube.getAttribute('time')); const xmlDate = new Date(cube.getAttribute('time'));
const xmlDateWithoutTime = new Date(xmlDate.getFullYear(), xmlDate.getMonth(), xmlDate.getDate()); const xmlDateWithoutTime = new Date(
if (!maxDate || maxDate < xmlDateWithoutTime) { xmlDate.getFullYear(),
for (const rateCube of Array.from(cube.childNodes)) { xmlDate.getMonth(),
if (rateCube.nodeType === doc.ELEMENT_NODE) { xmlDate.getDate()
const currencyCode = rateCube.getAttribute('currency'); );
const rate = rateCube.getAttribute('rate');
if (['USD', 'CNY', 'GBP'].includes(currencyCode)) {
const currency = await models.Currency.findOne({where: {code: currencyCode}});
if (!currency) throw new UserError(`Currency not found for code: ${currencyCode}`);
const existingRate = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: xmlDate}
});
if (existingRate) { if (!maxDate || xmlDateWithoutTime > maxDate) {
if (existingRate.value !== rate) if (lastProcessedDate && xmlDateWithoutTime > lastProcessedDate) {
await existingRate.updateAttributes({value: rate}); for (const currency of currencies) {
} else { await fillMissingDates(
await models.ReferenceRate.create({ models, currency, lastProcessedDate, xmlDateWithoutTime, myOptions
currencyFk: currency.id,
dated: xmlDate,
value: rate
});
}
const monday = 1;
if (xmlDateWithoutTime.getDay() === monday) {
const saturday = new Date(xmlDateWithoutTime);
saturday.setDate(xmlDateWithoutTime.getDate() - 2);
const sunday = new Date(xmlDateWithoutTime);
sunday.setDate(xmlDateWithoutTime.getDate() - 1);
for (const date of [saturday, sunday]) {
await models.ReferenceRate.upsertWithWhere(
{currencyFk: currency.id, dated: date},
{currencyFk: currency.id, dated: date, value: rate}
); );
} }
} }
} }
for (const rateCube of Array.from(cube.childNodes)) {
if (rateCube.nodeType === doc.ELEMENT_NODE) {
const currencyCode = rateCube.getAttribute('currency');
const rate = rateCube.getAttribute('rate');
const currency = currencies.find(c => c.code === currencyCode);
if (currency) {
const existingRate = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: xmlDateWithoutTime}
}, myOptions);
if (existingRate) {
if (existingRate.value !== rate)
await existingRate.updateAttributes({value: rate}, myOptions);
} else {
await models.ReferenceRate.create({
currencyFk: currency.id,
dated: xmlDateWithoutTime,
value: rate
}, myOptions);
} }
} }
} }
} }
lastProcessedDate = xmlDateWithoutTime;
}
}
if (tx) await tx.commit();
} catch (error) {
if (tx) await tx.rollback();
throw error;
}
};
async function getLastValidRate(models, currencyId, date, myOptions) {
return models.ReferenceRate.findOne({
where: {currencyFk: currencyId, dated: {lt: date}},
order: 'dated DESC'
}, myOptions);
}
async function fillMissingDates(models, currency, startDate, endDate, myOptions) {
const cursor = new Date(startDate);
cursor.setDate(cursor.getDate() + 1);
while (cursor < endDate) {
const existingRate = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: cursor}
}, myOptions);
if (!existingRate) {
const lastValid = await getLastValidRate(models, currency.id, cursor, myOptions);
if (lastValid) {
await models.ReferenceRate.create({
currencyFk: currency.id,
dated: new Date(cursor),
value: lastValid.value
}, myOptions);
}
}
cursor.setDate(cursor.getDate() + 1);
}
} }
}; };
};

View File

@ -1,52 +1,190 @@
describe('exchangeRateUpdate functionality', function() { describe('exchangeRateUpdate functionality', function() {
const axios = require('axios'); const axios = require('axios');
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
let tx; let options;
beforeEach(function() { function formatYmd(d) {
spyOn(axios, 'get').and.returnValue(Promise.resolve({ const mm = (d.getMonth() + 1).toString().padStart(2, '0');
data: `<Cube> const dd = d.getDate().toString().padStart(2, '0');
<Cube time='2024-04-12'> return `${d.getFullYear()}-${mm}-${dd}`;
}
afterEach(async() => {
await tx.rollback();
});
beforeEach(async() => {
tx = await models.Sale.beginTransaction({});
options = {transaction: tx};
spyOn(axios, 'get').and.returnValue(Promise.resolve({data: ''}));
});
it('should process XML data and create rates', async function() {
const d1 = Date.vnNew();
const d4 = Date.vnNew();
d4.setDate(d4.getDate() + 1);
const xml = `<Cube>
<Cube time='${formatYmd(d1)}'>
<Cube currency='USD' rate='1.1'/> <Cube currency='USD' rate='1.1'/>
<Cube currency='CNY' rate='1.2'/> <Cube currency='CNY' rate='1.2'/>
</Cube> </Cube>
</Cube>` <Cube time='${formatYmd(d4)}'>
})); <Cube currency='USD' rate='1.3'/>
}); </Cube>
</Cube>`;
it('should process XML data and update or create rates in the database', async function() { axios.get.and.returnValue(Promise.resolve({data: xml}));
spyOn(models.ReferenceRate, 'findOne').and.returnValue(Promise.resolve(null)); spyOn(models.ReferenceRate, 'findOne').and.returnValue(Promise.resolve(null));
spyOn(models.ReferenceRate, 'create').and.returnValue(Promise.resolve()); spyOn(models.ReferenceRate, 'create').and.returnValue(Promise.resolve());
await models.InvoiceIn.exchangeRateUpdate(options);
await models.InvoiceIn.exchangeRateUpdate(); expect(models.ReferenceRate.create).toHaveBeenCalledTimes(3);
expect(models.ReferenceRate.create).toHaveBeenCalledTimes(2);
}); });
it('should not create or update rates when no XML data is available', async function() { it('should handle no data', async function() {
axios.get.and.returnValue(Promise.resolve({})); axios.get.and.returnValue(Promise.resolve({}));
spyOn(models.ReferenceRate, 'create'); spyOn(models.ReferenceRate, 'create');
let e;
let thrownError = null;
try { try {
await models.InvoiceIn.exchangeRateUpdate(); await models.InvoiceIn.exchangeRateUpdate(options);
} catch (error) { } catch (err) {
thrownError = error; e = err;
} }
expect(thrownError.message).toBe('No cubes found. Exiting the method.'); expect(e.message).toBe('No cubes found. Exiting the method.');
expect(models.ReferenceRate.create).not.toHaveBeenCalled();
}); });
it('should handle errors gracefully', async function() { it('should handle errors', async function() {
axios.get.and.returnValue(Promise.reject(new Error('Network error'))); axios.get.and.returnValue(Promise.reject(new Error('Network error')));
let error; let e;
try { try {
await models.InvoiceIn.exchangeRateUpdate(); await models.InvoiceIn.exchangeRateUpdate(options);
} catch (e) { } catch (err) {
error = e; e = err;
} }
expect(error).toBeDefined(); expect(e).toBeDefined();
expect(error.message).toBe('Network error'); expect(e.message).toBe('Network error');
});
it('should update existing rate', async function() {
const existingRate = await models.ReferenceRate.findOne({
order: 'id DESC'
}, options);
if (!existingRate) return fail('No ReferenceRate records in DB');
const currency = await models.Currency.findById(existingRate.currencyFk, null, options);
const xml = `<Cube>
<Cube time='${formatYmd(existingRate.dated)}'>
<Cube currency='${currency.code}' rate='2.22'/>
</Cube>
</Cube>`;
axios.get.and.returnValue(Promise.resolve({data: xml}));
await models.InvoiceIn.exchangeRateUpdate(options);
const updatedRate = await models.ReferenceRate.findById(existingRate.id, null, options);
expect(updatedRate.value).toBeCloseTo('2.22');
});
it('should not update if same rate', async function() {
const existingRate = await models.ReferenceRate.findOne({order: 'id DESC'}, options);
if (!existingRate) return fail('No existing ReferenceRate in DB');
const currency = await models.Currency.findById(existingRate.currencyFk, null, options);
const oldValue = existingRate.value;
const xml = `<Cube>
<Cube time='${formatYmd(existingRate.dated)}'>
<Cube currency='${currency.code}' rate='${oldValue}'/>
</Cube>
</Cube>`;
axios.get.and.returnValue(Promise.resolve({data: xml}));
await models.InvoiceIn.exchangeRateUpdate(options);
const updatedRate = await models.ReferenceRate.findById(existingRate.id, null, options);
expect(updatedRate.value).toBe(oldValue);
});
it('should backfill missing dates', async function() {
const lastRate = await models.ReferenceRate.findOne({order: 'dated DESC'}, options);
if (!lastRate) return fail('No existing ReferenceRate data in DB');
const currency = await models.Currency.findById(lastRate.currencyFk, null, options);
const d1 = new Date(lastRate.dated);
d1.setDate(d1.getDate() + 1);
const d4 = new Date(lastRate.dated);
d4.setDate(d4.getDate() + 4);
const xml = `<Cube>
<Cube time='${formatYmd(d1)}'>
<Cube currency='${currency.code}' rate='1.0'/>
</Cube>
<Cube time='${formatYmd(d4)}'>
<Cube currency='${currency.code}' rate='2.0'/>
</Cube>
</Cube>`;
axios.get.and.returnValue(Promise.resolve({data: xml}));
const beforeCount = await models.ReferenceRate.count({}, options);
await models.InvoiceIn.exchangeRateUpdate(options);
const afterCount = await models.ReferenceRate.count({}, options);
expect(afterCount - beforeCount).toBe(4);
});
it('should create entries for day1 and day2 from the feed, and not backfill day3', async function() {
const lastRate = await models.ReferenceRate.findOne({order: 'dated DESC'}, options);
if (!lastRate) return fail('No existing ReferenceRate data in DB');
const currency = await models.Currency.findById(lastRate.currencyFk, null, options);
if (!currency) return fail(`No currency for ID ${lastRate.currencyFk}`);
const day1 = new Date(lastRate.dated);
day1.setDate(day1.getDate() + 1);
const day2 = new Date(lastRate.dated);
day2.setDate(day2.getDate() + 2);
const day3 = new Date(lastRate.dated);
day3.setDate(day3.getDate() + 3);
const xml = `<Cube>
<Cube time='${formatYmd(day1)}'>
<Cube currency='${currency.code}' rate='1.1'/>
</Cube>
<Cube time='${formatYmd(day2)}'>
<Cube currency='${currency.code}' rate='2.2'/>
</Cube>
</Cube>`;
axios.get.and.returnValue(Promise.resolve({data: xml}));
await models.InvoiceIn.exchangeRateUpdate(options);
const day3Record = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: day3}
}, options);
expect(day3Record).toBeNull();
const day1Record = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: day1}
}, options);
const day2Record = await models.ReferenceRate.findOne({
where: {currencyFk: currency.id, dated: day2}
}, options);
expect(day1Record.value).toBeCloseTo('1.1');
expect(day2Record.value).toBeCloseTo('2.2');
}); });
}); });

View File

@ -74,7 +74,8 @@ module.exports = Self => {
AND t.companyFk = ? AND t.companyFk = ?
AND NOT t.isDeleted AND NOT t.isDeleted
GROUP BY IF(c.hasToInvoiceByAddress, a.id, c.id) GROUP BY IF(c.hasToInvoiceByAddress, a.id, c.id)
HAVING SUM(t.totalWithVat) > 0;`; HAVING SUM(t.totalWithVat) > 0
ORDER BY c.id`;
const addresses = await Self.rawSql(query, [ const addresses = await Self.rawSql(query, [
minShipped, minShipped,

View File

@ -28,6 +28,7 @@ module.exports = Self => {
delete args.ctx; delete args.ctx;
if (!args.name) throw new UserError('The social name cannot be empty'); if (!args.name) throw new UserError('The social name cannot be empty');
if (args.name !== args.name.toUpperCase()) throw new UserError('Social name should be uppercase');
const data = {...args, ...{nickname: args.name}}; const data = {...args, ...{nickname: args.name}};
const supplier = await models.Supplier.create(data, myOptions); const supplier = await models.Supplier.create(data, myOptions);

View File

@ -1,4 +1,3 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter; const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters; const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
@ -91,6 +90,11 @@ module.exports = Self => {
arg: 'landed', arg: 'landed',
type: 'date', type: 'date',
description: 'The landed date' description: 'The landed date'
},
{
arg: 'awbFk',
type: 'number',
description: 'The awbFk id'
} }
], ],
returns: { returns: {
@ -168,14 +172,17 @@ module.exports = Self => {
t.totalEntries, t.totalEntries,
t.isRaid, t.isRaid,
t.daysInForward, t.daysInForward,
t.awbFk,
am.name agencyModeName, am.name agencyModeName,
a.code awbCode,
win.name warehouseInName, win.name warehouseInName,
wout.name warehouseOutName, wout.name warehouseOutName,
cnt.code continent cnt.code continent
FROM vn.travel t FROM travel t
JOIN vn.agencyMode am ON am.id = t.agencyModeFk JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN vn.warehouse win ON win.id = t.warehouseInFk JOIN warehouse win ON win.id = t.warehouseInFk
JOIN vn.warehouse wout ON wout.id = t.warehouseOutFk JOIN warehouse wout ON wout.id = t.warehouseOutFk
LEFT JOIN awb a ON a.id = t.awbFk
JOIN warehouse wo ON wo.id = t.warehouseOutFk JOIN warehouse wo ON wo.id = t.warehouseOutFk
JOIN country c ON c.id = wo.countryFk JOIN country c ON c.id = wo.countryFk
LEFT JOIN continent cnt ON cnt.id = c.continentFk) AS t` LEFT JOIN continent cnt ON cnt.id = c.continentFk) AS t`

View File

@ -41,7 +41,9 @@ module.exports = Self => {
* b.stickers)/1000000) AS DECIMAL(10,2)) m3, * b.stickers)/1000000) AS DECIMAL(10,2)) m3,
TRUNCATE(SUM(b.stickers)/(COUNT( b.id) / COUNT( DISTINCT b.id)),0) hb, TRUNCATE(SUM(b.stickers)/(COUNT( b.id) / COUNT( DISTINCT b.id)),0) hb,
CAST(SUM(b.freightValue*b.quantity) AS DECIMAL(10,2)) freightValue, CAST(SUM(b.freightValue*b.quantity) AS DECIMAL(10,2)) freightValue,
CAST(SUM(b.packageValue*b.quantity) AS DECIMAL(10,2)) packageValue CAST(SUM(b.packageValue*b.quantity) AS DECIMAL(10,2)) packageValue,
e.initialTemperature,
e.finalTemperature
FROM vn.travel t FROM vn.travel t
LEFT JOIN vn.entry e ON t.id = e.travelFk LEFT JOIN vn.entry e ON t.id = e.travelFk
LEFT JOIN vn.buy b ON b.entryFk = e.id LEFT JOIN vn.buy b ON b.entryFk = e.id

View File

@ -20,6 +20,9 @@
}, },
"ratio": { "ratio": {
"type": "number" "type": "number"
},
"hasToDownloadRate": {
"type": "boolean"
} }
}, },
"acls": [ "acls": [

View File

@ -42,6 +42,9 @@
"price": { "price": {
"type": "number" "type": "number"
}, },
"priceOptimum": {
"type": "number"
},
"bonus": { "bonus": {
"type": "number" "type": "number"
}, },

View File

@ -28,6 +28,9 @@
"price": { "price": {
"type": "number" "type": "number"
}, },
"priceOptimum": {
"type": "number"
},
"bonus": { "bonus": {
"type": "number" "type": "number"
}, },