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
},
"search.useIgnoreFiles": false,
"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/),
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
-
- (Facturas recibidas -> Bases negativas) Nueva sección
### Changed
-
@ -16,6 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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
### Added

View File

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

View File

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

View File

@ -30,16 +30,23 @@ module.exports = Self => {
const recipient = to.replace('@', '');
if (sender.name != recipient) {
await models.Chat.create({
const chat = await models.Chat.create({
senderFk: sender.id,
recipient: to,
dated: Date.vnNew(),
checkUserStatus: 0,
message: message,
status: 0,
status: 'sending',
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;
}

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

View File

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

View File

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

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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() => {
const tx = await models.Ticket.beginTransaction({});

View File

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

View File

@ -4,7 +4,8 @@
"options": {
"mysql": {
"table": "salix.User"
}
},
"resetPasswordTokenTTL": "604800"
},
"properties": {
"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'),
(4, 'SalesPerson', 'salesPerson'),
(5, 'Administrative', 'administrative'),
(6, 'Weight', 'weight');
(6, 'Weight', 'weight'),
(7, 'InvoiceOut', 'invoiceOut');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES
@ -738,7 +739,9 @@ INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `des
(9, 23, 5, 'care with the dog'),
(10, 23, 4, 'Reclama ticket: 8'),
(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
-- 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'),
('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
(1, 'CRI', 'Crisantemo', 2, 31, 35, 0, 'cool'),
(2, 'ITG', 'Anthurium', 1, 31, 35, 0, 'cool'),
(3, 'WPN', 'Paniculata', 2, 31, 35, 0, 'cool'),
(4, 'PRT', 'Delivery ports', 3, NULL, 35, 1, 'warm'),
(5, 'CON', 'Container', 3, NULL, 35, 1, 'warm'),
(6, 'ALS', 'Alstroemeria', 1, 31, 16, 0, 'warm');
(1, 'CRI', 'Crisantemo', 2, 31, 35, 0, 'cool', 0),
(2, 'ITG', 'Anthurium', 1, 31, 35, 0, 'cool', 1),
(3, 'WPN', 'Paniculata', 2, 31, 35, 0, 'cool', 0),
(4, 'PRT', 'Delivery ports', 3, NULL, 35, 1, 'warm', 0),
(5, 'CON', 'Container', 3, NULL, 35, 1, 'warm', 0),
(6, 'ALS', 'Alstroemeria', 1, 31, 16, 0, 'warm', 1);
INSERT INTO `vn`.`ink`(`id`, `name`, `picture`, `showOrder`, `hex`)
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),
(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
(1, -2, '2% retention', 2);
(1, -2, '2% retention', 2, 45);
INSERT INTO `vn`.`invoiceInDueDay`(`invoiceInFk`, `dueDated`, `bankFk`, `amount`)
VALUES
@ -2631,8 +2634,8 @@ INSERT INTO `vn`.`supplierAgencyTerm` (`agencyFk`, `supplierFk`, `minimumPackage
INSERT INTO `vn`.`chat` (`senderFk`, `recipient`, `dated`, `checkUserStatus`, `message`, `status`, `attempts`)
VALUES
(1101, '@PetterParker', util.VN_CURDATE(), 1, 'First test message', 0, 0),
(1101, '@PetterParker', util.VN_CURDATE(), 0, 'Second 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, 'pending');
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),
(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: {
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: {
header: 'vn-ticket-summary > vn-card > h5',
@ -1128,6 +1127,15 @@ export default {
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: {
anySearchResult: 'vn-travel-index vn-tbody > a',
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"]',
warehouseOut: 'vn-travel-create vn-autocomplete[ng-model="$ctrl.travel.warehouseOutFk"]',
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: {
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');
});
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);
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
.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() => {
pending('https://redmine.verdnatura.es/issues/5455');
await page.reload({waitUntil: ['networkidle0', 'domcontentloaded']});
await page.waitToClick(selectors.ticketSales.firstSaleHistoryButton);
await page.waitForSelector(selectors.ticketSales.firstSaleHistory);

View File

@ -9,7 +9,7 @@ describe('Ticket Create notes path', () => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'ticket');
await page.accessToSearchResult('1');
await page.accessToSearchResult('5');
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();
page = browser.page;
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');
});
@ -89,11 +90,13 @@ describe('Travel basic data path', () => {
});
it('should navigate to the travel logs', async() => {
pending('https://redmine.verdnatura.es/issues/5455');
await page.accessToSection('travel.card.log');
await page.waitForState('travel.card.log');
});
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');
expect(result).toContain('new reference!');

View File

@ -9,7 +9,8 @@ describe('Travel descriptor path', () => {
browser = await getBrowser();
page = browser.page;
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');
});
@ -81,7 +82,8 @@ describe('Travel descriptor path', () => {
await page.waitToClick('.cancel');
await page.waitToClick(selectors.globalItems.homeButton);
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');
const state = await page.getState();

View File

@ -10,7 +10,8 @@ describe('Travel thermograph path', () => {
browser = await getBrowser();
page = browser.page;
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');
});

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
No results: Sin resultados
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
February: Febrero
March: Marzo

View File

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

View File

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

View File

@ -271,5 +271,6 @@
"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}}",
"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 ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const MySQL = require('loopback-connector-mysql').MySQL;
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 limitSet = new Set([
@ -254,49 +253,49 @@ class VnMySQL extends MySQL {
}
create(model, data, opts, cb) {
const ctx = {data};
const ctx = { data };
this.invokeMethod('create',
arguments, model, ctx, opts, cb);
}
createAll(model, data, opts, cb) {
const ctx = {data};
const ctx = { data };
this.invokeMethod('createAll',
arguments, model, ctx, opts, cb);
}
save(model, data, opts, cb) {
const ctx = {data};
const ctx = { data };
this.invokeMethod('save',
arguments, model, ctx, opts, cb);
}
updateOrCreate(model, data, opts, cb) {
const ctx = {data};
const ctx = { data };
this.invokeMethod('updateOrCreate',
arguments, model, ctx, opts, cb);
}
replaceOrCreate(model, data, opts, cb) {
const ctx = {data};
const ctx = { data };
this.invokeMethod('replaceOrCreate',
arguments, model, ctx, opts, cb);
}
destroyAll(model, where, opts, cb) {
const ctx = {where};
const ctx = { where };
this.invokeMethod('destroyAll',
arguments, model, ctx, opts, cb);
}
update(model, where, data, opts, cb) {
const ctx = {where, data};
const ctx = { where, data };
this.invokeMethod('update',
arguments, model, ctx, opts, cb);
}
replaceById(model, id, data, opts, cb) {
const ctx = {id, data};
const ctx = { id, data };
this.invokeMethod('replaceById',
arguments, model, ctx, opts, cb);
}
@ -321,16 +320,16 @@ class VnMySQL extends MySQL {
let tx;
if (!opts.transaction) {
tx = await Transaction.begin(this, {});
opts = Object.assign({transaction: tx, httpCtx: opts.httpCtx}, opts);
opts = Object.assign({ transaction: tx, httpCtx: opts.httpCtx }, opts);
}
try {
// Fetch old values (update|delete) or login
let where, id, data, idName, limit, op, oldInstances, newInstances;
const hasGrabUser = settings.log && settings.log.grabUser;
if(hasGrabUser){
if (hasGrabUser) {
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);
}
else {
@ -344,8 +343,8 @@ class VnMySQL extends MySQL {
op = opMap.get(method);
if (!where) {
if (id) where = {[idName]: id};
else where = {[idName]: data[idName]};
if (id) where = { [idName]: id };
else where = { [idName]: data[idName] };
}
// Fetch old values
@ -365,7 +364,7 @@ class VnMySQL extends MySQL {
super[method].apply(this, fnArgs);
});
if(hasGrabUser)
if (hasGrabUser)
await this.executeP(`CALL account.myUser_logout()`, null, opts);
else {
// Fetch new values
@ -388,7 +387,7 @@ class VnMySQL extends MySQL {
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);
newInstances = await this.executeStmt(stmt, opts);
@ -431,7 +430,7 @@ class VnMySQL extends MySQL {
const stmt = new ParameterizedSQL(
'SELECT ' +
this.buildColumnNames(model, {fields}) +
this.buildColumnNames(model, { fields }) +
' FROM ' +
this.tableEscaped(model)
);
@ -537,13 +536,13 @@ exports.initialize = function initialize(dataSource, callback) {
modelBuilder.defineValueType.bind(modelBuilder) :
modelBuilder.constructor.registerType.bind(modelBuilder.constructor);
defineType(function Point() {});
defineType(function Point() { });
dataSource.EnumFactory = EnumFactory;
if (callback) {
if (dataSource.settings.lazyConnect) {
process.nextTick(function() {
process.nextTick(function () {
callback();
});
} 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 options = generateOptions(this.settings);
if (this.client) {
if (callback) {
process.nextTick(function() {
process.nextTick(function () {
callback(null, self.client);
});
}
@ -566,7 +565,7 @@ MySQL.prototype.connect = function(callback) {
function connectionHandler(options, callback) {
const client = mysql.createPool(options);
client.getConnection(function(err, connection) {
client.getConnection(function (err, connection) {
const conn = connection;
if (!err) {
if (self.debug)
@ -645,3 +644,31 @@ function generateOptions(settings) {
}
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

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

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('claim regularizeClaim()', () => {
const userId = 18;
@ -39,6 +40,20 @@ describe('claim regularizeClaim()', () => {
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() => {
const tx = await models.Claim.beginTransaction({});

View File

@ -151,7 +151,7 @@ class Controller extends Section {
isClaimEditable() {
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;
});
}

View File

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

View File

@ -1,6 +1,22 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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() => {
const tx = await models.Buy.beginTransaction({});
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": {
"type": "string"
},
"daysAgo": {
"type": "number"
}
},
"relations": {

View File

@ -6,4 +6,7 @@ module.exports = Self => {
require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(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 './create';
import './log';
import './serial';
import './serial-search-panel';
import './negative-bases';

View File

@ -7,6 +7,7 @@ Foreign value: Divisa
InvoiceIn: Facturas recibidas
InvoiceIn cloned: Factura clonada
InvoiceIn deleted: Factura eliminada
InvoiceIn Serial: Facturas por series
Invoice list: Listado de facturas recibidas
InvoiceIn booked: Factura contabilizada
Net: Neto
@ -22,3 +23,5 @@ Total stems: Total tallos
Show agricultural receipt as PDF: Ver recibo agrícola como PDF
Send agricultural receipt as PDF: Enviar recibo agrícola como PDF
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": {
"main": [
{
"state": "invoiceIn.index",
"icon": "icon-invoice-in"
}
{ "state": "invoiceIn.index", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.serial", "icon": "icon-invoice-in"},
{ "state": "invoiceIn.negative-bases", "icon": "icon-ticket"}
],
"card": [
{
@ -54,6 +53,24 @@
"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",
"state": "invoiceIn.card",

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 LoopBackContext = require('loopback-context');
describe('upsertFixedPrice()', () => {
const now = Date.vnNew();
@ -7,6 +8,17 @@ describe('upsertFixedPrice()', () => {
beforeAll(async() => {
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() => {

View File

@ -1,7 +1,7 @@
const axios = require('axios');
const uuid = require('uuid');
const fs = require('fs/promises');
const { createWriteStream } = require('fs');
const {createWriteStream} = require('fs');
const path = require('path');
const gm = require('gm');
@ -15,7 +15,7 @@ module.exports = Self => {
},
});
Self.download = async () => {
Self.download = async() => {
const models = Self.app.models;
const tempContainer = await models.TempContainer.container(
'salix-image'
@ -32,13 +32,13 @@ module.exports = Self => {
let tempFilePath;
let queueRow;
try {
const myOptions = { transaction: tx };
const myOptions = {transaction: tx};
queueRow = await Self.findOne(
{
fields: ['id', 'itemFk', 'url', 'attempts'],
where: {
url: { neq: null },
url: {neq: null},
attempts: {
lt: maxAttempts,
},
@ -59,7 +59,7 @@ module.exports = Self => {
'model',
'property',
],
where: { name: collectionName },
where: {name: collectionName},
include: {
relation: 'sizes',
scope: {
@ -116,16 +116,16 @@ module.exports = Self => {
const collectionDir = path.join(rootPath, collectionName);
// To max size
const { maxWidth, maxHeight } = collection;
const {maxWidth, maxHeight} = collection;
const fullSizePath = path.join(collectionDir, 'full');
const toFullSizePath = `${fullSizePath}/${fileName}`;
await fs.mkdir(fullSizePath, { recursive: true });
await fs.mkdir(fullSizePath, {recursive: true});
await new Promise((resolve, reject) => {
gm(tempFilePath)
.resize(maxWidth, maxHeight, '>')
.setFormat('png')
.write(toFullSizePath, function (err) {
.write(toFullSizePath, function(err) {
if (err) reject(err);
if (!err) resolve();
});
@ -133,12 +133,12 @@ module.exports = Self => {
// To 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 toSizePath = `${sizePath}/${fileName}`;
await fs.mkdir(sizePath, { recursive: true });
await fs.mkdir(sizePath, {recursive: true});
await new Promise((resolve, reject) => {
const gmInstance = gm(tempFilePath);
@ -153,7 +153,7 @@ module.exports = Self => {
gmInstance
.setFormat('png')
.write(toSizePath, function (err) {
.write(toSizePath, function(err) {
if (err) reject(err);
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 LoopBackContext = require('loopback-context');
describe('item clone()', () => {
let nextItemId;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
beforeEach(async() => {
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 LoopBackContext = require('loopback-context');
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() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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() => {
const tx = await models.Item.beginTransaction({});
const options = {transaction: tx};

View File

@ -40,11 +40,11 @@ Create: Crear
Client card: Ficha del cliente
Shipped: F. envío
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
SalesPerson: Comercial
Concept: Concepto
Units/Box: Unidades/Caja
Units/Box: Unidades/caja
# Sections
Items: Artículos

View File

@ -295,11 +295,26 @@ module.exports = Self => {
risk = t.debt + t.credit, totalProblems = totalProblems + 1
`);
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getWarnings');
stmt = new ParameterizedSQL(`
SELECT t.*, tp.*,
((tp.risk) + cc.riskTolerance < 0) AS hasHighRisk
CREATE TEMPORARY TABLE tmp.sale_getWarnings
(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
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`);
const hasProblems = args.problems;
@ -352,20 +367,37 @@ module.exports = Self => {
return {'t.alertLevel': value};
case 'pending':
if (value) {
return {and: [
{'t.alertLevel': 0},
{'t.alertLevelCode': {nin: [
'OK',
'BOARDING',
'PRINTED',
'PRINTED_AUTO',
'PICKER_DESIGNED'
]}}
]};
return {'t.alertLevelCode': {inq: [
'FIXING',
'FREE',
'NOT_READY',
'BLOCKED',
'EXPANDABLE',
'CHAINED',
'WAITING_FOR_PAYMENT'
]}};
} else {
return {and: [
{'t.alertLevel': {gt: 0}}
]};
return {'t.alertLevelCode': {inq: [
'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 'warehouseFk':
@ -387,6 +419,8 @@ module.exports = Self => {
tmp.filter,
tmp.ticket_problems,
tmp.sale_getProblems,
tmp.sale_getWarnings,
tmp.ticket_warnings,
tmp.risk`);
const sql = ParameterizedSQL.join(stmts, ';');

View File

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

View File

@ -68,6 +68,7 @@
<th field="stateFk">
<span translate>State</span>
</th>
<th field="isFragile"></th>
<th field="zoneFk">
<span translate>Zone</span>
</th>
@ -175,6 +176,14 @@
{{ticket.state}}
</span>
</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">
<span
title="{{ticket.zoneName}}"

View File

@ -1,6 +1,22 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
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();
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];

View File

@ -1,6 +1,20 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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;
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 LoopBackContext = require('loopback-context');
describe('sale canEdit()', () => {
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', () => {
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 LoopBackContext = require('loopback-context');
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() => {
const tx = await models.Sale.beginTransaction({});

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('sale reserve()', () => {
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() => {
const tx = await models.Sale.beginTransaction({});

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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 saleId = 25;

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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 = {
req: {
accessToken: {userId: 18},

View File

@ -1,6 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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 = {
req: {
accessToken: {userId: 9},

View File

@ -1,7 +1,21 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket addSale()', () => {
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() => {
const tx = await models.Ticket.beginTransaction({});

View File

@ -10,11 +10,15 @@ describe('ticket merge()', () => {
workerFk: 1
};
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
@ -35,16 +39,16 @@ describe('ticket merge()', () => {
try {
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);
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 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(salesTicketFuture.length).toEqual(2);
expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2);

View File

@ -1,6 +1,20 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
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;
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
Move: Mover
New ticket without route: Nuevo ticket sin ruta

View File

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

View File

@ -40,3 +40,4 @@ Refund: Abono
Promotion mana: Maná promoción
Claim mana: Maná reclamación
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,
s.id AS cargoSupplierFk,
s.nickname AS cargoSupplierNickname,
s.name AS supplierName,
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
FROM travel t
@ -167,6 +168,7 @@ module.exports = Self => {
SUM(b.stickers) AS stickers,
e.evaNotes,
e.notes,
e.invoiceAmount,
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
FROM tmp.travel tr

View File

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

View File

@ -6,6 +6,6 @@ Phy. KG: KG físico
Vol. KG: KG Vol.
Search by travel id or reference: Buscar por id de travel o referencia
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. Landed: F. llegada

View File

@ -2,6 +2,15 @@
<vn-auto-search
model="model">
</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
model="model"
class="vn-mb-xl vn-w-xl">
@ -9,15 +18,15 @@
<vn-table model="model">
<vn-thead>
<vn-tr>
<vn-th field="id" number filter-enabled="false">Id</vn-th>
<vn-th field="ref">Reference</vn-th>
<vn-th field="agencyModeFk">Agency</vn-th>
<vn-th field="warehouseOutFk">Warehouse Out</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="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-tr>
</vn-thead>
@ -25,7 +34,6 @@
<a ng-repeat="travel in model.data"
class="clickable vn-tr search-result"
ui-sref="travel.card.summary({id: {{::travel.id}}})">
<vn-td number>{{::travel.id}}</vn-td>
<vn-td>{{::travel.ref}}</vn-td>
<vn-td>{{::travel.agencyModeName}}</vn-td>
<vn-td>{{::travel.warehouseOutName}}</vn-td>
@ -34,14 +42,27 @@
{{::travel.shipped | date:'dd/MM/yyyy'}}
</span>
</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 center shrink-date>
<span class="chip {{$ctrl.compareDate(travel.landed)}}">
{{::travel.landed | date:'dd/MM/yyyy'}}
</span>
</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-horizontal class="buttons">
<vn-icon-button
@ -84,32 +105,3 @@
question="Do you want to clone this travel?"
message="All it's properties will be copied">
</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 Section from 'salix/components/section';
import './style.scss';
export default class Controller extends Section {
preview(travel) {
@ -30,37 +31,6 @@ export default class Controller extends Section {
if (timeDifference == 0) return 'warning';
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', {

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
Reference: Referencia
Warehouse Out: Almacén salida
Warehouse In: Almacén llegada
Warehouse Out: Alm salida
Warehouse In: Alm llegada
Shipped from: Salida desde
Shipped to: Salida hasta
Landed from: Llegada desde
@ -10,8 +10,8 @@ Shipped: F. salida
Landed: F. llegada
Delivered: Enviado
Received: Recibido
Travel id: Id envío
Search travels by id: Buscar envíos por identificador
Travel id: Id
Search travels by id: Buscar envíos por identificador o referencia
New travel: Nuevo envío
travel: envío

View File

@ -1,18 +1,4 @@
<vn-crud-model
vn-id="model"
url="Travels/filter"
limit="20"
order="shipped DESC, landed DESC">
</vn-crud-model>
<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 slot="menu">
<vn-left-menu></vn-left-menu>

View File

@ -4,28 +4,6 @@ import ModuleMain from 'salix/components/module-main';
export default class Travel extends ModuleMain {
constructor() {
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">
<form ng-submit="$ctrl.onSearch()" id="manifold-form">
<vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-side-menu side="right">
<vn-horizontal class="input">
<vn-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search travels by id"
vn-focus>
ng-model="$ctrl.search"
ng-keydown="$ctrl.onKeyPress($event, 'search')">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Reference"
ng-model="filter.ref">
</vn-textfield>
<vn-textfield
vn-one
label="Total entries"
ng-model="filter.totalEntries">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-textfield
vn-one
label="Travel id"
ng-model="filter.id">
</vn-textfield>
<vn-autocomplete vn-one
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-id="agency"
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes"
ng-model="$ctrl.filter.agencyModeFk"
data="$ctrl.agencyModes"
show-field="name"
value-field="id">
value-field="id"
on-change="$ctrl.applyFilters()">
</vn-autocomplete>
</vn-horizontal>
<section class="vn-px-md">
<vn-horizontal class="manifold-panel vn-pa-md">
<vn-date-picker
vn-one
label="Shipped from"
ng-model="filter.shippedFrom"
on-change="$ctrl.shippedFrom = value">
</vn-date-picker>
<vn-date-picker
vn-one
label="Shipped to"
ng-model="filter.shippedTo"
on-change="$ctrl.shippedTo = value">
</vn-date-picker>
<vn-none class="or vn-px-md" translate>Or</vn-none>
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-id="warehouseOut"
label="Warehouse Out"
ng-model="$ctrl.filter.warehouseOutFk"
data="$ctrl.warehouses"
show-field="name"
value-field="id"
on-change="$ctrl.applyFilters()">
</vn-autocomplete>
<vn-autocomplete
vn-id="warehouseIn"
label="Warehouse In"
ng-model="$ctrl.filter.warehouseInFk"
data="$ctrl.warehouses"
show-field="name"
value-field="id"
on-change="$ctrl.applyFilters()">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="input horizontal">
<vn-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
ng-model="$ctrl.filter.scopeDays"
on-change="$ctrl.applyFilters()"
display-controls="true"
info="Cannot choose a range of dates and days onward at the same time">
</vn-input-number>
<vn-icon color-marginal
icon="info"
vn-tooltip="Cannot choose a range of dates and days onward at the same time">
</vn-icon>
</vn-horizontal>
</section>
<vn-horizontal class="vn-px-lg">
<vn-horizontal class="input horizontal">
<vn-date-picker
vn-one
label="Landed from"
ng-model="filter.landedFrom">
ng-model="$ctrl.filter.landedFrom"
on-change="$ctrl.applyFilters()">
</vn-date-picker>
<vn-date-picker
vn-one
label="Landed to"
ng-model="filter.landedTo">
ng-model="$ctrl.filter.landedTo"
on-change="$ctrl.applyFilters()">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
label="Warehouse Out"
ng-model="filter.warehouseOutFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one
label="Warehouse In"
ng-model="filter.warehouseInFk"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg">
<vn-autocomplete vn-one
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-id="continent"
label="Continent Out"
ng-model="filter.continent"
url="Continents"
ng-model="$ctrl.filter.continent"
data="$ctrl.continents"
show-field="name"
value-field="code">
value-field="code"
on-change="$ctrl.applyFilters()">
</vn-autocomplete>
<vn-input-number
min="0"
label="Total entries"
ng-model="$ctrl.totalEntries"
ng-keydown="$ctrl.onKeyPress($event, 'totalEntries')">
</vn-input-number>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>
<div class="chips">
<vn-chip
ng-if="$ctrl.filter.search"
removable="true"
on-remove="$ctrl.removeParamFilter('search')"
class="colored">
<span>Id/{{$ctrl.$t('Reference')}}: {{$ctrl.filter.search}}</span>
</vn-chip>
<vn-chip
ng-if="agency.selection"
removable="true"
on-remove="$ctrl.removeParamFilter('agencyModeFk')"
class="colored">
<span>{{$ctrl.$t('Agency')}}: {{agency.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="warehouseOut.selection"
removable="true"
on-remove="$ctrl.removeParamFilter('warehouseOutFk')"
class="colored">
<span>{{$ctrl.$t('Warehouse Out')}}: {{warehouseOut.selection.name}}</span>
</vn-chip>
<vn-chip
ng-if="warehouseIn.selection"
removable="true"
on-remove="$ctrl.removeParamFilter('warehouseInFk')"
class="colored">
<span>{{$ctrl.$t('Warehouse In')}}: {{warehouseIn.selection.name}}</span>
</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 SearchPanel from 'core/components/searchbar/search-panel';
import './style.scss';
class Controller extends SearchPanel {
constructor($, $element) {
super($, $element);
this.filter = this.$.filter;
this.initFilter();
this.fetchData();
}
get shippedFrom() {
return this._shippedFrom;
$onChanges() {
if (this.model)
this.applyFilters();
}
set shippedFrom(value) {
this._shippedFrom = value;
this.filter.scopeDays = null;
fetchData() {
this.$http.get('AgencyModes').then(res => {
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() {
return this._shippedTo;
initFilter() {
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) {
this._shippedTo = value;
this.filter.scopeDays = null;
applyFilters(param) {
this.model.applyFilter({}, this.filter)
.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() {
return this._scopeDays;
removeParamFilter(param) {
if (this[param]) delete this[param];
delete this.filter[param];
this.applyFilters();
}
set scopeDays(value) {
this._scopeDays = value;
this.filter.shippedFrom = null;
this.filter.shippedTo = null;
onKeyPress($event, param) {
if ($event.key === 'Enter') {
this.filter[param] = this[param];
this.applyFilters(param === 'search');
}
}
}
ngModule.vnComponent('vnTravelSearchPanel', {
template: require('./index.html'),
controller: Controller
controller: Controller,
bindings: {
model: '<'
}
});

View File

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

View File

@ -0,0 +1,37 @@
@import "variables";
vn-travel-search-panel vn-side-menu {
.menu {
min-width: $right-menu-width;
}
& > div {
.input {
padding-left: $spacing-md;
padding-right: $spacing-md;
border-color: $color-spacer;
border-bottom: $border-thin;
}
.horizontal {
padding-left: $spacing-md;
padding-right: $spacing-md;
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;
}
.or {
align-self: center;
font-weight: bold;
font-size: 26px;
color: $color-font-secondary;
}
}
}

View File

@ -1,9 +1,9 @@
Reference: Referencia
Warehouse In: Almacén entrada
Warehouse Out: Almacén salida
Warehouse In: Alm. entrada
Warehouse Out: Alm. salida
Shipped: F. envío
Landed: F. entrega
Total entries: Entradas totales
Total entries: Ent. totales
Delivered: Enviada
Received: Recibida
Agency: Agencia

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