This commit is contained in:
Juan Ferrer 2019-11-14 14:07:48 +01:00
commit 4436acec10
323 changed files with 7941 additions and 5985 deletions

View File

@ -0,0 +1,96 @@
const request = require('request-promise-native');
module.exports = Self => {
Self.remoteMethodCtx('sendMessage', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [{
arg: 'to',
type: 'String',
required: true,
description: 'user (@) or channel (#) to send the message'
}, {
arg: 'message',
type: 'String',
required: true,
description: 'The message'
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/sendMessage`,
verb: 'POST'
}
});
Self.sendMessage = async(ctx, to, message) => {
const models = Self.app.models;
const accessToken = ctx.req.accessToken;
const sender = await models.Account.findById(accessToken.userId);
const recipient = to.replace('@', '');
if (sender.name != recipient)
return sendMessage(to, `@${sender.name}: ${message}`);
};
async function sendMessage(name, message) {
const models = Self.app.models;
const chatConfig = await models.ChatConfig.findOne();
if (!Self.token)
Self.token = await login();
const uri = `${chatConfig.uri}/chat.postMessage`;
return send(uri, {
'channel': name,
'text': message
}).catch(async error => {
if (error.statusCode === 401 && !Self.loginAttempted) {
Self.token = await login();
Self.loginAttempted = true;
return sendMessage(name, message);
}
throw new Error(error.message);
});
}
/**
* Returns a rocketchat token
* @return {Object} userId and authToken
*/
async function login() {
const models = Self.app.models;
const chatConfig = await models.ChatConfig.findOne();
const uri = `${chatConfig.uri}/login`;
return send(uri, {
user: chatConfig.user,
password: chatConfig.password
}).then(res => res.data);
}
function send(uri, body) {
if (process.env.NODE_ENV !== 'production') {
return new Promise(resolve => {
return resolve({statusCode: 200, message: 'Fake notification sent'});
});
}
const options = {
method: 'POST',
uri: uri,
body: body,
headers: {'content-type': 'application/json'},
json: true
};
if (Self.token) {
options.headers['X-Auth-Token'] = Self.token.authToken;
options.headers['X-User-Id'] = Self.token.userId;
}
return request(options);
}
};

View File

@ -0,0 +1,18 @@
const app = require('vn-loopback/server/server');
describe('chat sendMessage()', () => {
it('should return a "Fake notification sent" as response', async() => {
let ctx = {req: {accessToken: {userId: 1}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response.statusCode).toEqual(200);
expect(response.message).toEqual('Fake notification sent');
});
it('should not return a response', async() => {
let ctx = {req: {accessToken: {userId: 18}}};
let response = await app.models.Chat.sendMessage(ctx, '@salesPerson', 'I changed something');
expect(response).toBeUndefined();
});
});

View File

@ -14,6 +14,12 @@
"Container": {
"dataSource": "storage"
},
"Chat": {
"dataSource": "vn"
},
"ChatConfig": {
"dataSource": "vn"
},
"Delivery": {
"dataSource": "vn"
},

View File

@ -0,0 +1,32 @@
{
"name": "ChatConfig",
"description": "Chat API config",
"base": "VnModel",
"options": {
"mysql": {
"table": "chatConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "Number",
"description": "Identifier"
},
"uri": {
"type": "String"
},
"user": {
"type": "String"
},
"password": {
"type": "String"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

3
back/models/chat.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/chat/sendMessage')(Self);
};

12
back/models/chat.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "Chat",
"base": "VnModel",
"acls": [{
"property": "validations",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -35,9 +35,13 @@ CREATE TABLE `vn`.`zoneExclusion` (
KEY `zoneFk` (`zoneFk`),
CONSTRAINT `zoneExclusion_ibfk_1` FOREIGN KEY (`zoneFk`) REFERENCES `zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
ALTER TABLE `vn`.`zone`
DROP FOREIGN KEY `fk_zone_1`;
DROP FOREIGN KEY `fk_zone_1`;
ALTER TABLE `vn`.`zone`
DROP COLUMN `warehouseFk`,
DROP INDEX `fk_zone_1_idx`;
CHANGE COLUMN `warehouseFk` `warehouseFk` SMALLINT(6) UNSIGNED NULL DEFAULT NULL ;
ALTER TABLE `vn`.`zone`
ADD CONSTRAINT `fk_zone_1`
FOREIGN KEY (`warehouseFk`)
REFERENCES `vn`.`warehouse` (`id`)
ON DELETE NO ACTION
ON UPDATE CASCADE;

View File

@ -0,0 +1,11 @@
USE `vn`;
CREATE TABLE `vn`.`chatConfig` (
`id` INT NOT NULL AUTO_INCREMENT,
`uri` VARCHAR(255) NOT NULL,
`user` VARCHAR(50) NOT NULL,
`password` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`));
INSERT INTO `vn`.`chatConfig` (`uri`, `user`, `password`) VALUES ('https://chat.verdnatura.es/api/v1', 'VnBot', 'Ub606cux7op.');

View File

@ -0,0 +1,10 @@
USE `vn`;
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 1);
UPDATE `vn`.`country` SET `ibanLength` = '27' WHERE (`id` = 2);
UPDATE `vn`.`country` SET `ibanLength` = '22' WHERE (`id` = 3);
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 4);
UPDATE `vn`.`country` SET `ibanLength` = '18' WHERE (`id` = 5);
UPDATE `vn`.`country` SET `ibanLength` = '25' WHERE (`id` = 8);
UPDATE `vn`.`country` SET `ibanLength` = '27' WHERE (`id` = 19);
UPDATE `vn`.`country` SET `ibanLength` = '24' WHERE (`id` = 30);

View File

@ -0,0 +1,8 @@
USE `vn`;
UPDATE `vn`.`sample` SET `description` = 'Bienvenida como nuevo cliente' WHERE (`id` = '12');
UPDATE `vn`.`sample` SET `description` = 'Instalación y configuración de impresora de coronas' WHERE (`id` = '13');
UPDATE `vn`.`sample` SET `description` = 'Solicitud de domiciliación bancaria' WHERE (`id` = '14');
UPDATE `vn`.`sample` SET `description` = 'Aviso inicial por saldo deudor' WHERE (`id` = '15');
UPDATE `vn`.`sample` SET `description` = 'Aviso reiterado por saldo deudor' WHERE (`id` = '16');
UPDATE `vn`.`sample` SET `isVisible` = '0' WHERE (`id` = '17');

View File

@ -0,0 +1,86 @@
USE `vn`;
ALTER TABLE `vn`.`ticketRequest`
DROP FOREIGN KEY `fgnAtender`;
ALTER TABLE `vn`.`ticketRequest`
CHANGE COLUMN `atenderFk` `attenderFk` INT(11) NULL DEFAULT NULL ;
ALTER TABLE `vn`.`ticketRequest`
ADD CONSTRAINT `fgnAtender`
FOREIGN KEY (`attenderFk`)
REFERENCES `vn`.`worker` (`id`)
ON UPDATE CASCADE;
USE `vn2008`;
CREATE
OR REPLACE ALGORITHM = UNDEFINED
DEFINER = `root`@`%`
SQL SECURITY DEFINER
VIEW `vn2008`.`Ordenes` AS
SELECT
`tr`.`id` AS `Id_ORDEN`,
`tr`.`description` AS `ORDEN`,
`tr`.`requesterFk` AS `requesterFk`,
`tr`.`attenderFk` AS `attenderFk`,
`tr`.`quantity` AS `CANTIDAD`,
`tr`.`itemFk` AS `Id_ARTICLE`,
`tr`.`price` AS `PRECIOMAX`,
`tr`.`isOk` AS `isOk`,
`tr`.`saleFk` AS `Id_Movimiento`,
`tr`.`ticketFk` AS `ticketFk`,
`tr`.`response` AS `COMENTARIO`,
`tr`.`created` AS `odbc_date`,
`tr`.`ordered` AS `datORDEN`,
`tr`.`shipped` AS `datTICKET`,
`tr`.`salesPersonCode` AS `CodVENDEDOR`,
`tr`.`buyerCode` AS `CodCOMPRADOR`,
`tr`.`price__` AS `PREU`,
`tr`.`clientFk` AS `Id_CLIENTE`,
`tr`.`ok__` AS `OK`,
`tr`.`total` AS `TOTAL`,
`tr`.`buyed` AS `datCOMPRA`,
`tr`.`ko__` AS `KO`
FROM
`vn`.`ticketRequest` `tr`;
USE `vn`;
DROP TRIGGER IF EXISTS `vn`.`ticketRequest_beforeInsert`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `vn`.`ticketRequest_beforeInsert` BEFORE INSERT ON `ticketRequest` FOR EACH ROW
BEGIN
IF NEW.ticketFk IS NULL THEN
SET NEW.ticketFk = (SELECT s.ticketFk FROM sale s WHERE s.id = NEW.saleFk);
END IF;
IF NEW.requesterFk IS NULL THEN
SET NEW.requesterFk = (SELECT w.id FROM worker w WHERE w.code = NEW.salesPersonCode);
END IF;
IF NEW.attenderFk IS NULL THEN
SET NEW.attenderFk = (SELECT w.id FROM worker w WHERE w.code = NEW.buyerCode);
END IF;
END$$
DELIMITER ;
DROP TRIGGER IF EXISTS `vn`.`ticketRequest_beforeUpdate`;
DELIMITER $$
USE `vn`$$
CREATE DEFINER=`root`@`%` TRIGGER `vn`.`ticketRequest_beforeUpdate` BEFORE UPDATE ON `ticketRequest` FOR EACH ROW
BEGIN
IF NEW.saleFk <> OLD.saleFk THEN
SET NEW.ticketFk = (SELECT s.ticketFk FROM sale s WHERE s.id = NEW.saleFk);
END IF;
IF NEW.salesPersonCode <> OLD.salesPersonCode THEN
SET NEW.requesterFk = (SELECT w.id FROM worker w WHERE w.code = NEW.salesPersonCode);
END IF;
IF NEW.buyerCode <> OLD.buyerCode THEN
SET NEW.attenderFk = (SELECT w.id FROM worker w WHERE w.code = NEW.buyerCode);
END IF;
END$$
DELIMITER ;

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
ALTER TABLE `vn`.`itemTaxCountry` AUTO_INCREMENT = 1;
ALTER TABLE `vn2008`.`Consignatarios` AUTO_INCREMENT = 1;
ALTER TABLE `vn`.`address` AUTO_INCREMENT = 1;
ALTER TABLE `vn`.`zoneGeo` AUTO_INCREMENT = 1;
INSERT INTO `vn`.`ticketConfig` (`id`, `scopeDays`)
VALUES
('1', '6');
INSERT INTO `account`.`mailConfig` (`id`, `domain`)
VALUES
('1', 'verdnatura.es');
@ -14,16 +14,20 @@ INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'es'
FROM `account`.`role`;
INSERT INTO `vn2008`.`Trabajadores`(`Id_Trabajador`,`CodigoTrabajador`, `Nombre`, `Apellidos`, `user_id`, `boss`)
INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `userFk`, `bossFk`)
SELECT id,UPPER(LPAD(role, 3, '0')), name, name, id, 9
FROM `vn`.`user`;
UPDATE `vn2008`.`Trabajadores` SET boss = NULL WHERE Id_Trabajador = 20;
UPDATE `vn2008`.`Trabajadores` SET boss = 20
WHERE Id_Trabajador = 1 OR Id_Trabajador = 9;
UPDATE `vn`.`worker` SET bossFk = NULL WHERE id = 20;
UPDATE `vn`.`worker` SET bossFk = 20
WHERE id = 1 OR id = 9;
DELETE FROM `vn`.`worker` WHERE name ='customer';
DELETE FROM `vn`.`worker` WHERE firstName ='customer';
INSERT INTO `hedera`.`tpvConfig`(`id`, `currency`, `terminal`, `transactionType`, `maxAmount`, `employeeFk`, `testUrl`)
VALUES
(1, 978, 1, 0, 2000, 9, 0);
INSERT INTO `account`.`user`(`id`,`name`,`password`,`role`,`active`,`email`,`lang`)
VALUES
(101, 'BruceWayne', 'ac754a330530832ba1bf7687f577da91', 2, 1, 'BruceWayne@mydomain.com', 'es'),
@ -39,23 +43,24 @@ INSERT INTO `account`.`user`(`id`,`name`,`password`,`role`,`active`,`email`,`lan
(111, 'Missing', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'es'),
(112, 'Trash', 'ac754a330530832ba1bf7687f577da91', 2, 0, NULL, 'es');
INSERT INTO `vn2008`.`Trabajadores`(`CodigoTrabajador`, `Id_Trabajador`, `Nombre`, `Apellidos`, `user_id`,`boss`)
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`)
VALUES
('LGN', 106, 'David Charles', 'Haller', 106, 19),
('ANT', 107, 'Hank' , 'Pym' , 107, 19),
('DCX', 110, 'Charles' , 'Xavier', 108, 19),
('HLK', 109, 'Bruce' , 'Banner', 109, 19),
('JJJ', 108, 'Jessica' , 'Jones' , 110, 19);
(106, 'LGN', 'David Charles', 'Haller', 106, 19),
(107, 'ANT', 'Hank' , 'Pym' , 107, 19),
(108, 'DCX', 'Charles' , 'Xavier', 108, 19),
(109, 'HLK', 'Bruce' , 'Banner', 109, 19),
(110, 'JJJ', 'Jessica' , 'Jones' , 110, 19);
INSERT INTO `vn`.`country`(`id`, `country`, `isUeeMember`, `code`, `currencyFk`, `ibanLength`)
VALUES
(1, 'España', 0, 'ES', 1, 22),
(2, 'Italia', 1, 'IT', 1, 25),
(3, 'Alemania', 1, 'DE', 1, 20),
(4, 'Rumania', 1, 'RO', 1, 22),
(5, 'Holanda', 1, 'NL', 1, 16),
(19,'Francia', 1, 'FR', 1, 25),
(30,'Canarias', 1, 'IC', 1, 22);
(1, 'España', 0, 'ES', 1, 24),
(2, 'Italia', 1, 'IT', 1, 27),
(3, 'Alemania', 1, 'DE', 1, 22),
(4, 'Rumania', 1, 'RO', 1, 24),
(5, 'Holanda', 1, 'NL', 1, 18),
(8, 'Portugal', 1, 'PT', 1, 27),
(19,'Francia', 1, 'FR', 1, 27),
(30,'Canarias', 1, 'IC', 1, 24);
INSERT INTO `vn`.`warehouse`(`id`, `name`, `isComparative`, `isInventory`, `hasAvailable`, `isManaged`, `hasStowaway`, `hasDms`)
VALUES
@ -191,9 +196,9 @@ INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city
VALUES
(101, 'Bruce Wayne', '84612325V', 'Batman', 'Alfred', '1007 Mountain Drive, Gotham', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'BruceWayne@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(102, 'Petter Parker', '87945234L', 'Spider man', 'Aunt May', '20 Ingram Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'PetterParker@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(103, 'Clark Kent', '06815934E', 'Super man', 'lois lane', '344 Clinton Street', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'ClarkKent@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 0, 19, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(104, 'Tony Stark', '06089160W', 'Iron man', 'Pepper Potts', '10880 Malibu Point', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'TonyStark@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 18, 0, 1),
(105, 'Max Eisenhardt', '251628698', 'Magneto', 'Rogue', 'Unknown Whereabouts', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1),
(105, 'Max Eisenhardt', '251628698', 'Magneto', 'Rogue', 'Unknown Whereabouts', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 8, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1),
(106, 'DavidCharlesHaller', '53136686Q', 'Legion', 'Charles Xavier', 'Evil hideout', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1),
(107, 'Hank Pym', '09854837G', 'Ant man', 'Hawk', 'Anthill', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1),
(108, 'Charles Xavier', '22641921P', 'Professor X', 'Beast', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Silla', 46460, 1111111111, 222222222, 333333333, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5,CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, NULL, 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1),
@ -1456,7 +1461,7 @@ INSERT INTO `vn`.`receipt`(`id`, `invoiceFk`, `amountPaid`, `amountUnpaid`, `pay
(1, 'Cobro web', 100.50, 0.00, CURDATE(), 9, 1, 101, CURDATE(), 442, 1),
(2, 'Cobro web', 200.50, 0.00, DATE_ADD(CURDATE(), INTERVAL -5 DAY), 9, 1, 101, DATE_ADD(CURDATE(), INTERVAL -5 DAY), 442, 1),
(3, 'Cobro en efectivo', 300.00, 100.00, DATE_ADD(CURDATE(), INTERVAL -10 DAY), 9, 1, 102, DATE_ADD(CURDATE(), INTERVAL -10 DAY), 442, 0),
(4, 'Cobro en efectivo', -400.00, -50.00, DATE_ADD(CURDATE(), INTERVAL -15 DAY), 9, 1, 103, DATE_ADD(CURDATE(), INTERVAL -15 DAY), 442, 0);
(4, 'Cobro en efectivo', 400.00, -50.00, DATE_ADD(CURDATE(), INTERVAL -15 DAY), 9, 1, 103, DATE_ADD(CURDATE(), INTERVAL -15 DAY), 442, 0);
INSERT INTO `vn2008`.`workerTeam`(`id`, `team`, `user`)
VALUES
@ -1467,7 +1472,7 @@ INSERT INTO `vn2008`.`workerTeam`(`id`, `team`, `user`)
(5, 3, 103),
(6, 3, 104);
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `atenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
INSERT INTO `vn`.`ticketRequest`(`id`, `description`, `requesterFk`, `attenderFk`, `quantity`, `itemFk`, `price`, `isOk`, `saleFk`, `ticketFk`, `created`)
VALUES
(1, 'Ranged weapon longbow 2m', 18, 35, 5, 1, 9.10, 1, 1, 1, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),
(2, 'Melee weapon combat first 15cm', 18, 35, 10, 2, 1.07, 0, NULL, 1, DATE_ADD(CURDATE(), INTERVAL -15 DAY)),

File diff suppressed because it is too large Load Diff

View File

@ -47,16 +47,17 @@ TABLES=(
claimResult
ticketUpdateAction
state
sample
)
dump_tables ${TABLES[@]}
TABLES=(
vn2008
time
accion_dits
businessReasonEnd
container
department
escritos
Grupos
iva_group_codigo
tarifa_componentes
@ -80,7 +81,6 @@ dump_tables ${TABLES[@]}
TABLES=(
hedera
imageCollection
tpvConfig
tpvError
tpvResponse
)

View File

@ -5,25 +5,14 @@ describe('buyUltimateFromInterval()', () => {
let today;
let future;
beforeAll(() => {
let date = new Date();
let month = `${date.getMonth() + 1}`;
let futureMonth = `${date.getMonth() + 2}`;
let day = date.getDate();
let year = date.getFullYear();
let futureYear = year;
let now = new Date();
now.setHours(0, 0, 0, 0);
today = now;
if (month.toString().length < 2) month = '0' + month;
if (futureMonth.toString().length < 2) futureMonth = '0' + futureMonth;
if (futureMonth.toString() == '13') {
futureMonth = '01';
futureYear + 1;
}
if (day.toString().length < 2) day = `0${day}`;
today = [year, month, day].join('-');
future = [futureYear, futureMonth, day].join('-');
let futureDate = new Date(now);
let futureMonth = now.getMonth() + 1;
futureDate.setMonth(futureMonth);
future = futureDate;
});
it(`should create a temporal table with it's data`, async() => {
@ -65,8 +54,8 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
});
it(`should create a temporal table with it's data in which started value is assigned to ended`, async() => {
@ -101,8 +90,8 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
});
it(`should create a temporal table with it's data in which ended value is a date in the future`, async() => {
@ -137,7 +126,7 @@ describe('buyUltimateFromInterval()', () => {
expect(buyUltimateFromIntervalTable[0].buyFk).toEqual(3);
expect(buyUltimateFromIntervalTable[1].buyFk).toEqual(5);
expect(buyUltimateFromIntervalTable[0].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[1].landed).toEqual(new Date(today));
expect(buyUltimateFromIntervalTable[0].landed).toEqual(today);
expect(buyUltimateFromIntervalTable[1].landed).toEqual(today);
});
});

View File

@ -55,5 +55,6 @@
"You can't delete a confirmed order": "You can't delete a confirmed order",
"Value has an invalid format": "Value has an invalid format",
"The postcode doesn't exists. Ensure you put the correct format": "The postcode doesn't exists. Ensure you put the correct format",
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket"
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket",
"Has deleted the ticket id": "Has deleted the ticket id [#{{id}}]({{{url}}})"
}

View File

@ -109,8 +109,11 @@
"The postcode doesn't exists. Ensure you put the correct format": "El código postal no existe. Asegúrate de ponerlo con el formato correcto",
"The department name can't be repeated": "El nombre del departamento no puede repetirse",
"This phone already exists": "Este teléfono ya existe",
"You cannot move a parent to any of its sons": "You cannot move a parent to any of its sons",
"You cannot move a parent to its own sons": "You cannot move a parent to its own sons",
"You cannot move a parent to its own sons": "No puedes mover un elemento padre a uno de sus hijos",
"You can't create a claim for a removed ticket": "No puedes crear una reclamación para un ticket eliminado",
"AMOUNT_NOT_MATCH_GROUPING": "AMOUNT_NOT_MATCH_GROUPING"
"You cannot delete this ticket because is already invoiced, deleted or prepared": "No puedes eliminar este tiquet porque ya está facturado, eliminado o preparado",
"You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada",
"You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero",
"Has deleted the ticket id": "Ha eliminado el ticket id [#{{id}}]({{{url}}})",
"You cannot remove this ticket because is already invoiced, deleted or prepared": "You cannot remove this ticket because is already invoiced, deleted or prepared"
}

View File

@ -1,3 +1,3 @@
module.exports = function(app) {
require('../../../print/server.js')(app);
require('../../../print/boot.js')(app);
};

16
loopback/util/http.js Normal file
View File

@ -0,0 +1,16 @@
/**
* Serializes an object to a query params
*
* @param {Object} obj The params object
* @return {String} Serialized params
*/
exports.httpParamSerializer = function(obj) {
let query = '';
for (let param in obj) {
if (query != '')
query += '&';
query += `${param}=${obj[param]}`;
}
return query;
};

View File

@ -10,19 +10,19 @@
on-search="$ctrl.onSearch($params)">
</vn-auto-search>
<div class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-treeview
vn-id="treeview"
root-label="Locations"
fetch-func="$ctrl.onFetch($item)"
sort-func="$ctrl.onSort($a, $b)">
<vn-check
ng-model="item.selected"
on-change="$ctrl.onSelection(value, item)"
triple-state="true"
ng-click="$event.preventDefault()"
label="{{::item.name}}">
</vn-check>
</vn-treeview>
<vn-card class="vn-pa-lg vn-mt-md">
<vn-treeview
vn-id="treeview"
root-label="Locations"
fetch-func="$ctrl.onFetch($item)"
sort-func="$ctrl.onSort($a, $b)">
<vn-check acl-role="deliveryBoss"
ng-model="item.selected"
on-change="$ctrl.onSelection(value, item)"
triple-state="true"
ng-click="$event.preventDefault()"
label="{{::item.name}}">
</vn-check>
</vn-treeview>
</vn-card>
</div>

View File

@ -32,7 +32,7 @@ class Controller extends ModuleCard {
}, {
relation: 'client',
scope: {
fields: ['salesPersonFk', 'name'],
fields: ['salesPersonFk', 'name', 'email'],
include: {
relation: 'salesPerson',
scope: {

View File

@ -1,13 +1,14 @@
import ngModule from '../module';
class Controller {
constructor($scope, $state, $http, $translate, vnApp, aclService) {
constructor($scope, $state, $http, $translate, vnApp, aclService, $httpParamSerializer) {
this.$scope = $scope;
this.$state = $state;
this.$http = $http;
this.$translate = $translate;
this.vnApp = vnApp;
this.aclService = aclService;
this.$httpParamSerializer = $httpParamSerializer;
this.moreOptions = [
{callback: this.showPickupOrder, name: 'Show Pickup order'},
{callback: this.confirmPickupOrder, name: 'Send Pickup order'},
@ -60,7 +61,12 @@ class Controller {
}
showPickupOrder() {
let url = `report/rpt-claim-pickup-order?claimFk=${this.claim.id}`;
const params = {
clientId: this.claim.clientFk,
claimId: this.claim.id
};
const serializedParams = this.$httpParamSerializer(params);
let url = `api/report/claim-pickup-order?${serializedParams}`;
window.open(url);
}
@ -70,7 +76,14 @@ class Controller {
sendPickupOrder(response) {
if (response === 'accept') {
this.$http.post(`email/claim-pickup-order`, {claimFk: this.claim.id}).then(
const params = {
recipient: this.claim.client.email,
clientId: this.claim.clientFk,
claimId: this.claim.id
};
const serializedParams = this.$httpParamSerializer(params);
const url = `email/claim-pickup-order?${serializedParams}`;
this.$http.get(url).then(
() => this.vnApp.showMessage(this.$translate.instant('Notification sent!'))
);
}
@ -90,7 +103,7 @@ class Controller {
}
}
Controller.$inject = ['$scope', '$state', '$http', '$translate', 'vnApp', 'aclService'];
Controller.$inject = ['$scope', '$state', '$http', '$translate', 'vnApp', 'aclService', '$httpParamSerializer'];
ngModule.component('vnClaimDescriptor', {
template: require('./index.html'),

View File

@ -1,20 +1,27 @@
import './index.js';
describe('Item Component vnClaimDescriptor', () => {
let $httpParamSerializer;
let $httpBackend;
let controller;
beforeEach(ngModule('claim'));
beforeEach(angular.mock.inject(($componentController, _$httpBackend_) => {
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, _$httpParamSerializer_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnClaimDescriptor');
controller.claim = {id: 2};
controller.claim = {id: 2, clientFk: 101, client: {email: 'client@email'}};
}));
describe('showPickupOrder()', () => {
it('should open a new window showing a pickup order PDF document', () => {
let expectedPath = 'report/rpt-claim-pickup-order?claimFk=2';
const params = {
clientId: controller.claim.clientFk,
claimId: controller.claim.id
};
const serializedParams = $httpParamSerializer(params);
let expectedPath = `api/report/claim-pickup-order?${serializedParams}`;
spyOn(window, 'open');
controller.showPickupOrder();
@ -38,8 +45,15 @@ describe('Item Component vnClaimDescriptor', () => {
it('should make a query and call vnApp.showMessage() if the response is accept', () => {
spyOn(controller.vnApp, 'showMessage');
$httpBackend.when('POST', `email/claim-pickup-order`, {claimFk: 2}).respond();
$httpBackend.expect('POST', `email/claim-pickup-order`, {claimFk: 2}).respond();
const params = {
recipient: 'client@email',
clientId: controller.claim.clientFk,
claimId: controller.claim.id
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/claim-pickup-order?${serializedParams}`).respond();
$httpBackend.expect('GET', `email/claim-pickup-order?${serializedParams}`).respond();
controller.sendPickupOrder('accept');
$httpBackend.flush();

View File

@ -17,7 +17,7 @@ describe('Client activeWorkersWithRole', () => {
let isBuyer = await app.models.Account.hasRole(result[0].id, 'buyer');
expect(result.length).toEqual(11);
expect(result.length).toEqual(12);
expect(isBuyer).toBeTruthy();
});
});

View File

@ -2,6 +2,8 @@ let request = require('request-promise-native');
let UserError = require('vn-loopback/util/user-error');
let getFinalState = require('vn-loopback/util/hook').getFinalState;
let isMultiple = require('vn-loopback/util/hook').isMultiple;
const httpParamSerializer = require('vn-loopback/util/http').httpParamSerializer;
const LoopBackContext = require('loopback-context');
module.exports = Self => {
// Methods
@ -239,15 +241,18 @@ module.exports = Self => {
});
}
const options = {
method: 'POST',
uri: 'http://127.0.0.1:3000/api/email/payment-update',
body: {
clientFk: instance.id
},
json: true
// Send email to client
if (!instance.email) return;
const loopBackContext = LoopBackContext.getCurrentContext();
const headers = loopBackContext.active.http.req.headers;
const params = {
clientId: instance.id,
recipient: instance.email
};
await request(options);
const serializedParams = httpParamSerializer(params);
const query = `${headers.origin}/api/email/payment-update?${serializedParams}`;
await request.get(query);
}
});

View File

@ -43,6 +43,11 @@
"model": "Client",
"foreignKey": "clientFk"
},
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk"
},
"greugeType": {
"type": "belongsTo",
"model": "GreugeType",

View File

@ -1,4 +1,9 @@
<mg-ajax path="ClientSamples" options="vnPost"></mg-ajax>
<vn-crud-model auto-load="true"
url="Companies"
data="companiesData"
order="code">
</vn-crud-model>
<vn-watcher
vn-id="watcher"
data="$ctrl.clientSample"
@ -8,9 +13,13 @@
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete
vn-one
vn-id="sampleType"
<vn-textfield vn-one
label="Recipient"
ng-model="$ctrl.clientSample.recipient">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one vn-id="sampleType"
ng-model="$ctrl.clientSample.typeFk"
model="ClientSample.typeFk"
fields="['code','hasCompany']"
@ -19,11 +28,10 @@
value-field="id"
label="Sample">
</vn-autocomplete>
<vn-autocomplete
vn-one
ng-model="$ctrl.clientSample.companyFk"
<vn-autocomplete vn-one
ng-model="$ctrl.companyId"
model="ClientSample.companyFk"
url="Companies"
data="companiesData"
show-field="code"
value-field="id"
label="Company"
@ -41,5 +49,9 @@
<vn-dialog
vn-id="show-preview"
on-open="$ctrl.onPreviewOpen()">
<tpl-body></tpl-body>
<tpl-body class="client-sample-dialog">
<div class="loading">
<vn-spinner enable="true"></vn-spinner>
</div>
</tpl-body>
</vn-dialog>

View File

@ -1,33 +1,84 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
class Controller {
constructor($scope, $state, $http, vnApp, $translate) {
this.$scope = $scope;
this.$state = $state;
this.$stateParams = $state.params;
this.$http = $http;
class Controller extends Component {
constructor($element, $, vnApp, $httpParamSerializer, vnConfig) {
super($element, $);
this.vnApp = vnApp;
this.$translate = $translate;
this.$httpParamSerializer = $httpParamSerializer;
this.vnConfig = vnConfig;
this.clientSample = {
clientFk: this.$stateParams.id
clientFk: this.$params.id,
companyFk: vnConfig.companyFk
};
}
jsonToQuery(json) {
let query = '';
for (let param in json) {
if (query != '')
query += '&';
query += `${param}=${json[param]}`;
}
get client() {
return this._client;
}
return query;
set client(value) {
this._client = value;
if (value)
this.clientSample.recipient = value.email;
}
get companyId() {
if (!this.clientSample.companyFk)
this.clientSample.companyFk = this.vnConfig.companyFk;
return this.clientSample.companyFk;
}
set companyId(value) {
this.clientSample.companyFk = value;
}
showPreview() {
let sampleType = this.$scope.sampleType.selection;
let params = {clientFk: this.$stateParams.id};
let sampleType = this.$.sampleType.selection;
if (!sampleType)
return this.vnApp.showError(this.$translate.instant('Choose a sample'));
if (sampleType.hasCompany && !this.clientSample.companyFk)
return this.vnApp.showError(this.$translate.instant('Choose a company'));
const params = {
clientId: this.$params.id,
recipient: this.clientSample.recipient,
isPreview: true
};
if (sampleType.hasCompany)
params.companyId = this.clientSample.companyFk;
const serializedParams = this.$httpParamSerializer(params);
const query = `email/${sampleType.code}?${serializedParams}`;
this.$http.get(query).then(res => {
this.$.showPreview.show();
let dialog = document.body.querySelector('div.vn-dialog');
let body = dialog.querySelector('tpl-body');
let scroll = dialog.querySelector('div:first-child');
body.innerHTML = res.data;
scroll.scrollTop = 0;
});
}
onSubmit() {
this.$.watcher.check();
this.$.watcher.realSubmit().then(() =>
this.sendSample()
);
}
sendSample() {
let sampleType = this.$.sampleType.selection;
let params = {
clientId: this.$params.id,
recipient: this.clientSample.recipient
};
if (!sampleType)
return this.vnApp.showError(this.$translate.instant('Choose a sample'));
@ -36,50 +87,22 @@ class Controller {
return this.vnApp.showError(this.$translate.instant('Choose a company'));
if (sampleType.hasCompany)
params.companyFk = this.clientSample.companyFk;
params.companyId = this.clientSample.companyFk;
let query = `email/${sampleType.code}?${this.jsonToQuery(params)}`;
const serializedParams = this.$httpParamSerializer(params);
const query = `email/${sampleType.code}?${serializedParams}`;
this.$http.get(query).then(res => {
if (res.data) {
let dialog = this.$scope.showPreview.element;
let body = dialog.querySelector('tpl-body');
let scroll = dialog.querySelector('div:first-child');
body.innerHTML = res.data;
this.$scope.showPreview.show();
scroll.scrollTop = 0;
}
});
}
onSubmit() {
this.$scope.watcher.check();
this.$scope.watcher.realSubmit().then(() =>
this.sendSample()
);
}
sendSample() {
let sampleType = this.$scope.sampleType.selection;
let params = {clientFk: this.$stateParams.id};
if (sampleType.hasCompany)
params.companyFk = this.clientSample.companyFk;
let query = `email/${sampleType.code}?${this.jsonToQuery(params)}`;
this.$http.post(query).then(res => {
if (res) {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('client.card.sample.index');
}
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('client.card.sample.index');
});
}
}
Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate'];
Controller.$inject = ['$element', '$scope', 'vnApp', '$httpParamSerializer', 'vnConfig'];
ngModule.component('vnClientSampleCreate', {
template: require('./index.html'),
controller: Controller
controller: Controller,
bindings: {
client: '<'
}
});

View File

@ -2,14 +2,16 @@ import './index';
describe('Client', () => {
describe('Component vnClientSampleCreate', () => {
let $httpParamSerializer;
let $scope;
let $element;
let $httpBackend;
let $state;
let controller;
beforeEach(ngModule('client'));
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, $rootScope, _$state_) => {
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, $rootScope, _$state_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$scope.sampleType = {};
$scope.watcher = {
@ -35,30 +37,24 @@ describe('Client', () => {
$state = _$state_;
$state.params.id = 101;
$httpBackend = _$httpBackend_;
controller = $componentController('vnClientSampleCreate', {$scope, $state});
$httpParamSerializer = _$httpParamSerializer_;
$element = angular.element('<vn-client-sample-create></vn-client-sample-create>');
controller = $componentController('vnClientSampleCreate', {$element, $scope});
}));
describe('jsonToQuery()', () => {
it(`should convert a JSON object with clientFk property to query params`, () => {
let myObject = {clientFk: 101};
let result = controller.jsonToQuery(myObject);
expect(result).toEqual('clientFk=101');
});
it(`should convert a JSON object with clientFk and companyFk properties to query params`, () => {
let myObject = {clientFk: 101, companyFk: 442};
let result = controller.jsonToQuery(myObject);
expect(result).toEqual('clientFk=101&companyFk=442');
});
});
describe('showPreview()', () => {
it(`should perform a query (GET) and open a sample preview`, () => {
spyOn(controller.$scope.showPreview, 'show');
spyOn(controller.$.showPreview, 'show');
const element = document.createElement('div');
document.body.querySelector = () => {
return {
querySelector: () => {
return element;
}
};
};
controller.$scope.sampleType.selection = {
controller.$.sampleType.selection = {
hasCompany: false,
code: 'MyReport'
};
@ -69,18 +65,32 @@ describe('Client', () => {
let event = {preventDefault: () => {}};
$httpBackend.when('GET', `email/MyReport?clientFk=101`).respond(true);
$httpBackend.expect('GET', `email/MyReport?clientFk=101`);
const params = {
clientId: 101,
isPreview: true
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true);
$httpBackend.expect('GET', `email/MyReport?${serializedParams}`);
controller.showPreview(event);
$httpBackend.flush();
expect(controller.$scope.showPreview.show).toHaveBeenCalledWith();
expect(controller.$.showPreview.show).toHaveBeenCalledWith();
});
it(`should perform a query (GET) with companyFk param and open a sample preview`, () => {
spyOn(controller.$scope.showPreview, 'show');
spyOn(controller.$.showPreview, 'show');
const element = document.createElement('div');
document.body.querySelector = () => {
return {
querySelector: () => {
return element;
}
};
};
controller.$scope.sampleType.selection = {
controller.$.sampleType.selection = {
hasCompany: true,
code: 'MyReport'
};
@ -92,12 +102,19 @@ describe('Client', () => {
let event = {preventDefault: () => {}};
$httpBackend.when('GET', `email/MyReport?clientFk=101&companyFk=442`).respond(true);
$httpBackend.expect('GET', `email/MyReport?clientFk=101&companyFk=442`);
const params = {
clientId: 101,
companyId: 442,
isPreview: true
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true);
$httpBackend.expect('GET', `email/MyReport?${serializedParams}`);
controller.showPreview(event);
$httpBackend.flush();
expect(controller.$scope.showPreview.show).toHaveBeenCalledWith();
expect(controller.$.showPreview.show).toHaveBeenCalledWith();
});
});
@ -114,7 +131,7 @@ describe('Client', () => {
it(`should perform a query (GET) and call go() method`, () => {
spyOn(controller.$state, 'go');
controller.$scope.sampleType.selection = {
controller.$.sampleType.selection = {
hasCompany: false,
code: 'MyReport'
};
@ -123,8 +140,13 @@ describe('Client', () => {
clientFk: 101
};
$httpBackend.when('POST', `email/MyReport?clientFk=101`).respond(true);
$httpBackend.expect('POST', `email/MyReport?clientFk=101`);
const params = {
clientId: 101
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true);
$httpBackend.expect('GET', `email/MyReport?${serializedParams}`);
controller.sendSample();
$httpBackend.flush();
@ -134,7 +156,7 @@ describe('Client', () => {
it(`should perform a query (GET) with companyFk param and call go() method`, () => {
spyOn(controller.$state, 'go');
controller.$scope.sampleType.selection = {
controller.$.sampleType.selection = {
hasCompany: true,
code: 'MyReport'
};
@ -144,8 +166,14 @@ describe('Client', () => {
companyFk: 442
};
$httpBackend.when('POST', `email/MyReport?clientFk=101&companyFk=442`).respond(true);
$httpBackend.expect('POST', `email/MyReport?clientFk=101&companyFk=442`);
const params = {
clientId: 101,
companyId: 442
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true);
$httpBackend.expect('GET', `email/MyReport?${serializedParams}`);
controller.sendSample();
$httpBackend.flush();

View File

@ -1,36 +1,34 @@
vn-client-sample-create {
vn-dialog {
& > div {
padding: 0 !important
div.vn-dialog {
tpl-body.client-sample-dialog {
width: 800px;
.container, .container h1 {
font-family: "Roboto","Helvetica","Arial",sans-serif;
font-size: 1em !important;
h1 {
font-weight: bold;
margin: auto
}
p {
margin: 1em 0
}
footer p {
font-size: 10px !important;
line-height: 10px
}
}
tpl-body {
min-width: 800px;
.title h1 {
font-size: 2em !important;
margin: 0
}
.container, .container h1 {
font-family: "Roboto","Helvetica","Arial",sans-serif;
font-size: 1em !important;
h1 {
font-weight: bold;
margin: auto
}
p {
margin: 1em 0
}
footer p {
font-size: 10px !important;
line-height: 10px
}
}
.title h1 {
font-size: 2em !important;
margin: 0
}
.loading {
text-align: center
}
}
}
}

View File

@ -44,6 +44,9 @@
"ItemTypeTag": {
"dataSource": "vn"
},
"ItemShelvingSale": {
"dataSource": "vn"
},
"Origin": {
"dataSource": "vn"
},

View File

@ -0,0 +1,34 @@
{
"name": "ItemShelvingSale",
"base": "VnModel",
"options": {
"mysql": {
"table": "itemShelvingSale"
}
},
"properties": {
"id": {
"type": "Number",
"id": true,
"description": "Identifier"
},
"quantity": {
"type": "Number"
},
"created": {
"type": "Date"
}
},
"relations": {
"sale": {
"type": "belongsTo",
"model": "Sale",
"foreignKey": "saleFk"
},
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
}
}

View File

@ -36,6 +36,24 @@ class Controller extends ModuleCard {
scope: {
fields: ['id', 'name']
}
},
{
relation: 'worker',
scope: {
fields: ['userFk'],
include: {
relation: 'user',
scope: {
fields: ['id'],
include: {
relation: 'emailUser',
scope: {
fields: ['email']
}
}
}
}
}
}
]
};

View File

@ -1,12 +1,13 @@
import ngModule from '../module';
class Controller {
constructor($, $http, vnApp, $translate, aclService) {
constructor($, $http, vnApp, $translate, aclService, $httpParamSerializer) {
this.$http = $http;
this.vnApp = vnApp;
this.$translate = $translate;
this.$ = $;
this.aclService = aclService;
this.$httpParamSerializer = $httpParamSerializer;
this.moreOptions = [
{callback: this.showRouteReport, name: 'Show route report'},
{callback: this.sendRouteReport, name: 'Send route report'},
@ -36,13 +37,26 @@ class Controller {
}
showRouteReport() {
let url = `report/rpt-route?routeFk=${this.route.id}`;
const user = this.route.worker.user;
const params = {
clientId: user.id,
routeId: this.route.id
};
const serializedParams = this.$httpParamSerializer(params);
let url = `api/report/driver-route?${serializedParams}`;
window.open(url);
}
sendRouteReport() {
let url = `email/driver-route?routeFk=${this.route.id}`;
this.$http.post(url).then(() => {
const user = this.route.worker.user;
const params = {
recipient: user.emailUser.email,
clientId: user.id,
routeId: this.route.id
};
const serializedParams = this.$httpParamSerializer(params);
const url = `email/driver-route?${serializedParams}`;
this.$http.get(url).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Report sent'));
});
}
@ -62,7 +76,7 @@ class Controller {
}
}
Controller.$inject = ['$scope', '$http', 'vnApp', '$translate', 'aclService'];
Controller.$inject = ['$scope', '$http', 'vnApp', '$translate', 'aclService', '$httpParamSerializer'];
ngModule.component('vnRouteDescriptor', {
template: require('./index.html'),

View File

@ -76,7 +76,7 @@ module.exports = Self => {
case 'ticketFk':
return {'t.id': value};
case 'attenderFk':
return {'tr.atenderFk': value};
return {'tr.attenderFk': value};
case 'isOk':
return {'tr.isOk': value};
case 'clientFk':
@ -106,7 +106,7 @@ module.exports = Self => {
tr.ticketFk,
tr.quantity,
tr.price,
tr.atenderFk attenderFk,
tr.attenderFk,
tr.description,
tr.response,
tr.saleFk,
@ -131,7 +131,7 @@ module.exports = Self => {
LEFT JOIN sale s ON s.id = tr.saleFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.userFk
LEFT JOIN worker wka ON wka.id = tr.atenderFk
LEFT JOIN worker wka ON wka.id = tr.attenderFk
LEFT JOIN account.user ua ON ua.id = wka.userFk`);
stmt.merge(conn.makeSuffix(filter));

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('setDeleted', {
Self.remoteMethodCtx('setDeleted', {
description: 'Sets true the isDeleted value of a ticket',
accessType: 'WRITE',
accepts: [{
@ -21,16 +21,82 @@ module.exports = Self => {
}
});
Self.setDeleted = async id => {
try {
let claimOfATicket = await Self.app.models.Claim.findOne({where: {ticketFk: id}});
if (claimOfATicket)
throw new UserError('You must delete the claim id %d first', 'DELETE_CLAIM_FIRST', claimOfATicket.id);
Self.setDeleted = async(ctx, id) => {
const models = Self.app.models;
const isEditable = await Self.isEditable(ctx, id);
const $t = ctx.req.__; // $translate
let currentTicket = await Self.app.models.Ticket.findById(id);
return await currentTicket.updateAttributes({isDeleted: true});
} catch (e) {
throw e;
if (!isEditable)
throw new UserError('You cannot delete this ticket because is already invoiced, deleted or prepared');
// Check if has sales with shelving
const sales = await models.Sale.find({
include: {relation: 'itemShelving'},
where: {ticketFk: id}
});
const hasItemShelvingSales = sales.some(sale => {
return sale.itemShelving();
});
if (hasItemShelvingSales)
throw new UserError(`You cannot delete a ticket that part of it is being prepared`);
// Check for existing claim
const claimOfATicket = await models.Claim.findOne({where: {ticketFk: id}});
if (claimOfATicket)
throw new UserError('You must delete the claim id %d first', 'DELETE_CLAIM_FIRST', claimOfATicket.id);
// Check for existing purchase requests
const hasPurchaseRequests = await models.TicketRequest.count({
ticketFk: id,
isOk: true
});
if (hasPurchaseRequests)
throw new UserError('You must delete all the buy requests first');
// Remove ticket greuges
const ticketGreuges = await models.Greuge.find({where: {ticketFk: id}});
const ownGreuges = ticketGreuges.every(greuge => {
return greuge.ticketFk = id;
});
if (ownGreuges) {
for (const greuge of ticketGreuges) {
const instance = await models.Greuge.findById(greuge.id);
await instance.destroy();
}
}
const ticket = await models.Ticket.findById(id, {
include: {
relation: 'client',
scope: {
fields: ['id', 'salesPersonFk'],
include: {
relation: 'salesPerson',
scope: {
fields: ['id', 'userFk'],
include: {
relation: 'user'
}
}
}
}
}
});
// Send notification to salesPerson
const salesPerson = ticket.client().salesPerson();
if (salesPerson) {
const salesPersonUser = salesPerson.user().name;
const origin = ctx.req.headers.origin;
const message = $t(`Has deleted the ticket id`, {
id: id,
url: `${origin}/#!/ticket/${id}/summary`
});
await models.Chat.sendMessage(ctx, `@${salesPersonUser}`, message);
}
return ticket.updateAttribute('isDeleted', true);
};
};

View File

@ -2,12 +2,23 @@ const app = require('vn-loopback/server/server');
describe('ticket deleted()', () => {
let ticket;
let ctx;
beforeAll(async done => {
let originalTicket = await app.models.Ticket.findOne({where: {id: 16}});
originalTicket.id = null;
ticket = await app.models.Ticket.create(originalTicket);
ctx = {
req: {
accessToken: {userId: 106},
headers: {
origin: 'http://localhost:5000'
},
__: () => {}
}
};
done();
});
@ -22,7 +33,7 @@ describe('ticket deleted()', () => {
});
it('should set a ticket to deleted', async() => {
await app.models.Ticket.setDeleted(ticket.id);
await app.models.Ticket.setDeleted(ctx, ticket.id);
let deletedTicket = await app.models.Ticket.findOne({where: {id: ticket.id}, fields: ['isDeleted']});
@ -34,7 +45,7 @@ describe('ticket deleted()', () => {
let error;
try {
await app.models.Ticket.setDeleted(ticketId);
await app.models.Ticket.setDeleted(ctx, ticketId);
} catch (e) {
error = e;
}

View File

@ -72,6 +72,11 @@
"type": "hasOne",
"model": "SaleTracking",
"foreignKey": "saleFk"
},
"itemShelving": {
"type": "hasOne",
"model": "ItemShelvingSale",
"foreignKey": "saleFk"
}
}
}

View File

@ -33,13 +33,6 @@
"isOk": {
"type": "Boolean"
},
"attenderFk": {
"type": "Number",
"required": true,
"mysql": {
"columnName": "atenderFk"
}
},
"response": {
"type": "String"
}

View File

@ -20,7 +20,15 @@ class Controller extends ModuleCard {
}, {
relation: 'client',
scope: {
fields: ['salesPersonFk', 'name', 'isActive', 'isFreezed', 'isTaxDataChecked', 'credit'],
fields: [
'salesPersonFk',
'name',
'isActive',
'isFreezed',
'isTaxDataChecked',
'credit',
'email'
],
include: {
relation: 'salesPerson',
scope: {

View File

@ -197,7 +197,7 @@
<vn-confirm
vn-id="confirm-delivery-note"
on-response="$ctrl.sendDeliveryNote($response)"
on-accept="$ctrl.sendDeliveryNote()"
question="Send Delivery Note"
message="Are you sure you want to send it?">
</vn-confirm>

View File

@ -2,9 +2,10 @@ import ngModule from '../module';
import Component from 'core/lib/component';
class Controller extends Component {
constructor($element, $, aclService) {
constructor($element, $, aclService, $httpParamSerializer) {
super($element, $);
this.aclService = aclService;
this.$httpParamSerializer = $httpParamSerializer;
this.moreOptions = [
{name: 'Add turn', callback: this.showAddTurnDialog},
{name: 'Show Delivery Note', callback: this.showDeliveryNote},
@ -198,10 +199,27 @@ class Controller extends Component {
}
showDeliveryNote() {
let url = `report/rpt-delivery-note?ticketFk=${this.ticket.id}`;
const params = {
clientId: this.ticket.client.id,
ticketId: this.ticket.id
};
const serializedParams = this.$httpParamSerializer(params);
let url = `api/report/delivery-note?${serializedParams}`;
window.open(url);
}
sendDeliveryNote() {
const params = {
recipient: this.ticket.client.email,
clientId: this.ticket.client.id,
ticketId: this.ticket.id
};
const serializedParams = this.$httpParamSerializer(params);
this.$http.get(`email/delivery-note?${serializedParams}`).then(
() => this.vnApp.showMessage(this.$translate.instant('Notification sent!'))
);
}
showSMSDialog() {
const address = this.ticket.address;
this.newSMS = {
@ -272,17 +290,9 @@ class Controller extends Component {
confirmDeliveryNote() {
this.$.confirmDeliveryNote.show();
}
sendDeliveryNote(response) {
if (response === 'accept') {
this.$http.post(`email/delivery-note`, {ticketFk: this.ticket.id}).then(
() => this.vnApp.showMessage(this.$translate.instant('Notification sent!'))
);
}
}
}
Controller.$inject = ['$element', '$scope', 'aclService'];
Controller.$inject = ['$element', '$scope', 'aclService', '$httpParamSerializer'];
ngModule.component('vnTicketDescriptor', {
template: require('./index.html'),

View File

@ -1,13 +1,14 @@
import './index.js';
describe('Ticket Component vnTicketDescriptor', () => {
let $httpParamSerializer;
let $httpBackend;
let controller;
let $state;
beforeEach(ngModule('ticket'));
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, $rootScope, $compile, _$state_) => {
beforeEach(angular.mock.inject(($componentController, _$httpBackend_, $rootScope, $compile, _$state_, _$httpParamSerializer_) => {
let $element = $compile(`<vn-autocomplete></vn-autocomplete>`)($rootScope);
$state = _$state_;
$state.getCurrentPath = () => {
@ -17,8 +18,9 @@ describe('Ticket Component vnTicketDescriptor', () => {
];
};
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnTicketDescriptor', {$element});
controller._ticket = {id: 2, invoiceOut: {id: 1}};
controller._ticket = {id: 2, invoiceOut: {id: 1}, client: {id: 101, email: 'client@email'}};
controller.cardReload = ()=> {
return true;
};
@ -82,7 +84,12 @@ describe('Ticket Component vnTicketDescriptor', () => {
describe('showDeliveryNote()', () => {
it('should open a new window showing a delivery note PDF document', () => {
let expectedPath = 'report/rpt-delivery-note?ticketFk=2';
const params = {
clientId: controller.ticket.client.id,
ticketId: controller.ticket.id
};
const serializedParams = $httpParamSerializer(params);
let expectedPath = `api/report/delivery-note?${serializedParams}`;
spyOn(window, 'open');
controller.showDeliveryNote();
@ -90,6 +97,26 @@ describe('Ticket Component vnTicketDescriptor', () => {
});
});
describe('sendDeliveryNote()', () => {
it('should make a query and call vnApp.showMessage()', () => {
spyOn(controller.vnApp, 'showMessage');
const params = {
recipient: 'client@email',
clientId: controller.ticket.client.id,
ticketId: controller.ticket.id
};
const serializedParams = $httpParamSerializer(params);
$httpBackend.when('GET', `email/delivery-note?${serializedParams}`).respond();
$httpBackend.expect('GET', `email/delivery-note?${serializedParams}`).respond();
controller.sendDeliveryNote();
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalledWith('Notification sent!');
});
});
describe('makeInvoice()', () => {
it('should make a query and call $state.reload() method if the response is accept', () => {
spyOn(controller.$state, 'reload');

View File

@ -31,8 +31,7 @@ import './picture';
import './request/index';
import './request/create';
import './log';
import './weekly/index';
import './weekly/create';
import './weekly';
import './dms/index';
import './dms/create';
import './dms/edit';

View File

@ -197,11 +197,6 @@
"state": "ticket.weekly.index",
"component": "vn-ticket-weekly-index",
"description": "Weekly tickets"
}, {
"url": "/create",
"state": "ticket.weekly.create",
"component": "vn-ticket-weekly-create",
"description": "Add weekly ticket"
}, {
"url": "/request",
"state": "ticket.card.request",

View File

@ -1,69 +0,0 @@
<mg-ajax path="ticketWeeklies" options="vnPost"></mg-ajax>
<vn-watcher
vn-id="watcher"
data="$ctrl.ticketWeekly"
form="form"
save="post">
</vn-watcher>
<form name="form" vn-http-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-autocomplete vn-one vn-id="ticket"
url="tickets"
ng-model="$ctrl.ticketWeekly.ticketFk"
fields="['id', 'nickname', 'clientFk', 'warehouseFk']"
search-function="{nickname: $search}"
show-field="id"
value-field="id"
label="Ticket"
on-change="$ctrl.onChangeTicket(ticket.selection)">
<tpl-item>#{{id}} - {{nickname}}</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-one label="Weekday"
ng-model="$ctrl.ticketWeekly.weekDay"
data="$ctrl.weekdays"
show-field="name"
value-field="id"
translate-fields="['name']"
order="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="client" vn-one disabled="true"
url="clients"
fields="['id', 'name', 'salesPersonFk']"
ng-model="$ctrl.ticketWeekly.clientFk"
show-field="name"
value-field="id"
label="Client"
selection="$ctrl.clientSelection">
</vn-autocomplete>
<vn-autocomplete vn-one disabled="true"
ng-model="$ctrl.ticketWeekly.warehouseFk"
url="warehouses"
show-field="name"
value-field="id"
label="Warehouse">
</vn-autocomplete>
<vn-autocomplete vn-one disabled="true"
ng-model="$ctrl.ticketWeekly.salesPersonFk"
url="clients/activeWorkersWithRole"
search-function="{firstName: $search}"
show-field="firstName"
value-field="id"
where="{role: 'employee'}"
label="Salesperson">
<tpl-item>{{firstName}} {{lastName}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit label="Create"></vn-submit>
<vn-button ui-sref="ticket.weekly.index" label="Cancel"></vn-button>
</vn-button-bar>
</form>
<!-- New postcode dialog -->
<vn-client-postcode
vn-id="postcode"
on-response="$ctrl.onResponse($response)">
</vn-client-postcode>

View File

@ -1,49 +0,0 @@
import ngModule from '../../module';
export default class Controller {
constructor($scope, $state, $http, $translate, vnApp) {
this.$ = $scope;
this.$state = $state;
this.$http = $http;
this.$translate = $translate;
this.vnApp = vnApp;
this.ticketWeekly = {};
this.weekdays = [
{id: 0, name: 'Monday'},
{id: 1, name: 'Tuesday'},
{id: 2, name: 'Wednesday'},
{id: 3, name: 'Thursday'},
{id: 4, name: 'Friday'},
{id: 5, name: 'Saturday'},
{id: 6, name: 'Sunday'}
];
}
onChangeTicket(ticket) {
this.ticketWeekly.clientFk = ticket.clientFk;
this.ticketWeekly.warehouseFk = ticket.warehouseFk;
}
get clientSelection() {
return this._clientSelection;
}
set clientSelection(value) {
this._clientSelection = value;
if (value)
this.ticketWeekly.salesPersonFk = value.salesPersonFk;
}
onSubmit() {
return this.$.watcher.submit().then(
json => this.$state.go('ticket.weekly.index')
);
}
}
Controller.$inject = ['$scope', '$state', '$http', '$translate', 'vnApp'];
ngModule.component('vnTicketWeeklyCreate', {
template: require('./index.html'),
controller: Controller
});

View File

@ -1,58 +0,0 @@
import './index';
describe('Ticket', () => {
describe('Component vnTicketWeeklyCreate', () => {
let $componentController;
let $scope;
let $state;
let controller;
beforeEach(ngModule('ticket'));
beforeEach(angular.mock.inject((_$componentController_, $rootScope, _$state_) => {
$componentController = _$componentController_;
$scope = $rootScope.$new();
$state = _$state_;
$scope.watcher = {
submit: () => {
return {
then: callback => {
callback({data: {id: '1234'}});
}
};
}
};
controller = $componentController('vnTicketWeeklyCreate', {$scope, $state});
}));
describe('onChangeTicket() setter', () => {
it(`should define clientFk and warehouseFk properties on ticketWeekly object`, () => {
controller.onChangeTicket({clientFk: 101, warehouseFk: 1});
expect(controller.ticketWeekly.clientFk).toEqual(101);
expect(controller.ticketWeekly.warehouseFk).toEqual(1);
});
});
describe('clientSelection() setter', () => {
it(`should define salesPersonFk property on ticketWeekly object`, () => {
controller.clientSelection = {clientFk: 101, salesPersonFk: 106};
expect(controller.ticketWeekly.salesPersonFk).toEqual(106);
});
});
describe('onSubmit()', () => {
it(`should call submit() on the watcher then expect a callback`, () => {
spyOn(controller.$state, 'go');
controller.ticketWeekly = {
ticketFk: 11,
weekDay: 0
};
controller.onSubmit();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.weekly.index');
});
});
});
});

View File

@ -1,2 +0,0 @@
Weekday: Día de la semana
Add weekly ticket: Añadir ticket programado

View File

@ -94,9 +94,3 @@
question="This ticket will be removed from weekly tickets! Continue anyway?"
message="You are going to delete this weekly ticket">
</vn-confirm>
<a ui-sref="ticket.weekly.create"
vn-tooltip="Add weekly ticket"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>

View File

@ -1,4 +1,4 @@
import ngModule from '../../module';
import ngModule from '../module';
export default class Controller {
constructor($scope, vnApp, $translate, $http) {

View File

@ -30,8 +30,11 @@
"isConfirmed": {
"type": "Boolean"
},
"isRaid": {
"type": "Boolean"
"isVirtual": {
"type": "Boolean",
"mysql": {
"columnName": "isRaid"
}
},
"commission": {
"type": "Number"

View File

@ -1,170 +0,0 @@
/*
Author : Enrique Blasco BLanquer
Date: 29 de octubre de 2019
*/
let request = require('request-promise-native');
let UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('sendMessage', {
description: 'Send a RocketChat message',
accessType: 'WRITE',
accepts: [{
arg: 'from',
type: 'String',
required: true,
description: 'user who sends the message'
}, {
arg: 'to',
type: 'String',
required: true,
description: 'user (@) or channel (#) to send the message'
}, {
arg: 'message',
type: 'String',
required: true,
description: 'The message'
}],
returns: {
type: 'boolean',
root: true
},
http: {
path: `/sendMessage`,
verb: 'POST'
}
});
Self.sendMessage = async(from, to, message) => {
const rocketUser = await getRocketUser();
const userId = rocketUser.data.userId;
const authToken = rocketUser.data.authToken;
if (to.includes('@')) return await sendUserMessage(to.replace('@', ''), userId, authToken, '@' + from + ' te ha mandado un mensaje: ' + message);
else return await sendChannelMessage(to.replace('#', ''), userId, authToken, '@' + from + ' dice: ' + message);
};
/**
* Returns a rocketchat token
* @return {Object} userId and authToken
*/
async function getRocketUser() {
const url = 'https://chat.verdnatura.es/api/v1/login';
const options = {
method: 'POST',
uri: url,
body: {
user: 'VnBot',
password: 'Ub606cux7op.'
},
headers: {
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Send a user message
* @param {String} to user to send the message
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @param {String} message The message
* @return {Object} rocket info
*/
async function sendUserMessage(to, userId, authToken, message) {
const url = 'https://chat.verdnatura.es/api/v1/chat.postMessage';
const options = {
method: 'POST',
uri: url,
body: {
'channel': '@' + to,
'text': message
},
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Send a channel message
* @param {String} to channel to send the message
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @param {String} message The message
* @return {Object} rocket info
*/
async function sendChannelMessage(to, userId, authToken, message) {
const channelInfo = await getChannelId(to, userId, authToken);
const url = 'https://chat.verdnatura.es/api/v1/chat.sendMessage';
const channelId = channelInfo.channel._id;
const options = {
method: 'POST',
uri: url,
body: {
'message': {
'rid': channelId,
'msg': message
}
},
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
/**
* Get channel id
* @param {String} to channel to get id
* @param {String} userId rocket user id
* @param {String} authToken rocket token
* @return {Object} rocket info
*/
async function getChannelId(to, userId, authToken) {
const url = 'https://chat.verdnatura.es/api/v1/channels.info?roomName=' + to;
const options = {
method: 'GET',
uri: url,
headers: {
'X-Auth-Token': authToken,
'X-User-Id': userId,
'content-type': 'application/json'
},
json: true
};
return await request(options)
.then(function(parsedBody) {
return parsedBody;
})
.catch(function(err) {
throw new UserError(err);
});
}
};

View File

@ -3,5 +3,4 @@ module.exports = Self => {
require('../methods/worker/mySubordinates')(Self);
require('../methods/worker/isSubordinate')(Self);
require('../methods/worker/getWorkerInfo')(Self);
require('../methods/worker/sendMessage')(Self);
};

2
package-lock.json generated
View File

@ -13500,7 +13500,7 @@
"dependencies": {
"jsesc": {
"version": "0.5.0",
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
"dev": true
}

57
print/boot.js Normal file
View File

@ -0,0 +1,57 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const templatesPath = path.resolve(__dirname, './templates');
const componentsPath = path.resolve(__dirname, './core/components');
module.exports = app => {
global.appPath = __dirname;
process.env.OPENSSL_CONF = '/etc/ssl/';
// Extended locale intl polyfill
const IntlPolyfill = require('intl');
Intl.NumberFormat = IntlPolyfill.NumberFormat;
Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
// Init database instance
require('./core/database').init();
// Init SMTP Instance
require('./core/smtp').init();
//
require('./core/mixins');
require('./core/filters');
require('./core/directives');
// Init router
require('./core/router')(app);
/**
* Serve component static files
*/
const componentsDir = fs.readdirSync(componentsPath);
componentsDir.forEach(componentName => {
const componentDir = path.join(componentsPath, '/', componentName);
const assetsDir = `${componentDir}/assets`;
app.use(`/api/${componentName}/assets`, express.static(assetsDir));
});
/**
* Serve static files
*/
const templatesDir = fs.readdirSync(templatesPath);
templatesDir.forEach(directory => {
const templateTypeDir = path.join(templatesPath, '/', directory);
const templates = fs.readdirSync(templateTypeDir);
templates.forEach(templateName => {
const templateDir = path.join(templatesPath, '/', directory, '/', templateName);
const assetsDir = `${templateDir}/assets`;
app.use(`/api/${templateName}/assets`, express.static(assetsDir));
});
});
};

View File

@ -1,40 +1,33 @@
/**
* Email only stylesheet
*
*/
body {
background-color: #EEE
}
.container {
max-width: 600px;
min-width: 320px;
margin: 0 auto;
color: #555
}
.main {
-webkit-text-size-adjust: none;
-ms-text-size-adjust: none;
background-color: #FFF;
padding: 20px
font-weight: 400;
color: #555;
margin: 0
}
.main a {
.grid {
background-color: #FFF
}
.grid a {
color: #8dba25
}
.main h1 {
color: #999
.grid-block {
min-width: 300px;
max-width: 600px;
margin: 0 auto;
color: #333
}
.main h3 {
font-size: 16px
}
.title {
background-color: #95d831;
text-transform: uppercase;
text-align: center;
padding: 35px 0
}
.title h1 {
font-size: 32px;
color: #333;
margin: 0
h1 {
font-weight: 100;
font-size: 1.5em
}

View File

@ -1,10 +1,34 @@
.container {
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
font-size: 16px
/**
* CSS layout elements
*
*/
.grid {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px !important;
width: 100%
}
.grid-row {
background-color: transparent
}
.grid-block {
box-sizing: border-box;
min-height: 40px
}
.grid-block.empty {
height: 40px
}
.grid-block.white {
background-color: #FFF
}
.columns {
overflow: hidden
overflow: hidden;
box-sizing: border-box;
}
.columns .size100 {
@ -18,6 +42,7 @@
}
.columns .size50 {
box-sizing: border-box;
width: 50%;
float: left
}
@ -173,7 +198,7 @@ table {
}
.panel .row-oriented td, .panel .row-oriented th {
padding: 10px 0
padding: 8px 10px
}
.row-oriented > tbody > tr > td {
@ -199,8 +224,8 @@ table {
margin-left: -1px;
margin-right: 1px;
margin-top: 10px;
padding: 5px 0;
color: #999;
padding: 5px 0
}
.line .vertical-aligned {

View File

@ -1,3 +1,7 @@
/**
* CSS misc classes
*
*/
.uppercase {
text-transform: uppercase
}

View File

@ -1,5 +1,9 @@
/**
* Report only stylesheet
*
*/
body {
zoom: 0.55
zoom: 0.53
}
.title {

View File

@ -0,0 +1,349 @@
/**
* CSS spacing classes
*
* vn-[p|m][t|r|b|l|a|x|y]-[none|auto|xs|sm|md|lg|xl]
* T D S
*
* T - type
* - values: p (padding), m (margin)
*
* D - direction
* - values:
* t (top), r (right), b (bottom), l (left),
* a (all), x (both left & right), y (both top & bottom)
*
* S - size
* - values:
* none,
* auto (ONLY for specific margins: vn-ml-*, vn-mr-*, vn-mx-*),
* xs (extra small),
* sm (small),
* md (medium),
* lg (large),
* xl (extra large)
*/
/* ++++++++++++++++++++++++++++++++++++++++++++++++ Padding */
.vn-pa-none {
padding: 0;
}
.vn-pl-none {
padding-left: 0;
}
.vn-pr-none {
padding-right: 0;
}
.vn-pt-none {
padding-top: 0;
}
.vn-pb-none {
padding-bottom: 0;
}
.vn-py-none {
padding-top: 0;
padding-bottom: 0;
}
.vn-px-none {
padding-left: 0;
padding-right: 0;
}
.vn-pa-xs {
padding: 4px;
}
.vn-pl-xs {
padding-left: 4px;
}
.vn-pr-xs {
padding-right: 4px;
}
.vn-pt-xs {
padding-top: 4px;
}
.vn-pb-xs {
padding-bottom: 4px;
}
.vn-py-xs {
padding-top: 4px;
padding-bottom: 4px;
}
.vn-px-xs {
padding-left: 4px;
padding-right: 4px;
}
/* Small */
.vn-pa-sm {
padding: 8px;
}
.vn-pl-sm {
padding-left: 8px;
}
.vn-pr-sm {
padding-right: 8px;
}
.vn-pt-sm {
padding-top: 8px;
}
.vn-pb-sm {
padding-bottom: 8px;
}
.vn-py-sm {
padding-top: 8px;
padding-bottom: 8px;
}
.vn-px-sm {
padding-left: 8px;
padding-right: 8px;
}
/* Medium */
.vn-pa-md {
padding: 16px;
}
.vn-pl-md {
padding-left: 16px;
}
.vn-pr-md {
padding-right: 16px;
}
.vn-pt-md {
padding-top: 16px;
}
.vn-pb-md {
padding-bottom: 16px;
}
.vn-py-md {
padding-top: 16px;
padding-bottom: 16px;
}
.vn-px-md {
padding-left: 16px;
padding-right: 16px;
}
/* Large */
.vn-pa-lg {
padding: 32px;
}
.vn-pl-lg {
padding-left: 32px;
}
.vn-pr-lg {
padding-right: 32px;
}
.vn-pt-lg {
padding-top: 32px;
}
.vn-pb-lg {
padding-bottom: 32px;
}
.vn-py-lg {
padding-top: 32px;
padding-bottom: 32px;
}
.vn-px-lg {
padding-left: 32px;
padding-right: 32px;
}
/* Extra large */
.vn-pa-xl {
padding: 100px;
}
.vn-pl-xl {
padding-left: 100px;
}
.vn-pr-xl {
padding-right: 100px;
}
.vn-pt-xl {
padding-top: 100px;
}
.vn-pb-xl {
padding-bottom: 100px;
}
.vn-py-xl {
padding-top: 100px;
padding-bottom: 100px;
}
.vn-px-xl {
padding-left: 100px;
padding-right: 100px;
}
/* ++++++++++++++++++++++++++++++++++++++++++++++++ Margin */
/* None */
.vn-ma-none {
padding: 0;
}
.vn-ml-none {
padding-left: 0;
}
.vn-mr-none {
padding-right: 0;
}
.vn-mt-none {
padding-top: 0;
}
.vn-mb-none {
padding-bottom: 0;
}
.vn-my-none {
padding-top: 0;
padding-bottom: 0;
}
.vn-mx-none {
padding-left: 0;
padding-right: 0;
}
/* Auto */
.vn-ml-none {
padding-left: auto;
}
.vn-mr-none {
padding-right: auto;
}
.vn-mx-none {
padding-left: auto;
padding-right: auto;
}
/* Extra small */
.vn-ma-xs {
margin: 4px;
}
.vn-mt-xs {
margin-top: 4px;
}
.vn-ml-xs {
margin-left: 4px;
}
.vn-mr-xs {
margin-right: 4px;
}
.vn-mb-xs {
margin-bottom: 4px;
}
.vn-my-xs {
margin-top: 4px;
margin-bottom: 4px;
}
.vn-mx-xs {
margin-left: 4px;
margin-right: 4px;
}
/* Small */
.vn-ma-sm {
margin: 8px;
}
.vn-mt-sm {
margin-top: 8px;
}
.vn-ml-sm {
margin-left: 8px;
}
.vn-mr-sm {
margin-right: 8px;
}
.vn-mb-sm {
margin-bottom: 8px;
}
.vn-my-sm {
margin-top: 8px;
margin-bottom: 8px;
}
.vn-mx-sm {
margin-left: 8px;
margin-right: 8px;
}
/* Medium */
.vn-ma-md {
margin: 16px;
}
.vn-mt-md {
margin-top: 16px;
}
.vn-ml-md {
margin-left: 16px;
}
.vn-mr-md {
margin-right: 16px;
}
.vn-mb-md {
margin-bottom: 16px;
}
.vn-my-md {
margin-top: 16px;
margin-bottom: 16px;
}
.vn-mx-md {
margin-left: 16px;
margin-right: 16px;
}
/* Large */
.vn-ma-lg {
margin: 32px;
}
.vn-mt-lg {
margin-top: 32px;
}
.vn-ml-lg {
margin-left: 32px;
}
.vn-mr-lg {
margin-right: 32px;
}
.vn-mb-lg {
margin-bottom: 32px;
}
.vn-my-lg {
margin-top: 32px;
margin-bottom: 32px;
}
.vn-mx-lg {
margin-left: 32px;
margin-right: 32px;
}
/* Extra large */
.vn-ma-xl {
margin: 100px;
}
.vn-mt-xl {
margin-top: 100px;
}
.vn-ml-xl {
margin-left: 100px;
}
.vn-mr-xl {
margin-right: 100px;
}
.vn-mb-xl {
margin-bottom: 100px;
}
.vn-my-xl {
margin-top: 100px;
margin-bottom: 100px;
}
.vn-mx-xl {
margin-left: 100px;
margin-right: 100px;
}

View File

@ -1,10 +1,15 @@
{
"app": {
"host": "http://localhost:5000",
"port": 3000,
"defaultLanguage": "es",
"senderMail": "nocontestar@verdnatura.es",
"senderName": "Verdnatura"
},
"i18n": {
"locale": "es",
"fallbackLocale": "es",
"silentTranslationWarn": false
},
"pdf": {
"format": "A4",
"border": "1.5cm",

View File

@ -1,25 +0,0 @@
[
{"type": "email", "name": "client-welcome"},
{"type": "email", "name": "printer-setup"},
{"type": "email", "name": "payment-update"},
{"type": "email", "name": "letter-debtor-st"},
{"type": "email", "name": "letter-debtor-nd"},
{"type": "email", "name": "claim-pickup-order"},
{"type": "email", "name": "sepa-core"},
{"type": "email", "name": "client-lcr"},
{"type": "email", "name": "driver-route"},
{"type": "email", "name": "delivery-note"},
{"type": "report", "name": "rpt-delivery-note"},
{"type": "report", "name": "rpt-claim-pickup-order"},
{"type": "report", "name": "rpt-letter-debtor"},
{"type": "report", "name": "rpt-sepa-core"},
{"type": "report", "name": "rpt-receipt"},
{"type": "report", "name": "rpt-zone"},
{"type": "report", "name": "rpt-route"},
{"type": "report", "name": "rpt-lcr"},
{"type": "report", "name": "rpt-item-label"},
{"type": "static", "name": "email-header"},
{"type": "static", "name": "email-footer"},
{"type": "static", "name": "report-header"},
{"type": "static", "name": "report-footer"}
]

104
print/core/component.js Normal file
View File

@ -0,0 +1,104 @@
const Vue = require('vue');
const VueI18n = require('vue-i18n');
const renderer = require('vue-server-renderer').createRenderer();
Vue.use(VueI18n);
const fs = require('fs');
const yaml = require('js-yaml');
const juice = require('juice');
const path = require('path');
const config = require('./config');
class Component {
constructor(name) {
this.name = name;
}
get path() {
return `./components/${this.name}`;
}
get template() {
const templatePath = `${this.path}/${this.name}.html`;
const fullPath = path.resolve(__dirname, templatePath);
return fs.readFileSync(fullPath, 'utf8');
}
get locale() {
if (!this._locale)
this.getLocale();
return this._locale;
}
getLocale() {
const mergedLocale = {messages: {}};
const localePath = path.resolve(__dirname, `${this.path}/locale`);
if (!fs.existsSync(localePath))
return mergedLocale;
const localeDir = fs.readdirSync(localePath);
localeDir.forEach(locale => {
const fullPath = path.join(localePath, '/', locale);
const yamlLocale = fs.readFileSync(fullPath, 'utf8');
const jsonLocale = yaml.safeLoad(yamlLocale);
const localeName = locale.replace('.yml', '');
mergedLocale.messages[localeName] = jsonLocale;
});
this._locale = mergedLocale;
}
get stylesheet() {
let mergedStyles = '';
const stylePath = path.resolve(__dirname, `${this.path}/assets/css`);
if (!fs.existsSync(stylePath))
return mergedStyles;
return require(`${stylePath}/import`);
}
get attachments() {
const attachmentsPath = `${this.path}/attachments.json`;
const fullPath = path.resolve(__dirname, attachmentsPath);
if (!fs.existsSync(fullPath))
return [];
return require(fullPath);
}
build() {
const fullPath = path.resolve(__dirname, this.path);
if (!fs.existsSync(fullPath))
throw new Error(`Sample "${this.name}" not found`);
const component = require(`${this.path}/${this.name}`);
component.i18n = this.locale;
component.attachments = this.attachments;
component.template = juice.inlineContent(this.template, this.stylesheet, {
inlinePseudoElements: true
});
return component;
}
async render() {
const component = this.build();
const i18n = new VueI18n(config.i18n);
const app = new Vue({
i18n: i18n,
render: h => h(component, {
props: this.args
})
});
return renderer.renderToString(app);
}
}
module.exports = Component;

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,22 @@
div {
display: inline-block;
box-sizing: border-box
}
a {
background-color: #F5F5F5;
border: 1px solid #CCC;
display: flex;
vertical-align: middle;
box-sizing: border-box;
min-width: 150px;
text-decoration: none;
border-radius: 3px;
color: #8dba25
}
a > div.icon {
font-weight: bold;
font-size: 18px;
color: #555
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M2 12.5C2 9.46 4.46 7 7.5 7H18c2.21 0 4 1.79 4 4s-1.79 4-4 4H9.5C8.12 15 7 13.88 7 12.5S8.12 10 9.5 10H17v2H9.41c-.55 0-.55 1 0 1H18c1.1 0 2-.9 2-2s-.9-2-2-2H7.5C5.57 9 4 10.57 4 12.5S5.57 16 7.5 16H17v2H7.5C4.46 18 2 15.54 2 12.5z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@ -0,0 +1,6 @@
<div class="vn-mx-xs" v-if="attachment.component">
<a target="_blank" class="vn-py-sm vn-px-md" v-bind:href="path">
<div class="text">{{attachment.filename}}</div>
<div class="icon vn-pl-md">&#x25BC;</div>
</a>
</div>

View File

@ -0,0 +1,37 @@
module.exports = {
name: 'attachment',
computed: {
path() {
const filename = this.attachment.filename;
const component = this.attachment.component;
if (this.attachment.cid)
return `/api/${component}/assets/files/${filename}`;
else
return `/api/report/${component}?${this.getHttpParams()}`;
}
},
methods: {
getHttpParams() {
const props = this.args;
let query = '';
for (let param in props) {
if (query != '')
query += '&';
query += `${param}=${props[param]}`;
}
return query;
}
},
props: {
attachment: {
type: Object,
required: true
},
args: {
type: Object,
required: false
}
}
};

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -1,21 +1,12 @@
@media (max-width: 400px) {
.buttons a {
display: block;
width: 100%
}
}
.buttons {
font-size: 14px !important;
width: 100%
}
.buttons a {
display: inline-block;
box-sizing: border-box;
text-decoration: none;
font-size: 16px;
width: 100%;
color: #fff;
width: 50%
}
.buttons .btn {
@ -23,18 +14,20 @@
text-align: center
}
.buttons .btn .text {
display: inline-block;
padding: 22px 0
}
.buttons .btn .icon {
background-color: #95d831;
box-sizing: border-box;
font-weight: bold;
text-align: center;
padding: 16.5px 0;
float: right;
width: 70px
color: #333;
float: left
}
.buttons .btn .text {
display: inline-block;
}
.networks {

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,22 @@
[
{
"filename": "facebook.png",
"path": "/assets/images/facebook.png",
"cid": "facebook.png"
},
{
"filename": "twitter.png",
"path": "/assets/images/twitter.png",
"cid": "twitter.png"
},
{
"filename": "instagram.png",
"path": "/assets/images/instagram.png",
"cid": "instagram.png"
},
{
"filename": "linkedin.png",
"path": "/assets/images/linkedin.png",
"cid": "linkedin.png"
}
]

View File

@ -0,0 +1,47 @@
<footer>
<!-- Action button block -->
<div class="buttons">
<div class="columns">
<div class="size50">
<a href="https://www.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">
<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>
</div>
</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"/>
</a>
<a href="https://www.twitter.com/Verdnatura" target="_blank">
<img v-bind:src="getEmailSrc('twitter.png')" alt="Twitter"/>
</a>
<a href="https://www.instagram.com/Verdnatura" target="_blank">
<img v-bind:src="getEmailSrc('instagram.png')" alt="Instagram"/>
</a>
<a href="https://www.linkedin.com/company/verdnatura" target="_blank">
<img v-bind:src="getEmailSrc('linkedin.png')" alt="Linkedin"/>
</a>
</div>
<!-- Privacy block -->
<div class="privacy">
<p>{{$t('privacy.fiscalAddress')}}</p>
<p>{{$t('privacy.disclaimer')}}</p>
<p>{{$t('privacy.law')}}</p>
</div>
<!-- Privacy block end -->
</footer>

View File

@ -0,0 +1,4 @@
module.exports = {
name: 'email-footer',
props: ['isPreview', 'locale']
};

View File

@ -0,0 +1,19 @@
buttons:
webAcccess: Visita nuestra Web
info: Ayúdanos a mejorar
privacy:
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla
· www.verdnatura.es · clientes@verdnatura.es
disclaimer: '- AVISO - Este mensaje es privado y confidencial, y debe ser utilizado
exclusivamente por la persona destinataria del mismo. Si has recibido este mensaje
por error, te rogamos lo comuniques al remitente y borres dicho mensaje y cualquier
documento adjunto que pudiera contener. Verdnatura Levante SL no renuncia a la
confidencialidad ni a ningún privilegio por causa de transmisión errónea o mal
funcionamiento. Igualmente no se hace responsable de los cambios, alteraciones,
errores u omisiones que pudieran hacerse al mensaje una vez enviado.'
law: En cumplimiento de lo dispuesto en la Ley Orgánica 15/1999, de Protección de
Datos de Carácter Personal, te comunicamos que los datos personales que facilites
se incluirán en ficheros automatizados de VERDNATURA LEVANTE S.L., pudiendo en
todo momento ejercitar los derechos de acceso, rectificación, cancelación y oposición,
comunicándolo por escrito al domicilio social de la entidad. La finalidad del
fichero es la gestión administrativa, contabilidad, y facturación.

View File

@ -0,0 +1,19 @@
buttons:
webAcccess: Visitez notre site web
info: Aidez-nous à améliorer
privacy:
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla
· www.verdnatura.es · clientes@verdnatura.es
disclaimer: '- AVIS - Ce message est privé et confidentiel et doit être utilisé.exclusivamente
por la persona destinataria del mismo. Si has recibido este mensajepor error,
te rogamos lo comuniques al remitente y borres dicho mensaje y cualquier documentoadjunto
que pudiera contener. Verdnatura Levante SL no renuncia a la confidencialidad
ni aningún privilegio por causa de transmisión errónea o mal funcionamiento. Igualmente
no se haceresponsable de los cambios, alteraciones, errores u omisiones que pudieran
hacerse al mensaje una vez enviado.'
law: En cumplimiento de lo dispuesto en la Ley Orgánica 15/1999, de Protección de
Datos de Carácter Personal, te comunicamos que los datos personales que facilites
se incluirán en ficheros automatizados de VERDNATURA LEVANTE S.L.,pudiendo en
todo momento ejercitar los derechos de acceso, rectificación, cancelación y oposición,
comunicándolo porescrito al domicilio social de la entidad. La finalidad del fichero
es la gestión administrativa, contabilidad, y facturación.

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,19 @@
header .logo {
margin-bottom: 15px;
}
header .logo img {
width: 50%
}
header .topbar {
background-color: #95d831;
height: 10px
}
.topbar:after {
overflow: hidden;
display: block;
content: ' ';
clear: both;
}

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="500"
height="68.596313"
viewBox="0 0 499.99999 68.596313"
enable-background="new 0 0 226.229 31.038"
xml:space="preserve"
id="svg2"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="verdnatura-white.svg"><metadata
id="metadata61"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs59" /><sodipodi:namedview
pagecolor="#333333"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1013"
id="namedview57"
showgrid="false"
inkscape:zoom="1.5909426"
inkscape:cx="268.25598"
inkscape:cy="112.75218"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="svg2"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" /><g
id="Background"
transform="translate(2.2478643e-6,43.261169)" /><g
id="Guides"
transform="translate(2.2478643e-6,43.261169)" /><g
id="g883"
transform="matrix(2.2101465,0,0,2.2101465,0,-594.44542)"><g
transform="translate(0,268.962)"
style="fill:#ffffff"
id="g9"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path11"
d="M 10.417,30.321 0,0 h 8.233 l 4.26,15.582 0.349,1.276 c 0.521,1.866 0.918,3.431 1.191,4.693 0.15,-0.618 0.335,-1.345 0.555,-2.182 0.219,-0.837 0.528,-1.935 0.925,-3.293 L 19.981,0 h 8.19 l -10.5,30.321 z" /></g><g
transform="translate(0,268.962)"
id="g13"><path
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path15"
d="m 139.809,19.787 c -0.665,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.283,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.204,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.653,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.925,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.307,-1.159 3.021,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.646,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.076,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.673,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.991,0 3.602,0.241 4.833,0.722 1.231,0.481 2.095,1.209 2.59,2.185 0.339,0.701 0.483,1.536 0.432,2.504 -0.052,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z" /></g><g
transform="translate(0,268.962)"
id="g17"><path
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path19"
d="m 185.7,30.321 6.27,-22.393 h 7.049 l -1.097,3.918 c 1.213,-1.537 2.502,-2.659 3.867,-3.366 1.365,-0.707 2.951,-1.074 4.758,-1.101 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.912,-0.093 -0.303,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -2.104,0.168 -2.932,0.504 -0.829,0.336 -1.561,0.854 -2.197,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.359,4.232 l -2.104,7.516 H 185.7 Z" /></g><g
transform="translate(0,268.962)"
id="g21"><path
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path23"
d="m 217.631,19.787 c -0.664,0.357 -1.748,0.686 -3.25,0.988 -0.727,0.137 -1.282,0.254 -1.667,0.35 -0.95,0.247 -1.661,0.563 -2.134,0.947 -0.472,0.384 -0.799,0.899 -0.979,1.544 -0.223,0.796 -0.155,1.438 0.205,1.925 0.359,0.488 0.945,0.731 1.757,0.731 1.252,0 2.375,-0.36 3.369,-1.081 0.994,-0.721 1.654,-1.665 1.98,-2.831 z m 5.106,10.534 h -7.458 c 0.017,-0.356 0.048,-0.726 0.094,-1.11 l 0.159,-1.192 c -1.318,1.026 -2.627,1.786 -3.927,2.279 -1.299,0.493 -2.643,0.739 -4.031,0.739 -2.158,0 -3.7,-0.593 -4.625,-1.779 -0.926,-1.187 -1.106,-2.788 -0.542,-4.804 0.519,-1.851 1.431,-3.356 2.737,-4.515 1.306,-1.159 3.02,-1.972 5.142,-2.438 1.169,-0.247 2.641,-0.515 4.413,-0.803 2.647,-0.412 4.082,-1.016 4.304,-1.812 l 0.151,-0.539 c 0.182,-0.65 0.077,-1.145 -0.317,-1.483 -0.393,-0.339 -1.071,-0.508 -2.033,-0.508 -1.045,0 -1.934,0.214 -2.666,0.643 -0.731,0.428 -1.289,1.058 -1.672,1.887 h -6.748 c 1.065,-2.53 2.64,-4.413 4.723,-5.65 2.083,-1.237 4.724,-1.856 7.923,-1.856 1.99,0 3.601,0.241 4.833,0.722 1.232,0.481 2.095,1.209 2.591,2.185 0.339,0.701 0.483,1.536 0.431,2.504 -0.051,0.969 -0.377,2.525 -0.978,4.669 l -2.375,8.483 c -0.284,1.014 -0.416,1.812 -0.396,2.395 0.02,0.583 0.188,0.962 0.503,1.141 z" /></g><g
transform="translate(0,268.962)"
id="g25"><path
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path27"
d="m 188.386,7.928 -6.269,22.393 h -7.174 l 0.864,-3.085 c -1.227,1.246 -2.476,2.163 -3.746,2.751 -1.27,0.588 -2.625,0.882 -4.067,0.882 -2.471,0 -4.154,-0.634 -5.048,-1.901 -0.895,-1.268 -0.993,-3.149 -0.294,-5.644 l 4.31,-15.396 h 7.338 l -3.508,12.53 c -0.516,1.842 -0.641,3.109 -0.375,3.803 0.266,0.694 0.967,1.041 2.105,1.041 1.275,0 2.323,-0.422 3.142,-1.267 0.819,-0.845 1.497,-2.223 2.031,-4.133 l 3.353,-11.974 z" /></g><g
transform="translate(0,268.962)"
id="g29"><path
inkscape:connector-curvature="0"
style="clip-rule:evenodd;fill:#8ed300;fill-opacity:1;fill-rule:evenodd"
id="path31"
d="m 149.937,12.356 1.239,-4.428 h 2.995 l 1.771,-6.326 h 7.338 l -1.771,6.326 h 3.753 l -1.24,4.428 h -3.753 l -2.716,9.702 c -0.416,1.483 -0.498,2.465 -0.247,2.946 0.25,0.48 0.905,0.721 1.964,0.721 l 0.549,-0.011 0.39,-0.031 -1.31,4.678 c -0.811,0.148 -1.596,0.263 -2.354,0.344 -0.758,0.081 -1.48,0.122 -2.167,0.122 -2.543,0 -4.108,-0.621 -4.695,-1.863 -0.587,-1.242 -0.313,-3.887 0.82,-7.936 l 2.428,-8.672 z" /></g><g
transform="translate(0,268.962)"
style="fill:#ffffff"
id="g33"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path35"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z" /><g
style="fill:#ffffff"
id="g37"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path39"
d="m 73.875,18.896 c -0.561,2.004 -0.616,3.537 -0.167,4.601 0.449,1.064 1.375,1.595 2.774,1.595 1.399,0 2.605,-0.524 3.62,-1.574 1.015,-1.05 1.806,-2.59 2.375,-4.622 0.526,-1.879 0.556,-3.334 0.09,-4.363 -0.466,-1.029 -1.393,-1.543 -2.778,-1.543 -1.304,0 -2.487,0.528 -3.551,1.585 -1.064,1.057 -1.852,2.496 -2.363,4.321 z M 96.513,0 88.024,30.321 h -7.337 l 0.824,-2.944 c -1.166,1.22 -2.369,2.121 -3.61,2.703 -1.241,0.582 -2.583,0.874 -4.025,0.874 -2.802,0 -4.772,-1.081 -5.912,-3.243 -1.139,-2.162 -1.218,-4.993 -0.238,-8.493 0.988,-3.528 2.668,-6.404 5.042,-8.627 2.374,-2.224 4.927,-3.336 7.661,-3.336 1.47,0 2.695,0.296 3.676,0.887 0.981,0.591 1.681,1.465 2.099,2.62 L 89.217,0 Z" /></g></g><g
transform="translate(0,268.962)"
style="fill:#ffffff"
id="g41"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path43"
d="M 46.488,30.321 52.757,7.928 h 7.049 l -1.098,3.918 C 59.921,10.309 61.21,9.187 62.576,8.48 63.942,7.773 68.591,7.406 70.398,7.379 l -2.03,7.25 c -0.304,-0.042 -0.608,-0.072 -0.911,-0.093 -0.304,-0.02 -0.592,-0.03 -0.867,-0.03 -1.126,0 -5.167,0.168 -5.997,0.504 -0.829,0.336 -1.561,0.854 -2.196,1.555 -0.406,0.467 -0.789,1.136 -1.149,2.007 -0.361,0.872 -0.814,2.282 -1.36,4.232 l -2.104,7.516 h -7.296 z" /></g><g
transform="translate(0,268.962)"
style="fill:#ffffff"
id="g45"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path47"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z" /><g
style="fill:#ffffff"
id="g49"><path
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd"
inkscape:connector-curvature="0"
id="path51"
d="m 32.673,16.742 8.351,-0.021 c 0.375,-1.436 0.308,-2.558 -0.201,-3.365 -0.509,-0.807 -1.402,-1.211 -2.68,-1.211 -1.209,0 -2.285,0.397 -3.229,1.19 -0.944,0.793 -1.69,1.93 -2.241,3.407 z m 6.144,6.536 h 7.043 c -1.347,2.456 -3.172,4.356 -5.477,5.7 -2.305,1.345 -4.885,2.017 -7.74,2.017 -3.473,0 -5.923,-1.054 -7.351,-3.161 -1.427,-2.107 -1.632,-4.98 -0.613,-8.618 1.038,-3.707 2.875,-6.641 5.512,-8.803 2.637,-2.163 5.678,-3.244 9.123,-3.244 3.555,0 6.04,1.099 7.456,3.298 1.417,2.198 1.582,5.234 0.498,9.109 l -0.239,0.814 -0.167,0.484 H 31.721 c -0.441,1.575 -0.438,2.777 0.01,3.606 0.448,0.829 1.332,1.244 2.65,1.244 0.975,0 1.836,-0.206 2.583,-0.617 0.747,-0.411 1.366,-1.021 1.853,-1.829 z" /></g></g><g
transform="translate(0,268.962)"
id="g53"><path
inkscape:connector-curvature="0"
style="fill:#8ed300;fill-opacity:1"
id="path55"
d="m 112.881,30.643 -6.404,-18.639 -6.455,18.639 h -7.254 l 9.565,-30.321 h 8.19 l 4.434,15.582 0.35,1.276 c 0.521,1.866 0.917,3.431 1.191,4.693 l 0.555,-2.182 c 0.219,-0.837 0.528,-1.935 0.925,-3.293 l 4.468,-16.076 h 8.19 l -10.501,30.321 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,7 @@
[
{
"filename": "logo-black.png",
"path": "/assets/images/logo-black.png",
"cid": "logo-black.png"
}
]

View File

@ -0,0 +1,8 @@
<header>
<div class="logo">
<a href="https://www.verdnatura.es" target="_blank">
<img v-bind:src="getEmailSrc('logo-black.png')" alt="VerdNatura"/>
</a>
</div>
<div class="topbar"></div>
</header>

View File

@ -0,0 +1,4 @@
module.exports = {
name: 'email-header',
props: ['locale']
};

View File

@ -1,6 +1,6 @@
const CssReader = require(`${appPath}/lib/cssReader`);
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new CssReader([
module.exports = new Stylesheet([
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${appPath}/common/css/misc.css`,

View File

@ -0,0 +1,10 @@
numPages: Página {{page}} de {{pages}}
law:
phytosanitary: 'VERDNATURA LEVANTE SL - Pasaporte Fitosanitario R.P. Generalitat
Valenciana - Nº Comerciante: ES17462130'
privacy: En cumplimiento de lo dispuesto en la Ley Orgánica 15/1999, de Protección
de Datos de Carácter Personal, le comunicamos que los datos personales que facilite
se incluirán en ficheros automatizados de VERDNATURA LEVANTE S.L., pudiendo en
todo momento ejercitar los derechos de acceso, rectificación, cancelación y oposición,
comunicándolo por escrito al domicilio social de la entidad. La finalidad del
fichero es la gestión administrativa, contabilidad, y facturación.

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