Merge branch 'dev' into 7296-routesMonitorDropVirtualColumn
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Robert Ferrús 2024-06-28 11:49:08 +00:00
commit d4fba0a95f
19 changed files with 273 additions and 116 deletions

View File

@ -1,7 +1,3 @@
const axios = require('axios');
const {DOMParser} = require('xmldom');
const fs = require('fs');
const ejs = require('ejs');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
@ -23,23 +19,20 @@ module.exports = Self => {
}
});
Self.createShipment = async(expeditionFk, options) => {
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
Self.createShipment = async expeditionFk => {
const models = Self.app.models;
const mrw = await models.MrwConfig.findOne(null, myOptions);
const mrw = await Self.getConfig();
if (!mrw)
throw new UserError(`Some mrwConfig parameters are not set`);
const today = Date.vnNew();
const [hours, minutes] = mrw?.expeditionDeadLine ? mrw.expeditionDeadLine.split(':').map(Number) : [0, 0];
const deadLine = Date.vnNew();
deadLine.setHours(hours, minutes, 0);
if (today > deadLine && (!mrw.notified || mrw.notified.setHours(0, 0, 0, 0) !== today.setHours(0, 0, 0, 0))) {
await models.NotificationQueue.create({notificationFk: 'mrw-deadline'});
await mrw.updateAttributes({notified: Date.vnNow()});
}
const query =
`SELECT
@ -64,7 +57,8 @@ module.exports = Self => {
JOIN ticket t ON e.ticketFk = t.id
JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN mrwService ms ON ms.agencyModeCodeFk = am.code
LEFT JOIN mrwServiceWeekday mw ON mw.weekdays | 1 << WEEKDAY(t.landed)
LEFT JOIN mrwServiceWeekday mw ON mw.agencyModeCodeFk = am.code
AND mw.weekDays & (1 << WEEKDAY(t.landed))
JOIN client c ON t.clientFk = c.id
JOIN address a ON t.addressFk = a.id
LEFT JOIN addressObservation oa ON oa.addressFk = a.id
@ -76,44 +70,25 @@ module.exports = Self => {
WHERE e.id = ?
LIMIT 1`;
const [expeditionData] = await Self.rawSql(query, [expeditionFk], myOptions);
const [expeditionData] = await Self.rawSql(query, [expeditionFk]);
if (!expeditionData)
throw new UserError(`This expedition is not a MRW shipment`);
const today = Date.vnNew();
today.setHours(0, 0, 0, 0);
if (expeditionData?.shipped.setHours(0, 0, 0, 0) < today)
if (expeditionData?.shipped.setHours(0, 0, 0, 0) < today.setHours(0, 0, 0, 0))
throw new UserError(`This ticket has a shipped date earlier than today`);
const shipmentResponse = await sendXmlDoc('createShipment', {mrw, expeditionData}, 'application/soap+xml');
const shipmentId = getTextByTag(shipmentResponse, 'NumeroEnvio');
const shipmentResponse = await Self.sendXmlDoc(
__dirname + `/createShipment.ejs`,
{mrw, expeditionData},
'application/soap+xml'
);
const shipmentId = Self.getTextByTag(shipmentResponse, 'NumeroEnvio');
if (!shipmentId)
throw new UserError(getTextByTag(shipmentResponse, 'Mensaje'));
if (!shipmentId) throw new UserError(Self.getTextByTag(shipmentResponse, 'Mensaje'));
const getLabelResponse = await sendXmlDoc('getLabel', {mrw, shipmentId}, 'text/xml');
const file = getTextByTag(getLabelResponse, 'EtiquetaFile');
if (tx) await tx.commit();
const file = await models.MrwConfig.getLabel(shipmentId);
return {shipmentId, file};
};
function getTextByTag(xmlDoc, tag) {
return xmlDoc?.getElementsByTagName(tag)[0]?.textContent;
}
async function sendXmlDoc(xmlDock, params, contentType) {
const parser = new DOMParser();
const xmlTemplate = fs.readFileSync(__dirname + `/${xmlDock}.ejs`, 'utf-8');
const renderedTemplate = ejs.render(xmlTemplate, params);
const data = await axios.post(params.mrw.url, renderedTemplate, {
headers: {
'Content-Type': `${contentType}; charset=utf-8`
}
});
return parser.parseFromString(data.data, 'text/xml');
}
};

View File

@ -0,0 +1,27 @@
module.exports = Self => {
Self.remoteMethod('getLabel', {
description: 'Return a base64Binary label from de MRW WebService',
accessType: 'READ',
accepts: [{
arg: 'shipmentId',
type: 'string',
required: true
}],
returns: {
type: 'string',
root: true
},
http: {
path: `/getLabel`,
verb: 'GET'
}
});
Self.getLabel = async shipmentId => {
const mrw = await Self.getConfig();
const getLabelResponse = await Self.sendXmlDoc(__dirname + `/getLabel.ejs`, {mrw, shipmentId}, 'text/xml');
return Self.getTextByTag(getLabelResponse, 'EtiquetaFile');
};
};

View File

@ -2,6 +2,7 @@ const models = require('vn-loopback/server/server').models;
const axios = require('axios');
const fs = require('fs');
const filter = {notificationFk: 'mrw-deadline'};
const mockBase64Binary = 'base64BinaryString';
const ticket1 = {
'id': '44',
@ -28,25 +29,52 @@ const expedition1 = {
'editorFk': 100
};
let tx;
let options;
describe('MRWConfig createShipment()', () => {
beforeEach(async() => {
options = tx = undefined;
tx = await models.MrwConfig.beginTransaction({});
options = {transaction: tx};
beforeAll(async() => {
await models.Agency.create(
{'id': 999, 'name': 'mrw'},
options
{'id': 999, 'name': 'mrw'}
);
await models.AgencyMode.create(
{'id': 999, 'name': 'mrw', 'agencyFk': 999, 'code': 'mrw'},
options
{'id': 999, 'name': 'mrw', 'agencyFk': 999, 'code': 'mrw'}
);
await createMrwConfig();
await models.Application.rawSql(
`INSERT INTO vn.mrwService
SET agencyModeCodeFk = 'mrw',
clientType = 1,
serviceType = 1,
kg = 1`, null
);
await models.Ticket.create(ticket1);
await models.Expedition.create(expedition1);
});
afterAll(async() => {
await cleanFixtures();
await models.Ticket.destroyAll(ticket1);
await models.Expedition.destroyAll(ticket1);
});
beforeEach(async() => {
const mockPostResponses = [
{data: fs.readFileSync(__dirname + '/mockGetLabel.xml', 'utf-8')},
{data: fs.readFileSync(__dirname + '/mockCreateShipment.xml', 'utf-8')}
];
spyOn(axios, 'post').and.callFake(() => Promise.resolve(mockPostResponses.pop()));
await cleanFixtures();
});
async function cleanFixtures() {
await models.NotificationQueue.destroyAll(filter);
await models.MrwConfig.updateAll({id: 1}, {expeditionDeadLine: null, notified: null});
}
async function createMrwConfig() {
await models.MrwConfig.create(
{
'id': 1,
@ -55,67 +83,80 @@ describe('MRWConfig createShipment()', () => {
'password': 'password',
'franchiseCode': 'franchiseCode',
'subscriberCode': 'subscriberCode'
}, options
}
);
}
await models.Application.rawSql(
`INSERT INTO vn.mrwService
SET agencyModeCodeFk = 'mrw',
clientType = 1,
serviceType = 1,
kg = 1`, null, options
);
await models.Ticket.create(ticket1, options);
await models.Expedition.create(expedition1, options);
});
afterEach(async() => {
await tx.rollback();
});
async function getLastNotification() {
return models.NotificationQueue.findOne({
order: 'id DESC',
where: filter
});
}
it('should create a shipment and return a base64Binary label', async() => {
const mockPostResponses = [
{data: fs.readFileSync(__dirname + '/mockGetLabel.xml', 'utf-8')},
{data: fs.readFileSync(__dirname + '/mockCreateShipment.xml', 'utf-8')}
];
spyOn(axios, 'post').and.callFake(() => Promise.resolve(mockPostResponses.pop()));
const {file} = await models.MrwConfig.createShipment(expedition1.id, options);
const {file} = await models.MrwConfig.createShipment(expedition1.id);
expect(file).toEqual(mockBase64Binary);
});
it('should fail if mrwConfig has no data', async() => {
let error;
await models.MrwConfig.destroyAll();
await models.MrwConfig.createShipment(expedition1.id).catch(e => {
error = e;
}).finally(async() => {
expect(error.message).toEqual(`Some mrwConfig parameters are not set`);
expect(error.message).toEqual(`MRW service is not configured`);
});
await createMrwConfig();
expect(error).toBeDefined();
});
it('should fail if expeditionFk is not a MrwExpedition', async() => {
let error;
await models.MrwConfig.createShipment(undefined, options).catch(e => {
await models.MrwConfig.createShipment(undefined).catch(e => {
error = e;
}).finally(async() => {
expect(error.message).toEqual(`This expedition is not a MRW shipment`);
});
});
it(' should fail if the creation date of this ticket is before the current date it', async() => {
it('should fail if the creation date of this ticket is before the current date', async() => {
let error;
const yesterday = Date.vnNew();
yesterday.setDate(yesterday.getDate() - 1);
await models.Ticket.updateAll({id: ticket1.id}, {shipped: yesterday}, options);
await models.MrwConfig.createShipment(expedition1.id, options).catch(e => {
await models.Ticket.updateAll({id: ticket1.id}, {shipped: yesterday});
await models.MrwConfig.createShipment(expedition1.id).catch(e => {
error = e;
}).finally(async() => {
expect(error.message).toEqual(`This ticket has a shipped date earlier than today`);
});
await models.Ticket.updateAll({id: ticket1.id}, {shipped: Date.vnNew()});
});
it('should send mail if you are past the dead line and is not notified today', async() => {
await models.MrwConfig.updateAll({id: 1}, {expeditionDeadLine: '10:00:00', notified: null});
await models.MrwConfig.createShipment(expedition1.id);
const notification = await getLastNotification();
expect(notification.notificationFk).toEqual(filter.notificationFk);
});
it('should send mail if you are past the dead line and it is notified from another day', async() => {
await models.MrwConfig.updateAll({id: 1}, {expeditionDeadLine: '10:00:00', notified: new Date()});
await models.MrwConfig.createShipment(expedition1.id);
const notification = await getLastNotification();
expect(notification.notificationFk).toEqual(filter.notificationFk);
});
it('should not send mail if you are past the dead line and it is notified', async() => {
await models.MrwConfig.updateAll({id: 1}, {expeditionDeadLine: '10:00:00', notified: Date.vnNew()});
await models.MrwConfig.createShipment(expedition1.id);
const notification = await getLastNotification();
expect(notification).toEqual(null);
});
});

View File

@ -25,6 +25,9 @@
},
"isSocialNameUnique": {
"type": "boolean"
},
"continentFk": {
"type": "number"
}
},
"relations": {
@ -32,6 +35,11 @@
"type": "belongsTo",
"model": "Currency",
"foreignKey": "currencyFk"
},
"continent": {
"type": "belongsTo",
"model": "Continent",
"foreignKey": "continentFk"
}
},
"acls": [

View File

@ -1,4 +1,35 @@
module.exports = Self => {
require('../methods/mrw-config/createShipment')(Self);
require('../methods/mrw-config/getLabel')(Self);
require('../methods/mrw-config/cancelShipment')(Self);
const fs = require('fs');
const ejs = require('ejs');
const UserError = require('vn-loopback/util/user-error');
const {DOMParser} = require('xmldom');
const axios = require('axios');
Self.getConfig = async function() {
const mrw = await Self.app.models.MrwConfig.findOne(null);
if (!mrw) throw new UserError(`MRW service is not configured`);
return mrw;
};
Self.getTextByTag = function(xmlDoc, tag) {
return xmlDoc?.getElementsByTagName(tag)[0]?.textContent;
};
Self.sendXmlDoc = async function(path, params, contentType) {
const parser = new DOMParser();
const xmlTemplate = fs.readFileSync(path, 'utf-8');
const renderedTemplate = ejs.render(xmlTemplate, params);
const data = await axios.post(params.mrw.url, renderedTemplate, {
headers: {
'Content-Type': `${contentType}; charset=utf-8`
}
});
return parser.parseFromString(data.data, 'text/xml');
};
};

View File

@ -39,6 +39,12 @@
},
"defaultWeight": {
"type": "number"
},
"expeditionDeadLine": {
"type": "string"
},
"notified":{
"type": "date"
}
}
}

View File

@ -24,6 +24,16 @@
},
"isManaged":{
"type": "boolean"
},
"countryFk": {
"type": "number"
}
},
"relations": {
"country": {
"type": "belongsTo",
"model": "Country",
"foreignKey": "countryFk"
}
},
"acls": [

View File

@ -2868,7 +2868,8 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
(5, 'modified-entry', 'An entry has been modified'),
(6, 'book-entry-deleted', 'accounting entries deleted'),
(7, 'zone-included','An email to notify zoneCollisions'),
(8, 'backup-printer-selected','A backup printer has been selected');
(8, 'backup-printer-selected','A backup printer has been selected'),
(9, 'mrw-deadline','The MRW deadline has passed');
TRUNCATE `util`.`notificationAcl`;
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
@ -2881,7 +2882,8 @@ INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
(5, 9),
(6, 9),
(7, 9),
(8, 66);
(8, 66),
(9, 56);
TRUNCATE `util`.`notificationQueue`;
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)

View File

@ -1,6 +1,9 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_splitItemPackingType`(vTicketFk INT, vOriginalItemPackingTypeFk VARCHAR(1))
proc:BEGIN
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_splitItemPackingType`(
vTicketFk INT,
vOriginalItemPackingTypeFk VARCHAR(1)
)
BEGIN
/**
* Clona y reparte las ventas de un ticket en funcion del tipo de empaquetado.
* Respeta el id inicial para el tipo propuesto.
@ -22,22 +25,28 @@ proc:BEGIN
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
RESIGNAL;
END;
DELETE FROM vn.sale
WHERE quantity = 0
AND ticketFk = vTicketFk;
DROP TEMPORARY TABLE IF EXISTS tmp.sale;
CREATE TEMPORARY TABLE tmp.sale
CREATE OR REPLACE TEMPORARY TABLE tmp.sale
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT s.id, i.itemPackingTypeFk , IFNULL(sv.litros, 0) litros
FROM vn.sale s
JOIN vn.item i ON i.id = s.itemFk
LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id
WHERE s.ticketFk = vTicketFk;
DROP TEMPORARY TABLE IF EXISTS tmp.saleGroup;
CREATE TEMPORARY TABLE tmp.saleGroup
SELECT itemPackingTypeFk , sum(litros) AS totalLitros
CREATE OR REPLACE TEMPORARY TABLE tmp.saleGroup
(PRIMARY KEY (itemPackingTypeFk))
ENGINE = MEMORY
SELECT itemPackingTypeFk, SUM(litros) totalLitros
FROM tmp.sale
GROUP BY itemPackingTypeFk;
@ -45,10 +54,11 @@ proc:BEGIN
FROM tmp.saleGroup
WHERE itemPackingTypeFk IS NOT NULL;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketIPT;
CREATE TEMPORARY TABLE tmp.ticketIPT
(ticketFk INT,
itemPackingTypeFk VARCHAR(1));
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketIPT (
ticketFk INT,
itemPackingTypeFk VARCHAR(1),
PRIMARY KEY (ticketFk)
) ENGINE = MEMORY;
CASE vPackingTypesToSplit
WHEN 0 THEN
@ -89,7 +99,7 @@ proc:BEGIN
SELECT itemPackingTypeFk INTO vItemPackingTypeFk
FROM tmp.saleGroup sg
WHERE NOT ISNULL(sg.itemPackingTypeFk)
WHERE sg.itemPackingTypeFk IS NOT NULL
ORDER BY sg.itemPackingTypeFk
LIMIT 1;
@ -100,7 +110,8 @@ proc:BEGIN
WHERE ts.itemPackingTypeFk IS NULL;
END CASE;
DROP TEMPORARY TABLE tmp.sale;
DROP TEMPORARY TABLE tmp.saleGroup;
DROP TEMPORARY TABLE
tmp.sale,
tmp.saleGroup;
END$$
DELIMITER ;

View File

@ -0,0 +1,12 @@
-- Place your SQL code here
ALTER TABLE vn.mrwConfig ADD IF NOT EXISTS notified TIMESTAMP NULL
COMMENT 'Date when it was notified that the web service deadline was exceeded';
INSERT IGNORE INTO util.notification
SET name = 'mrw-deadline',
description = 'The MRW deadline has passed';
INSERT IGNORE INTO util.notificationAcl (notificationFk, roleFK)
SELECT LAST_INSERT_ID(), r.id
FROM account.role r
WHERE r.name = 'delivery'

View File

@ -234,4 +234,4 @@
"It has been invoiced but the PDF of refund not be generated": "It has been invoiced but the PDF of refund not be generated",
"Cannot add holidays on this day": "Cannot add holidays on this day",
"Cannot send mail": "Cannot send mail"
}
}

View File

@ -162,7 +162,7 @@ module.exports = Self => {
const stmts = [];
let stmt;
stmts.push(`SET @_optimizer_search_depth = @@optimizer_search_depth`);
stmts.push(`SET SESSION optimizer_search_depth = 0`);

View File

@ -1,19 +1,16 @@
<footer>
<!-- Action button block -->
<div class="buttons">
<div class="columns">
<div class="size50">
<a href="https://verdnatura.es" target="_blank">
<div class="btn">
<!-- <span class="icon vn-pa-sm"><img v-bind:src="getEmailSrc('action.png')"/></span> -->
<span class="text vn-pa-sm">{{ $t('buttons.webAcccess')}}</span>
</div>
</a>
</div>
<div class="size50">
<a href="https://goo.gl/forms/j8WSL151ZW6QtlT72" target="_blank">
<a href="https://form.jotformeu.com/91673677858377" target="_blank">
<div class="btn">
<!-- <span class="icon vn-pa-sm"><img v-bind:src="getEmailSrc('info.png')"/></span> -->
<span class="text vn-pa-sm">{{ $t('buttons.info')}}</span>
</div>
</a>
@ -21,7 +18,6 @@
</div>
</div>
<!-- Networks block -->
<div class="networks">
<a href="https://www.facebook.com/Verdnatura" target="_blank">
<img v-bind:src="getEmailSrc('facebook.png')" alt="Facebook"/>
@ -37,11 +33,9 @@
</a>
</div>
<!-- Privacy block -->
<div class="privacy">
<p>{{$t('fiscalAddress')}}</p>
<p>{{$t('disclaimer')}}</p>
<p>{{$t('privacy')}}</p>
</div>
<!-- Privacy block end -->
</footer>

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,5 @@
subject: Exceeding MRW Cut-off Time
title: Exceeding MRW Cut-off Time
greeting: Dear Team.
body: Please be informed that we have exceeded the cut-off time indicated by MRW. From this moment, all generated labels will have a delivery date for tomorrow.It is necessary to contact the MRW representatives to manage any urgencies or clarifications that may arise.
footer: Best regards.

View File

@ -0,0 +1,5 @@
subject: Superación de la Hora de Corte de MRW
title: Superación de la Hora de Corte de MRW
greeting: Estimado equipo.
body: Les informo que hemos superado la hora de corte indicada por MRW. A partir de este momento, todas las etiquetas generadas tendrán fecha de entrega para mañana.Es necesario que se pongan en contacto con los responsables de MRW para gestionar cualquier urgencia o aclaración que puedan necesitar.
footer: Saludos cordiales.

View File

@ -0,0 +1,10 @@
<email-body>
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('greeting')}}</p>
<p>{{$t('body')}}</p>
<p>{{$t('footer')}}</p>
</div>
</div>
</email-body>

View File

@ -0,0 +1,9 @@
const Component = require(`vn-print/core/component`);
const emailBody = new Component('email-body');
module.exports = {
name: 'mrw-deadline',
components: {
'email-body': emailBody.build(),
}
};

View File

@ -12,7 +12,7 @@ SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '
JOIN vn.sale s ON t.id = s.ticketFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
)SELECT SUM(t.packages),
)SELECT SUM(t.packages) packages,
a.incotermsFk,
ic.name incotermsName,
MAX(t.weight) weight,