Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5410-client.defaulter_getChecked
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Vicent Llopis 2023-04-04 13:40:12 +02:00
commit c89f95a33e
88 changed files with 2100 additions and 503 deletions

View File

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

View File

@ -5,11 +5,12 @@ 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
- (Clientes -> Morosos) Ahora se puede filtrar por las columnas "Desde" y "Fecha Ú. O.". También se envia un email al comercial cuando se añade una nota.
- (Monitor tickets) Muestra un icono al lado de la zona, si el ticket es frágil y se envía por agencia
- (Facturas recibidas -> Bases negativas) Nueva sección
### Changed
-
@ -17,6 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- (Clientes -> Morosos) Ahora se mantienen los elementos seleccionados al hacer sroll.
## [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

@ -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

@ -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

@ -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

@ -8,7 +8,7 @@ WHERE code IN ('ZKA', 'ZKE');
UPDATE `vn`.`itemType`
SET isFragile = 1
WHERE id IN (SELECT it.id
FROM itemCategory ic
JOIN itemType it ON it.categoryFk = ic.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,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);
@ -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

@ -1127,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)',
@ -1138,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

@ -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');
});

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
@ -42,9 +42,9 @@ December: Diciembre
Monday: Lunes
Tuesday: Martes
Wednesday: Miércoles
Thursday: Jueves
Friday: Viernes
Saturday: Sábado
Thursday: Jueves
Friday: Viernes
Saturday: Sábado
Sunday: Domingo
Has delivery: Hay reparto
Loading: Cargando
@ -63,4 +63,4 @@ Loading...: Cargando...
No results found: Sin resultados
No data: Sin datos
Undo changes: Deshacer cambios
Load more results: Cargar más resultados
Load more results: Cargar más resultados

View File

@ -2,6 +2,7 @@
$font-size: 11pt;
$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,18 +343,18 @@ 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
switch (op) {
case 'update':
case 'delete':
// Single entity operation
const stmt = this.buildSelectStmt(op, data, idName, model, where, limit);
stmt.merge(`FOR UPDATE`);
oldInstances = await this.executeStmt(stmt, opts);
case 'update':
case 'delete':
// Single entity operation
const stmt = this.buildSelectStmt(op, data, idName, model, where, limit);
stmt.merge(`FOR UPDATE`);
oldInstances = await this.executeStmt(stmt, opts);
}
}
@ -365,30 +364,30 @@ 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
const ids = [];
switch (op) {
case 'insert':
case 'update': {
case 'insert':
case 'update': {
switch (method) {
case 'createAll':
for (const row of res[1])
ids.push(row[idName]);
break;
case 'create':
ids.push(res[1]);
break;
case 'update':
if (data[idName] != null)
ids.push(data[idName]);
break;
case 'createAll':
for (const row of res[1])
ids.push(row[idName]);
break;
case 'create':
ids.push(res[1]);
break;
case 'update':
if (data[idName] != null)
ids.push(data[idName]);
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,9 +430,9 @@ class VnMySQL extends MySQL {
const stmt = new ParameterizedSQL(
'SELECT ' +
this.buildColumnNames(model, {fields}) +
' FROM ' +
this.tableEscaped(model)
this.buildColumnNames(model, { fields }) +
' FROM ' +
this.tableEscaped(model)
);
stmt.merge(this.buildWhere(model, where));
if (limit) stmt.merge(`LIMIT 1`);
@ -505,8 +504,8 @@ class VnMySQL extends MySQL {
if (oldI) {
Object.keys(oldI).forEach(prop => {
const hasChanges = oldI[prop] instanceof Date ?
oldI[prop]?.getTime() != newI[prop]?.getTime() :
oldI[prop] != newI[prop];
oldI[prop]?.getTime() != newI[prop]?.getTime() :
oldI[prop] != newI[prop];
if (!hasChanges) {
delete oldI[prop];
@ -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 = {};
@ -56,25 +56,25 @@ module.exports = Self => {
let where = buildFilter(args, (param, value) => {
switch (param) {
case 'search':
return {
or: [
{changedModel: {like: `%${value}%`}},
{oldInstance: {like: `%${value}%`}}
]
};
case 'userFk':
return {'cl.userFk': value};
case 'created':
value.setHours(0, 0, 0, 0);
to = new Date(value);
to.setHours(23, 59, 59, 999);
case 'search':
return {
or: [
{ changedModel: { like: `%${value}%` } },
{ oldInstance: { like: `%${value}%` } }
]
};
case 'userFk':
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

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

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",
@ -133,4 +150,4 @@
]
}
]
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

@ -367,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':

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
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,23 +18,22 @@
<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>
<vn-tbody>
<a ng-repeat="travel in model.data"
<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
@ -49,7 +70,7 @@
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
<vn-icon-button
vn-anchor="::{state: 'entry.create', params: {travelFk: travel.id}}"
vn-tooltip="Add entry"
icon="icon-ticket">
@ -78,38 +99,9 @@
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
<vn-confirm
vn-id="clone"
on-accept="$ctrl.onCloneAccept($data)"
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,12 +10,12 @@ 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
# Sections
Travels: Envíos
Log: Historial
Thermographs: Termógrafos
Thermographs: Termógrafos

View File

@ -1,20 +1,6 @@
<vn-crud-model
vn-id="model"
url="Travels/filter"
limit="20"
order="shipped DESC, landed DESC">
</vn-crud-model>
<vn-portal slot="topbar">
<vn-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>
</vn-portal>
<ui-view></ui-view>
<ui-view></ui-view>

View File

@ -4,28 +4,6 @@ import ModuleMain from 'salix/components/module-main';
export default class Travel extends ModuleMain {
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-textfield
vn-one
label="General search"
ng-model="filter.search"
info="Search travels by id"
vn-focus>
</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
label="Agency"
ng-model="filter.agencyModeFk"
url="AgencyModes"
show-field="name"
value-field="id">
</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-input-number
vn-one
min="0"
step="1"
label="Days onward"
ng-model="filter.scopeDays"
on-change="$ctrl.scopeDays = value"
display-controls="true">
</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-date-picker
vn-one
label="Landed from"
ng-model="filter.landedFrom">
</vn-date-picker>
<vn-date-picker
vn-one
label="Landed to"
ng-model="filter.landedTo">
</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
label="Continent Out"
ng-model="filter.continent"
url="Continents"
show-field="name"
value-field="code">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit>
</vn-horizontal>
</form>
</div>
<vn-side-menu side="right">
<vn-horizontal class="input">
<vn-textfield
label="General search"
info="Search travels by id"
ng-model="$ctrl.search"
ng-keydown="$ctrl.onKeyPress($event, 'search')">
</vn-textfield>
</vn-horizontal>
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-id="agency"
label="Agency"
ng-model="$ctrl.filter.agencyModeFk"
data="$ctrl.agencyModes"
show-field="name"
value-field="id"
on-change="$ctrl.applyFilters()">
</vn-autocomplete>
</vn-horizontal>
<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
min="0"
step="1"
label="Days onward"
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-horizontal>
<vn-horizontal class="input horizontal">
<vn-date-picker
label="Landed from"
ng-model="$ctrl.filter.landedFrom"
on-change="$ctrl.applyFilters()">
</vn-date-picker>
<vn-date-picker
label="Landed to"
ng-model="$ctrl.filter.landedTo"
on-change="$ctrl.applyFilters()">
</vn-date-picker>
</vn-horizontal>
<vn-horizontal class="input horizontal">
<vn-autocomplete
vn-id="continent"
label="Continent Out"
ng-model="$ctrl.filter.continent"
data="$ctrl.continents"
show-field="name"
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>
<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

View File

@ -0,0 +1,57 @@
module.exports = Self => {
Self.remoteMethodCtx('createWagonType', {
description: 'Creates a new wagon type',
accessType: 'WRITE',
accepts: [
{
arg: 'name',
type: 'String',
required: true
},
{
arg: 'divisible',
type: 'boolean',
required: true
}, {
arg: 'trays',
type: 'any',
required: true
}
],
http: {
path: `/createWagonType`,
verb: 'PATCH'
}
});
Self.createWagonType = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const newWagonType = await models.WagonType.create({name: args.name, divisible: args.divisible}, myOptions);
args.trays.forEach(async tray => {
await models.WagonTypeTray.create({
typeFk: newWagonType.id,
height: tray.position,
colorFk: tray.color.id
}, myOptions);
});
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,43 @@
module.exports = Self => {
Self.remoteMethodCtx('deleteWagonType', {
description: 'Deletes a wagon type',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'Number',
required: true
}
],
http: {
path: `/deleteWagonType`,
verb: 'DELETE'
}
});
Self.deleteWagonType = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
await models.Wagon.destroyAll({typeFk: args.id}, myOptions);
await models.WagonTypeTray.destroyAll({typeFk: args.id}, myOptions);
await models.WagonType.destroyAll({id: args.id}, myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,64 @@
module.exports = Self => {
Self.remoteMethodCtx('editWagonType', {
description: 'Edits a new wagon type',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'String',
required: true
},
{
arg: 'name',
type: 'String',
required: true
},
{
arg: 'divisible',
type: 'boolean',
required: true
}, {
arg: 'trays',
type: 'any',
required: true
}
],
http: {
path: `/editWagonType`,
verb: 'PATCH'
}
});
Self.editWagonType = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const wagonType = await models.WagonType.findById(args.id, null, myOptions);
wagonType.updateAttributes({name: args.name, divisible: args.divisible}, myOptions);
models.WagonTypeTray.destroyAll({typeFk: args.id}, myOptions);
args.trays.forEach(async tray => {
await models.WagonTypeTray.create({
typeFk: args.id,
height: tray.position,
colorFk: tray.color.id
}, myOptions);
});
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,63 @@
const models = require('vn-loopback/server/server').models;
describe('WagonType crudWagonType()', () => {
const ctx = {
args: {
name: 'Mock wagon type',
divisible: true,
trays: [{position: 0, color: {id: 1}},
{position: 50, color: {id: 2}},
{position: 100, color: {id: 3}}]
}
};
it(`should create, edit and delete a new wagon type and its trays`, async() => {
const tx = await models.WagonType.beginTransaction({});
try {
const options = {transaction: tx};
// create
await models.WagonType.createWagonType(ctx, options);
const newWagonType = await models.WagonType.findOne({where: {name: ctx.args.name}}, options);
const newWagonTrays = await models.WagonTypeTray.find({where: {typeFk: newWagonType.id}}, options);
expect(newWagonType).not.toEqual(null);
expect(newWagonType.name).toEqual(ctx.args.name);
expect(newWagonType.divisible).toEqual(ctx.args.divisible);
expect(newWagonTrays.length).toEqual(ctx.args.trays.length);
ctx.args = {
id: newWagonType.id,
name: 'Edited wagon type',
divisible: false,
trays: [{position: 0, color: {id: 1}}]
};
// edit
await models.WagonType.editWagonType(ctx, options);
const editedWagonType = await models.WagonType.findById(newWagonType.id, null, options);
const editedWagonTrays = await models.WagonTypeTray.find({where: {typeFk: newWagonType.id}}, options);
expect(editedWagonType.name).toEqual(ctx.args.name);
expect(editedWagonType.divisible).toEqual(ctx.args.divisible);
expect(editedWagonTrays.length).toEqual(ctx.args.trays.length);
// delete
await models.WagonType.deleteWagonType(ctx, options);
const deletedWagonType = await models.WagonType.findById(newWagonType.id, null, options);
const deletedWagonTrays = await models.WagonTypeTray.find({where: {typeFk: newWagonType.id}}, options);
expect(deletedWagonType).toEqual(null);
expect(deletedWagonTrays).toEqual([]);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,23 @@
{
"Wagon": {
"dataSource": "vn"
},
"WagonType": {
"dataSource": "vn"
},
"WagonTypeColor": {
"dataSource": "vn"
},
"WagonTypeTray": {
"dataSource": "vn"
},
"WagonConfig": {
"dataSource": "vn"
},
"CollectionWagon": {
"dataSource": "vn"
},
"CollectionWagonTicket": {
"dataSource": "vn"
}
}

View File

@ -0,0 +1,43 @@
{
"name": "CollectionWagonTicket",
"base": "VnModel",
"options": {
"mysql": {
"table": "collectionWagonTicket"
}
},
"properties": {
"ticketFk": {
"id": true,
"type": "number"
},
"wagonFk": {
"type": "number",
"required": true
},
"trayFk": {
"type": "number",
"required": true
},
"side": {
"type": "string"
}
},
"relations": {
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk"
},
"wagon": {
"type": "belongsTo",
"model": "Wagon",
"foreignKey": "wagonFk"
},
"tray": {
"type": "belongsTo",
"model": "WagonTypeTray",
"foreignKey": "trayFk"
}
}
}

View File

@ -0,0 +1,34 @@
{
"name": "CollectionWagon",
"base": "VnModel",
"options": {
"mysql": {
"table": "collectionWagon"
}
},
"properties": {
"collectionFk": {
"id": true,
"type": "number"
},
"wagonFk": {
"type": "number",
"required": true
},
"position": {
"type": "number"
}
},
"relations": {
"collection": {
"type": "belongsTo",
"model": "Collection",
"foreignKey": "collectionFk"
},
"wagon": {
"type": "belongsTo",
"model": "Wagon",
"foreignKey": "wagonFk"
}
}
}

View File

@ -0,0 +1,30 @@
{
"name": "WagonConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "wagonConfig"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"width": {
"type": "number"
},
"height": {
"type": "string"
},
"maxWagonHeight": {
"type": "number"
},
"minHeightBetweenTrays": {
"type": "number"
},
"maxTrays": {
"type": "number"
}
}
}

View File

@ -0,0 +1,21 @@
{
"name": "WagonTypeColor",
"base": "VnModel",
"options": {
"mysql": {
"table": "wagonTypeColor"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"name": {
"type": "string"
},
"rgb": {
"type": "string"
}
}
}

View File

@ -0,0 +1,36 @@
{
"name": "WagonTypeTray",
"base": "VnModel",
"options": {
"mysql": {
"table": "wagonTypeTray"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"typeFk": {
"type": "number"
},
"height": {
"type": "number"
},
"colorFk": {
"type": "number"
}
},
"relations": {
"type": {
"type": "belongsTo",
"model": "WagonType",
"foreignKey": "typeFk"
},
"color": {
"type": "belongsTo",
"model": "WagonTypeColor",
"foreignKey": "colorFk"
}
}
}

View File

@ -0,0 +1,5 @@
module.exports = Self => {
require('../methods/wagonType/createWagonType')(Self);
require('../methods/wagonType/editWagonType')(Self);
require('../methods/wagonType/deleteWagonType')(Self);
};

View File

@ -0,0 +1,21 @@
{
"name": "WagonType",
"base": "VnModel",
"options": {
"mysql": {
"table": "wagonType"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"name": {
"type": "string"
},
"divisible": {
"type": "boolean"
}
}
}

View File

@ -0,0 +1,34 @@
{
"name": "Wagon",
"base": "VnModel",
"options": {
"mysql": {
"table": "wagon"
}
},
"properties": {
"id": {
"id": true,
"type": "number"
},
"label": {
"type": "number"
},
"volume": {
"type": "number"
},
"plate": {
"type": "string"
},
"typeFk": {
"type": "number"
}
},
"relations": {
"type": {
"type": "belongsTo",
"model": "WagonType",
"foreignKey": "typeFk"
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "salix-back",
"version": "23.12.01",
"version": "23.14.01",
"author": "Verdnatura Levante SL",
"description": "Salix backend",
"license": "GPL-3.0",

View File

@ -240,13 +240,18 @@
</tfoot>
</table>
</div>
<div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer'">
<div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer' || ticketObservations">
<div class="size50 pull-left no-page-break">
<div class="panel">
<div class="header">{{$t('observations')}}</div>
<div class="body">
<div>{{$t('wireTransfer')}}</div>
<div>{{$t('accountNumber', [invoice.iban])}}</div>
<div v-if="invoice.payMethodCode == 'wireTransfer'">
<div>{{$t('wireTransfer')}}</div>
<div>{{$t('accountNumber', [invoice.iban])}}</div>
</div>
<div v-if="ticketObservations">
{{ticketObservations}}
</div>
</div>
</div>
</div>

View File

@ -21,11 +21,15 @@ module.exports = {
const map = new Map();
this.ticketObservations = '';
for (let ticket of tickets) {
ticket.sales = [];
map.set(ticket.id, ticket);
if (ticket.description) this.ticketObservations += ticket.description + ' ';
}
this.ticketObservations = this.ticketObservations.trim();
for (let sale of sales) {
const ticket = map.get(sale.ticketFk);

View File

@ -1,8 +1,12 @@
SELECT
t.id,
t.shipped,
t.nickname
FROM invoiceOut io
JOIN ticket t ON t.refFk = io.ref
t.nickname,
tto.description
FROM invoiceOut io
JOIN ticket t ON t.refFk = io.REF
LEFT JOIN observationType ot ON ot.code = 'invoiceOut'
LEFT JOIN ticketObservation tto ON tto.ticketFk = t.id
AND tto.observationTypeFk = ot.id
WHERE t.refFk = ?
ORDER BY t.shipped
ORDER BY t.shipped