Merge branch 'dev' into 5439-newFilterEnPeticionesDeCompra
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Satorres 2023-04-05 06:51:21 +00:00
commit 8741a9f913
120 changed files with 2511 additions and 678 deletions

View File

@ -6,5 +6,9 @@
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"search.useIgnoreFiles": false, "search.useIgnoreFiles": false,
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint",
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
} }

View File

@ -5,10 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2312.01] - 2023-04-06 ## [2314.01] - 2023-04-20
### Added ### Added
- - (Facturas recibidas -> Bases negativas) Nueva sección
### Changed ### Changed
- -
@ -16,6 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- -
## [2312.01] - 2023-04-06
### Added
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
### Changed
- (Monitor tickets) Cuando se filtra por 'Pendiente' ya no muestra los estados de 'Previa'
- (Envíos -> Extra comunitarios) Se agrupan las entradas del mismo travel. Añadidos campos Referencia y Importe.
- (Envíos -> Índice) Cambiado el buscador superior por uno lateral
## [2310.01] - 2023-03-23 ## [2310.01] - 2023-03-23
### Added ### Added

View File

@ -2,6 +2,7 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('changePassword', { Self.remoteMethod('changePassword', {
description: 'Changes the user password', description: 'Changes the user password',
accessType: 'WRITE',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',

View File

@ -1,6 +1,7 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('setPassword', { Self.remoteMethod('setPassword', {
description: 'Sets the user password', description: 'Sets the user password',
accessType: 'WRITE',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',

View File

@ -30,16 +30,23 @@ module.exports = Self => {
const recipient = to.replace('@', ''); const recipient = to.replace('@', '');
if (sender.name != recipient) { if (sender.name != recipient) {
await models.Chat.create({ const chat = await models.Chat.create({
senderFk: sender.id, senderFk: sender.id,
recipient: to, recipient: to,
dated: Date.vnNew(), dated: Date.vnNew(),
checkUserStatus: 0, checkUserStatus: 0,
message: message, message: message,
status: 0, status: 'sending',
attempts: 0 attempts: 0
}); });
try {
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
await Self.updateChat(chat, 'sent');
} catch (error) {
await Self.updateChat(chat, 'error', error);
}
return true; return true;
} }

View File

@ -24,18 +24,13 @@ module.exports = Self => {
} }
}); });
Self.sendCheckingPresence = async(ctx, recipientId, message, options) => { Self.sendCheckingPresence = async(ctx, recipientId, message) => {
if (!recipientId) return false; if (!recipientId) return false;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const sender = await models.Account.findById(userId); const sender = await models.Account.findById(userId, {fields: ['id']});
const recipient = await models.Account.findById(recipientId, null, myOptions); const recipient = await models.Account.findById(recipientId, null);
// Prevent sending messages to yourself // Prevent sending messages to yourself
if (recipientId == userId) return false; if (recipientId == userId) return false;
@ -46,16 +41,23 @@ module.exports = Self => {
if (process.env.NODE_ENV == 'test') if (process.env.NODE_ENV == 'test')
message = `[Test:Environment to user ${userId}] ` + message; message = `[Test:Environment to user ${userId}] ` + message;
await models.Chat.create({ const chat = await models.Chat.create({
senderFk: sender.id, senderFk: sender.id,
recipient: `@${recipient.name}`, recipient: `@${recipient.name}`,
dated: Date.vnNew(), dated: Date.vnNew(),
checkUserStatus: 1, checkUserStatus: 1,
message: message, message: message,
status: 0, status: 'sending',
attempts: 0 attempts: 0
}); });
try {
await Self.sendCheckingUserStatus(chat);
await Self.updateChat(chat, 'sent');
} catch (error) {
await Self.updateChat(chat, 'error', error);
}
return true; return true;
}; };
}; };

View File

@ -3,7 +3,6 @@ module.exports = Self => {
Self.remoteMethodCtx('sendQueued', { Self.remoteMethodCtx('sendQueued', {
description: 'Send a RocketChat message', description: 'Send a RocketChat message',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [],
returns: { returns: {
type: 'object', type: 'object',
root: true root: true
@ -16,14 +15,17 @@ module.exports = Self => {
Self.sendQueued = async() => { Self.sendQueued = async() => {
const models = Self.app.models; const models = Self.app.models;
const maxAttempts = 3;
const sentStatus = 1;
const errorStatus = 2;
const chats = await models.Chat.find({ const chats = await models.Chat.find({
where: { where: {
status: {neq: sentStatus}, status: {
attempts: {lt: maxAttempts} nin: [
'sent',
'sending'
]
},
attempts: {lt: 3}
} }
}); });
@ -31,16 +33,16 @@ module.exports = Self => {
if (chat.checkUserStatus) { if (chat.checkUserStatus) {
try { try {
await Self.sendCheckingUserStatus(chat); await Self.sendCheckingUserStatus(chat);
await updateChat(chat, sentStatus); await Self.updateChat(chat, 'sent');
} catch (error) { } catch (error) {
await updateChat(chat, errorStatus, error); await Self.updateChat(chat, 'error', error);
} }
} else { } else {
try { try {
await Self.sendMessage(chat.senderFk, chat.recipient, chat.message); await Self.sendMessage(chat.senderFk, chat.recipient, chat.message);
await updateChat(chat, sentStatus); await Self.updateChat(chat, 'sent');
} catch (error) { } catch (error) {
await updateChat(chat, errorStatus, error); await Self.updateChat(chat, 'error', error);
} }
} }
} }
@ -128,15 +130,17 @@ module.exports = Self => {
* @param {object} chat - The chat * @param {object} chat - The chat
* @param {string} status - The new status * @param {string} status - The new status
* @param {string} error - The error * @param {string} error - The error
* @param {object} options - Query options
* @return {Promise} - The request promise * @return {Promise} - The request promise
*/ */
async function updateChat(chat, status, error) {
Self.updateChat = async(chat, status, error) => {
return chat.updateAttributes({ return chat.updateAttributes({
status: status, status: status,
attempts: ++chat.attempts, attempts: ++chat.attempts,
error: error error: error
}); });
} };
/** /**
* Returns the current user status on Rocketchat * Returns the current user status on Rocketchat

View File

@ -10,7 +10,7 @@ describe('Chat sendCheckingPresence()', () => {
const chat = { const chat = {
checkUserStatus: 1, checkUserStatus: 1,
status: 0, status: 'pending',
attempts: 0 attempts: 0
}; };
@ -27,7 +27,7 @@ describe('Chat sendCheckingPresence()', () => {
const chat = { const chat = {
checkUserStatus: 0, checkUserStatus: 0,
status: 0, status: 'pending',
attempts: 0 attempts: 0
}; };

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('setSaleQuantity()', () => { describe('setSaleQuantity()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should change quantity sale', async() => { it('should change quantity sale', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});

View File

@ -69,15 +69,15 @@ module.exports = Self => {
const result = response.headers.get('set-cookie'); const result = response.headers.get('set-cookie');
const [firtHeader] = result.split(' '); const [firtHeader] = result.split(' ');
const firtCookie = firtHeader.substring(0, firtHeader.length - 1); const cookie = firtHeader.substring(0, firtHeader.length - 1);
const body = await response.text(); const body = await response.text();
const dom = new jsdom.JSDOM(body); const dom = new jsdom.JSDOM(body);
const token = dom.window.document.querySelector('[name="__CSRFToken__"]').value; const token = dom.window.document.querySelector('[name="__CSRFToken__"]').value;
await login(token, firtCookie); await login(token, cookie);
} }
async function login(token, firtCookie) { async function login(token, cookie) {
const data = { const data = {
__CSRFToken__: token, __CSRFToken__: token,
do: 'scplogin', do: 'scplogin',
@ -90,21 +90,18 @@ module.exports = Self => {
body: new URLSearchParams(data), body: new URLSearchParams(data),
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Cookie': firtCookie 'Cookie': cookie
} }
}; };
const response = await fetch(ostUri, params); await fetch(ostUri, params);
const result = response.headers.get('set-cookie');
const [firtHeader] = result.split(' ');
const secondCookie = firtHeader.substring(0, firtHeader.length - 1);
await close(token, secondCookie); await close(token, cookie);
} }
async function close(token, secondCookie) { async function close(token, cookie) {
for (const ticketId of ticketsId) { for (const ticketId of ticketsId) {
try { try {
const lock = await getLockCode(token, secondCookie, ticketId); const lock = await getLockCode(token, cookie, ticketId);
if (!lock.code) { if (!lock.code) {
let error = `Can't get lock code`; let error = `Can't get lock code`;
if (lock.msg) error += `: ${lock.msg}`; if (lock.msg) error += `: ${lock.msg}`;
@ -127,7 +124,7 @@ module.exports = Self => {
method: 'POST', method: 'POST',
body: form, body: form,
headers: { headers: {
'Cookie': secondCookie 'Cookie': cookie
} }
}; };
await fetch(ostUri, params); await fetch(ostUri, params);
@ -139,13 +136,13 @@ module.exports = Self => {
} }
} }
async function getLockCode(token, secondCookie, ticketId) { async function getLockCode(token, cookie, ticketId) {
const ostUri = `${config.host}/ajax.php/lock/ticket/${ticketId}`; const ostUri = `${config.host}/ajax.php/lock/ticket/${ticketId}`;
const params = { const params = {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRFToken': token, 'X-CSRFToken': token,
'Cookie': secondCookie 'Cookie': cookie
} }
}; };
const response = await fetch(ostUri, params); const response = await fetch(ostUri, params);

View File

@ -4,7 +4,8 @@
"options": { "options": {
"mysql": { "mysql": {
"table": "salix.User" "table": "salix.User"
} },
"resetPasswordTokenTTL": "604800"
}, },
"properties": { "properties": {
"id": { "id": {

View File

@ -0,0 +1,16 @@
ALTER TABLE `vn`.`chat` ADD statusNew enum('pending','sent','error','sending') DEFAULT 'pending' NOT NULL;
UPDATE `vn`.`chat`
SET statusNew = 'pending'
WHERE status = 0;
UPDATE `vn`.`chat`
SET statusNew = 'sent'
WHERE status = 1;
UPDATE `vn`.`chat`
SET statusNew = 'error'
WHERE status = 2;
ALTER TABLE `vn`.`chat` CHANGE status status__ tinyint(1) DEFAULT NULL NULL;
ALTER TABLE `vn`.`chat` CHANGE statusNew status enum('pending','sent','error','sending') CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT 'pending' NOT NULL;

View File

@ -0,0 +1,4 @@
ALTER TABLE `vn`.`invoiceInConfig` ADD daysAgo INT UNSIGNED DEFAULT 45 COMMENT 'Días en el pasado para mostrar facturas en invoiceIn series en salix';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'getSerial', 'READ', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1,14 @@
ALTER TABLE `vn`.`itemType` ADD isFragile tinyint(1) NULL;
ALTER TABLE `vn`.`itemType` MODIFY COLUMN isFragile tinyint(1) DEFAULT 0 NOT NULL;
UPDATE `vn`.`itemType`
SET isFragile = 1
WHERE code IN ('ZKA', 'ZKE');
UPDATE `vn`.`itemType`
SET isFragile = 1
WHERE id IN (SELECT it.id
FROM `vn`.`itemCategory` ic
JOIN `vn`.`itemType` it ON it.categoryFk = ic.id
WHERE ic.code = 'plant');

View File

@ -0,0 +1,3 @@
DROP TRIGGER `vn`.`supplierAccount_afterInsert`;
DROP TRIGGER `vn`.`supplierAccount_afterUpdate`;
DROP TRIGGER `vn`.`supplierAccount_afterDelete`;

View File

@ -0,0 +1,47 @@
DROP PROCEDURE IF EXISTS `vn`.`ticket_getWarnings`;
DELIMITER $$
$$
CREATE PROCEDURE `vn`.`ticket_getWarnings`()
BEGIN
/**
* Calcula las adventencias para un conjunto de tickets.
* Agrupados por ticket
*
* @table tmp.sale_getWarnings(ticketFk) Identificadores de los tickets a calcular
* @return tmp.ticket_warnings
*/
DROP TEMPORARY TABLE IF EXISTS tmp.sale_warnings;
CREATE TEMPORARY TABLE tmp.sale_warnings (
ticketFk INT(11),
saleFk INT(11),
isFragile INTEGER(1) DEFAULT 0,
PRIMARY KEY (ticketFk, saleFk)
) ENGINE = MEMORY;
-- Frágil
INSERT INTO tmp.sale_warnings(ticketFk, saleFk, isFragile)
SELECT tt.ticketFk, s.id, TRUE
FROM tmp.sale_getWarnings tt
LEFT JOIN sale s ON s.ticketFk = tt.ticketFk
LEFT JOIN item i ON i.id = s.itemFk
LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN agencyMode am ON am.id = tt.agencyModeFk
LEFT JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
WHERE dm.code IN ('AGENCY')
AND it.isFragile;
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_warnings;
CREATE TEMPORARY TABLE tmp.ticket_warnings
(PRIMARY KEY (ticketFk))
ENGINE = MEMORY
SELECT
sw.ticketFk,
MAX(sw.isFragile) AS isFragile
FROM tmp.sale_warnings sw
GROUP BY sw.ticketFk;
DROP TEMPORARY TABLE
tmp.sale_warnings;
END$$
DELIMITER ;

View File

@ -0,0 +1,72 @@
CREATE TABLE `vn`.`wagonType` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL UNIQUE,
`divisible` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `vn`.`wagonTypeColor` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL UNIQUE,
`rgb` varchar(30) NOT NULL UNIQUE,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `vn`.`wagonTypeTray` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`typeFk` int(11) unsigned,
`height` int(11) unsigned NOT NULL,
`colorFk` int(11) unsigned,
PRIMARY KEY (`id`),
UNIQUE KEY (`typeFk`,`height`),
CONSTRAINT `wagonTypeTray_type` FOREIGN KEY (`typeFk`) REFERENCES `wagonType` (`id`) ON UPDATE CASCADE,
CONSTRAINT `wagonTypeTray_color` FOREIGN KEY (`colorFk`) REFERENCES `wagonTypeColor` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `vn`.`wagonConfig` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`width` int(11) unsigned DEFAULT 1350,
`height` int(11) unsigned DEFAULT 1900,
`maxWagonHeight` int(11) unsigned DEFAULT 200,
`minHeightBetweenTrays` int(11) unsigned DEFAULT 50,
`maxTrays` int(11) unsigned DEFAULT 6,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3;
CREATE TABLE `vn`.`collectionWagon` (
`collectionFk` int(11) NOT NULL,
`wagonFk` int(11) NOT NULL,
`position` int(11) unsigned,
PRIMARY KEY (`collectionFk`,`position`),
UNIQUE KEY `collectionWagon_unique` (`collectionFk`,`wagonFk`),
CONSTRAINT `collectionWagon_collection` FOREIGN KEY (`collectionFk`) REFERENCES `collection` (`id`) ON UPDATE CASCADE,
CONSTRAINT `collectionWagon_wagon` FOREIGN KEY (`wagonFk`) REFERENCES `wagon` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
CREATE TABLE `vn`.`collectionWagonTicket` (
`ticketFk` int(11) NOT NULL,
`wagonFk` int(11) NOT NULL,
`trayFk` int(11) unsigned NOT NULL,
`side` SET('L', 'R') NULL,
PRIMARY KEY (`ticketFk`),
CONSTRAINT `collectionWagonTicket_ticket` FOREIGN KEY (`ticketFk`) REFERENCES `ticket` (`id`) ON UPDATE CASCADE,
CONSTRAINT `collectionWagonTicket_wagon` FOREIGN KEY (`wagonFk`) REFERENCES `wagon` (`id`) ON UPDATE CASCADE,
CONSTRAINT `collectionWagonTicket_tray` FOREIGN KEY (`trayFk`) REFERENCES `wagonTypeTray` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
ALTER TABLE `vn`.`wagon` ADD `typeFk` int(11) unsigned NOT NULL;
ALTER TABLE `vn`.`wagon` ADD `label` int(11) unsigned NOT NULL;
ALTER TABLE `vn`.`wagon` ADD CONSTRAINT `wagon_type` FOREIGN KEY (`typeFk`) REFERENCES `wagonType` (`id`) ON UPDATE CASCADE;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('WagonType', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonTypeColor', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonTypeTray', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonConfig', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('CollectionWagon', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('CollectionWagonTicket', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('Wagon', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'createWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'deleteWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'editWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'negativeBases', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'negativeBasesCsv', 'READ', 'ALLOW', 'ROLE', 'administrative');

View File

@ -501,7 +501,8 @@ INSERT INTO `vn`.`observationType`(`id`,`description`, `code`)
(3, 'Delivery', 'delivery'), (3, 'Delivery', 'delivery'),
(4, 'SalesPerson', 'salesPerson'), (4, 'SalesPerson', 'salesPerson'),
(5, 'Administrative', 'administrative'), (5, 'Administrative', 'administrative'),
(6, 'Weight', 'weight'); (6, 'Weight', 'weight'),
(7, 'InvoiceOut', 'invoiceOut');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`) INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES VALUES
@ -738,7 +739,9 @@ INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `des
(9, 23, 5, 'care with the dog'), (9, 23, 5, 'care with the dog'),
(10, 23, 4, 'Reclama ticket: 8'), (10, 23, 4, 'Reclama ticket: 8'),
(11, 24, 4, 'Reclama ticket: 7'), (11, 24, 4, 'Reclama ticket: 7'),
(12, 11, 3, 'Delivery after 10am'); (12, 11, 3, 'Delivery after 10am'),
(13, 1, 7, 'observation of ticket one'),
(14, 2, 7, 'observation of ticket two');
-- FIX for state hours on local, inter_afterInsert -- FIX for state hours on local, inter_afterInsert
-- UPDATE vncontrol.inter SET odbc_date = DATE_ADD(util.VN_CURDATE(), INTERVAL -10 SECOND); -- UPDATE vncontrol.inter SET odbc_date = DATE_ADD(util.VN_CURDATE(), INTERVAL -10 SECOND);
@ -838,14 +841,14 @@ INSERT INTO `vn`.`temperature`(`code`, `name`, `description`)
('warm', 'Warm', 'Warm'), ('warm', 'Warm', 'Warm'),
('cool', 'Cool', 'Cool'); ('cool', 'Cool', 'Cool');
INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`, `workerFk`, `isPackaging`, `temperatureFk`) INSERT INTO `vn`.`itemType`(`id`, `code`, `name`, `categoryFk`, `life`, `workerFk`, `isPackaging`, `temperatureFk`, `isFragile`)
VALUES VALUES
(1, 'CRI', 'Crisantemo', 2, 31, 35, 0, 'cool'), (1, 'CRI', 'Crisantemo', 2, 31, 35, 0, 'cool', 0),
(2, 'ITG', 'Anthurium', 1, 31, 35, 0, 'cool'), (2, 'ITG', 'Anthurium', 1, 31, 35, 0, 'cool', 1),
(3, 'WPN', 'Paniculata', 2, 31, 35, 0, 'cool'), (3, 'WPN', 'Paniculata', 2, 31, 35, 0, 'cool', 0),
(4, 'PRT', 'Delivery ports', 3, NULL, 35, 1, 'warm'), (4, 'PRT', 'Delivery ports', 3, NULL, 35, 1, 'warm', 0),
(5, 'CON', 'Container', 3, NULL, 35, 1, 'warm'), (5, 'CON', 'Container', 3, NULL, 35, 1, 'warm', 0),
(6, 'ALS', 'Alstroemeria', 1, 31, 16, 0, 'warm'); (6, 'ALS', 'Alstroemeria', 1, 31, 16, 0, 'warm', 1);
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`) INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`)
VALUES VALUES
@ -2487,9 +2490,9 @@ REPLACE INTO `vn`.`invoiceIn`(`id`, `serialNumber`,`serial`, `supplierFk`, `issu
(9, 1009, 'R', 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1242, 1, 442, 1), (9, 1009, 'R', 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1242, 1, 442, 1),
(10, 1010, 'R', 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1243, 1, 442, 1); (10, 1010, 'R', 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1243, 1, 442, 1);
INSERT INTO `vn`.`invoiceInConfig` (`id`, `retentionRate`, `retentionName`, `sageWithholdingFk`) INSERT INTO `vn`.`invoiceInConfig` (`id`, `retentionRate`, `retentionName`, `sageWithholdingFk`, `daysAgo`)
VALUES VALUES
(1, -2, '2% retention', 2); (1, -2, '2% retention', 2, 45);
INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`) INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`)
VALUES VALUES
@ -2631,8 +2634,8 @@ INSERT INTO `vn`.`supplierAgencyTerm` (`agencyFk`, `supplierFk`, `minimumPackage
INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`) INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`)
VALUES VALUES
(1101, '@PetterParker', util.VN_CURDATE(), 1, 'First test message', 0, 0), (1101, '@PetterParker', util.VN_CURDATE(), 1, 'First test message', 0, 'sent'),
(1101, '@PetterParker', util.VN_CURDATE(), 0, 'Second test message', 0, 0); (1101, '@PetterParker', util.VN_CURDATE(), 0, 'Second test message', 0, 'pending');
INSERT INTO `vn`.`mobileAppVersionControl` (`appName`, `version`, `isVersionCritical`) INSERT INTO `vn`.`mobileAppVersionControl` (`appName`, `version`, `isVersionCritical`)
@ -2837,4 +2840,27 @@ INSERT INTO `vn`.`workerTimeControlMail` (`id`, `workerFk`, `year`, `week`, `sta
(3, 9, 2000, 51, 'CONFIRMED', util.VN_NOW(), 1, NULL), (3, 9, 2000, 51, 'CONFIRMED', util.VN_NOW(), 1, NULL),
(4, 9, 2001, 1, 'SENDED', util.VN_NOW(), 1, NULL); (4, 9, 2001, 1, 'SENDED', util.VN_NOW(), 1, NULL);
INSERT INTO `vn`.`wagonConfig` (`id`, `width`, `height`, `maxWagonHeight`, `minHeightBetweenTrays`, `maxTrays`)
VALUES
(1, 1350, 1900, 200, 50, 6);
INSERT INTO `vn`.`wagonTypeColor` (`id`, `name`, `rgb`)
VALUES
(1, 'white', '#ffffff'),
(2, 'red', '#ff0000'),
(3, 'green', '#00ff00'),
(4, 'blue', '#0000ff');
INSERT INTO `vn`.`wagonType` (`id`, `name`, `divisible`)
VALUES
(1, 'Wagon Type #1', 1);
INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
VALUES
(1, 1, 100, 1),
(2, 1, 50, 2),
(3, 1, 0, 3);

View File

@ -524,7 +524,6 @@ export default {
}, },
itemLog: { itemLog: {
anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr', anyLineCreated: 'vn-item-log > vn-log vn-tbody > vn-tr',
fifthLineCreatedProperty: 'vn-item-log > vn-log vn-tbody > vn-tr:nth-child(5) table tr:nth-child(5) td.after',
}, },
ticketSummary: { ticketSummary: {
header: 'vn-ticket-summary > vn-card > h5', header: 'vn-ticket-summary > vn-card > h5',
@ -1128,6 +1127,15 @@ export default {
saveButton: 'vn-invoice-in-tax vn-submit', saveButton: 'vn-invoice-in-tax vn-submit',
}, },
invoiceInIndex: {
topbarSearchParams: 'vn-searchbar div.search-params > span',
},
invoiceInSerial: {
daysAgo: 'vn-invoice-in-serial-search-panel vn-input-number[ng-model="$ctrl.filter.daysAgo"]',
serial: 'vn-invoice-in-serial-search-panel vn-textfield[ng-model="$ctrl.filter.serial"]',
chip: 'vn-chip > vn-icon',
goToIndex: 'vn-invoice-in-serial vn-icon-button[icon="icon-invoice-in"]',
},
travelIndex: { travelIndex: {
anySearchResult: 'vn-travel-index vn-tbody > a', anySearchResult: 'vn-travel-index vn-tbody > a',
firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)', firstSearchResult: 'vn-travel-index vn-tbody > a:nth-child(1)',
@ -1139,7 +1147,16 @@ export default {
landingDate: 'vn-travel-create vn-date-picker[ng-model="$ctrl.travel.landed"]', landingDate: 'vn-travel-create vn-date-picker[ng-model="$ctrl.travel.landed"]',
warehouseOut: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseOutFk"]', warehouseOut: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseOutFk"]',
warehouseIn: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseInFk"]', warehouseIn: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseInFk"]',
save: 'vn-travel-create vn-submit > button' save: 'vn-travel-create vn-submit > button',
generalSearchFilter: 'vn-travel-search-panel vn-textfield[ng-model="$ctrl.search"]',
agencyFilter: 'vn-travel-search-panel vn-autocomplete[ng-model="$ctrl.filter.agencyModeFk"]',
warehouseOutFilter: 'vn-travel-search-panel vn-autocomplete[ng-model="$ctrl.filter.warehouseOutFk"]',
warehouseInFilter: 'vn-travel-search-panel vn-autocomplete[ng-model="$ctrl.filter.warehouseInFk"]',
scopeDaysFilter: 'vn-travel-search-panel vn-input-number[ng-model="$ctrl.filter.scopeDays"]',
continentFilter: 'vn-travel-search-panel vn-autocomplete[ng-model="$ctrl.filter.continent"]',
totalEntriesFilter: 'vn-travel-search-panel vn-input-number[ng-model="$ctrl.totalEntries"]',
chip: 'vn-travel-search-panel vn-chip > vn-icon',
}, },
travelExtraCommunity: { travelExtraCommunity: {
anySearchResult: 'vn-travel-extra-community > vn-card div > tbody > tr[ng-attr-id="{{::travel.id}}"]', anySearchResult: 'vn-travel-extra-community > vn-card div > tbody > tr[ng-attr-id="{{::travel.id}}"]',

View File

@ -48,14 +48,14 @@ describe('Item log path', () => {
await page.accessToSection('item.card.log'); await page.accessToSection('item.card.log');
}); });
it(`should confirm the log is showing 5 entries`, async() => { it(`should confirm the log is showing 4 entries`, async() => {
await page.waitForSelector(selectors.itemLog.anyLineCreated); await page.waitForSelector(selectors.itemLog.anyLineCreated);
const anyLineCreatedCount = await page.countElement(selectors.itemLog.anyLineCreated); const anyLineCreatedCount = await page.countElement(selectors.itemLog.anyLineCreated);
expect(anyLineCreatedCount).toEqual(5); expect(anyLineCreatedCount).toEqual(4);
}); });
it(`should confirm the log is showing the intrastat for the created item`, async() => { xit(`should confirm the log is showing the intrastat for the created item`, async() => {
const fifthLineCreatedProperty = await page const fifthLineCreatedProperty = await page
.waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText'); .waitToGetProperty(selectors.itemLog.fifthLineCreatedProperty, 'innerText');

View File

@ -197,6 +197,7 @@ describe('Ticket Edit sale path', () => {
}); });
it('should check in the history that logs has been added', async() => { it('should check in the history that logs has been added', async() => {
pending('https://redmine.verdnatura.es/issues/5455');
await page.reload({waitUntil: ['networkidle0', 'domcontentloaded']}); await page.reload({waitUntil: ['networkidle0', 'domcontentloaded']});
await page.waitToClick(selectors.ticketSales.firstSaleHistoryButton); await page.waitToClick(selectors.ticketSales.firstSaleHistoryButton);
await page.waitForSelector(selectors.ticketSales.firstSaleHistory); await page.waitForSelector(selectors.ticketSales.firstSaleHistory);

View File

@ -9,7 +9,7 @@ describe('Ticket Create notes path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('employee', 'ticket'); await page.loginAndModule('employee', 'ticket');
await page.accessToSearchResult('1'); await page.accessToSearchResult('5');
await page.accessToSection('ticket.card.observation'); await page.accessToSection('ticket.card.observation');
}); });

View File

@ -0,0 +1,29 @@
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn negative bases path', () => {
let browser;
let page;
const httpRequests = [];
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
page.on('request', req => {
if (req.url().includes(`InvoiceIns/negativeBases`))
httpRequests.push(req.url());
});
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSection('invoiceIn.negative-bases');
});
afterAll(async() => {
await browser.close();
});
it('should show negative bases in a date range', async() => {
const request = httpRequests.find(req =>
req.includes(`from`) && req.includes(`to`));
expect(request).toBeDefined();
});
});

View File

@ -0,0 +1,48 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn serial path', () => {
let browser;
let page;
let httpRequest;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSection('invoiceIn.serial');
page.on('request', req => {
if (req.url().includes(`InvoiceIns/getSerial`))
httpRequest = req.url();
});
});
afterAll(async() => {
await browser.close();
});
it('should check that passes the correct params to back', async() => {
await page.overwrite(selectors.invoiceInSerial.daysAgo, '30');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('daysAgo=30');
await page.overwrite(selectors.invoiceInSerial.serial, 'R');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('serial=R');
await page.click(selectors.invoiceInSerial.chip);
});
it('should go to index and check if the search-panel has the correct params', async() => {
await page.click(selectors.invoiceInSerial.goToIndex);
const params = await page.$$(selectors.invoiceInIndex.topbarSearchParams);
const serial = await params[0].getProperty('title');
const isBooked = await params[1].getProperty('title');
const from = await params[2].getProperty('title');
expect(await serial.jsonValue()).toContain('serial');
expect(await isBooked.jsonValue()).toContain('not isBooked');
expect(await from.jsonValue()).toContain('from');
});
});

View File

@ -9,7 +9,8 @@ describe('Travel basic data path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('buyer', 'travel'); await page.loginAndModule('buyer', 'travel');
await page.accessToSearchResult('3'); await page.write(selectors.travelIndex.generalSearchFilter, '3');
await page.keyboard.press('Enter');
await page.accessToSection('travel.card.basicData'); await page.accessToSection('travel.card.basicData');
}); });
@ -89,11 +90,13 @@ describe('Travel basic data path', () => {
}); });
it('should navigate to the travel logs', async() => { it('should navigate to the travel logs', async() => {
pending('https://redmine.verdnatura.es/issues/5455');
await page.accessToSection('travel.card.log'); await page.accessToSection('travel.card.log');
await page.waitForState('travel.card.log'); await page.waitForState('travel.card.log');
}); });
it('should check the 1st log contains details from the changes made', async() => { it('should check the 1st log contains details from the changes made', async() => {
pending('https://redmine.verdnatura.es/issues/5455');
const result = await page.waitToGetProperty(selectors.travelLog.firstLogFirstTD, 'innerText'); const result = await page.waitToGetProperty(selectors.travelLog.firstLogFirstTD, 'innerText');
expect(result).toContain('new reference!'); expect(result).toContain('new reference!');

View File

@ -9,7 +9,8 @@ describe('Travel descriptor path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('buyer', 'travel'); await page.loginAndModule('buyer', 'travel');
await page.accessToSearchResult('1'); await page.write(selectors.travelIndex.generalSearchFilter, '1');
await page.keyboard.press('Enter');
await page.waitForState('travel.card.summary'); await page.waitForState('travel.card.summary');
}); });
@ -81,7 +82,8 @@ describe('Travel descriptor path', () => {
await page.waitToClick('.cancel'); await page.waitToClick('.cancel');
await page.waitToClick(selectors.globalItems.homeButton); await page.waitToClick(selectors.globalItems.homeButton);
await page.selectModule('travel'); await page.selectModule('travel');
await page.accessToSearchResult('3'); await page.write(selectors.travelIndex.generalSearchFilter, '3');
await page.keyboard.press('Enter');
await page.waitForState('travel.card.summary'); await page.waitForState('travel.card.summary');
const state = await page.getState(); const state = await page.getState();

View File

@ -10,7 +10,8 @@ describe('Travel thermograph path', () => {
browser = await getBrowser(); browser = await getBrowser();
page = browser.page; page = browser.page;
await page.loginAndModule('buyer', 'travel'); await page.loginAndModule('buyer', 'travel');
await page.accessToSearchResult('3'); await page.write(selectors.travelIndex.generalSearchFilter, '3');
await page.keyboard.press('Enter');
await page.accessToSection('travel.card.thermograph.index'); await page.accessToSection('travel.card.thermograph.index');
}); });

View File

@ -0,0 +1,62 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Travel search panel path', () => {
let browser;
let page;
let httpRequest;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('buyer', 'travel');
page.on('request', req => {
if (req.url().includes(`Travels/filter`))
httpRequest = req.url();
});
});
afterAll(async() => {
await browser.close();
});
it('should filter using all the fields', async() => {
await page.click(selectors.travelIndex.chip);
await page.write(selectors.travelIndex.generalSearchFilter, 'travel');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('search=travel');
await page.click(selectors.travelIndex.chip);
await page.autocompleteSearch(selectors.travelIndex.agencyFilter, 'Entanglement');
expect(httpRequest).toContain('agencyModeFk');
await page.click(selectors.travelIndex.chip);
await page.autocompleteSearch(selectors.travelIndex.warehouseOutFilter, 'Warehouse One');
expect(httpRequest).toContain('warehouseOutFk');
await page.click(selectors.travelIndex.chip);
await page.autocompleteSearch(selectors.travelIndex.warehouseInFilter, 'Warehouse Two');
expect(httpRequest).toContain('warehouseInFk');
await page.click(selectors.travelIndex.chip);
await page.overwrite(selectors.travelIndex.scopeDaysFilter, '15');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('scopeDays=15');
await page.click(selectors.travelIndex.chip);
await page.autocompleteSearch(selectors.travelIndex.continentFilter, 'Asia');
expect(httpRequest).toContain('continent');
await page.click(selectors.travelIndex.chip);
await page.write(selectors.travelIndex.totalEntriesFilter, '1');
await page.keyboard.press('Enter');
expect(httpRequest).toContain('totalEntries=1');
});
});

View File

@ -26,7 +26,7 @@ Value should have at most %s characters: El valor debe tener un máximo de %s ca
Enter a new search: Introduce una nueva búsqueda Enter a new search: Introduce una nueva búsqueda
No results: Sin resultados No results: Sin resultados
Ups! It seems there was an error: ¡Vaya! Parece que ha habido un error Ups! It seems there was an error: ¡Vaya! Parece que ha habido un error
General search: Busqueda general General search: Búsqueda general
January: Enero January: Enero
February: Febrero February: Febrero
March: Marzo March: Marzo
@ -42,9 +42,9 @@ December: Diciembre
Monday: Lunes Monday: Lunes
Tuesday: Martes Tuesday: Martes
Wednesday: Miércoles Wednesday: Miércoles
Thursday: Jueves Thursday: Jueves
Friday: Viernes Friday: Viernes
Saturday: Sábado Saturday: Sábado
Sunday: Domingo Sunday: Domingo
Has delivery: Hay reparto Has delivery: Hay reparto
Loading: Cargando Loading: Cargando
@ -63,4 +63,4 @@ Loading...: Cargando...
No results found: Sin resultados No results found: Sin resultados
No data: Sin datos No data: Sin datos
Undo changes: Deshacer cambios Undo changes: Deshacer cambios
Load more results: Cargar más resultados Load more results: Cargar más resultados

View File

@ -2,6 +2,7 @@
$font-size: 11pt; $font-size: 11pt;
$menu-width: 256px; $menu-width: 256px;
$right-menu-width: 318px;
$topbar-height: 56px; $topbar-height: 56px;
$mobile-width: 800px; $mobile-width: 800px;
$float-spacing: 20px; $float-spacing: 20px;

View File

@ -88,13 +88,13 @@ vn-layout {
} }
&.right-menu { &.right-menu {
& > vn-topbar > .end { & > vn-topbar > .end {
width: 80px + $menu-width; width: 80px + $right-menu-width;
} }
& > .main-view { & > .main-view {
padding-right: $menu-width; padding-right: $right-menu-width;
} }
[fixed-bottom-right] { [fixed-bottom-right] {
right: $menu-width; right: $right-menu-width;
} }
} }
& > .main-view { & > .main-view {

View File

@ -271,5 +271,6 @@
"This locker has already been assigned": "Esta taquilla ya ha sido asignada", "This locker has already been assigned": "Esta taquilla ya ha sido asignada",
"Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}", "Tickets with associated refunds": "No se pueden borrar tickets con abonos asociados. Este ticket está asociado al abono Nº {{id}}",
"Not exist this branch": "La rama no existe", "Not exist this branch": "La rama no existe",
"This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado" "This ticket cannot be signed because it has not been boxed": "Este ticket no puede firmarse porque no ha sido encajado",
"Insert a date range": "Inserte un rango de fechas"
} }

View File

@ -1,8 +1,7 @@
const mysql = require('mysql'); const mysql = require('mysql');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const MySQL = require('loopback-connector-mysql').MySQL; const MySQL = require('loopback-connector-mysql').MySQL;
const EnumFactory = require('loopback-connector-mysql').EnumFactory; const EnumFactory = require('loopback-connector-mysql').EnumFactory;
const Transaction = require('loopback-connector').Transaction; const { Transaction, SQLConnector, ParameterizedSQL } = require('loopback-connector');
const fs = require('fs'); const fs = require('fs');
const limitSet = new Set([ const limitSet = new Set([
@ -254,49 +253,49 @@ class VnMySQL extends MySQL {
} }
create(model, data, opts, cb) { create(model, data, opts, cb) {
const ctx = {data}; const ctx = { data };
this.invokeMethod('create', this.invokeMethod('create',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
createAll(model, data, opts, cb) { createAll(model, data, opts, cb) {
const ctx = {data}; const ctx = { data };
this.invokeMethod('createAll', this.invokeMethod('createAll',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
save(model, data, opts, cb) { save(model, data, opts, cb) {
const ctx = {data}; const ctx = { data };
this.invokeMethod('save', this.invokeMethod('save',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
updateOrCreate(model, data, opts, cb) { updateOrCreate(model, data, opts, cb) {
const ctx = {data}; const ctx = { data };
this.invokeMethod('updateOrCreate', this.invokeMethod('updateOrCreate',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
replaceOrCreate(model, data, opts, cb) { replaceOrCreate(model, data, opts, cb) {
const ctx = {data}; const ctx = { data };
this.invokeMethod('replaceOrCreate', this.invokeMethod('replaceOrCreate',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
destroyAll(model, where, opts, cb) { destroyAll(model, where, opts, cb) {
const ctx = {where}; const ctx = { where };
this.invokeMethod('destroyAll', this.invokeMethod('destroyAll',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
update(model, where, data, opts, cb) { update(model, where, data, opts, cb) {
const ctx = {where, data}; const ctx = { where, data };
this.invokeMethod('update', this.invokeMethod('update',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
replaceById(model, id, data, opts, cb) { replaceById(model, id, data, opts, cb) {
const ctx = {id, data}; const ctx = { id, data };
this.invokeMethod('replaceById', this.invokeMethod('replaceById',
arguments, model, ctx, opts, cb); arguments, model, ctx, opts, cb);
} }
@ -321,16 +320,16 @@ class VnMySQL extends MySQL {
let tx; let tx;
if (!opts.transaction) { if (!opts.transaction) {
tx = await Transaction.begin(this, {}); tx = await Transaction.begin(this, {});
opts = Object.assign({transaction: tx, httpCtx: opts.httpCtx}, opts); opts = Object.assign({ transaction: tx, httpCtx: opts.httpCtx }, opts);
} }
try { try {
// Fetch old values (update|delete) or login // Fetch old values (update|delete) or login
let where, id, data, idName, limit, op, oldInstances, newInstances; let where, id, data, idName, limit, op, oldInstances, newInstances;
const hasGrabUser = settings.log && settings.log.grabUser; const hasGrabUser = settings.log && settings.log.grabUser;
if(hasGrabUser){ if (hasGrabUser) {
const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId; const userId = opts.httpCtx && opts.httpCtx.active.accessToken.userId;
const user = await Model.app.models.Account.findById(userId, {fields: ['name']}, opts); const user = await Model.app.models.Account.findById(userId, { fields: ['name'] }, opts);
await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts); await this.executeP(`CALL account.myUser_loginWithName(?)`, [user.name], opts);
} }
else { else {
@ -344,18 +343,18 @@ class VnMySQL extends MySQL {
op = opMap.get(method); op = opMap.get(method);
if (!where) { if (!where) {
if (id) where = {[idName]: id}; if (id) where = { [idName]: id };
else where = {[idName]: data[idName]}; else where = { [idName]: data[idName] };
} }
// Fetch old values // Fetch old values
switch (op) { switch (op) {
case 'update': case 'update':
case 'delete': case 'delete':
// Single entity operation // Single entity operation
const stmt = this.buildSelectStmt(op, data, idName, model, where, limit); const stmt = this.buildSelectStmt(op, data, idName, model, where, limit);
stmt.merge(`FOR UPDATE`); stmt.merge(`FOR UPDATE`);
oldInstances = await this.executeStmt(stmt, opts); oldInstances = await this.executeStmt(stmt, opts);
} }
} }
@ -365,30 +364,30 @@ class VnMySQL extends MySQL {
super[method].apply(this, fnArgs); super[method].apply(this, fnArgs);
}); });
if(hasGrabUser) if (hasGrabUser)
await this.executeP(`CALL account.myUser_logout()`, null, opts); await this.executeP(`CALL account.myUser_logout()`, null, opts);
else { else {
// Fetch new values // Fetch new values
const ids = []; const ids = [];
switch (op) { switch (op) {
case 'insert': case 'insert':
case 'update': { case 'update': {
switch (method) { switch (method) {
case 'createAll': case 'createAll':
for (const row of res[1]) for (const row of res[1])
ids.push(row[idName]); ids.push(row[idName]);
break; break;
case 'create': case 'create':
ids.push(res[1]); ids.push(res[1]);
break; break;
case 'update': case 'update':
if (data[idName] != null) if (data[idName] != null)
ids.push(data[idName]); ids.push(data[idName]);
break; break;
} }
const newWhere = ids.length ? {[idName]: ids} : where; const newWhere = ids.length ? { [idName]: ids } : where;
const stmt = this.buildSelectStmt(op, data, idName, model, newWhere, limit); const stmt = this.buildSelectStmt(op, data, idName, model, newWhere, limit);
newInstances = await this.executeStmt(stmt, opts); newInstances = await this.executeStmt(stmt, opts);
@ -431,9 +430,9 @@ class VnMySQL extends MySQL {
const stmt = new ParameterizedSQL( const stmt = new ParameterizedSQL(
'SELECT ' + 'SELECT ' +
this.buildColumnNames(model, {fields}) + this.buildColumnNames(model, { fields }) +
' FROM ' + ' FROM ' +
this.tableEscaped(model) this.tableEscaped(model)
); );
stmt.merge(this.buildWhere(model, where)); stmt.merge(this.buildWhere(model, where));
if (limit) stmt.merge(`LIMIT 1`); if (limit) stmt.merge(`LIMIT 1`);
@ -505,8 +504,8 @@ class VnMySQL extends MySQL {
if (oldI) { if (oldI) {
Object.keys(oldI).forEach(prop => { Object.keys(oldI).forEach(prop => {
const hasChanges = oldI[prop] instanceof Date ? const hasChanges = oldI[prop] instanceof Date ?
oldI[prop]?.getTime() != newI[prop]?.getTime() : oldI[prop]?.getTime() != newI[prop]?.getTime() :
oldI[prop] != newI[prop]; oldI[prop] != newI[prop];
if (!hasChanges) { if (!hasChanges) {
delete oldI[prop]; delete oldI[prop];
@ -537,13 +536,13 @@ exports.initialize = function initialize(dataSource, callback) {
modelBuilder.defineValueType.bind(modelBuilder) : modelBuilder.defineValueType.bind(modelBuilder) :
modelBuilder.constructor.registerType.bind(modelBuilder.constructor); modelBuilder.constructor.registerType.bind(modelBuilder.constructor);
defineType(function Point() {}); defineType(function Point() { });
dataSource.EnumFactory = EnumFactory; dataSource.EnumFactory = EnumFactory;
if (callback) { if (callback) {
if (dataSource.settings.lazyConnect) { if (dataSource.settings.lazyConnect) {
process.nextTick(function() { process.nextTick(function () {
callback(); callback();
}); });
} else } else
@ -551,13 +550,13 @@ exports.initialize = function initialize(dataSource, callback) {
} }
}; };
MySQL.prototype.connect = function(callback) { MySQL.prototype.connect = function (callback) {
const self = this; const self = this;
const options = generateOptions(this.settings); const options = generateOptions(this.settings);
if (this.client) { if (this.client) {
if (callback) { if (callback) {
process.nextTick(function() { process.nextTick(function () {
callback(null, self.client); callback(null, self.client);
}); });
} }
@ -566,7 +565,7 @@ MySQL.prototype.connect = function(callback) {
function connectionHandler(options, callback) { function connectionHandler(options, callback) {
const client = mysql.createPool(options); const client = mysql.createPool(options);
client.getConnection(function(err, connection) { client.getConnection(function (err, connection) {
const conn = connection; const conn = connection;
if (!err) { if (!err) {
if (self.debug) if (self.debug)
@ -645,3 +644,31 @@ function generateOptions(settings) {
} }
return options; return options;
} }
SQLConnector.prototype.all = function find(model, filter, options, cb) {
const self = this;
// Order by id if no order is specified
filter = filter || {};
const stmt = this.buildSelect(model, filter, options);
this.execute(stmt.sql, stmt.params, options, function (err, data) {
if (err) {
return cb(err, []);
}
try {
const objs = data.map(function (obj) {
return self.fromRow(model, obj);
});
if (filter && filter.include) {
self.getModelDefinition(model).model.include(
objs, filter.include, options, cb,
);
} else {
cb(null, objs);
}
} catch (error) {
cb(error, [])
}
});
};

View File

@ -26,13 +26,13 @@ module.exports = Self => {
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const state = await models.ClaimState.findById(id, { const state = await models.ClaimState.findById(id, {
include: { include: {
relation: 'writeRole' relation: 'writeRole'
} }
}, myOptions); }, myOptions);
const roleWithGrants = state && state.writeRole().name; const roleWithGrants = state && state.writeRole().name;
return await models.Account.hasRole(userId, roleWithGrants, myOptions); return await models.Account.hasRole(userId, roleWithGrants, myOptions);
}; };
}; };

View File

@ -1,7 +1,7 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter; const buildFilter = require('vn-loopback/util/filter').buildFilter;
const {mergeFilters, mergeWhere} = require('vn-loopback/util/filter'); const { mergeFilters, mergeWhere } = require('vn-loopback/util/filter');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('logs', { Self.remoteMethodCtx('logs', {
@ -12,27 +12,27 @@ module.exports = Self => {
arg: 'id', arg: 'id',
type: 'Number', type: 'Number',
description: 'The claim id', description: 'The claim id',
http: {source: 'path'} http: { source: 'path' }
}, },
{ {
arg: 'filter', arg: 'filter',
type: 'object', type: 'object',
http: {source: 'query'} http: { source: 'query' }
}, },
{ {
arg: 'search', arg: 'search',
type: 'string', type: 'string',
http: {source: 'query'} http: { source: 'query' }
}, },
{ {
arg: 'userFk', arg: 'userFk',
type: 'number', type: 'number',
http: {source: 'query'} http: { source: 'query' }
}, },
{ {
arg: 'created', arg: 'created',
type: 'date', type: 'date',
http: {source: 'query'} http: { source: 'query' }
}, },
], ],
returns: { returns: {
@ -45,7 +45,7 @@ module.exports = Self => {
} }
}); });
Self.logs = async(ctx, id, filter, options) => { Self.logs = async (ctx, id, filter, options) => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const args = ctx.args; const args = ctx.args;
const myOptions = {}; const myOptions = {};
@ -56,25 +56,25 @@ module.exports = Self => {
let where = buildFilter(args, (param, value) => { let where = buildFilter(args, (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
return { return {
or: [ or: [
{changedModel: {like: `%${value}%`}}, { changedModel: { like: `%${value}%` } },
{oldInstance: {like: `%${value}%`}} { oldInstance: { like: `%${value}%` } }
] ]
}; };
case 'userFk': case 'userFk':
return {'cl.userFk': value}; return { 'cl.userFk': value };
case 'created': case 'created':
value.setHours(0, 0, 0, 0); value.setHours(0, 0, 0, 0);
to = new Date(value); to = new Date(value);
to.setHours(23, 59, 59, 999); to.setHours(23, 59, 59, 999);
return {creationDate: {between: [value, to]}}; return { creationDate: { between: [value, to] } };
} }
}); });
where = mergeWhere(where, {['cl.originFk']: id}); where = mergeWhere(where, { ['cl.originFk']: id });
filter = mergeFilters(args.filter, {where}); filter = mergeFilters(args.filter, { where });
const stmts = []; const stmts = [];
@ -102,8 +102,8 @@ module.exports = Self => {
const logs = []; const logs = [];
for (const row of result) { for (const row of result) {
const changes = []; const changes = [];
const oldInstance = JSON.parse(row.oldInstance); const oldInstance = JSON.parse(row.oldInstance) || {};
const newInstance = JSON.parse(row.newInstance); const newInstance = JSON.parse(row.newInstance) || {};
const mergedProperties = [...Object.keys(oldInstance), ...Object.keys(newInstance)]; const mergedProperties = [...Object.keys(oldInstance), ...Object.keys(newInstance)];
const properties = new Set(mergedProperties); const properties = new Set(mergedProperties);
for (const property of properties) { for (const property of properties) {

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('claim regularizeClaim()', () => { describe('claim regularizeClaim()', () => {
const userId = 18; const userId = 18;
@ -39,6 +40,20 @@ describe('claim regularizeClaim()', () => {
return await models.ClaimEnd.create(claimEnds, options); return await models.ClaimEnd.create(claimEnds, options);
} }
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should send a chat message with value "Trash" and then change claim state to resolved', async() => { it('should send a chat message with value "Trash" and then change claim state to resolved', async() => {
const tx = await models.Claim.beginTransaction({}); const tx = await models.Claim.beginTransaction({});

View File

@ -151,7 +151,7 @@ class Controller extends Section {
isClaimEditable() { isClaimEditable() {
if (!this.claim) return; if (!this.claim) return;
this.$http.get(`ClaimStates/${this.claim.id}/isEditable`).then(res => { this.$http.get(`ClaimStates/${this.claim.claimStateFk}/isEditable`).then(res => {
this.isRewritable = res.data; this.isRewritable = res.data;
}); });
} }

View File

@ -22,7 +22,8 @@ describe('claim', () => {
controller = $componentController('vnClaimDetail', {$element, $scope}); controller = $componentController('vnClaimDetail', {$element, $scope});
controller.claim = { controller.claim = {
ticketFk: 1, ticketFk: 1,
id: 2} id: 2,
claimStateFk: 2}
; ;
controller.salesToClaim = [{saleFk: 1}, {saleFk: 2}]; controller.salesToClaim = [{saleFk: 1}, {saleFk: 2}];
controller.salesClaimed = [{id: 1, sale: {}}]; controller.salesClaimed = [{id: 1, sale: {}}];

View File

@ -63,4 +63,4 @@ Consumption: Consumo
Compensation Account: Cuenta para compensar Compensation Account: Cuenta para compensar
Amount to return: Cantidad a devolver Amount to return: Cantidad a devolver
Delivered amount: Cantidad entregada Delivered amount: Cantidad entregada
Unpaid: Impagado Unpaid: Impagado

View File

@ -1,6 +1,22 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('Buy editLatestsBuys()', () => { describe('Buy editLatestsBuys()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should change the value of a given column for the selected buys', async() => { it('should change the value of a given column for the selected buys', async() => {
const tx = await models.Buy.beginTransaction({}); const tx = await models.Buy.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -0,0 +1,65 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('getSerial', {
description: 'Return invoiceIn serial',
accessType: 'READ',
accepts: [{
arg: 'filter',
type: 'object'
}, {
arg: 'daysAgo',
type: 'number',
required: true
}, {
arg: 'serial',
type: 'string'
}],
returns: {
type: 'object',
root: true
},
http: {
path: '/getSerial',
verb: 'GET'
}
});
Self.getSerial = async(ctx, options) => {
const conn = Self.dataSource.connector;
const args = ctx.args;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const issued = Date.vnNew();
const where = buildFilter(args, (param, value) => {
switch (param) {
case 'daysAgo':
issued.setDate(issued.getDate() - value);
return {'i.issued': {gte: issued}};
case 'serial':
return {'i.serial': {like: `%${value}%`}};
}
});
filter = mergeFilters(args.filter, {where});
const stmt = new ParameterizedSQL(
`SELECT i.serial, SUM(IF(i.isBooked, 0,1)) pending, COUNT(*) total
FROM vn.invoiceIn i`
);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(`GROUP BY i.serial`);
stmt.merge(conn.makeOrderBy(filter.order));
stmt.merge(conn.makeLimit(filter));
const result = await conn.executeStmt(stmt, myOptions);
return result;
};
};

View File

@ -0,0 +1,112 @@
const UserError = require('vn-loopback/util/user-error');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethodCtx('negativeBases', {
description: 'Find all negative bases',
accessType: 'READ',
accepts: [
{
arg: 'from',
type: 'date',
description: 'From date'
},
{
arg: 'to',
type: 'date',
description: 'To date'
},
{
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
},
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/negativeBases`,
verb: 'GET'
}
});
Self.negativeBases = async(ctx, options) => {
const conn = Self.dataSource.connector;
const args = ctx.args;
if (!args.from || !args.to)
throw new UserError(`Insert a date range`);
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const stmts = [];
let stmt;
stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket`);
stmts.push(new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM ticket t
WHERE shipped BETWEEN ? AND ?
AND refFk IS NULL`, [args.from, args.to]));
stmts.push(`CALL vn.ticket_getTax(NULL)`);
stmts.push(`DROP TEMPORARY TABLE IF EXISTS tmp.filter`);
stmts.push(new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.filter
ENGINE = MEMORY
SELECT
co.code company,
cou.country,
c.id clientId,
c.socialName clientSocialName,
SUM(s.quantity * s.price * ( 100 - s.discount ) / 100) amount,
negativeBase.taxableBase,
negativeBase.ticketFk,
c.isActive,
c.hasToInvoice,
c.isTaxDataChecked,
w.id comercialId,
CONCAT(w.firstName, ' ', w.lastName) comercialName
FROM vn.ticket t
JOIN vn.company co ON co.id = t.companyFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.client c ON c.id = t.clientFk
JOIN vn.country cou ON cou.id = c.countryFk
LEFT JOIN vn.worker w ON w.id = c.salesPersonFk
LEFT JOIN (
SELECT ticketFk, taxableBase
FROM tmp.ticketAmount
GROUP BY ticketFk
HAVING taxableBase < 0
) negativeBase ON negativeBase.ticketFk = t.id
WHERE t.shipped BETWEEN ? AND ?
AND t.refFk IS NULL
AND c.typeFk IN ('normal','trust')
GROUP BY t.clientFk, negativeBase.taxableBase
HAVING amount <> 0`, [args.from, args.to]));
stmt = new ParameterizedSQL(`
SELECT f.*
FROM tmp.filter f`);
stmt.merge(conn.makeWhere(args.filter.where));
stmt.merge(conn.makeOrderBy(args.filter.order));
const negativeBasesIndex = stmts.push(stmt) - 1;
stmts.push(`DROP TEMPORARY TABLE tmp.filter, tmp.ticket, tmp.ticketTax, tmp.ticketAmount`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return negativeBasesIndex === 0 ? result : result[negativeBasesIndex];
};
};

View File

@ -0,0 +1,53 @@
const {toCSV} = require('vn-loopback/util/csv');
module.exports = Self => {
Self.remoteMethodCtx('negativeBasesCsv', {
description: 'Returns the negative bases as .csv',
accessType: 'READ',
accepts: [{
arg: 'negativeBases',
type: ['object'],
required: true
},
{
arg: 'from',
type: 'date',
description: 'From date'
},
{
arg: 'to',
type: 'date',
description: 'To date'
}],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/negativeBasesCsv',
verb: 'GET'
}
});
Self.negativeBasesCsv = async ctx => {
const args = ctx.args;
const content = toCSV(args.negativeBases);
return [
content,
'text/csv',
`attachment; filename="negative-bases-${new Date(args.from).toLocaleDateString()}-${new Date(args.to).toLocaleDateString()}.csv"`
];
};
};

View File

@ -0,0 +1,24 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn getSerial()', () => {
it('should check that returns without serial param', async() => {
const ctx = {args: {daysAgo: 45}};
const result = await models.InvoiceIn.getSerial(ctx);
expect(result.length).toBeGreaterThan(0);
});
it('should check that returns with serial param', async() => {
const ctx = {args: {daysAgo: 45, serial: 'R'}};
const result = await models.InvoiceIn.getSerial(ctx);
expect(result.length).toBeGreaterThan(0);
});
it('should check that returns with non exist serial param', async() => {
const ctx = {args: {daysAgo: 45, serial: 'Mock serial'}};
const result = await models.InvoiceIn.getSerial(ctx);
expect(result.length).toEqual(0);
});
});

View File

@ -0,0 +1,47 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn negativeBases()', () => {
it('should return all negative bases in a date range', async() => {
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
const ctx = {
args: {
from: new Date().setMonth(new Date().getMonth() - 12),
to: new Date(),
filter: {}
}
};
try {
const result = await models.InvoiceIn.negativeBases(ctx, options);
expect(result.length).toBeGreaterThan(0);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should throw an error if a date range is not in args', async() => {
let error;
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
const ctx = {
args: {
filter: {}
}
};
try {
await models.InvoiceIn.negativeBases(ctx, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Insert a date range`);
});
});

View File

@ -17,6 +17,9 @@
}, },
"retentionName": { "retentionName": {
"type": "string" "type": "string"
},
"daysAgo": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -6,4 +6,7 @@ module.exports = Self => {
require('../methods/invoice-in/getTotals')(Self); require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(Self); require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self); require('../methods/invoice-in/invoiceInEmail')(Self);
require('../methods/invoice-in/getSerial')(Self);
require('../methods/invoice-in/negativeBases')(Self);
require('../methods/invoice-in/negativeBasesCsv')(Self);
}; };

View File

@ -13,3 +13,6 @@ import './dueDay';
import './intrastat'; import './intrastat';
import './create'; import './create';
import './log'; import './log';
import './serial';
import './serial-search-panel';
import './negative-bases';

View File

@ -7,6 +7,7 @@ Foreign value: Divisa
InvoiceIn: Facturas recibidas InvoiceIn: Facturas recibidas
InvoiceIn cloned: Factura clonada InvoiceIn cloned: Factura clonada
InvoiceIn deleted: Factura eliminada InvoiceIn deleted: Factura eliminada
InvoiceIn Serial: Facturas por series
Invoice list: Listado de facturas recibidas Invoice list: Listado de facturas recibidas
InvoiceIn booked: Factura contabilizada InvoiceIn booked: Factura contabilizada
Net: Neto Net: Neto
@ -22,3 +23,5 @@ Total stems: Total tallos
Show agricultural receipt as PDF: Ver recibo agrícola como PDF Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
New InvoiceIn: Nueva Factura New InvoiceIn: Nueva Factura
Days ago: Últimos días
Negative bases: Bases negativas

View File

@ -0,0 +1,133 @@
<vn-crud-model
vn-id="model"
url="InvoiceIns/negativeBases"
auto-load="true"
params="$ctrl.params">
</vn-crud-model>
<vn-portal slot="topbar">
</vn-portal>
<vn-card>
<smart-table
model="model"
options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions>
<vn-date-picker
vn-one
label="From"
ng-model="$ctrl.params.from"
on-change="model.refresh()">
</vn-date-picker>
<vn-date-picker
vn-one
label="To"
ng-model="$ctrl.params.to"
on-change="model.refresh()">
</vn-date-picker>
<vn-button
disabled="model._orgData.length == 0"
icon="download"
ng-click="$ctrl.downloadCSV()"
vn-tooltip="Download as CSV">
</vn-button>
</slot-actions>
<slot-table>
<table>
<thead>
<tr>
<th field="company">
<span translate>Company</span>
</th>
<th field="country">
<span translate>Country</span>
</th>
<th field="clientId">
<span translate>Id Client</span>
</th>
<th field="clientSocialName">
<span translate>Client</span>
</th>
<th field="amount">
<span translate>Amount</span>
</th>
<th field="taxableBase">
<span translate>Base</span>
</th>
<th field="ticketFk">
<span translate>Id Ticket</span>
</th>
<th field="isActive">
<span translate>Active</span>
</th>
<th field="hasToInvoice">
<span translate>Has To Invoice</span>
</th>
<th field="isTaxDataChecked">
<span translate>Verified data</span>
</th>
<th field="comercialName">
<span translate>Comercial</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in model.data">
<td>{{client.company | dashIfEmpty}}</td>
<td>{{client.country | dashIfEmpty}}</td>
<td>
<vn-span
class="link"
ng-click="clientDescriptor.show($event, client.clientId)">
{{::client.clientId | dashIfEmpty}}
</vn-span>
</td>
<td>{{client.clientSocialName | dashIfEmpty}}</td>
<td>{{client.amount | currency: 'EUR':2 | dashIfEmpty}}</td>
<td>{{client.taxableBase | dashIfEmpty}}</td>
<td>
<vn-span
class="link"
ng-click="ticketDescriptor.show($event, client.ticketFk)">
{{::client.ticketFk | dashIfEmpty}}
</vn-span>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.isActive">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.hasToInvoice">
</vn-check>
</td>
<td>
<vn-check
disabled="true"
ng-model="client.isTaxDataChecked">
</vn-check>
</td>
<td>
<vn-span
class="link"
ng-click="workerDescriptor.show($event, client.comercialId)">
{{::client.comercialName | dashIfEmpty}}
</vn-span>
</td>
</tr>
</tbody>
</table>
</slot-table>
</smart-table>
</vn-card>
<vn-ticket-descriptor-popover
vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover>
<vn-client-descriptor-popover
vn-id="client-descriptor">
</vn-client-descriptor-popover>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,84 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
constructor($element, $, vnReport) {
super($element, $);
this.vnReport = vnReport;
const now = new Date();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
this.params = {
from: firstDayOfMonth,
to: lastDayOfMonth
};
this.$checkAll = false;
this.smartTableOptions = {
activeButtons: {
search: true,
}, columns: [
{
field: 'isActive',
searchable: false
},
{
field: 'hasToInvoice',
searchable: false
},
{
field: 'isTaxDataChecked',
searchable: false
},
]
};
}
exprBuilder(param, value) {
switch (param) {
case 'company':
return {'company': value};
case 'country':
return {'country': value};
case 'clientId':
return {'clientId': value};
case 'clientSocialName':
return {'clientSocialName': value};
case 'amount':
return {'amount': value};
case 'taxableBase':
return {'taxableBase': value};
case 'ticketFk':
return {'ticketFk': value};
case 'comercialName':
return {'comercialName': value};
}
}
downloadCSV() {
const data = [];
this.$.model._orgData.forEach(element => {
data.push(Object.keys(element).map(key => {
return {newName: this.$t(key), value: element[key]};
}).filter(item => item !== null)
.reduce((result, item) => {
result[item.newName] = item.value;
return result;
}, {}));
});
this.vnReport.show('InvoiceIns/negativeBasesCsv', {
negativeBases: data,
from: this.params.from,
to: this.params.to
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport'];
ngModule.vnComponent('vnNegativeBases', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,14 @@
Has To Invoice: Facturar
Download as CSV: Descargar como CSV
company: Compañía
country: País
clientId: Id Cliente
clientSocialName: Cliente
amount: Importe
taxableBase: Base
ticketFk: Id Ticket
isActive: Activo
hasToInvoice: Facturar
isTaxDataChecked: Datos comprobados
comercialId: Id Comercial
comercialName: Comercial

View File

@ -0,0 +1,10 @@
@import "./variables";
vn-negative-bases {
vn-date-picker{
padding-right: 5%;
}
slot-actions{
align-items: center;
}
}

View File

@ -9,10 +9,9 @@
], ],
"menus": { "menus": {
"main": [ "main": [
{ { "state": "invoiceIn.index", "icon": "icon-invoice-in"},
"state": "invoiceIn.index", { "state": "invoiceIn.serial", "icon": "icon-invoice-in"},
"icon": "icon-invoice-in" { "state": "invoiceIn.negative-bases", "icon": "icon-ticket"}
}
], ],
"card": [ "card": [
{ {
@ -54,6 +53,24 @@
"administrative" "administrative"
] ]
}, },
{
"url": "/negative-bases",
"state": "invoiceIn.negative-bases",
"component": "vn-negative-bases",
"description": "Negative bases",
"acl": [
"administrative"
]
},
{
"url": "/serial",
"state": "invoiceIn.serial",
"component": "vn-invoice-in-serial",
"description": "InvoiceIn Serial",
"acl": [
"administrative"
]
},
{ {
"url": "/:id", "url": "/:id",
"state": "invoiceIn.card", "state": "invoiceIn.card",
@ -133,4 +150,4 @@
] ]
} }
] ]
} }

View File

@ -0,0 +1,27 @@
<vn-side-menu side="right">
<vn-horizontal class="input">
<vn-input-number
label="Days ago"
ng-model="$ctrl.filter.daysAgo"
vn-focus
ng-keydown="$ctrl.onKeyPress($event)"
min="0">
</vn-input-number>
</vn-horizontal>
<vn-horizontal class="input">
<vn-textfield
label="Serial"
ng-model="$ctrl.filter.serial"
ng-keydown="$ctrl.onKeyPress($event)">
</vn-textfield>
</vn-horizontal>
<div class="chips">
<vn-chip
ng-if="$ctrl.filter.serial"
removable="true"
on-remove="$ctrl.removeItemFilter('serial')"
class="colored">
<span>{{$ctrl.$t('Serial')}}: {{$ctrl.filter.serial}}</span>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -0,0 +1,44 @@
import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel';
import './style.scss';
class Controller extends SearchPanel {
constructor($element, $) {
super($element, $);
this.filter = {};
const filter = {
fields: ['daysAgo']
};
this.$http.get('InvoiceInConfigs', {filter}).then(res => {
if (res.data) {
this.invoiceInConfig = res.data[0];
this.addFilters();
}
});
}
removeItemFilter(param) {
this.filter[param] = null;
this.addFilters();
}
onKeyPress($event) {
if ($event.key === 'Enter')
this.addFilters();
}
addFilters() {
if (!this.filter.daysAgo)
this.filter.daysAgo = this.invoiceInConfig.daysAgo;
return this.model.addFilter({}, this.filter);
}
}
ngModule.component('vnInvoiceInSerialSearchPanel', {
template: require('./index.html'),
controller: Controller,
bindings: {
model: '<'
}
});

View File

@ -0,0 +1,43 @@
import './index.js';
describe('InvoiceIn', () => {
describe('Component serial-search-panel', () => {
let controller;
let $scope;
beforeEach(ngModule('invoiceIn'));
beforeEach(inject(($componentController, $rootScope) => {
$scope = $rootScope.$new();
const $element = angular.element('<vn-invoice-in-serial-search-panel></vn-invoice-in-serial-search-panel>');
controller = $componentController('vnInvoiceInSerialSearchPanel', {$element, $scope});
controller.model = {
addFilter: jest.fn(),
};
controller.invoiceInConfig = {
daysAgo: 45,
};
}));
describe('addFilters()', () => {
it('should add default daysAgo if it is not already set', () => {
controller.filter = {
serial: 'R',
};
controller.addFilters();
expect(controller.filter.daysAgo).toEqual(controller.invoiceInConfig.daysAgo);
});
it('should not add default daysAgo if it is already set', () => {
controller.filter = {
daysAgo: 1,
serial: 'R',
};
controller.addFilters();
expect(controller.filter.daysAgo).toEqual(1);
});
});
});
});

View File

@ -0,0 +1,24 @@
@import "variables";
vn-invoice-in-serial-search-panel vn-side-menu div {
& > .input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
& > .horizontal {
grid-auto-flow: column;
grid-column-gap: $spacing-sm;
align-items: center;
}
& > .chips {
display: flex;
flex-wrap: wrap;
padding: $spacing-md;
overflow: hidden;
max-width: 100%;
border-color: $color-spacer;
border-top: $border-thin;
}
}

View File

@ -0,0 +1,40 @@
<vn-crud-model
vn-id="model"
url="InvoiceIns/getSerial"
limit="20">
</vn-crud-model>
<vn-portal slot="topbar">
</vn-portal>
<vn-invoice-in-serial-search-panel
model="model">
</vn-invoice-in-serial-search-panel>
<vn-data-viewer
model="model"
class="vn-w-lg">
<vn-card>
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="serial">Serial</vn-th>
<vn-th field="pending">Pending</vn-th>
<vn-th field="total">Total</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="invoiceIn in model.data">
<vn-td>{{::invoiceIn.serial}}</vn-td>
<vn-td>{{::invoiceIn.pending}}</vn-td>
<vn-td>{{::invoiceIn.total}}</vn-td>
<vn-td shrink>
<vn-icon-button
vn-click-stop="$ctrl.goToIndex(model.userParams.daysAgo, invoiceIn.serial)"
vn-tooltip="Go to InvoiceIn"
icon="icon-invoice-in">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>

View File

@ -0,0 +1,22 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
}
goToIndex(daysAgo, serial) {
const issued = Date.vnNew();
issued.setDate(issued.getDate() - daysAgo);
this.$state.go('invoiceIn.index',
{q: `{"serial": "${serial}", "isBooked": false, "from": ${issued.getTime()}}`});
}
}
Controller.$inject = ['$element', '$scope'];
ngModule.vnComponent('vnInvoiceInSerial', {
template: require('./index.html'),
controller: Controller
});

View File

@ -0,0 +1,3 @@
Serial: Serie
Pending: Pendientes
Go to InvoiceIn: Ir al listado de facturas recibidas

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('upsertFixedPrice()', () => { describe('upsertFixedPrice()', () => {
const now = Date.vnNew(); const now = Date.vnNew();
@ -7,6 +8,17 @@ describe('upsertFixedPrice()', () => {
beforeAll(async() => { beforeAll(async() => {
originalFixedPrice = await models.FixedPrice.findById(fixedPriceId); originalFixedPrice = await models.FixedPrice.findById(fixedPriceId);
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
}); });
it(`should toggle the hasMinPrice boolean if there's a minPrice and update the rest of the data`, async() => { it(`should toggle the hasMinPrice boolean if there's a minPrice and update the rest of the data`, async() => {

View File

@ -1,7 +1,7 @@
const axios = require('axios'); const axios = require('axios');
const uuid = require('uuid'); const uuid = require('uuid');
const fs = require('fs/promises'); const fs = require('fs/promises');
const { createWriteStream } = require('fs'); const {createWriteStream} = require('fs');
const path = require('path'); const path = require('path');
const gm = require('gm'); const gm = require('gm');
@ -15,7 +15,7 @@ module.exports = Self => {
}, },
}); });
Self.download = async () => { Self.download = async() => {
const models = Self.app.models; const models = Self.app.models;
const tempContainer = await models.TempContainer.container( const tempContainer = await models.TempContainer.container(
'salix-image' 'salix-image'
@ -32,13 +32,13 @@ module.exports = Self => {
let tempFilePath; let tempFilePath;
let queueRow; let queueRow;
try { try {
const myOptions = { transaction: tx }; const myOptions = {transaction: tx};
queueRow = await Self.findOne( queueRow = await Self.findOne(
{ {
fields: ['id', 'itemFk', 'url', 'attempts'], fields: ['id', 'itemFk', 'url', 'attempts'],
where: { where: {
url: { neq: null }, url: {neq: null},
attempts: { attempts: {
lt: maxAttempts, lt: maxAttempts,
}, },
@ -59,7 +59,7 @@ module.exports = Self => {
'model', 'model',
'property', 'property',
], ],
where: { name: collectionName }, where: {name: collectionName},
include: { include: {
relation: 'sizes', relation: 'sizes',
scope: { scope: {
@ -116,16 +116,16 @@ module.exports = Self => {
const collectionDir = path.join(rootPath, collectionName); const collectionDir = path.join(rootPath, collectionName);
// To max size // To max size
const { maxWidth, maxHeight } = collection; const {maxWidth, maxHeight} = collection;
const fullSizePath = path.join(collectionDir, 'full'); const fullSizePath = path.join(collectionDir, 'full');
const toFullSizePath = `${fullSizePath}/${fileName}`; const toFullSizePath = `${fullSizePath}/${fileName}`;
await fs.mkdir(fullSizePath, { recursive: true }); await fs.mkdir(fullSizePath, {recursive: true});
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
gm(tempFilePath) gm(tempFilePath)
.resize(maxWidth, maxHeight, '>') .resize(maxWidth, maxHeight, '>')
.setFormat('png') .setFormat('png')
.write(toFullSizePath, function (err) { .write(toFullSizePath, function(err) {
if (err) reject(err); if (err) reject(err);
if (!err) resolve(); if (!err) resolve();
}); });
@ -133,12 +133,12 @@ module.exports = Self => {
// To collection sizes // To collection sizes
for (const size of collection.sizes()) { for (const size of collection.sizes()) {
const { width, height } = size; const {width, height} = size;
const sizePath = path.join(collectionDir, `${width}x${height}`); const sizePath = path.join(collectionDir, `${width}x${height}`);
const toSizePath = `${sizePath}/${fileName}`; const toSizePath = `${sizePath}/${fileName}`;
await fs.mkdir(sizePath, { recursive: true }); await fs.mkdir(sizePath, {recursive: true});
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const gmInstance = gm(tempFilePath); const gmInstance = gm(tempFilePath);
@ -153,7 +153,7 @@ module.exports = Self => {
gmInstance gmInstance
.setFormat('png') .setFormat('png')
.write(toSizePath, function (err) { .write(toSizePath, function(err) {
if (err) reject(err); if (err) reject(err);
if (!err) resolve(); if (!err) resolve();
}); });

View File

@ -1,105 +0,0 @@
const https = require('https');
const fs = require('fs-extra');
const path = require('path');
const uuid = require('uuid');
module.exports = Self => {
Self.remoteMethod('downloadImages', {
description: 'Returns last entries',
accessType: 'WRITE',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/downloadImages`,
verb: 'POST'
}
});
Self.downloadImages = async() => {
const models = Self.app.models;
const container = await models.TempContainer.container('salix-image');
const tempPath = path.join(container.client.root, container.name);
const maxAttempts = 3;
const images = await Self.find({
where: {attempts: {eq: maxAttempts}}
});
for (let image of images) {
const currentStamp = Date.vnNew().getTime();
const updatedStamp = image.updated.getTime();
const graceTime = Math.abs(currentStamp - updatedStamp);
const maxTTL = 3600 * 48 * 1000; // 48 hours in ms;
if (graceTime >= maxTTL)
await Self.destroyById(image.itemFk);
}
download();
async function download() {
const image = await Self.findOne({
where: {url: {neq: null}, attempts: {lt: maxAttempts}},
order: 'priority, attempts, updated'
});
if (!image) return;
const fileName = `${uuid.v4()}.png`;
const filePath = path.join(tempPath, fileName);
const imageUrl = image.url.replace('http://', 'https://');
https.get(imageUrl, async response => {
if (response.statusCode != 200) {
const error = new Error(`Could not download the image. Status code ${response.statusCode}`);
return await errorHandler(image.itemFk, error, filePath);
}
const writeStream = fs.createWriteStream(filePath);
writeStream.on('open', () => response.pipe(writeStream));
writeStream.on('error', async error =>
await errorHandler(image.itemFk, error, filePath));
writeStream.on('finish', () => writeStream.end());
writeStream.on('close', async function() {
try {
await models.Image.registerImage('catalog', filePath, fileName, image.itemFk);
await image.destroy();
download();
} catch (error) {
await errorHandler(image.itemFk, error, filePath);
}
});
}).on('error', async error => {
await errorHandler(image.itemFk, error, filePath);
});
}
async function errorHandler(rowId, error, filePath) {
try {
const row = await Self.findById(rowId);
if (!row) return;
if (row.attempts < maxAttempts) {
await row.updateAttributes({
error: error,
attempts: row.attempts + 1,
updated: Date.vnNew()
});
}
if (filePath && fs.existsSync(filePath))
await fs.unlink(filePath);
download();
} catch (err) {
throw new Error(`Image download failed: ${err}`);
}
}
};
};

View File

@ -1,7 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('item clone()', () => { describe('item clone()', () => {
let nextItemId; let nextItemId;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
beforeEach(async() => { beforeEach(async() => {
let query = `SELECT i1.id + 1 as id FROM vn.item i1 let query = `SELECT i1.id + 1 as id FROM vn.item i1

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('item new()', () => { describe('item new()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new item, adding the name as a tag', async() => { it('should create a new item, adding the name as a tag', async() => {
const tx = await models.Item.beginTransaction({}); const tx = await models.Item.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('regularize()', () => { describe('regularize()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 18},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new ticket and add a line', async() => { it('should create a new ticket and add a line', async() => {
const tx = await models.Item.beginTransaction({}); const tx = await models.Item.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};

View File

@ -40,11 +40,11 @@ Create: Crear
Client card: Ficha del cliente Client card: Ficha del cliente
Shipped: F. envío Shipped: F. envío
stems: Tallos stems: Tallos
Weight/Piece: Peso/tallo Weight/Piece: Peso (gramos)/tallo
Search items by id, name or barcode: Buscar articulos por identificador, nombre o codigo de barras Search items by id, name or barcode: Buscar articulos por identificador, nombre o codigo de barras
SalesPerson: Comercial SalesPerson: Comercial
Concept: Concepto Concept: Concepto
Units/Box: Unidades/Caja Units/Box: Unidades/caja
# Sections # Sections
Items: Artículos Items: Artículos

View File

@ -295,11 +295,26 @@ module.exports = Self => {
risk = t.debt + t.credit, totalProblems = totalProblems + 1 risk = t.debt + t.credit, totalProblems = totalProblems + 1
`); `);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getWarnings');
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT t.*, tp.*, CREATE TEMPORARY TABLE tmp.sale_getWarnings
((tp.risk) + cc.riskTolerance < 0) AS hasHighRisk (INDEX (ticketFk, agencyModeFk))
ENGINE = MEMORY
SELECT f.id ticketFk, f.agencyModeFk
FROM tmp.filter f`);
stmts.push(stmt);
stmts.push('CALL ticket_getWarnings()');
stmt = new ParameterizedSQL(`
SELECT t.*,
tp.*,
((tp.risk) + cc.riskTolerance < 0) AS hasHighRisk,
tw.*
FROM tmp.tickets t FROM tmp.tickets t
LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = t.id LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = t.id
LEFT JOIN tmp.ticket_warnings tw ON tw.ticketFk = t.id
JOIN clientConfig cc`); JOIN clientConfig cc`);
const hasProblems = args.problems; const hasProblems = args.problems;
@ -352,20 +367,37 @@ module.exports = Self => {
return {'t.alertLevel': value}; return {'t.alertLevel': value};
case 'pending': case 'pending':
if (value) { if (value) {
return {and: [ return {'t.alertLevelCode': {inq: [
{'t.alertLevel': 0}, 'FIXING',
{'t.alertLevelCode': {nin: [ 'FREE',
'OK', 'NOT_READY',
'BOARDING', 'BLOCKED',
'PRINTED', 'EXPANDABLE',
'PRINTED_AUTO', 'CHAINED',
'PICKER_DESIGNED' 'WAITING_FOR_PAYMENT'
]}} ]}};
]};
} else { } else {
return {and: [ return {'t.alertLevelCode': {inq: [
{'t.alertLevel': {gt: 0}} 'ON_PREPARATION',
]}; 'ON_CHECKING',
'CHECKED',
'PACKING',
'PACKED',
'INVOICED',
'ON_DELIVERY',
'PREPARED',
'WAITING_FOR_PICKUP',
'DELIVERED',
'PRINTED_BACK',
'LAST_CALL',
'PREVIOUS_PREPARATION',
'ASSISTED_PREPARATION',
'BOARD',
'PRINTED STOWAWAY',
'OK STOWAWAY',
'HALF_PACKED',
'COOLER_PREPARATION'
]}};
} }
case 'agencyModeFk': case 'agencyModeFk':
case 'warehouseFk': case 'warehouseFk':
@ -387,6 +419,8 @@ module.exports = Self => {
tmp.filter, tmp.filter,
tmp.ticket_problems, tmp.ticket_problems,
tmp.sale_getProblems, tmp.sale_getProblems,
tmp.sale_getWarnings,
tmp.ticket_warnings,
tmp.risk`); tmp.risk`);
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');

View File

@ -12,4 +12,5 @@ Theoretical: Teórica
Practical: Práctica Practical: Práctica
Preparation: Preparación Preparation: Preparación
Auto-refresh: Auto-refresco Auto-refresh: Auto-refresco
Toggle auto-refresh every 2 minutes: Conmuta el refresco automático cada 2 minutos Toggle auto-refresh every 2 minutes: Conmuta el refresco automático cada 2 minutos
Is fragile: Es frágil

View File

@ -19,13 +19,13 @@
<vn-horizontal class="header"> <vn-horizontal class="header">
<vn-one translate> <vn-one translate>
Tickets monitor Tickets monitor
</vn-one> </vn-one>
</vn-horizontal> </vn-horizontal>
<vn-card> <vn-card>
<smart-table <smart-table
model="model" model="model"
view-config-id="ticketsMonitor" view-config-id="ticketsMonitor"
options="$ctrl.smartTableOptions" options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"> expr-builder="$ctrl.exprBuilder(param, value)">
<slot-actions> <slot-actions>
<vn-check <vn-check
@ -68,6 +68,7 @@
<th field="stateFk"> <th field="stateFk">
<span translate>State</span> <span translate>State</span>
</th> </th>
<th field="isFragile"></th>
<th field="zoneFk"> <th field="zoneFk">
<span translate>Zone</span> <span translate>Zone</span>
</th> </th>
@ -80,7 +81,7 @@
<tbody> <tbody>
<tr ng-repeat="ticket in model.data track by ticket.id" <tr ng-repeat="ticket in model.data track by ticket.id"
vn-anchor="{ vn-anchor="{
state: 'ticket.card.summary', state: 'ticket.card.summary',
params: {id: ticket.id}, params: {id: ticket.id},
target: '_blank' target: '_blank'
}"> }">
@ -169,12 +170,20 @@
class="link"> class="link">
{{ticket.refFk}} {{ticket.refFk}}
</span> </span>
<span <span
ng-show="!ticket.refFk" ng-show="!ticket.refFk"
class="chip {{ticket.classColor}}"> class="chip {{ticket.classColor}}">
{{ticket.state}} {{ticket.state}}
</span> </span>
</td> </td>
<td number>
<vn-icon
ng-show="ticket.isFragile"
translate-attr="{title: 'Is fragile'}"
class="bright"
icon="local_bar">
</vn-icon>
</td>
<td name="zone"> <td name="zone">
<span <span
title="{{ticket.zoneName}}" title="{{ticket.zoneName}}"
@ -191,8 +200,8 @@
<td actions> <td actions>
<vn-icon-button <vn-icon-button
vn-anchor="{ vn-anchor="{
state: 'ticket.card.sale', state: 'ticket.card.sale',
params: {id: ticket.id}, params: {id: ticket.id},
target: '_blank' target: '_blank'
}" }"
vn-tooltip="Go to lines" vn-tooltip="Go to lines"
@ -213,7 +222,7 @@
<vn-ticket-descriptor-popover <vn-ticket-descriptor-popover
vn-id="ticketDescriptor"> vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover> </vn-ticket-descriptor-popover>
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="workerDescriptor"> vn-id="workerDescriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
<vn-client-descriptor-popover <vn-client-descriptor-popover
@ -236,22 +245,22 @@
ng-click="contextmenu.filterBySelection()"> ng-click="contextmenu.filterBySelection()">
Filter by selection Filter by selection
</vn-item> </vn-item>
<vn-item translate <vn-item translate
ng-if="contextmenu.isFilterAllowed()" ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()"> ng-click="contextmenu.excludeSelection()">
Exclude selection Exclude selection
</vn-item> </vn-item>
<vn-item translate <vn-item translate
ng-if="contextmenu.isFilterAllowed()" ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()"> ng-click="contextmenu.removeFilter()">
Remove filter Remove filter
</vn-item> </vn-item>
<vn-item translate <vn-item translate
ng-click="contextmenu.removeAllFilters()"> ng-click="contextmenu.removeAllFilters()">
Remove all filters Remove all filters
</vn-item> </vn-item>
<vn-item translate <vn-item translate
ng-if="contextmenu.isActionAllowed()" ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()"> ng-click="contextmenu.copyValue()">
Copy value Copy value
</vn-item> </vn-item>

View File

@ -1,6 +1,22 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('route clone()', () => { describe('route clone()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const createdDate = Date.vnNew(); const createdDate = Date.vnNew();
it('should throw an error if the amount of ids pased to the clone function do no match the database', async() => { it('should throw an error if the amount of ids pased to the clone function do no match the database', async() => {
const ids = [996, 997, 998, 999]; const ids = [996, 997, 998, 999];

View File

@ -1,6 +1,20 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('route updateWorkCenter()', () => { describe('route updateWorkCenter()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const routeId = 1; const routeId = 1;
it('should set the commission work center if the worker has workCenter', async() => { it('should set the commission work center if the worker has workCenter', async() => {

View File

@ -1,7 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale canEdit()', () => { describe('sale canEdit()', () => {
const employeeId = 1; const employeeId = 1;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
describe('sale editTracked', () => { describe('sale editTracked', () => {
it('should return true if the role is production regardless of the saleTrackings', async() => { it('should return true if the role is production regardless of the saleTrackings', async() => {

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale deleteSales()', () => { describe('sale deleteSales()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should throw an error if the ticket of the given sales is not editable', async() => { it('should throw an error if the ticket of the given sales is not editable', async() => {
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale reserve()', () => { describe('sale reserve()', () => {
const ctx = { const ctx = {
@ -9,6 +10,20 @@ describe('sale reserve()', () => {
} }
}; };
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should throw an error if the ticket can not be modified', async() => { it('should throw an error if the ticket can not be modified', async() => {
const tx = await models.Sale.beginTransaction({}); const tx = await models.Sale.beginTransaction({});

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale updateConcept()', () => { describe('sale updateConcept()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const ctx = {req: {accessToken: {userId: 9}}}; const ctx = {req: {accessToken: {userId: 9}}};
const saleId = 25; const saleId = 25;

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale updatePrice()', () => { describe('sale updatePrice()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 18},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: 18}, accessToken: {userId: 18},

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale updateQuantity()', () => { describe('sale updateQuantity()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const ctx = { const ctx = {
req: { req: {
accessToken: {userId: 9}, accessToken: {userId: 9},

View File

@ -1,7 +1,21 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket addSale()', () => { describe('ticket addSale()', () => {
const ticketId = 13; const ticketId = 13;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should create a new sale for the ticket with id 13', async() => { it('should create a new sale for the ticket with id 13', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});

View File

@ -10,11 +10,15 @@ describe('ticket merge()', () => {
workerFk: 1 workerFk: 1
}; };
const activeCtx = { beforeAll(async() => {
accessToken: {userId: 9}, const activeCtx = {
}; accessToken: {userId: 9},
http: {
beforeEach(() => { req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx active: activeCtx
}); });
@ -35,16 +39,16 @@ describe('ticket merge()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const chatNotificationBeforeMerge = await models.Chat.find(); const chatNotificationBeforeMerge = await models.Chat.find(null, options);
await models.Ticket.merge(ctx, [tickets], options); await models.Ticket.merge(ctx, [tickets], options);
const createdTicketLog = await models.TicketLog.find({where: {originFk: tickets.originId}}, options); const createdTicketLog = await models.TicketLog.find({where: {originFk: tickets.destinationId}}, options);
const deletedTicket = await models.Ticket.findOne({where: {id: tickets.originId}}, options); const deletedTicket = await models.Ticket.findOne({where: {id: tickets.originId}}, options);
const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets.destinationId}}, options); const salesTicketFuture = await models.Sale.find({where: {ticketFk: tickets.destinationId}}, options);
const chatNotificationAfterMerge = await models.Chat.find(); const chatNotificationAfterMerge = await models.Chat.find(null, options);
expect(createdTicketLog.length).toEqual(2); expect(createdTicketLog.length).toEqual(1);
expect(deletedTicket.isDeleted).toEqual(true); expect(deletedTicket.isDeleted).toEqual(true);
expect(salesTicketFuture.length).toEqual(2); expect(salesTicketFuture.length).toEqual(2);
expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2); expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2);

View File

@ -1,6 +1,20 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale updateDiscount()', () => { describe('sale updateDiscount()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
const originalSaleId = 8; const originalSaleId = 8;
it('should throw an error if no sales were selected', async() => { it('should throw an error if no sales were selected', async() => {

View File

@ -1,4 +1,4 @@
Status log: Hitorial de estados Status log: Historial de estados
Expedition removed: Expedición eliminada Expedition removed: Expedición eliminada
Move: Mover Move: Mover
New ticket without route: Nuevo ticket sin ruta New ticket without route: Nuevo ticket sin ruta

View File

@ -28,8 +28,9 @@
</vn-button-menu> </vn-button-menu>
<vn-button icon="keyboard_arrow_down" <vn-button icon="keyboard_arrow_down"
label="More" label="More"
ng-click="moreOptions.show($event)" disabled="!$ctrl.hasSelectedSales()"
ng-show="$ctrl.hasSelectedSales()"> vn-tooltip="Select lines to see the options"
ng-click="moreOptions.show($event)">
</vn-button> </vn-button>
<vn-button <vn-button
disabled="!$ctrl.hasSelectedSales() || !$ctrl.isEditable" disabled="!$ctrl.hasSelectedSales() || !$ctrl.isEditable"

View File

@ -40,3 +40,4 @@ Refund: Abono
Promotion mana: Maná promoción Promotion mana: Maná promoción
Claim mana: Maná reclamación Claim mana: Maná reclamación
History: Historial History: Historial
Select lines to see the options: Seleccione lineas para ver las opciones

View File

@ -130,6 +130,7 @@ module.exports = Self => {
SUM(b.stickers) AS stickers, SUM(b.stickers) AS stickers,
s.id AS cargoSupplierFk, s.id AS cargoSupplierFk,
s.nickname AS cargoSupplierNickname, s.nickname AS cargoSupplierNickname,
s.name AS supplierName,
CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg, CAST(SUM(b.weight * b.stickers) as DECIMAL(10,0)) as loadedKg,
CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) as DECIMAL(10,0)) as volumeKg CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) as DECIMAL(10,0)) as volumeKg
FROM travel t FROM travel t
@ -167,6 +168,7 @@ module.exports = Self => {
SUM(b.stickers) AS stickers, SUM(b.stickers) AS stickers,
e.evaNotes, e.evaNotes,
e.notes, e.notes,
e.invoiceAmount,
CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) as loadedkg, CAST(SUM(b.weight * b.stickers) AS DECIMAL(10,0)) as loadedkg,
CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) AS DECIMAL(10,0)) as volumeKg CAST(SUM(vc.aerealVolumetricDensity * b.stickers * IF(pkg.volume, pkg.volume, pkg.width * pkg.depth * pkg.height) / 1000000) AS DECIMAL(10,0)) as volumeKg
FROM tmp.travel tr FROM tmp.travel tr

View File

@ -3,7 +3,7 @@
url="Travels/extraCommunityFilter" url="Travels/extraCommunityFilter"
user-params="::$ctrl.defaultFilter" user-params="::$ctrl.defaultFilter"
data="travels" data="travels"
order="shipped ASC, landed ASC, travelFk, loadPriority, agencyModeFk, evaNotes" order="landed ASC, shipped ASC, travelFk, loadPriority, agencyModeFk, supplierName, evaNotes"
limit="20" limit="20"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
@ -48,6 +48,9 @@
<th field="agencyModeFk"> <th field="agencyModeFk">
<span translate>Agency</span> <span translate>Agency</span>
</th> </th>
<th field="invoiceAmount">
<span translate>Amount</span>
</th>
<th field="ref"> <th field="ref">
<span translate>Reference</span> <span translate>Reference</span>
</th> </th>
@ -107,6 +110,7 @@
{{::travel.cargoSupplierNickname}} {{::travel.cargoSupplierNickname}}
</span> </span>
</td> </td>
<td></td>
<td>{{::travel.agencyModeName}}</td> <td>{{::travel.agencyModeName}}</td>
<td vn-click-stop> <td vn-click-stop>
<vn-td-editable name="reference" expand> <vn-td-editable name="reference" expand>
@ -157,22 +161,15 @@
{{::entry.supplierName}} {{::entry.supplierName}}
</span> </span>
</td> </td>
<td number>{{::entry.invoiceAmount | currency: 'EUR': 2}}</td>
<td></td> <td></td>
<td class="td-editable">{{::entry.ref}}</td> <td class="td-editable">{{::entry.invoiceNumber}}</td>
<td number>{{::entry.stickers}}</td> <td number>{{::entry.stickers}}</td>
<td number></td> <td number></td>
<td number>{{::entry.loadedkg}}</td> <td number>{{::entry.loadedkg}}</td>
<td number>{{::entry.volumeKg}}</td> <td number>{{::entry.volumeKg}}</td>
<td> <td></td>
<span ng-if="::entry.notes" vn-tooltip="{{::entry.notes}}"> <td></td>
{{::entry.notes}}
</span>
</td>
<td>
<span ng-if="::entry.evaNotes" vn-tooltip="{{::entry.evaNotes}}">
{{::entry.evaNotes}}
</span>
</td>
<td></td> <td></td>
<td></td> <td></td>
</tr> </tr>

View File

@ -6,6 +6,6 @@ Phy. KG: KG físico
Vol. KG: KG Vol. Vol. KG: KG Vol.
Search by travel id or reference: Buscar por id de travel o referencia Search by travel id or reference: Buscar por id de travel o referencia
Search by extra community travel: Buscar por envío extra comunitario Search by extra community travel: Buscar por envío extra comunitario
Continent Out: Continente salida Continent Out: Cont. salida
W. Shipped: F. envío W. Shipped: F. envío
W. Landed: F. llegada W. Landed: F. llegada

View File

@ -2,6 +2,15 @@
<vn-auto-search <vn-auto-search
model="model"> model="model">
</vn-auto-search> </vn-auto-search>
<vn-travel-search-panel
model="model">
</vn-travel-search-panel>
<vn-crud-model
vn-id="model"
url="Travels/filter"
limit="20"
order="shipped DESC, landed DESC">
</vn-crud-model>
<vn-data-viewer <vn-data-viewer
model="model" model="model"
class="vn-mb-xl vn-w-xl"> class="vn-mb-xl vn-w-xl">
@ -9,23 +18,22 @@
<vn-table model="model"> <vn-table model="model">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
<vn-th field="id" number filter-enabled="false">Id</vn-th>
<vn-th field="ref">Reference</vn-th> <vn-th field="ref">Reference</vn-th>
<vn-th field="agencyModeFk">Agency</vn-th> <vn-th field="agencyModeFk">Agency</vn-th>
<vn-th field="warehouseOutFk">Warehouse Out</vn-th> <vn-th field="warehouseOutFk">Warehouse Out</vn-th>
<vn-th field="shipped" center shrink-date>Shipped</vn-th> <vn-th field="shipped" center shrink-date>Shipped</vn-th>
<vn-th field="isDelivered" center>Delivered</vn-th> <vn-th shrink></vn-th>
<vn-th field="warehouseInFk">Warehouse In</vn-th> <vn-th field="warehouseInFk">Warehouse In</vn-th>
<vn-th field="landed" center shrink-date>Landed</vn-th> <vn-th field="landed" center shrink-date>Landed</vn-th>
<vn-th field="isReceived" center>Received</vn-th> <vn-th shrink></vn-th>
<vn-th shrink field="totalEntries">Total entries</vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
<a ng-repeat="travel in model.data" <a ng-repeat="travel in model.data"
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="travel.card.summary({id: {{::travel.id}}})"> ui-sref="travel.card.summary({id: {{::travel.id}}})">
<vn-td number>{{::travel.id}}</vn-td>
<vn-td>{{::travel.ref}}</vn-td> <vn-td>{{::travel.ref}}</vn-td>
<vn-td>{{::travel.agencyModeName}}</vn-td> <vn-td>{{::travel.agencyModeName}}</vn-td>
<vn-td>{{::travel.warehouseOutName}}</vn-td> <vn-td>{{::travel.warehouseOutName}}</vn-td>
@ -34,14 +42,27 @@
{{::travel.shipped | date:'dd/MM/yyyy'}} {{::travel.shipped | date:'dd/MM/yyyy'}}
</span> </span>
</vn-td> </vn-td>
<vn-td center><vn-check ng-model="travel.isDelivered" disabled="true"></vn-check></vn-td> <vn-td shrink>
<vn-icon
icon="flight_takeoff"
translate-attr="{title: 'Delivered'}"
ng-class="{active: travel.isDelivered}">
</vn-icon>
</vn-td>
<vn-td expand>{{::travel.warehouseInName}}</vn-td> <vn-td expand>{{::travel.warehouseInName}}</vn-td>
<vn-td center shrink-date> <vn-td center shrink-date>
<span class="chip {{$ctrl.compareDate(travel.landed)}}"> <span class="chip {{$ctrl.compareDate(travel.landed)}}">
{{::travel.landed | date:'dd/MM/yyyy'}} {{::travel.landed | date:'dd/MM/yyyy'}}
</span> </span>
</vn-td> </vn-td>
<vn-td center><vn-check ng-model="travel.isReceived" disabled="true"></vn-check></vn-td> <vn-td shrink>
<vn-icon
icon="flight_land"
translate-attr="{title: 'Received'}"
ng-class="{active: travel.isReceived}">
</vn-icon>
</vn-td>
<vn-td shrink>{{::travel.totalEntries}}</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-horizontal class="buttons"> <vn-horizontal class="buttons">
<vn-icon-button <vn-icon-button
@ -49,7 +70,7 @@
vn-tooltip="Clone" vn-tooltip="Clone"
icon="icon-clone"> icon="icon-clone">
</vn-icon-button> </vn-icon-button>
<vn-icon-button <vn-icon-button
vn-anchor="::{state: 'entry.create', params: {travelFk: travel.id}}" vn-anchor="::{state: 'entry.create', params: {travelFk: travel.id}}"
vn-tooltip="Add entry" vn-tooltip="Add entry"
icon="icon-ticket"> icon="icon-ticket">
@ -78,38 +99,9 @@
fixed-bottom-right> fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button> <vn-float-button icon="add"></vn-float-button>
</a> </a>
<vn-confirm <vn-confirm
vn-id="clone" vn-id="clone"
on-accept="$ctrl.onCloneAccept($data)" on-accept="$ctrl.onCloneAccept($data)"
question="Do you want to clone this travel?" question="Do you want to clone this travel?"
message="All it's properties will be copied"> message="All it's properties will be copied">
</vn-confirm> </vn-confirm>
<vn-contextmenu vn-id="contextmenu" targets="['vn-data-viewer']" model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()" >
Remove all filters
</vn-item>
<vn-item translate
ng-if="contextmenu.isActionAllowed()"
ng-click="contextmenu.copyValue()">
Copy value
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -1,5 +1,6 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section { export default class Controller extends Section {
preview(travel) { preview(travel) {
@ -30,37 +31,6 @@ export default class Controller extends Section {
if (timeDifference == 0) return 'warning'; if (timeDifference == 0) return 'warning';
if (timeDifference < 0) return 'success'; if (timeDifference < 0) return 'success';
} }
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'t.id': value}
: {'t.ref': {like: `%${value}%`}};
case 'ref':
return {'t.ref': {like: `%${value}%`}};
case 'shipped':
return {'t.shipped': {between: this.dateRange(value)}};
case 'landed':
return {'t.landed': {between: this.dateRange(value)}};
case 'id':
case 'agencyModeFk':
case 'warehouseOutFk':
case 'warehouseInFk':
case 'totalEntries':
param = `t.${param}`;
return {[param]: value};
}
}
dateRange(value) {
const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0);
const maxHour = new Date(value);
maxHour.setHours(23, 59, 59, 59);
return [minHour, maxHour];
}
} }
ngModule.vnComponent('vnTravelIndex', { ngModule.vnComponent('vnTravelIndex', {

View File

@ -0,0 +1,11 @@
@import "variables";
vn-travel-index {
vn-icon {
color: $color-font-secondary
}
vn-icon.active {
color: $color-success
}
}

View File

@ -1,7 +1,7 @@
#Ordenar alfabeticamente #Ordenar alfabeticamente
Reference: Referencia Reference: Referencia
Warehouse Out: Almacén salida Warehouse Out: Alm salida
Warehouse In: Almacén llegada Warehouse In: Alm llegada
Shipped from: Salida desde Shipped from: Salida desde
Shipped to: Salida hasta Shipped to: Salida hasta
Landed from: Llegada desde Landed from: Llegada desde
@ -10,12 +10,12 @@ Shipped: F. salida
Landed: F. llegada Landed: F. llegada
Delivered: Enviado Delivered: Enviado
Received: Recibido Received: Recibido
Travel id: Id envío Travel id: Id
Search travels by id: Buscar envíos por identificador Search travels by id: Buscar envíos por identificador o referencia
New travel: Nuevo envío New travel: Nuevo envío
travel: envío travel: envío
# Sections # Sections
Travels: Envíos Travels: Envíos
Log: Historial Log: Historial
Thermographs: Termógrafos Thermographs: Termógrafos

View File

@ -1,20 +1,6 @@
<vn-crud-model
vn-id="model"
url="Travels/filter"
limit="20"
order="shipped DESC, landed DESC">
</vn-crud-model>
<vn-portal slot="topbar"> <vn-portal slot="topbar">
<vn-searchbar
vn-focus
panel="vn-travel-search-panel"
info="Search travels by id"
model="model"
fetch-params="$ctrl.fetchParams($params)"
suggested-filter="$ctrl.filterParams">
</vn-searchbar>
</vn-portal> </vn-portal>
<vn-portal slot="menu"> <vn-portal slot="menu">
<vn-left-menu></vn-left-menu> <vn-left-menu></vn-left-menu>
</vn-portal> </vn-portal>
<ui-view></ui-view> <ui-view></ui-view>

View File

@ -4,28 +4,6 @@ import ModuleMain from 'salix/components/module-main';
export default class Travel extends ModuleMain { export default class Travel extends ModuleMain {
constructor() { constructor() {
super(); super();
this.filterParams = {
scopeDays: 1
};
}
fetchParams($params) {
if (!Object.entries($params).length)
$params.scopeDays = 1;
if (typeof $params.scopeDays === 'number') {
const shippedFrom = Date.vnNew();
shippedFrom.setHours(0, 0, 0, 0);
const shippedTo = new Date(shippedFrom.getTime());
shippedTo.setDate(shippedTo.getDate() + $params.scopeDays);
shippedTo.setHours(23, 59, 59, 999);
Object.assign($params, {shippedFrom, shippedTo});
}
return $params;
} }
} }

View File

@ -1,49 +0,0 @@
import './index.js';
describe('Travel Component vnTravel', () => {
let controller;
beforeEach(ngModule('travel'));
beforeEach(inject($componentController => {
let $element = angular.element(`<div></div>`);
controller = $componentController('vnTravel', {$element});
}));
describe('fetchParams()', () => {
it('should return a range of dates with passed scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
const shippedFrom = Date.vnNew();
shippedFrom.setHours(0, 0, 0, 0);
const shippedTo = new Date(shippedFrom.getTime());
shippedTo.setDate(shippedTo.getDate() + params.scopeDays);
shippedTo.setHours(23, 59, 59, 999);
const expectedParams = {
shippedFrom,
scopeDays: params.scopeDays,
shippedTo
};
expect(params).toEqual(expectedParams);
});
it('should return default value for scope days', () => {
let params = controller.fetchParams({
scopeDays: 1
});
expect(params.scopeDays).toEqual(1);
});
it('should return the given scope days', () => {
let params = controller.fetchParams({
scopeDays: 2
});
expect(params.scopeDays).toEqual(2);
});
});
});

View File

@ -1,109 +1,146 @@
<div class="search-panel"> <vn-side-menu side="right">
<form ng-submit="$ctrl.onSearch()" id="manifold-form"> <vn-horizontal class="input">
<vn-horizontal class="vn-px-lg vn-pt-lg"> <vn-textfield
<vn-textfield label="General search"
vn-one info="Search travels by id"
label="General search" ng-model="$ctrl.search"
ng-model="filter.search" ng-keydown="$ctrl.onKeyPress($event, 'search')">
info="Search travels by id" </vn-textfield>
vn-focus> </vn-horizontal>
</vn-textfield> <vn-horizontal class="input horizontal">
</vn-horizontal> <vn-autocomplete
<vn-horizontal class="vn-px-lg"> vn-id="agency"
<vn-textfield label="Agency"
vn-one ng-model="$ctrl.filter.agencyModeFk"
label="Reference" data="$ctrl.agencyModes"
ng-model="filter.ref"> show-field="name"
</vn-textfield> value-field="id"
<vn-textfield on-change="$ctrl.applyFilters()">
vn-one </vn-autocomplete>
label="Total entries" </vn-horizontal>
ng-model="filter.totalEntries"> <vn-horizontal class="input horizontal">
</vn-textfield> <vn-autocomplete
</vn-horizontal> vn-id="warehouseOut"
<vn-horizontal class="vn-px-lg"> label="Warehouse Out"
<vn-textfield ng-model="$ctrl.filter.warehouseOutFk"
vn-one data="$ctrl.warehouses"
label="Travel id" show-field="name"
ng-model="filter.id"> value-field="id"
</vn-textfield> on-change="$ctrl.applyFilters()">
<vn-autocomplete vn-one </vn-autocomplete>
label="Agency" <vn-autocomplete
ng-model="filter.agencyModeFk" vn-id="warehouseIn"
url="AgencyModes" label="Warehouse In"
show-field="name" ng-model="$ctrl.filter.warehouseInFk"
value-field="id"> data="$ctrl.warehouses"
</vn-autocomplete> show-field="name"
</vn-horizontal> value-field="id"
<section class="vn-px-md"> on-change="$ctrl.applyFilters()">
<vn-horizontal class="manifold-panel vn-pa-md"> </vn-autocomplete>
<vn-date-picker </vn-horizontal>
vn-one <vn-horizontal class="input horizontal">
label="Shipped from" <vn-input-number
ng-model="filter.shippedFrom" min="0"
on-change="$ctrl.shippedFrom = value"> step="1"
</vn-date-picker> label="Days onward"
<vn-date-picker ng-model="$ctrl.filter.scopeDays"
vn-one on-change="$ctrl.applyFilters()"
label="Shipped to" display-controls="true"
ng-model="filter.shippedTo" info="Cannot choose a range of dates and days onward at the same time">
on-change="$ctrl.shippedTo = value"> </vn-input-number>
</vn-date-picker> </vn-horizontal>
<vn-none class="or vn-px-md" translate>Or</vn-none> <vn-horizontal class="input horizontal">
<vn-input-number <vn-date-picker
vn-one label="Landed from"
min="0" ng-model="$ctrl.filter.landedFrom"
step="1" on-change="$ctrl.applyFilters()">
label="Days onward" </vn-date-picker>
ng-model="filter.scopeDays" <vn-date-picker
on-change="$ctrl.scopeDays = value" label="Landed to"
display-controls="true"> ng-model="$ctrl.filter.landedTo"
</vn-input-number> on-change="$ctrl.applyFilters()">
<vn-icon color-marginal </vn-date-picker>
icon="info" </vn-horizontal>
vn-tooltip="Cannot choose a range of dates and days onward at the same time"> <vn-horizontal class="input horizontal">
</vn-icon> <vn-autocomplete
</vn-horizontal> vn-id="continent"
</section> label="Continent Out"
<vn-horizontal class="vn-px-lg"> ng-model="$ctrl.filter.continent"
<vn-date-picker data="$ctrl.continents"
vn-one show-field="name"
label="Landed from" value-field="code"
ng-model="filter.landedFrom"> on-change="$ctrl.applyFilters()">
</vn-date-picker> </vn-autocomplete>
<vn-date-picker <vn-input-number
vn-one min="0"
label="Landed to" label="Total entries"
ng-model="filter.landedTo"> ng-model="$ctrl.totalEntries"
</vn-date-picker> ng-keydown="$ctrl.onKeyPress($event, 'totalEntries')">
</vn-horizontal> </vn-input-number>
<vn-horizontal class="vn-px-lg"> </vn-horizontal>
<vn-autocomplete vn-one <div class="chips">
label="Warehouse Out" <vn-chip
ng-model="filter.warehouseOutFk" ng-if="$ctrl.filter.search"
url="Warehouses" removable="true"
show-field="name" on-remove="$ctrl.removeParamFilter('search')"
value-field="id"> class="colored">
</vn-autocomplete> <span>Id/{{$ctrl.$t('Reference')}}: {{$ctrl.filter.search}}</span>
<vn-autocomplete vn-one </vn-chip>
label="Warehouse In" <vn-chip
ng-model="filter.warehouseInFk" ng-if="agency.selection"
url="Warehouses" removable="true"
show-field="name" on-remove="$ctrl.removeParamFilter('agencyModeFk')"
value-field="id"> class="colored">
</vn-autocomplete> <span>{{$ctrl.$t('Agency')}}: {{agency.selection.name}}</span>
</vn-horizontal> </vn-chip>
<vn-horizontal class="vn-px-lg"> <vn-chip
<vn-autocomplete vn-one ng-if="warehouseOut.selection"
label="Continent Out" removable="true"
ng-model="filter.continent" on-remove="$ctrl.removeParamFilter('warehouseOutFk')"
url="Continents" class="colored">
show-field="name" <span>{{$ctrl.$t('Warehouse Out')}}: {{warehouseOut.selection.name}}</span>
value-field="code"> </vn-chip>
</vn-autocomplete> <vn-chip
</vn-horizontal> ng-if="warehouseIn.selection"
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg"> removable="true"
<vn-submit label="Search"></vn-submit> on-remove="$ctrl.removeParamFilter('warehouseInFk')"
</vn-horizontal> class="colored">
</form> <span>{{$ctrl.$t('Warehouse In')}}: {{warehouseIn.selection.name}}</span>
</div> </vn-chip>
<vn-chip
ng-if="$ctrl.filter.scopeDays"
removable="true"
on-remove="$ctrl.removeParamFilter('scopeDays')"
class="colored">
<span>{{$ctrl.$t('Days onward')}}: {{$ctrl.filter.scopeDays}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.landedFrom"
removable="true"
on-remove="$ctrl.removeParamFilter('landedFrom')"
class="colored">
<span>{{$ctrl.$t('Landed from')}}: {{$ctrl.filter.landedFrom | date:'dd/MM/yyyy'}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.landedTo"
removable="true"
on-remove="$ctrl.removeParamFilter('landedTo')"
class="colored">
<span>{{$ctrl.$t('Landed to')}}: {{$ctrl.filter.landedTo | date:'dd/MM/yyyy'}}</span>
</vn-chip>
<vn-chip
ng-if="continent.selection"
removable="true"
on-remove="$ctrl.removeParamFilter('continent')"
class="colored">
<span>{{$ctrl.$t('Continent Out')}}: {{continent.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="$ctrl.filter.totalEntries"
removable="true"
on-remove="$ctrl.removeParamFilter('totalEntries')"
class="colored">
<span>{{$ctrl.$t('Total entries')}}: {{$ctrl.filter.totalEntries}}</span>
</vn-chip>
</div>
</vn-side-menu>

View File

@ -1,43 +1,69 @@
import ngModule from '../module'; import ngModule from '../module';
import SearchPanel from 'core/components/searchbar/search-panel'; import SearchPanel from 'core/components/searchbar/search-panel';
import './style.scss';
class Controller extends SearchPanel { class Controller extends SearchPanel {
constructor($, $element) { constructor($, $element) {
super($, $element); super($, $element);
this.filter = this.$.filter; this.initFilter();
this.fetchData();
} }
get shippedFrom() { $onChanges() {
return this._shippedFrom; if (this.model)
this.applyFilters();
} }
set shippedFrom(value) { fetchData() {
this._shippedFrom = value; this.$http.get('AgencyModes').then(res => {
this.filter.scopeDays = null; this.agencyModes = res.data;
});
this.$http.get('Warehouses').then(res => {
this.warehouses = res.data;
});
this.$http.get('Continents').then(res => {
this.continents = res.data;
});
} }
get shippedTo() { initFilter() {
return this._shippedTo; this.filter = {};
if (this.$params.q) {
this.filter = JSON.parse(this.$params.q);
this.search = this.filter.search;
this.totalEntries = this.filter.totalEntries;
}
if (!this.filter.scopeDays) this.filter.scopeDays = 7;
} }
set shippedTo(value) { applyFilters(param) {
this._shippedTo = value; this.model.applyFilter({}, this.filter)
this.filter.scopeDays = null; .then(() => {
if (param && this.model._orgData.length === 1)
this.$state.go('travel.card.summary', {id: this.model._orgData[0].id});
else
this.$state.go(this.$state.current.name, {q: JSON.stringify(this.filter)}, {location: 'replace'});
});
} }
get scopeDays() { removeParamFilter(param) {
return this._scopeDays; if (this[param]) delete this[param];
delete this.filter[param];
this.applyFilters();
} }
set scopeDays(value) { onKeyPress($event, param) {
this._scopeDays = value; if ($event.key === 'Enter') {
this.filter[param] = this[param];
this.filter.shippedFrom = null; this.applyFilters(param === 'search');
this.filter.shippedTo = null; }
} }
} }
ngModule.vnComponent('vnTravelSearchPanel', { ngModule.vnComponent('vnTravelSearchPanel', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller controller: Controller,
bindings: {
model: '<'
}
}); });

View File

@ -8,41 +8,31 @@ describe('Travel Component vnTravelSearchPanel', () => {
beforeEach(inject($componentController => { beforeEach(inject($componentController => {
controller = $componentController('vnTravelSearchPanel', {$element: null}); controller = $componentController('vnTravelSearchPanel', {$element: null});
controller.$t = () => {}; controller.$t = () => {};
controller.filter = {};
})); }));
describe('shippedFrom() setter', () => { describe('applyFilters()', () => {
it('should clear the scope days when setting the from property', () => { it('should apply filters', async() => {
controller.filter.scopeDays = 1; controller.filter = {foo: 'bar'};
controller.model = {
applyFilter: jest.fn().mockResolvedValue(),
_orgData: [{id: 1}]
};
controller.$state = {
current: {
name: 'foo'
},
go: jest.fn()
};
controller.shippedFrom = Date.vnNew(); await controller.applyFilters(true);
expect(controller.filter.scopeDays).toBeNull(); expect(controller.model.applyFilter).toHaveBeenCalledWith({}, controller.filter);
expect(controller.shippedFrom).toBeDefined(); expect(controller.$state.go).toHaveBeenCalledWith('travel.card.summary', {id: 1});
});
});
describe('shippedTo() setter', () => { await controller.applyFilters(false);
it('should clear the scope days when setting the to property', () => {
controller.filter.scopeDays = 1;
controller.shippedTo = Date.vnNew(); expect(controller.$state.go).toHaveBeenCalledWith(controller.$state.current.name,
{q: JSON.stringify(controller.filter)}, {location: 'replace'});
expect(controller.filter.scopeDays).toBeNull();
expect(controller.shippedTo).toBeDefined();
});
});
describe('scopeDays() setter', () => {
it('should clear the date range when setting the scopeDays property', () => {
controller.filter.shippedFrom = Date.vnNew();
controller.filter.shippedTo = Date.vnNew();
controller.scopeDays = 1;
expect(controller.filter.shippedFrom).toBeNull();
expect(controller.filter.shippedTo).toBeNull();
expect(controller.scopeDays).toBeDefined();
}); });
}); });
}); });

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