diff --git a/db/dump/fixtures.before.sql b/db/dump/fixtures.before.sql
index ff896b84d..788854b4e 100644
--- a/db/dump/fixtures.before.sql
+++ b/db/dump/fixtures.before.sql
@@ -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
- (1, 'EUR', 'Euro', 1),
- (2, 'USD', 'Dollar USA', 1.4),
- (3, 'GBP', 'Libra', 1),
- (4, 'JPY', 'Yen Japones', 1),
- (5, 'CNY', 'Yuan Chino', 1.2);
+ (1, 'EUR', 'Euro', 1, FALSE),
+ (2, 'USD', 'Dollar USA', 1.4, TRUE),
+ (3, 'GBP', 'Libra', 1, TRUE),
+ (4, 'JPY', 'Yen Japones', 1, FALSE),
+ (5, 'CNY', 'Yuan Chino', 1.2, TRUE);
INSERT INTO `vn`.`country`(`id`, `name`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`, `continentFk`, `hasDailyInvoice`, `CEE`)
VALUES
diff --git a/db/versions/11405-blackMoss/00-entryAlter.sql b/db/versions/11405-blackMoss/00-entryAlter.sql
new file mode 100644
index 000000000..3320b9dd3
--- /dev/null
+++ b/db/versions/11405-blackMoss/00-entryAlter.sql
@@ -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';
diff --git a/db/versions/11406-bronzeMoss/00-currrencyAlter.sql b/db/versions/11406-bronzeMoss/00-currrencyAlter.sql
new file mode 100644
index 000000000..86465545e
--- /dev/null
+++ b/db/versions/11406-bronzeMoss/00-currrencyAlter.sql
@@ -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';
diff --git a/db/versions/11406-bronzeMoss/01-currrencyUpdate.sql b/db/versions/11406-bronzeMoss/01-currrencyUpdate.sql
new file mode 100644
index 000000000..5e0882de2
--- /dev/null
+++ b/db/versions/11406-bronzeMoss/01-currrencyUpdate.sql
@@ -0,0 +1,3 @@
+UPDATE `vn`.`currency`
+ SET `hasToDownloadRate` = TRUE
+ WHERE `code` IN ('USD', 'CNY', 'GBP');
diff --git a/db/versions/11410-blackTulip/00-firstScript.sql b/db/versions/11410-blackTulip/00-firstScript.sql
new file mode 100644
index 000000000..e300c4b7c
--- /dev/null
+++ b/db/versions/11410-blackTulip/00-firstScript.sql
@@ -0,0 +1,2 @@
+RENAME TABLE bi.f_tvc TO bi.f_tvc__;
+ALTER TABLE bi.f_tvc__ COMMENT='@deprecated 2025-01-15';
\ No newline at end of file
diff --git a/modules/client/back/methods/client/createAddress.js b/modules/client/back/methods/client/createAddress.js
index 2709632cb..6bd4a26c1 100644
--- a/modules/client/back/methods/client/createAddress.js
+++ b/modules/client/back/methods/client/createAddress.js
@@ -52,6 +52,14 @@ module.exports = function(Self) {
arg: 'customsAgentFk',
type: 'number'
},
+ {
+ arg: 'longitude',
+ type: 'number'
+ },
+ {
+ arg: 'latitude',
+ type: 'number'
+ },
{
arg: 'isActive',
type: 'boolean'
diff --git a/modules/entry/back/methods/entry/filter.js b/modules/entry/back/methods/entry/filter.js
index d7740dd4e..e5eae85fd 100644
--- a/modules/entry/back/methods/entry/filter.js
+++ b/modules/entry/back/methods/entry/filter.js
@@ -119,6 +119,16 @@ module.exports = Self => {
arg: 'invoiceAmount',
type: 'number',
description: `The invoice amount`
+ },
+ {
+ arg: 'initialTemperature',
+ type: 'number',
+ description: 'Initial temperature value'
+ },
+ {
+ arg: 'finalTemperature',
+ type: 'number',
+ description: 'Final temperature value'
}
],
returns: {
@@ -170,6 +180,10 @@ module.exports = Self => {
case 'invoiceInFk':
param = `e.${param}`;
return {[param]: value};
+ case 'initialTemperature':
+ return {'e.initialTemperature': {lte: value}};
+ case 'finalTemperature':
+ return {'e.finalTemperature': {gte: value}};
}
});
filter = mergeFilters(ctx.args.filter, {where});
@@ -204,6 +218,8 @@ module.exports = Self => {
e.gestDocFk,
e.invoiceInFk,
e.invoiceAmount,
+ e.initialTemperature,
+ e.finalTemperature,
t.landed,
s.name supplierName,
s.nickname supplierAlias,
diff --git a/modules/entry/back/models/entry.json b/modules/entry/back/models/entry.json
index 4a09c7d6a..1ff062119 100644
--- a/modules/entry/back/models/entry.json
+++ b/modules/entry/back/models/entry.json
@@ -68,6 +68,12 @@
},
"invoiceAmount": {
"type": "number"
+ },
+ "initialTemperature": {
+ "type": "number"
+ },
+ "finalTemperature": {
+ "type": "number"
}
},
"relations": {
diff --git a/modules/invoiceIn/back/methods/invoice-in/exchangeRateUpdate.js b/modules/invoiceIn/back/methods/invoice-in/exchangeRateUpdate.js
index 989b1d4a2..99ff4cd79 100644
--- a/modules/invoiceIn/back/methods/invoice-in/exchangeRateUpdate.js
+++ b/modules/invoiceIn/back/methods/invoice-in/exchangeRateUpdate.js
@@ -13,66 +13,114 @@ module.exports = Self => {
}
});
- Self.exchangeRateUpdate = async() => {
- const response = await axios.get('http://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml');
- const xmlData = response.data;
-
- const doc = new DOMParser({errorHandler: {warning: () => {}}})?.parseFromString(xmlData, 'text/xml');
- const cubes = doc?.getElementsByTagName('Cube');
- if (!cubes || cubes.length === 0)
- throw new UserError('No cubes found. Exiting the method.');
-
+ Self.exchangeRateUpdate = async(options = {}) => {
const models = Self.app.models;
+ const myOptions = {};
+ let tx;
- const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'});
+ if (typeof options == 'object')
+ Object.assign(myOptions, options);
- const maxDate = maxDateRecord?.dated ? new Date(maxDateRecord.dated) : null;
+ 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 xmlData = response.data;
+
+ const doc = new DOMParser({errorHandler: {warning: () => {}}})
+ .parseFromString(xmlData, 'text/xml');
+ const cubes = doc?.getElementsByTagName('Cube');
+ if (!cubes || cubes.length === 0)
+ throw new UserError('No cubes found. Exiting the method.');
+
+ const currencies = await models.Currency.find({where: {hasToDownloadRate: true}}, myOptions);
+ const maxDateRecord = await models.ReferenceRate.findOne({order: 'dated DESC'}, myOptions);
+ const maxDate = maxDateRecord?.dated ? new Date(maxDateRecord.dated) : null;
+ let lastProcessedDate = maxDate;
+
+ for (const cube of Array.from(cubes)) {
+ if (cube.nodeType === doc.ELEMENT_NODE && cube.attributes.getNamedItem('time')) {
+ const xmlDate = new Date(cube.getAttribute('time'));
+ const xmlDateWithoutTime = new Date(
+ xmlDate.getFullYear(),
+ xmlDate.getMonth(),
+ xmlDate.getDate()
+ );
+
+ if (!maxDate || xmlDateWithoutTime > maxDate) {
+ if (lastProcessedDate && xmlDateWithoutTime > lastProcessedDate) {
+ for (const currency of currencies) {
+ await fillMissingDates(
+ models, currency, lastProcessedDate, xmlDateWithoutTime, myOptions
+ );
+ }
+ }
+ }
- for (const cube of Array.from(cubes)) {
- if (cube.nodeType === doc.ELEMENT_NODE && cube.attributes.getNamedItem('time')) {
- const xmlDate = new Date(cube.getAttribute('time'));
- const xmlDateWithoutTime = new Date(xmlDate.getFullYear(), xmlDate.getMonth(), xmlDate.getDate());
- if (!maxDate || maxDate < xmlDateWithoutTime) {
for (const rateCube of Array.from(cube.childNodes)) {
if (rateCube.nodeType === doc.ELEMENT_NODE) {
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 currency = currencies.find(c => c.code === currencyCode);
+ if (currency) {
const existingRate = await models.ReferenceRate.findOne({
- where: {currencyFk: currency.id, dated: xmlDate}
- });
+ where: {currencyFk: currency.id, dated: xmlDateWithoutTime}
+ }, myOptions);
if (existingRate) {
if (existingRate.value !== rate)
- await existingRate.updateAttributes({value: rate});
+ await existingRate.updateAttributes({value: rate}, myOptions);
} else {
await models.ReferenceRate.create({
currencyFk: currency.id,
- dated: xmlDate,
+ dated: xmlDateWithoutTime,
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}
- );
- }
+ }, 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);
+ }
+ }
};
diff --git a/modules/invoiceIn/back/methods/invoice-in/specs/exchangeRateUpdate.spec.js b/modules/invoiceIn/back/methods/invoice-in/specs/exchangeRateUpdate.spec.js
index 0fd7ea165..c3dcca5ae 100644
--- a/modules/invoiceIn/back/methods/invoice-in/specs/exchangeRateUpdate.spec.js
+++ b/modules/invoiceIn/back/methods/invoice-in/specs/exchangeRateUpdate.spec.js
@@ -1,52 +1,190 @@
describe('exchangeRateUpdate functionality', function() {
const axios = require('axios');
const models = require('vn-loopback/server/server').models;
+ let tx; let options;
- beforeEach(function() {
- spyOn(axios, 'get').and.returnValue(Promise.resolve({
- data: `
-
-
-
-
- `
- }));
+ function formatYmd(d) {
+ const mm = (d.getMonth() + 1).toString().padStart(2, '0');
+ const dd = d.getDate().toString().padStart(2, '0');
+ return `${d.getFullYear()}-${mm}-${dd}`;
+ }
+
+ afterEach(async() => {
+ await tx.rollback();
});
- it('should process XML data and update or create rates in the database', async function() {
+ 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 = `
+
+
+
+
+
+
+
+ `;
+ axios.get.and.returnValue(Promise.resolve({data: xml}));
spyOn(models.ReferenceRate, 'findOne').and.returnValue(Promise.resolve(null));
spyOn(models.ReferenceRate, 'create').and.returnValue(Promise.resolve());
+ await models.InvoiceIn.exchangeRateUpdate(options);
- await models.InvoiceIn.exchangeRateUpdate();
-
- expect(models.ReferenceRate.create).toHaveBeenCalledTimes(2);
+ expect(models.ReferenceRate.create).toHaveBeenCalledTimes(3);
});
- 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({}));
spyOn(models.ReferenceRate, 'create');
-
- let thrownError = null;
+ let e;
try {
- await models.InvoiceIn.exchangeRateUpdate();
- } catch (error) {
- thrownError = error;
+ await models.InvoiceIn.exchangeRateUpdate(options);
+ } catch (err) {
+ 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')));
- let error;
-
+ let e;
try {
- await models.InvoiceIn.exchangeRateUpdate();
- } catch (e) {
- error = e;
+ await models.InvoiceIn.exchangeRateUpdate(options);
+ } catch (err) {
+ e = err;
}
- expect(error).toBeDefined();
- expect(error.message).toBe('Network error');
+ expect(e).toBeDefined();
+ 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 = `
+
+
+
+ `;
+
+ 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 = `
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+
+
+ `;
+
+ 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');
});
});
diff --git a/modules/travel/back/methods/travel/getEntries.js b/modules/travel/back/methods/travel/getEntries.js
index 50088ccfa..2399f8bc4 100644
--- a/modules/travel/back/methods/travel/getEntries.js
+++ b/modules/travel/back/methods/travel/getEntries.js
@@ -41,7 +41,9 @@ module.exports = Self => {
* b.stickers)/1000000) AS DECIMAL(10,2)) m3,
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.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
LEFT JOIN vn.entry e ON t.id = e.travelFk
LEFT JOIN vn.buy b ON b.entryFk = e.id
diff --git a/modules/travel/back/models/currency.json b/modules/travel/back/models/currency.json
index f3241fad1..427a18e31 100644
--- a/modules/travel/back/models/currency.json
+++ b/modules/travel/back/models/currency.json
@@ -20,6 +20,9 @@
},
"ratio": {
"type": "number"
+ },
+ "hasToDownloadRate": {
+ "type": "boolean"
}
},
"acls": [