Merge branch 'dev' into 6861-Reservas-front
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Sergio De la torre 2024-07-01 14:39:19 +00:00
commit 5992d7f53f
46 changed files with 674 additions and 201 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

@ -9,7 +9,16 @@ BEGIN
*/
DECLARE vPrinterFk INT;
DECLARE vUserFk INT DEFAULT account.myUser_getId();
DECLARE vIsInExpeditionPallet BOOL;
SELECT COUNT(id) INTO vIsInExpeditionPallet
FROM expeditionPallet
WHERE id = vSelf;
IF NOT vIsInExpeditionPallet THEN
CALL util.throw("ExpeditionPallet not exists");
END IF;
SELECT o.labelerFk INTO vPrinterFk
FROM operator o
WHERE o.workerFk = vUserFk;

View File

@ -1,9 +1,9 @@
DELIMITER $$
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`multipleInventory`(
vDate DATE,
vWarehouseFk TINYINT,
vMaxDays TINYINT
)
)
proc: BEGIN
DECLARE vDateTomorrow DATE DEFAULT vDate + INTERVAL 1 DAY;
DECLARE vDateFrom DATE DEFAULT vDate;
@ -36,9 +36,12 @@ proc: BEGIN
ADD `life` TINYINT NOT NULL DEFAULT '0';
-- Calculo del inventario
UPDATE tmp.itemInventory ai
JOIN (
SELECT itemFk Id_Article, SUM(quantity) Subtotal
CREATE OR REPLACE TEMPORARY TABLE tItemInventoryCalc
(PRIMARY KEY (itemFk))
ENGINE = MEMORY
SELECT itemFk,
SUM(quantity) quantity,
SUM(quantity) visible
FROM (
SELECT s.itemFk, - s.quantity quantity
FROM sale s
@ -69,18 +72,13 @@ proc: BEGIN
AND w.isComparative
AND NOT e.isExcludedFromAvailable
AND NOT e.isRaid
) sub2
GROUP BY itemFk
) sub ON ai.id = sub.Id_Article
SET ai.inventory = sub.Subtotal,
ai.visible = sub.Subtotal,
ai.avalaible = sub.Subtotal,
ai.sd = sub.Subtotal;
) sub
GROUP BY itemFk;
-- Cálculo del visible
UPDATE tmp.itemInventory ai
UPDATE tItemInventoryCalc iic
JOIN (
SELECT itemFk Id_Article, SUM(quantity) Subtotal
SELECT itemFk, SUM(quantity) visible
FROM (
SELECT s.itemFk, s.quantity
FROM sale s
@ -117,8 +115,15 @@ proc: BEGIN
AND w.isComparative
) sub2
GROUP BY itemFk
) sub ON ai.id = sub.Id_Article
SET ai.visible = ai.visible + sub.Subtotal;
) sub ON sub.itemFk = iic.itemFk
SET iic.visible = iic.visible + sub.visible;
UPDATE tmp.itemInventory ai
JOIN tItemInventoryCalc iic ON iic.itemFk = ai.id
SET ai.inventory = iic.quantity,
ai.visible = iic.visible,
ai.avalaible = iic.quantity,
ai.sd = iic.quantity;
-- Calculo del disponible
CREATE OR REPLACE TEMPORARY TABLE tmp.itemCalc
@ -189,6 +194,7 @@ proc: BEGIN
DROP TEMPORARY TABLE
tmp.itemTravel,
tmp.itemCalc,
tItemInventoryCalc,
tmp.itemAtp;
END$$
DELIMITER ;
END$$
DELIMITER ;

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,27 @@ 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
ENGINE = MEMORY
SELECT itemPackingTypeFk, SUM(litros) totalLitros
FROM tmp.sale
GROUP BY itemPackingTypeFk;
@ -45,10 +53,10 @@ 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)
) ENGINE = MEMORY;
CASE vPackingTypesToSplit
WHEN 0 THEN
@ -89,7 +97,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 +108,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

@ -0,0 +1,18 @@
UPDATE salix.ACL
SET principalId = 'teamBoss'
WHERE property IN ('addTimeEntry', 'deleteTimeEntry', 'updateTimeEntry', 'weeklyHourRecordEmail');
UPDATE salix.ACL SET principalId = 'developer' WHERE property = 'sendMail';
UPDATE salix.ACL
SET property = 'updateMailState'
WHERE property = 'updateWorkerTimeControlMail';
INSERT INTO salix.ACL(model, property, accessType, permission, principalType, principalId)
VALUES
('WorkerTimeControl', 'addTimeEntry', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('WorkerTimeControl', 'deleteTimeEntry', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('WorkerTimeControl', 'updateTimeEntry', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('WorkerTimeControl', 'weeklyHourRecordEmail', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('WorkerTimeControl', 'sendMail', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('WorkerTimeControl', 'updateMailState', 'WRITE', 'ALLOW', 'ROLE', 'hr');

View File

@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS vn.travelKgPercentage (
className VARCHAR(50)
);
INSERT INTO vn.travelKgPercentage (value, className)
INSERT IGNORE INTO vn.travelKgPercentage (value, className)
VALUES
(80, 'primary'),
(100, 'alert');

View File

@ -1,18 +0,0 @@
-- Place your SQL code here
CREATE TABLE IF NOT EXISTS vn.travelKgPercentage (
value INT(3) PRIMARY KEY,
className VARCHAR(50)
);
INSERT IGNORE INTO vn.travelKgPercentage (value, className)
VALUES
(80, 'primary'),
(100, 'alert');
INSERT IGNORE INTO salix.ACL
SET model = 'TravelKgPercentage',
property = '*',
accessType = 'READ',
permission = 'ALLOW',
principalType = 'ROLE',
principalId = 'employee';

View File

@ -0,0 +1,2 @@
-- Place your SQL code here
ALTER TABLE vn.routesMonitor DROP COLUMN expeditionTruckFk;

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

@ -6,6 +6,9 @@
"table": "account.mailAliasAccount"
}
},
"mixins": {
"Loggable": true
},
"properties": {
"id": {
"type": "number",

View File

@ -0,0 +1,36 @@
module.exports = Self => {
Self.remoteMethodCtx('buyLabel', {
description: 'Returns the entry buys labels',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The entry id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/buy-label',
verb: 'GET'
}
});
Self.buyLabel = (ctx, id) => Self.printReport(ctx, id, 'buy-label');
};

View File

@ -9,6 +9,7 @@ module.exports = Self => {
require('../methods/entry/entryOrderPdf')(Self);
require('../methods/entry/addFromPackaging')(Self);
require('../methods/entry/addFromBuy')(Self);
require('../methods/entry/buyLabel')(Self);
Self.observe('before save', async function(ctx, options) {
if (ctx.isNewInstance) return;

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,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('getMailStates', {
description: 'Get the states of a month about time control mail',
@ -36,6 +38,8 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!await models.Worker.isSubordinate(ctx, workerId)) throw new UserError(`You don't have enough privileges`);
const times = await models.Time.find({
fields: ['week'],
where: {

View File

@ -1,4 +1,5 @@
const moment = require('moment');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('resendWeeklyHourEmail', {
@ -34,6 +35,11 @@ module.exports = Self => {
const yearNumber = dated.getFullYear();
const weekNumber = moment(dated).isoWeek();
const isSubordinate = await models.Worker.isSubordinate(ctx, workerId, myOptions);
const isTeamBoss = await models.ACL.checkAccessAcl(ctx, 'Worker', 'isTeamBoss', 'WRITE');
if (!isSubordinate || (workerId === ctx.req.accessToken.userId && !isTeamBoss))
throw new UserError(`You don't have enough privileges`);
const workerTimeControlMail = await models.WorkerTimeControlMail.findOne({
where: {

View File

@ -1,28 +1,36 @@
const models = require('vn-loopback/server/server').models;
describe('workerTimeControl getMailStates()', () => {
const workerId = 9;
const ctx = {args: {
month: 12,
year: 2000
}};
const developerId = 9;
const developerBossId = 120;
const employeeId = 1;
let ctx;
let tx;
let opts;
beforeEach(async() => {
ctx = {req: {accessToken: {userId: developerBossId}}, args: {month: 12, year: 2000}};
tx = await models.WorkerTimeControl.beginTransaction({});
opts = {transaction: tx};
});
afterEach(async() => await tx.rollback());
it('should get the states of a month about time control mail', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const response = await models.WorkerTimeControl.getMailStates(ctx, developerId, opts);
expect(response[0].state).toEqual('REVISE');
expect(response[1].state).toEqual('SENDED');
expect(response[2].state).toEqual('CONFIRMED');
});
it('should throw an error if they are not subordinates', async() => {
ctx.req.accessToken.userId = employeeId;
try {
const options = {transaction: tx};
const response = await models.WorkerTimeControl.getMailStates(ctx, workerId, options);
expect(response[0].state).toEqual('REVISE');
expect(response[1].state).toEqual('SENDED');
expect(response[2].state).toEqual('CONFIRMED');
await tx.rollback();
await models.WorkerTimeControl.getMailStates(ctx, developerId, opts);
} catch (e) {
await tx.rollback();
throw e;
expect(e.message).toEqual('You don\'t have enough privileges');
}
});
});

View File

@ -1,10 +1,11 @@
const models = require('vn-loopback/server/server').models;
describe('updateWorkerTimeControlMail()', () => {
describe('updateMailState()', () => {
const developerId = 9;
const employeeId = 1;
it('should update WorkerTimeControlMail if exist record', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const args = {
workerId: 9,
week: 50,
year: 2000,
state: 'CONFIRMED'
@ -15,15 +16,15 @@ describe('updateWorkerTimeControlMail()', () => {
const options = {transaction: tx};
const beforeMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
workerFk: developerId,
year: args.year,
week: args.week,
}
}, options);
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
await models.WorkerTimeControl.updateMailState(ctx, developerId, options);
const afterMail = await models.WorkerTimeControlMail.findOne({
where: {
workerFk: args.workerId,
workerFk: developerId,
year: args.year,
week: args.week,
}
@ -42,7 +43,6 @@ describe('updateWorkerTimeControlMail()', () => {
it('should insert WorkerTimeControlMail if exist record', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const args = {
workerId: 1,
week: 51,
year: 2000,
state: 'SENDED'
@ -53,15 +53,15 @@ describe('updateWorkerTimeControlMail()', () => {
const options = {transaction: tx};
const beforeMail = await models.WorkerTimeControlMail.find({
where: {
workerFk: args.workerId,
workerFk: employeeId,
year: args.year,
week: args.week,
}
}, options);
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
await models.WorkerTimeControl.updateMailState(ctx, employeeId, options);
const afterMail = await models.WorkerTimeControlMail.find({
where: {
workerFk: args.workerId,
workerFk: employeeId,
year: args.year,
week: args.week,
}
@ -80,7 +80,7 @@ describe('updateWorkerTimeControlMail()', () => {
it('should throw error if not exist any record in this week', async() => {
const tx = await models.WorkerTimeControlMail.beginTransaction({});
const ctx = {args: {
workerId: 1,
workerId: employeeId,
week: 1,
year: 0,
state: 'SENDED'
@ -89,7 +89,7 @@ describe('updateWorkerTimeControlMail()', () => {
let error;
try {
const options = {transaction: tx};
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, options);
await models.WorkerTimeControl.updateMailState(ctx, employeeId, options);
await tx.rollback();
} catch (e) {

View File

@ -1,12 +1,13 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('updateWorkerTimeControlMail', {
Self.remoteMethodCtx('updateMailState', {
description: 'Updates the state of WorkerTimeControlMail',
accessType: 'WRITE',
accepts: [{
arg: 'workerId',
arg: 'id',
type: 'number',
required: true
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'year',
@ -32,12 +33,12 @@ module.exports = Self => {
root: true
},
http: {
path: `/updateWorkerTimeControlMail`,
path: `/:id/updateMailState`,
verb: 'POST'
}
});
Self.updateWorkerTimeControlMail = async(ctx, options) => {
Self.updateMailState = async(ctx, id, options) => {
const models = Self.app.models;
const args = ctx.args;
const myOptions = {};
@ -59,14 +60,14 @@ module.exports = Self => {
{
year: args.year,
week: args.week,
workerFk: args.workerId
workerFk: id
},
{
state: args.state,
reason: args.workerId,
reason: args.reason,
year: args.year,
week: args.week,
workerFk: args.workerId
workerFk: id
},
myOptions);

View File

@ -35,7 +35,7 @@ module.exports = Self => {
root: true
},
http: {
path: '/weekly-hour-hecord-email',
path: '/weekly-hour-record-email',
verb: 'POST'
}
});
@ -61,7 +61,7 @@ module.exports = Self => {
const url = `${salix.url}worker/${args.workerId}/time-control?timestamp=${timestamp}`;
ctx.args.url = url;
await models.WorkerTimeControl.updateWorkerTimeControlMail(ctx, myOptions);
await models.WorkerTimeControl.updateMailState(ctx, ctx.workerId, myOptions);
return Self.sendTemplate(ctx, 'weekly-hour-record');
};

View File

@ -6,7 +6,7 @@ module.exports = Self => {
require('../methods/worker-time-control/deleteTimeEntry')(Self);
require('../methods/worker-time-control/updateTimeEntry')(Self);
require('../methods/worker-time-control/sendMail')(Self);
require('../methods/worker-time-control/updateWorkerTimeControlMail')(Self);
require('../methods/worker-time-control/updateMailState')(Self);
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
require('../methods/worker-time-control/getMailStates')(Self);
require('../methods/worker-time-control/resendWeeklyHourEmail')(Self);

View File

@ -36,15 +36,24 @@
"model": "VnUser",
"foreignKey": "userFk"
},
"worker": {
"type": "hasOne",
"model": "Worker",
"foreignKey": "id"
"worker": {
"type": "hasOne",
"model": "Worker",
"foreignKey": "id"
},
"warehouse": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseFk"
}
}
}
},
"acls": [
{
"property": "updateMailState",
"accessType": "WRITE",
"permission": "ALLOW",
"principalType": "ROLE",
"principalId": "$owner"
}
]
}

View File

@ -430,7 +430,7 @@ class Controller extends Section {
workerId: this.worker.id,
state: 'SENDED'
};
this.$http.post(`WorkerTimeControls/weekly-hour-hecord-email`, params)
this.$http.post(`WorkerTimeControls/weekly-hour-record-email`, params)
.then(() => {
this.getMailStates(this.date);
this.vnApp.showSuccess(this.$t('Email sended'));

View File

@ -260,7 +260,7 @@ describe('Component vnWorkerTimeControl', () => {
controller.date = today;
controller.weekNumber = 1;
$httpBackend.expect('POST', 'WorkerTimeControls/weekly-hour-hecord-email').respond();
$httpBackend.expect('POST', 'WorkerTimeControls/weekly-hour-record-email').respond();
controller.resendEmail();
$httpBackend.flush();

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

@ -0,0 +1,12 @@
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/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,41 @@
html {
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
margin-top: -7px;
font-size: 28px;
}
table {
border: 1px solid;
width: 100%;
font-size: inherit;
}
td {
border: 1px solid;
padding: 5px;
width: 100%;
}
span {
font-size: 48px;
font-weight: bold;
}
.lbl {
color: gray;
font-weight: lighter;
font-size: 18px;
display: block;
}
.cell {
width: 157px;
height: 50px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.barcode {
text-align: center;
}
#variant {
width: 314px;
}
#producer {
width: 471px;
}

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html>
<table v-for="buy in buys" style="break-before: page">
<tbody>
<tr>
<td colspan="2">
<div id="variant" class="cell">
<span class="lbl">{{$t('variety')}}</span>
{{buy.name}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('size')}}</span>
{{buy.size}}
</div>
</td>
</tr>
<tr>
<td>
<div class="cell">
<span class="lbl">{{$t('category')}}</span>
{{buy.category}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('color')}}</span>
{{buy.color}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('origin')}}</span>
{{buy.code}}
</div>
</td>
</tr>
<tr>
<td>
<div class="cell">
<span class="lbl">{{$t('packing')}}</span>
{{buy.packing}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('grouping')}}</span>
{{buy.grouping}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('saleUnit')}}</span>
{{buy.stems}}
</div>
</td>
</tr>
<tr>
<td colspan="3" class="barcode">
<div v-html="getBarcode(buy.id)"></div>
<span>{{buy.id}}</span>
</td>
</tr>
<tr>
<td colspan="3">
<div id="producer" class="cell">
<span class="lbl">{{$t('producer')}}</span>
{{buy.producer}}
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="cell">
<span class="lbl">{{$t('control')}}</span>
{{`${weekNum} / ${dayNum}`}}
</div>
</td>
<td>
<div class="cell">
<span class="lbl">{{$t('boxNum')}}</span>
{{`${buy.labelNum} / ${maxLabelNum}`}}
</div>
</td>
</tr>
</tbody>
</table>
</html>

View File

@ -0,0 +1,39 @@
const vnReport = require('../../../core/mixins/vn-report.js');
const {DOMImplementation, XMLSerializer} = require('xmldom');
const jsBarcode = require('jsbarcode');
const moment = require('moment');
module.exports = {
name: 'buy-label',
mixins: [vnReport],
async serverPrefetch() {
this.buys = await this.rawSqlFromDef('buys', [this.id]);
this.maxLabelNum = Math.max(...this.buys.map(buy => buy.labelNum));
const date = new Date();
this.weekNum = moment(date).isoWeek();
this.dayNum = moment(date).day();
},
methods: {
getBarcode(id) {
const xmlSerializer = new XMLSerializer();
const document = new DOMImplementation().createDocument('http://www.w3.org/1999/xhtml', 'html', null);
const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
jsBarcode(svgNode, id, {
xmlDocument: document,
format: 'code128',
displayValue: false,
width: 3.8,
height: 115,
});
return xmlSerializer.serializeToString(svgNode);
}
},
props: {
id: {
type: Number,
required: true,
description: 'The entry id'
}
}
};

View File

@ -0,0 +1,12 @@
reportName: Entry buys
variety: Bariety
size: Size
category: Category
color: Color
origin: Origin
packing: Packing
grouping: Grouping
unitSale: Un. sale
producer: Producer
control: Control
boxNum: Box no.

View File

@ -0,0 +1,12 @@
reportName: Etiqueta de compras
variety: Variedad
size: Medida
category: Categoría
color: Color
origin: Origen
packing: Packing
grouping: Grouping
saleUnit: Sale un.
producer: Productor
control: Control
boxNum: Caja nº

View File

@ -0,0 +1,11 @@
{
"width": "10cm",
"height": "10cm",
"margin": {
"top": "0.17cm",
"right": "0.2cm",
"bottom": "0cm",
"left": "0cm"
},
"printBackground": true
}

View File

@ -0,0 +1,17 @@
SELECT ROW_NUMBER() OVER(ORDER BY b.id) labelNum,
i.name,
i.`size`,
i.category,
ink.id color,
o.code,
b.packing,
b.`grouping`,
i.stems,
b.id,
p.name producer
FROM buy b
JOIN item i ON i.id = b.itemFk
LEFT JOIN producer p ON p.id = i.producerFk
LEFT JOIN ink ON ink.id = i.inkFk
LEFT JOIN origin o ON o.id = i.originFk
WHERE b.entryFk = ?

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,