5244-component_workerAutocomplete #1679

Merged
vicent merged 33 commits from 5244-component_workerAutocomplete into dev 2023-08-25 08:56:46 +00:00
130 changed files with 12833 additions and 15037 deletions
Showing only changes of commit 1e759119db - Show all commits

View File

@ -5,14 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2326.01] - 2023-06-29
### Added
### Changed
### Fixed
-
## [2324.01] - 2023-06-08 ## [2324.01] - 2023-06-08
### Added ### Added
- - (Tickets -> Abono) Al abonar permite crear el ticket abono con almacén o sin almmacén
- (General -> Desplegables) Mejorada eficiencia de carga de datos
### Changed ### Changed
- - (General -> Permisos) Mejorada seguridad
### Fixed ### Fixed
- -
@ -24,13 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- (Tickets -> Crear Factura) Al facturar se envia automáticamente el pdf al cliente - (Tickets -> Crear Factura) Al facturar se envia automáticamente el pdf al cliente
- (Artículos -> Histórico) Filtro para mostrar lo anterior al inventario - (Artículos -> Histórico) Filtro para mostrar lo anterior al inventario
- (Trabajadores -> Nuevo trabajador) Permite elegir el método de pago
### Changed ### Changed
- (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador' - (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador'
### Fixed ### Fixed
- (Tickets -> Líneas) Se permite hacer split de líneas al mismo ticket - (Tickets -> Líneas) Se permite hacer split de líneas al mismo ticket
- (Tickets -> Cambiar estado) Ahora muestra la lista completa de todos los estados

View File

@ -11,9 +11,9 @@ RUN apt-get update \
ca-certificates \ ca-certificates \
gnupg2 \ gnupg2 \
graphicsmagick \ graphicsmagick \
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \ && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \ && apt-get install -y --no-install-recommends nodejs \
&& npm install -g npm@8.19.2 && npm install -g npm@9.6.6
# Puppeteer # Puppeteer

8
Jenkinsfile vendored
View File

@ -39,7 +39,7 @@ pipeline {
NODE_ENV = "" NODE_ENV = ""
} }
steps { steps {
nodejs('node-v14') { nodejs('node-v20') {
sh 'npm install --no-audit --prefer-offline' sh 'npm install --no-audit --prefer-offline'
sh 'gulp install --ci' sh 'gulp install --ci'
} }
@ -57,14 +57,14 @@ pipeline {
parallel { parallel {
stage('Frontend') { stage('Frontend') {
steps { steps {
nodejs('node-v14') { nodejs('node-v20') {
sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2' sh 'jest --ci --reporters=default --reporters=jest-junit --maxWorkers=2'
} }
} }
} }
stage('Backend') { stage('Backend') {
steps { steps {
nodejs('node-v14') { nodejs('node-v20') {
sh 'npm run test:back:ci' sh 'npm run test:back:ci'
} }
} }
@ -80,7 +80,7 @@ pipeline {
CREDENTIALS = credentials('docker-registry') CREDENTIALS = credentials('docker-registry')
} }
steps { steps {
nodejs('node-v14') { nodejs('node-v20') {
sh 'gulp build' sh 'gulp build'
} }

View File

@ -1,9 +1,9 @@
LOAD DATA LOCAL INFILE ? LOAD DATA LOCAL INFILE ?
INTO TABLE `edi`.`item` INTO TABLE `edi`.`item`
CHARACTER SET ascii
FIELDS TERMINATED BY ';' FIELDS TERMINATED BY ';'
LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12) LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12)
SET SET id = @col2,
id = @col2,
product_name = @col4, product_name = @col4,
name = @col5, name = @col5,
plant_id = @col7, plant_id = @col7,
@ -11,3 +11,4 @@ LOAD DATA LOCAL INFILE ?
entry_date = STR_TO_DATE(@col10, '%Y%m%d'), entry_date = STR_TO_DATE(@col10, '%Y%m%d'),
expiry_date = IFNULL(NULL,STR_TO_DATE(@col11, '%Y%m%d')), expiry_date = IFNULL(NULL,STR_TO_DATE(@col11, '%Y%m%d')),
change_date_time = STR_TO_DATE(@col12, '%Y%m%d%H%i') change_date_time = STR_TO_DATE(@col12, '%Y%m%d%H%i')

View File

@ -3,236 +3,237 @@ const path = require('path');
const fs = require('fs-extra'); const fs = require('fs-extra');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateData', { Self.remoteMethodCtx('updateData', {
description: 'Updates schema data from external provider', description: 'Updates schema data from external provider',
accessType: 'WRITE', accessType: 'WRITE',
returns: { returns: {
type: 'object', type: 'object',
root: true root: true
}, },
http: { http: {
path: `/updateData`, path: `/updateData`,
verb: 'POST' verb: 'POST'
} }
}); });
Self.updateData = async() => { Self.updateData = async() => {
const models = Self.app.models; const models = Self.app.models;
// Get files checksum // Get files checksum
const tx = await Self.beginTransaction({}); const tx = await Self.beginTransaction({});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options); const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
const updatableFiles = []; const updatableFiles = [];
for (const file of files) { for (const file of files) {
const fileChecksum = await getChecksum(file); const fileChecksum = await getChecksum(file);
if (file.checksum != fileChecksum) { if (file.checksum != fileChecksum) {
updatableFiles.push({ updatableFiles.push({
name: file.name, name: file.name,
checksum: fileChecksum checksum: fileChecksum
}); });
} else } else
console.debug(`File already updated, skipping...`); console.debug(`File already updated, skipping...`);
} }
if (updatableFiles.length === 0) if (updatableFiles.length === 0)
return false; return false;
// Download files // Download files
const container = await models.TempContainer.container('edi'); const container = await models.TempContainer.container('edi');
const tempPath = path.join(container.client.root, container.name); const tempPath = path.join(container.client.root, container.name);
let remoteFile; let remoteFile;
let tempDir; let tempDir;
let tempFile; let tempFile;
const fileNames = updatableFiles.map(file => file.name); const fileNames = updatableFiles.map(file => file.name);
const tables = await Self.rawSql(` const tables = await Self.rawSql(`
SELECT fileName, toTable, file SELECT fileName, toTable, file
FROM edi.tableConfig FROM edi.tableConfig
WHERE file IN (?)`, [fileNames], options); WHERE file IN (?)`, [fileNames], options);
for (const table of tables) { for (const table of tables) {
const fileName = table.file; const fileName = table.file;
remoteFile = `codes/${fileName}.ZIP`; remoteFile = `codes/${fileName}.ZIP`;
tempDir = `${tempPath}/${fileName}`; tempDir = `${tempPath}/${fileName}`;
tempFile = `${tempPath}/${fileName}.zip`; tempFile = `${tempPath}/${fileName}.zip`;
try { try {
await fs.readFile(tempFile); await fs.readFile(tempFile);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error.code === 'ENOENT') {
console.debug(`Downloading file ${fileName}...`); console.debug(`Downloading file ${fileName}...`);
const downloadOutput = await downloadFile(remoteFile, tempFile); const downloadOutput = await downloadFile(remoteFile, tempFile);
if (downloadOutput.error) if (downloadOutput.error)
continue; continue;
} }
} }
await extractFile(fileName, tempFile, tempDir); await extractFile(fileName, tempFile, tempDir);
console.debug(`Updating table ${table.toTable}...`); console.debug(`Updating table ${table.toTable}...`);
await dumpData(tempDir, table, options); await dumpData(tempDir, table, options);
} }
// Update files checksum // Update files checksum
for (const file of updatableFiles) { for (const file of updatableFiles) {
console.log(`Updating file ${file.name} checksum...`); console.log(`Updating file ${file.name} checksum...`);
await Self.rawSql(` await Self.rawSql(`
UPDATE edi.fileConfig UPDATE edi.fileConfig
SET checksum = ? SET checksum = ?
WHERE name = ?`, WHERE name = ?`,
[file.checksum, file.name], options); [file.checksum, file.name], options);
} }
await tx.commit(); await tx.commit();
// Clean files // Clean files
try { try {
console.debug(`Cleaning files...`); console.debug(`Cleaning files...`);
await fs.remove(tempPath); await fs.remove(tempPath);
} catch (error) { } catch (error) {
if (error.code !== 'ENOENT') if (error.code !== 'ENOENT')
throw e; throw e;
} }
return true; return true;
} catch (error) { } catch (error) {
await tx.rollback(); await tx.rollback();
throw error; throw error;
} }
}; };
let ftpClient; let ftpClient;
async function getFtpClient() { async function getFtpClient() {
if (!ftpClient) { if (!ftpClient) {
const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig'); const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig');
console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`); console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`);
const FtpClient = require('ftps'); const FtpClient = require('ftps');
ftpClient = new FtpClient({ ftpClient = new FtpClient({
host: ftpConfig.host, host: ftpConfig.host,
username: ftpConfig.user, username: ftpConfig.user,
password: ftpConfig.password, password: ftpConfig.password,
procotol: 'ftp' procotol: 'ftp',
}); additionalLftpCommands: 'set ssl:verify-certificate no'
} });
}
return ftpClient; return ftpClient;
} }
async function getChecksum(file) { async function getChecksum(file) {
const ftpClient = await getFtpClient(); const ftpClient = await getFtpClient();
console.debug(`Checking checksum for file ${file.name}...`); console.debug(`Checking checksum for file ${file.name}...`);
ftpClient.cat(`codes/${file.name}.txt`); ftpClient.cat(`codes/${file.name}.TXT`);
const response = await new Promise((resolve, reject) => { const response = await new Promise((resolve, reject) => {
ftpClient.exec((err, response) => { ftpClient.exec((err, response) => {
if (err || response.error) { if (err || response.error) {
console.debug(`Error downloading checksum file... ${response.error}`); console.debug(`Error downloading checksum file... ${response.error}`);
return reject(err); return reject(err);
} }
resolve(response); resolve(response);
}); });
}); });
if (response && response.data) { if (response && response.data) {
const fileContents = response.data; const fileContents = response.data;
const rows = fileContents.split('\n'); const rows = fileContents.split('\n');
const row = rows[4]; const row = rows[4];
const columns = row.split(/\s+/); const columns = row.split(/\s+/);
let fileChecksum; let fileChecksum;
if (file.keyValue) if (file.keyValue)
fileChecksum = columns[1]; fileChecksum = columns[1];
if (!file.keyValue) if (!file.keyValue)
fileChecksum = columns[0]; fileChecksum = columns[0];
return fileChecksum; return fileChecksum;
} }
} }
async function downloadFile(remoteFile, tempFile) { async function downloadFile(remoteFile, tempFile) {
const ftpClient = await getFtpClient(); const ftpClient = await getFtpClient();
ftpClient.get(remoteFile, tempFile); ftpClient.get(remoteFile, tempFile);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ftpClient.exec((err, response) => { ftpClient.exec((err, response) => {
if (err || response.error) { if (err || response.error) {
console.debug(`Error downloading file... ${response.error}`); console.debug(`Error downloading file... ${response.error}`);
return reject(err); return reject(err);
} }
resolve(response); resolve(response);
}); });
}); });
} }
async function extractFile(fileName, tempFile, tempDir) { async function extractFile(fileName, tempFile, tempDir) {
const JSZip = require('jszip'); const JSZip = require('jszip');
try { try {
await fs.mkdir(tempDir); await fs.mkdir(tempDir);
console.debug(`Extracting file ${fileName}...`); console.debug(`Extracting file ${fileName}...`);
} catch (error) { } catch (error) {
if (error.code !== 'EEXIST') if (error.code !== 'EEXIST')
throw e; throw e;
} }
const fileStream = await fs.readFile(tempFile); const fileStream = await fs.readFile(tempFile);
if (fileStream) { if (fileStream) {
const zip = new JSZip(); const zip = new JSZip();
const zipContents = await zip.loadAsync(fileStream); const zipContents = await zip.loadAsync(fileStream);
if (!zipContents) return; if (!zipContents) return;
const fileNames = Object.keys(zipContents.files); const fileNames = Object.keys(zipContents.files);
for (const fileName of fileNames) { for (const fileName of fileNames) {
const fileContent = await zip.file(fileName).async('nodebuffer'); const fileContent = await zip.file(fileName).async('nodebuffer');
const dest = path.join(tempDir, fileName); const dest = path.join(tempDir, fileName);
await fs.writeFile(dest, fileContent); await fs.writeFile(dest, fileContent);
} }
} }
} }
async function dumpData(tempDir, table, options) { async function dumpData(tempDir, table, options) {
const toTable = table.toTable; const toTable = table.toTable;
const baseName = table.fileName; const baseName = table.fileName;
console.log(`Emptying table ${toTable}...`); console.log(`Emptying table ${toTable}...`);
const tableName = `edi.${toTable}`; const tableName = `edi.${toTable}`;
await Self.rawSql(`DELETE FROM ??`, [tableName]); await Self.rawSql(`DELETE FROM ??`, [tableName]);
const dirFiles = await fs.readdir(tempDir); const dirFiles = await fs.readdir(tempDir);
const files = dirFiles.filter(file => file.startsWith(baseName)); const files = dirFiles.filter(file => file.startsWith(baseName));
for (const file of files) { for (const file of files) {
console.log(`Dumping data from file ${file}...`); console.log(`Dumping data from file ${file}...`);
const templatePath = path.join(__dirname, `./sql/${toTable}.sql`); const templatePath = path.join(__dirname, `./sql/${toTable}.sql`);
const sqlTemplate = await fs.readFile(templatePath, 'utf8'); const sqlTemplate = await fs.readFile(templatePath, 'utf8');
const filePath = path.join(tempDir, file); const filePath = path.join(tempDir, file);
await Self.rawSql(sqlTemplate, [filePath], options); await Self.rawSql(sqlTemplate, [filePath], options);
await Self.rawSql(` await Self.rawSql(`
UPDATE edi.tableConfig UPDATE edi.tableConfig
SET updated = ? SET updated = ?
WHERE fileName = ? WHERE fileName = ?
`, [Date.vnNew(), baseName], options); `, [Date.vnNew(), baseName], options);
} }
console.log(`Updated table ${toTable}\n`); console.log(`Updated table ${toTable}\n`);
} }
}; };

View File

@ -56,8 +56,6 @@ CREATE TABLE `vn`.`collectionWagonTicket` (
ALTER TABLE `vn`.`wagon` ADD `typeFk` int(11) unsigned NOT NULL; 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 `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`) INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES VALUES
('WagonType', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'), ('WagonType', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
@ -70,3 +68,4 @@ INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `pri
('WagonType', 'createWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'), ('WagonType', 'createWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'deleteWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'), ('WagonType', 'deleteWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'editWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'); ('WagonType', 'editWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`wagon` ADD CONSTRAINT `wagon_type` FOREIGN KEY (`typeFk`) REFERENCES `wagonType` (`id`) ON UPDATE CASCADE;

View File

@ -1,7 +1,7 @@
ALTER TABLE `vn`.`workerConfig` ADD payMethodFk tinyint(3) unsigned NULL; ALTER TABLE `vn`.`workerConfig` ADD payMethodFk tinyint(3) unsigned NULL;
ALTER TABLE `vn`.`workerConfig` ADD CONSTRAINT workerConfig_FK FOREIGN KEY (roleFk) REFERENCES account.`role`(id) ON DELETE RESTRICT ON UPDATE CASCADE; ALTER TABLE `vn`.`workerConfig` ADD CONSTRAINT workerConfig_FK FOREIGN KEY (roleFk) REFERENCES account.`role`(id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE `vn`.`workerConfig` ADD CONSTRAINT workerConfig_FK_1 FOREIGN KEY (payMethodFk) REFERENCES `vn`.`payMethod`(id) ON DELETE SET NULL ON UPDATE CASCADE; ALTER TABLE `vn`.`workerConfig` ADD CONSTRAINT workerConfig_FK_1 FOREIGN KEY (payMethodFk) REFERENCES `vn`.`payMethod`(id) ON DELETE SET NULL ON UPDATE CASCADE;
-- Cuando se apruebe el PR quitar y poner en redmine para hacerse manualmente
UPDATE `vn`.`workerConfig` UPDATE `vn`.`workerConfig`
SET payMethodFk = 4 SET payMethodFk = 4
WHERE id=1; WHERE id=1;

View File

@ -0,0 +1,127 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canAdvance`(vDateFuture DATE, vDateToAdvance DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve los tickets y la cantidad de lineas de venta que se pueden adelantar.
*
* @param vDateFuture Fecha de los tickets que se quieren adelantar.
* @param vDateToAdvance Fecha a cuando se quiere adelantar.
* @param vWarehouseFk Almacén
*/
DECLARE vDateInventory DATE;
SELECT inventoried INTO vDateInventory FROM config;
DROP TEMPORARY TABLE IF EXISTS tmp.stock;
CREATE TEMPORARY TABLE tmp.stock
(itemFk INT PRIMARY KEY,
amount INT)
ENGINE = MEMORY;
INSERT INTO tmp.stock(itemFk, amount)
SELECT itemFk, SUM(quantity) amount FROM
(
SELECT itemFk, quantity
FROM itemTicketOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryIn
WHERE landed >= vDateInventory
AND landed < vDateFuture
AND isVirtualStock = FALSE
AND warehouseInFk = vWarehouseFk
UNION ALL
SELECT itemFk, quantity
FROM itemEntryOut
WHERE shipped >= vDateInventory
AND shipped < vDateFuture
AND warehouseOutFk = vWarehouseFk
) t
GROUP BY itemFk HAVING amount != 0;
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT
origin.ticketFk futureId,
dest.ticketFk id,
dest.state,
origin.futureState,
origin.futureIpt,
dest.ipt,
origin.workerFk,
origin.futureLiters,
origin.futureLines,
dest.shipped,
origin.shipped futureShipped,
dest.totalWithVat,
origin.totalWithVat futureTotalWithVat,
dest.agency,
origin.futureAgency,
dest.lines,
dest.liters,
origin.futureLines - origin.hasStock AS notMovableLines,
(origin.futureLines = origin.hasStock) AS isFullMovable,
origin.classColor futureClassColor,
dest.classColor
FROM (
SELECT
s.ticketFk,
t.workerFk,
t.shipped,
t.totalWithVat,
st.name futureState,
t.addressFk,
am.name futureAgency,
count(s.id) futureLines,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) futureIpt,
CAST(SUM(litros) AS DECIMAL(10,0)) futureLiters,
SUM((s.quantity <= IFNULL(st.amount,0))) hasStock,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
LEFT JOIN tmp.stock st ON st.itemFk = i.id
WHERE t.shipped BETWEEN vDateFuture AND util.dayend(vDateFuture)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) origin
JOIN (
SELECT
t.id ticketFk,
t.addressFk,
st.name state,
GROUP_CONCAT(DISTINCT ipt.code ORDER BY ipt.code) ipt,
t.shipped,
t.totalWithVat,
am.name agency,
CAST(SUM(litros) AS DECIMAL(10,0)) liters,
CAST(COUNT(*) AS DECIMAL(10,0)) `lines`,
st.classColor
FROM ticket t
JOIN sale s ON s.ticketFk = t.id
JOIN saleVolume sv ON sv.saleFk = s.id
JOIN item i ON i.id = s.itemFk
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN state st ON st.id = ts.stateFk
JOIN agencyMode am ON t.agencyModeFk = am.id
LEFT JOIN itemPackingType ipt ON ipt.code = i.itemPackingTypeFk
WHERE t.shipped BETWEEN vDateToAdvance AND util.dayend(vDateToAdvance)
AND t.warehouseFk = vWarehouseFk
AND st.order <= 5
GROUP BY t.id
) dest ON dest.addressFk = origin.addressFk
WHERE origin.hasStock != 0;
DROP TEMPORARY TABLE tmp.stock;
END$$
DELIMITER ;

View File

@ -0,0 +1,74 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vWarehouseFk INT)
BEGIN
/**
* Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro
*
* @param vOriginDated Fecha en cuestión
* @param vFutureDated Fecha en el futuro a sondear
* @param vWarehouseFk Identificador de vn.warehouse
*/
DROP TEMPORARY TABLE IF EXISTS tmp.filter;
CREATE TEMPORARY TABLE tmp.filter
(INDEX (id))
SELECT sv.ticketFk id,
sub2.id futureId,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt,
CAST(sum(litros) AS DECIMAL(10,0)) liters,
CAST(count(*) AS DECIMAL(10,0)) `lines`,
st.name state,
sub2.iptd futureIpt,
sub2.state futureState,
t.clientFk,
t.warehouseFk,
ts.alertLevel,
t.shipped,
sub2.shipped futureShipped,
t.workerFk,
st.code stateCode,
sub2.code futureStateCode,
st.classColor,
sub2.classColor futureClassColor
FROM vn.saleVolume sv
JOIN vn.sale s ON s.id = sv.saleFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.ticket t ON t.id = sv.ticketFk
JOIN vn.address a ON a.id = t.addressFk
JOIN vn.province p ON p.id = a.provinceFk
JOIN vn.country c ON c.id = p.countryFk
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.alertLevel al ON al.id = ts.alertLevel
LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id
LEFT JOIN (
SELECT *
FROM (
SELECT
t.addressFk,
t.id,
t.shipped,
st.name state,
st.code,
st.classColor,
GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd
FROM vn.ticket t
JOIN vn.ticketState ts ON ts.ticketFk = t.id
JOIN vn.state st ON st.id = ts.stateFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
WHERE t.shipped BETWEEN vFutureDated
AND util.dayend(vFutureDated)
AND t.warehouseFk = vWarehouseFk
GROUP BY t.id
) sub
GROUP BY sub.addressFk
) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id
WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated)
AND t.warehouseFk = vWarehouseFk
AND al.code = 'FREE'
AND tp.ticketFk IS NULL
GROUP BY sv.ticketFk
HAVING futureId;
END$$
DELIMITER ;

View File

@ -0,0 +1,28 @@
CREATE TABLE `vn`.`buyConfig` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`monthsAgo` int(11) NOT NULL DEFAULT 6 COMMENT 'Meses desde la última compra',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
CREATE TABLE `vn`.`travelConfig` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`warehouseInFk` smallint(6) unsigned NOT NULL DEFAULT 8 COMMENT 'Warehouse de origen',
`warehouseOutFk` smallint(6) unsigned NOT NULL DEFAULT 60 COMMENT 'Warehouse destino',
`agencyFk` int(11) NOT NULL DEFAULT 1378 COMMENT 'Agencia por defecto',
`companyFk` smallint(5) unsigned NOT NULL DEFAULT 442 COMMENT 'Compañía por defecto',
PRIMARY KEY (`id`),
KEY `travelConfig_FK` (`warehouseInFk`),
KEY `travelConfig_FK_1` (`warehouseOutFk`),
KEY `travelConfig_FK_2` (`agencyFk`),
KEY `travelConfig_FK_3` (`companyFk`),
CONSTRAINT `travelConfig_FK` FOREIGN KEY (`warehouseInFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_1` FOREIGN KEY (`warehouseOutFk`) REFERENCES `warehouse` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_2` FOREIGN KEY (`agencyFk`) REFERENCES `agencyMode` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `travelConfig_FK_3` FOREIGN KEY (`companyFk`) REFERENCES `company` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Entry', 'addFromPackaging', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Entry', 'addFromBuy', 'WRITE', 'ALLOW', 'ROLE', 'production'),
('Supplier', 'getItemsPackaging', 'READ', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`sector` DROP COLUMN `printerFk`;
ALTER TABLE `vn`.`sector` ADD COLUMN `mainPrinterFk` tinyint(3) unsigned;
ALTER TABLE `vn`.`sector` ADD CONSTRAINT sector_FK_1 FOREIGN KEY (mainPrinterFk) REFERENCES vn.printer(id) ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`ticket` MODIFY COLUMN warehouseFk smallint(6) unsigned DEFAULT NULL NULL;

View File

View File

@ -179,6 +179,8 @@ INSERT INTO `vn`.`printer` (`id`, `name`, `path`, `isLabeler`, `sectorFk`, `ipAd
(2, 'printer2', 'path2', 1, 1 , NULL), (2, 'printer2', 'path2', 1, 1 , NULL),
(4, 'printer4', 'path4', 0, NULL, '10.1.10.4'); (4, 'printer4', 'path4', 0, NULL, '10.1.10.4');
UPDATE `vn`.`sector` SET mainPrinterFk = 1 WHERE id = 1;
INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`) INSERT INTO `vn`.`worker`(`id`, `code`, `firstName`, `lastName`, `userFk`,`bossFk`, `phone`, `sectorFk`, `labelerFk`)
VALUES VALUES
(1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL), (1106, 'LGN', 'David Charles', 'Haller', 1106, 19, 432978106, NULL, NULL),
@ -905,7 +907,7 @@ INSERT INTO `vn`.`itemFamily`(`code`, `description`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`, INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `expenceFk`,
`comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`, `packingShelve`, `weightByPiece`) `comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`, `packingShelve`, `weightByPiece`)
VALUES VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'VT', 0, NULL, 'V', 0, 15,3), (1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 2000000000, NULL, 0, '1', NULL, 0, 1, 'EMB', 0, NULL, 'V', 0, 15,3),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H', 0, 10,2), (2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 2000000000, NULL, 0, '2', NULL, 0, 2, 'VT', 0, NULL, 'H', 0, 10,2),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL, 0, 5,5), (3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 4751000000, NULL, 0, '3', NULL, 0, 5, 'VT', 0, NULL, NULL, 0, 5,5),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL,NULL), (4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 4751000000, NULL, 0, '4', NULL, 0, 3, 'VT', 0, NULL, NULL, 0, NULL,NULL),
@ -2729,6 +2731,7 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
VALUES VALUES
(1, 'print-email', 'notification fixture one'), (1, 'print-email', 'notification fixture one'),
(2, 'invoice-electronic', 'A electronic invoice has been generated'), (2, 'invoice-electronic', 'A electronic invoice has been generated'),
(3, 'not-main-printer-configured', 'A printer distinct than main has been configured'),
(4, 'supplier-pay-method-update', 'A supplier pay method has been updated'); (4, 'supplier-pay-method-update', 'A supplier pay method has been updated');
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
@ -2886,6 +2889,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
(2, 1, 50, 2), (2, 1, 50, 2),
(3, 1, 0, 3); (3, 1, 0, 3);
INSERT INTO `vn`.`travelConfig` (`id`, `warehouseInFk`, `warehouseOutFk`, `agencyFk`, `companyFk`)
VALUES
(1, 1, 1, 1, 442);
INSERT INTO `vn`.`buyConfig` (`id`, `monthsAgo`)
VALUES
(1, 6);

View File

@ -61950,7 +61950,6 @@ BEGIN
* @vWarehouseFk almacen donde buscar * @vWarehouseFk almacen donde buscar
* @vDate Si la fecha es null, muestra el histórico desde el inventario. Si la fecha no es null, muestra histórico desde la fecha pasada. * @vDate Si la fecha es null, muestra el histórico desde el inventario. Si la fecha no es null, muestra histórico desde la fecha pasada.
*/ */
DECLARE vDateInventory DATETIME; DECLARE vDateInventory DATETIME;
DECLARE vInvCalculated INT; DECLARE vInvCalculated INT;

View File

@ -43,7 +43,7 @@ services:
- node.role == worker - node.role == worker
resources: resources:
limits: limits:
memory: 4G memory: 8G
configs: configs:
datasources: datasources:
external: true external: true

View File

@ -22,7 +22,8 @@ export async function getBrowser() {
env.E2E_SHOW = true; env.E2E_SHOW = true;
} }
const headless = !env.E2E_SHOW; const headless = env.E2E_SHOW ? false : 'new';
const browser = await Puppeteer.launch({ const browser = await Puppeteer.launch({
args, args,
defaultViewport: null, defaultViewport: null,

View File

@ -595,6 +595,8 @@ export default {
moreMenuUpdateDiscount: 'vn-item[name="discount"]', moreMenuUpdateDiscount: 'vn-item[name="discount"]',
moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]', moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]',
moreMenuRefund: 'vn-item[name="refund"]', moreMenuRefund: 'vn-item[name="refund"]',
refundWithWarehouse: 'vn-item[name="refundWithWarehouse"]',
refundWithoutWarehouse: 'vn-item[name="refundWithoutWarehouse"]',
moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input', moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input',
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text', transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable', transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',

View File

@ -64,6 +64,6 @@ describe('SmartTable SearchBar integration', () => {
await page.reload({ await page.reload({
waitUntil: 'networkidle2' waitUntil: 'networkidle2'
}); });
await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '13'); await page.waitForTextInField(selectors.itemFixedPrice.firstItemID, '3');
}); });
}); });

View File

@ -88,7 +88,8 @@ describe('Item fixed prices path', () => {
it('should reload the section and check the created price has the expected ID', async() => { it('should reload the section and check the created price has the expected ID', async() => {
await page.goto(`http://localhost:5000/#!/item/fixed-price`); await page.goto(`http://localhost:5000/#!/item/fixed-price`);
await page.autocompleteSearch($.warehouseFilter, 'Warehouse one');
await page.click($.chip);
const result = await page.waitToGetProperty($.fourthItemID, 'value'); const result = await page.waitToGetProperty($.fourthItemID, 'value');
expect(result).toContain('13'); expect(result).toContain('13');

View File

@ -220,14 +220,25 @@ describe('Ticket Edit sale path', () => {
it('should log in as salesAssistant and navigate to ticket sales', async() => { it('should log in as salesAssistant and navigate to ticket sales', async() => {
await page.loginAndModule('salesAssistant', 'ticket'); await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16'); await page.accessToSearchResult('17');
await page.accessToSection('ticket.card.sale'); await page.accessToSection('ticket.card.sale');
}); });
it('should select the third sale and create a refund', async() => { it('should select the first sale and create a refund with warehouse', async() => {
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund); await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithWarehouse);
await page.waitForSnackbar();
await page.waitForState('ticket.card.sale');
});
it('should select the first sale and create a refund without warehouse', async() => {
await page.accessToSearchResult('18');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuRefund);
await page.waitToClick(selectors.ticketSales.refundWithoutWarehouse);
await page.waitForSnackbar(); await page.waitForSnackbar();
await page.waitForState('ticket.card.sale'); await page.waitForState('ticket.card.sale');
}); });
@ -246,7 +257,6 @@ describe('Ticket Edit sale path', () => {
it('should select the third sale and create a claim of it', async() => { it('should select the third sale and create a claim of it', async() => {
await page.accessToSearchResult('16'); await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale'); await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu); await page.waitToClick(selectors.ticketSales.moreMenu);
await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim); await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim);
@ -316,7 +326,7 @@ describe('Ticket Edit sale path', () => {
it('should confirm the transfered quantity is the correct one', async() => { it('should confirm the transfered quantity is the correct one', async() => {
const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText'); const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText');
expect(result).toContain('10'); expect(result).toContain('20');
}); });
it('should go back to the original ticket sales section', async() => { it('should go back to the original ticket sales section', async() => {

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642' describe('Ticket Future path', () => {
xdescribe('Ticket Future path', () => {
let browser; let browser;
let page; let page;
let httpRequest; let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Future path', () => {
await browser.close(); await browser.close();
}); });
it('should show errors snackbar because of the required data', async() => { it('should search with required data, check three last tickets and move to the future', async() => {
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.warehouseFk); await page.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit); await page.waitToClick(selectors.ticketFuture.submit);
@ -43,69 +42,58 @@ xdescribe('Ticket Future path', () => {
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('originDated is a required argument'); expect(message.text).toContain('originDated is a required argument');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toBeDefined();
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketFuture.ipt, 'H');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('ipt=H');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.ipt);
await page.autocompleteSearch(selectors.ticketFuture.futureIpt, 'H');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('futureIpt=H');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.futureIpt);
await page.autocompleteSearch(selectors.ticketFuture.state, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('state=FREE');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.state);
await page.autocompleteSearch(selectors.ticketFuture.futureState, 'Free');
await page.waitToClick(selectors.ticketFuture.submit);
expect(httpRequest).toContain('futureState=FREE');
await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
await page.clearInput(selectors.ticketFuture.state);
await page.clearInput(selectors.ticketFuture.futureState);
await page.waitToClick(selectors.ticketFuture.submit);
await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
await page.waitToClick(selectors.ticketFuture.multiCheck);
await page.waitToClick(selectors.ticketFuture.firstCheck);
await page.waitToClick(selectors.ticketFuture.moveButton);
await page.waitToClick(selectors.globalItems.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
}); });
// it('should search with the required data', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toBeDefined();
// });
// it('should search with the origin IPT', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketFuture.ipt, 'H');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('ipt=H');
// });
// it('should search with the destination IPT', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.ipt);
// await page.autocompleteSearch(selectors.ticketFuture.futureIpt, 'H');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('futureIpt=H');
// });
// it('should search with the origin grouped state', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.futureIpt);
// await page.autocompleteSearch(selectors.ticketFuture.state, 'Free');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('state=FREE');
// });
// it('should search with the destination grouped state', async() => {
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.state);
// await page.autocompleteSearch(selectors.ticketFuture.futureState, 'Free');
// await page.waitToClick(selectors.ticketFuture.submit);
// expect(httpRequest).toContain('futureState=FREE');
// await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketFuture.futureState);
// await page.waitToClick(selectors.ticketFuture.submit);
// });
// it('should check the three last tickets and move to the future', async() => {
// await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 4);
// await page.waitToClick(selectors.ticketFuture.multiCheck);
// await page.waitToClick(selectors.ticketFuture.firstCheck);
// await page.waitToClick(selectors.ticketFuture.moveButton);
// await page.waitToClick(selectors.globalItems.acceptButton);
// const message = await page.waitForSnackbar();
// expect(message.text).toContain('Tickets moved successfully!');
// });
}); });

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js'; import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642' describe('Ticket Advance path', () => {
xdescribe('Ticket Advance path', () => {
let browser; let browser;
let page; let page;
let httpRequest; let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Advance path', () => {
await browser.close(); await browser.close();
}); });
it('should show errors snackbar because of the required data', async() => { it('should search with the required data, check the first ticket and move to the present', async() => {
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.warehouseFk); await page.clearInput(selectors.ticketAdvance.warehouseFk);
@ -44,45 +43,37 @@ xdescribe('Ticket Advance path', () => {
message = await page.waitForSnackbar(); message = await page.waitForSnackbar();
expect(message.text).toContain('dateFuture is a required argument'); expect(message.text).toContain('dateFuture is a required argument');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toBeDefined();
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketAdvance.futureIpt, 'H');
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toContain('futureIpt=H');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.futureIpt);
await page.waitToClick(selectors.ticketAdvance.submit);
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.ticketAdvance.ipt, 'H');
await page.waitToClick(selectors.ticketAdvance.submit);
expect(httpRequest).toContain('ipt=H');
await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
await page.clearInput(selectors.ticketAdvance.ipt);
await page.waitToClick(selectors.ticketAdvance.submit);
await page.waitToClick(selectors.ticketAdvance.firstCheck);
await page.waitToClick(selectors.ticketAdvance.moveButton);
await page.waitToClick(selectors.ticketAdvance.acceptButton);
message = await page.waitForSnackbar();
expect(message.text).toContain('Tickets moved successfully!');
}); });
// it('should search with the required data', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toBeDefined();
// });
// it('should search with the origin IPT', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketAdvance.futureIpt, 'H');
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toContain('futureIpt=H');
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketAdvance.futureIpt);
// await page.waitToClick(selectors.ticketAdvance.submit);
// });
// it('should search with the destination IPT', async() => {
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.autocompleteSearch(selectors.ticketAdvance.ipt, 'H');
// await page.waitToClick(selectors.ticketAdvance.submit);
// expect(httpRequest).toContain('ipt=H');
// await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton);
// await page.clearInput(selectors.ticketAdvance.ipt);
// await page.waitToClick(selectors.ticketAdvance.submit);
// });
// it('should check the first ticket and move to the present', async() => {
// await page.waitToClick(selectors.ticketAdvance.firstCheck);
// await page.waitToClick(selectors.ticketAdvance.moveButton);
// await page.waitToClick(selectors.ticketAdvance.acceptButton);
// const message = await page.waitForSnackbar();
// expect(message.text).toContain('Tickets moved successfully!');
// });
}); });

View File

@ -49,7 +49,11 @@ describe('Claim summary path', () => {
}); });
it(`should click on the first sale ID making the item descriptor visible`, async() => { it(`should click on the first sale ID making the item descriptor visible`, async() => {
await page.waitToClick(selectors.claimSummary.firstSaleItemId); const firstItem = selectors.claimSummary.firstSaleItemId;
await page.evaluate(selectors => {
document.querySelector(selectors).scrollIntoView();
}, firstItem);
await page.click(firstItem);
await page.waitImgLoad(selectors.claimSummary.firstSaleDescriptorImage); await page.waitImgLoad(selectors.claimSummary.firstSaleDescriptorImage);
const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover); const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover);

View File

@ -53,12 +53,10 @@
</span> </span>
</div> </div>
</div> </div>
<div <div class="model vn-pb-sm vn-px-sm"
class="model vn-pb-sm vn-px-sm"
title="{{::log.changedModelValue}}"
ng-if="::log.changedModelId || log.changedModelValue"> ng-if="::log.changedModelId || log.changedModelValue">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span> <span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<span class="model-value">{{::log.changedModelValue}}</span> <span class="model-value" title="{{::log.changedModelValue}}">{{::log.changedModelValue}}</span>
</div> </div>
<div class="changes vn-pa-sm" <div class="changes vn-pa-sm"
ng-class="{expanded: log.expand}" ng-class="{expanded: log.expand}"
@ -75,16 +73,16 @@
<span class="json-field" title="{{::prop.name}}"> <span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}: {{::prop.nameI18n}}:
</span> </span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value><span ng-if="::!$last">,</span> <vn-json-value value="::prop.val.val"></vn-json-value><span ng-if="::!$last">,</span>
</span> </span>
<div ng-if="log.expand" class="expanded-json"> <div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props"> <div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}"> <span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}: {{::prop.nameI18n}}:
</span> </span>
<vn-json-value value="::$ctrl.mainVal(prop, log.action)"></vn-json-value> <vn-log-value val="::prop.val"></vn-log-value>
<span ng-if="::log.action == 'update'"> <span ng-if="::log.action == 'update'">
<vn-json-value value="::prop.old"></vn-json-value> <vn-log-value val="::prop.old"></vn-log-value>
</span> </span>
</div> </div>
</div> </div>
@ -163,12 +161,17 @@
data="$ctrl.models" data="$ctrl.models"
class="changed-model"> class="changed-model">
</vn-autocomplete> </vn-autocomplete>
<!-- FIXME: Cannot use LIKE with JSON columns
<vn-textfield <vn-textfield
label="Changes" label="Changes"
ng-model="filter.changes"> ng-model="filter.changes">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by changes"
pointer>
</vn-icon>
</append>
</vn-textfield> </vn-textfield>
-->
<vn-vertical> <vn-vertical>
<vn-check <vn-check
label="Creates" label="Creates"

View File

@ -64,29 +64,47 @@ export default class Controller extends Section {
set logs(value) { set logs(value) {
this._logs = value; this._logs = value;
if (!value) return; if (!value) return;
const empty = {}; const empty = {};
const validations = window.validations; const validations = window.validations;
const castJsonValue = this.castJsonValue;
for (const log of value) { for (const log of value) {
const oldValues = log.oldInstance || empty; const notDelete = log.action != 'delete';
const newValues = log.newInstance || empty; const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
const locale = validations[log.changedModel]?.locale || empty; const locale = validations[log.changedModel]?.locale || empty;
log.changedModelI18n = firstUpper(locale.name) || log.changedModel; log.changedModelI18n = firstUpper(locale.name) || log.changedModel;
let props = Object.keys(oldValues).concat(Object.keys(newValues)); let props = Object.keys(olds).concat(Object.keys(vals));
props = [...new Set(props)]; props = [...new Set(props)];
log.props = []; log.props = [];
for (const prop of props) { for (const prop of props) {
if (prop.endsWith('$')) continue;
log.props.push({ log.props.push({
name: prop, name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop, nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: this.castJsonValue(oldValues[prop]), old: getVal(olds, prop),
new: this.castJsonValue(newValues[prop]) val: getVal(vals, prop)
}); });
} }
log.props.sort( log.props.sort(
(a, b) => a.nameI18n.localeCompare(b.nameI18n)); (a, b) => a.nameI18n.localeCompare(b.nameI18n));
} }
function getVal(vals, prop) {
let val, id;
const showProp = `${prop}$`;
if (vals[showProp] != null) {
val = vals[showProp];
id = vals[prop];
} else
val = vals[prop];
return {val: castJsonValue(val), id};
}
} }
get models() { get models() {
@ -113,10 +131,6 @@ export default class Controller extends Section {
: value; : value;
} }
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
relativeDate(dateVal) { relativeDate(dateVal) {
if (dateVal == null) return ''; if (dateVal == null) return '';
const date = new Date(dateVal); const date = new Date(dateVal);
@ -150,14 +164,16 @@ export default class Controller extends Section {
if (value == null || value == '') return null; if (value == null || value == '') return null;
switch (prop) { switch (prop) {
case 'search': case 'search':
const or = [{changedModelId: value}]; const or = [];
if (/^[\w_-]+$/.test(value))
or.push({changedModelId: value});
if (!/^[0-9]+$/.test(value)) if (!/^[0-9]+$/.test(value))
or.push({changedModelValue: {like: `%${value}%`}}); or.push({changedModelValue: {like: `%${value}%`}});
return {or}; return or.length ? {or} : null;
case 'changes': case 'changes':
return {or: [ return {or: [
{oldInstance: {like: `%${value}%`}}, {oldJson: {like: `%${value}%`}},
{newInstance: {like: `%${value}%`}}, {newJson: {like: `%${value}%`}},
{description: {like: `%${value}%`}} {description: {like: `%${value}%`}}
]}; ]};
case 'who': case 'who':
@ -238,3 +254,12 @@ ngModule.vnComponent('vnLog', {
url: '@' url: '@'
} }
}); });
ngModule.component('vnLogValue', {
template:
'<vn-json-value value="::$ctrl.val.val"></vn-json-value>' +
'<span ng-if="::$ctrl.val.id" class="id-value"> #{{::$ctrl.val.id}}</span>',
bindings: {
val: '<?',
}
});

View File

@ -2,6 +2,9 @@ Date: Fecha
Concept: Concepto Concept: Concepto
Search: Buscar Search: Buscar
Search by id or concept: Buscar por identificador o concepto Search by id or concept: Buscar por identificador o concepto
Search by changes: |
Buscar por cambios realizados. Los atributos deben buscarse por su nombre
interno, para obtenerlo situar el cursor sobre el nombre.
Entity: Entidad Entity: Entidad
Action: Acción Action: Acción
Author: Autor Author: Autor

View File

@ -105,6 +105,7 @@ vn-log {
& > .model-id { & > .model-id {
color: $color-font-secondary; color: $color-font-secondary;
font-size: .9rem; font-size: .9rem;
float: right;
} }
} }
} }
@ -144,3 +145,7 @@ vn-log {
} }
} }
} }
vn-log-value > .id-value {
font-size: .9rem;
color: $color-font-secondary;
}

View File

@ -20,8 +20,6 @@ class Controller {
name: config.languages[code] ? config.languages[code] : code name: config.languages[code] ? config.languages[code] : code
}); });
} }
vnConfig.initialize();
} }
set lang(value) { set lang(value) {

View File

@ -10,6 +10,9 @@ function config($stateProvider, $urlRouterProvider) {
.state('layout', { .state('layout', {
abstract: true, abstract: true,
template: '<vn-layout></vn-layout>', template: '<vn-layout></vn-layout>',
resolve: {
config: ['vnConfig', vnConfig => vnConfig.initialize()]
}
}) })
.state('outLayout', { .state('outLayout', {
abstract: true, abstract: true,

View File

@ -1,4 +1,61 @@
{ {
"name": "Log", "name": "Log",
"base": "VnModel" "base": "VnModel",
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"oldJson": {
"type": "String",
"mysql": {"columnName": "oldInstance"}
},
"newJson": {
"type": "String",
"mysql": {"columnName": "newInstance"}
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
} }

View File

@ -1,6 +1,8 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const utils = require('loopback/lib/utils');
const {util} = require('webpack');
module.exports = function(Self) { module.exports = function(Self) {
Self.ParameterizedSQL = ParameterizedSQL; Self.ParameterizedSQL = ParameterizedSQL;
@ -164,23 +166,21 @@ module.exports = function(Self) {
function rewriteMethod(methodName) { function rewriteMethod(methodName) {
const realMethod = this[methodName]; const realMethod = this[methodName];
return async(data, options, cb) => { return function(...args) {
if (options instanceof Function) { let cb;
cb = options; const lastArg = args[args.length - 1];
options = null; if (lastArg instanceof Function) {
} cb = lastArg;
args.pop();
} else
cb = utils.createPromiseCallback();
try { args.push(function(err, res) {
const result = await realMethod.call(this, data, options); if (err) err = replaceErr(err, replaceErrFunc);
cb(err, res);
if (cb) cb(null, result); });
else return result; realMethod.apply(this, args);
} catch (err) { return cb.promise;
let myErr = replaceErr(err, replaceErrFunc);
if (cb) cb(myErr);
else
throw myErr;
}
}; };
} }

View File

@ -1,4 +1,5 @@
name: subrole name: subrole
columns: columns:
id: id
role: rol role: rol
inheritsFrom: inherits inheritsFrom: inherits

View File

@ -1,4 +1,5 @@
name: subrol name: subrol
columns: columns:
id: id
role: rol role: rol
inheritsFrom: hereda inheritsFrom: hereda

View File

@ -15,3 +15,7 @@ columns:
image: image image: image
hasGrant: has grant hasGrant: has grant
userFk: user userFk: user
recoverPass: recover password
role: role
sync: pending sync
lastPassChange: password changed

View File

@ -15,3 +15,7 @@ columns:
image: imagen image: imagen
hasGrant: puede delegar hasGrant: puede delegar
userFk: usuario userFk: usuario
recoverPass: recuperar contraseña
role: rol
sync: Pendiente de sincronizar
lastPassChange: contraseña modificada

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "account.roleLog" "table": "account.roleLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "account.userLog" "table": "account.userLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "claimLog" "table": "claimLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: sender
sender: sender number
destination: destination
message: message
statusCode: status code
status: status
created: created

View File

@ -0,0 +1,10 @@
name: SMS
columns:
id: id
senderFk: remitente
sender: número remitente
destination: destinatario
message: mensaje
statusCode: código estado
status: estado
created: creado

View File

@ -59,6 +59,12 @@ module.exports = function(Self) {
fields: ['id', 'name'] fields: ['id', 'name']
} }
}, },
{
relation: 'businessType',
scope: {
fields: ['description']
}
},
{ {
relation: 'account', relation: 'account',
scope: { scope: {

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "clientLog" "table": "clientLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -3,7 +3,7 @@
"base": "VnModel", "base": "VnModel",
"options": { "options": {
"mysql": { "mysql": {
"table": "payMethod" "table": "payMethod"
} }
}, },
"properties": { "properties": {

View File

@ -43,6 +43,10 @@
{{$ctrl.client.salesPersonUser.name}} {{$ctrl.client.salesPersonUser.name}}
</span> </span>
</vn-label-value> </vn-label-value>
<vn-label-value
label="Business type"
value="{{$ctrl.client.businessType.description}}">
</vn-label-value>
</div> </div>
<div class="icons"> <div class="icons">
<vn-icon <vn-icon

View File

@ -8,3 +8,4 @@ Client invoices list: Listado de facturas del cliente
Pay method: Forma de pago Pay method: Forma de pago
Unpaid Dated: "Fecha: {{dated | date:'dd/MM/yyyy'}}" Unpaid Dated: "Fecha: {{dated | date:'dd/MM/yyyy'}}"
Unpaid Amount: "Importe: {{amount | currency: 'EUR':2}}" Unpaid Amount: "Importe: {{amount | currency: 'EUR':2}}"
Business type: Tipo de negocio

View File

@ -0,0 +1,107 @@
module.exports = Self => {
Self.remoteMethodCtx('addFromBuy', {
description: 'Modify a field of a buy or creates a new one with default values',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The entry id',
http: {source: 'path'}
}, {
arg: 'item',
type: 'number',
required: true,
description: 'The item id',
}, {
arg: 'printedStickers',
type: 'number',
required: true,
description: 'The field to modify',
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/addFromBuy`,
verb: 'POST'
}
});
Self.addFromBuy = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {userId};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
let buy = await models.Buy.findOne({where: {entryFk: args.id}}, myOptions);
if (buy)
await buy.updateAttribute('printedStickers', args.printedStickers, myOptions);
else {
const userConfig = await models.UserConfig.findById(userId, {fields: ['warehouseFk']}, myOptions);
await Self.rawSql(
'CALL vn.buyUltimate(?,?)',
[userConfig.warehouseFk, null],
myOptions
);
let buyUltimate = await Self.rawSql(
`SELECT buyFk
FROM tmp.buyUltimate
WHERE itemFk = ?`,
[args.item],
myOptions
);
buyUltimate = await models.Buy.findById(buyUltimate[0].buyFk, null, myOptions);
buy = await models.Buy.create({
entryFk: args.id,
itemFk: args.item,
quantity: 0,
dispatched: buyUltimate.dispatched,
buyingValue: buyUltimate.buyingValue,
freightValue: buyUltimate.freightValue,
isIgnored: buyUltimate.isIgnored,
stickers: buyUltimate.stickers,
packing: buyUltimate.packing,
grouping: buyUltimate.grouping,
groupingMode: buyUltimate.groupingMode,
containerFk: buyUltimate.containerFk,
comissionValue: buyUltimate.comissionValue,
packageValue: buyUltimate.packageValue,
location: buyUltimate.location,
packageFk: buyUltimate.packageFk,
price1: buyUltimate.price1,
price2: buyUltimate.price2,
price3: buyUltimate.price3,
minPrice: buyUltimate.minPrice,
printedStickers: args.printedStickers,
workerFk: buyUltimate.workerFk,
isChecked: buyUltimate.isChecked,
isPickedOff: buyUltimate.isPickedOff,
created: buyUltimate.created,
ektFk: buyUltimate.ektFk,
weight: buyUltimate.weight,
deliveryFk: buyUltimate.deliveryFk,
itemOriginalFk: buyUltimate.itemOriginalFk
}, myOptions);
}
if (tx) await tx.commit();
return buy;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,72 @@
module.exports = Self => {
Self.remoteMethodCtx('addFromPackaging', {
description: 'Create a receipt or return entry for a supplier with a specific travel',
accessType: 'WRITE',
accepts: [{
arg: 'supplier',
type: 'number',
required: true,
description: 'The supplier id',
},
{
arg: 'isTravelReception',
type: 'boolean',
required: true,
description: 'Indicates if the travel associated with the entry is a return or receipt travel'
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/addFromPackaging`,
verb: 'POST'
}
});
Self.addFromPackaging = async(ctx, options) => {
const args = ctx.args;
const models = Self.app.models;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const travelConfig = await models.TravelConfig.findOne({}, myOptions);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const travel = await models.Travel.create({
shipped: args.isTravelReception ? yesterday : today,
landed: args.isTravelReception ? today : tomorrow,
agencyModeFk: travelConfig.agencyFk,
warehouseInFk: travelConfig.warehouseOutFk,
warehouseOutFk: travelConfig.warehouseInFk
}, myOptions);
const entry = await models.Entry.create({
supplierFk: args.supplier,
travelFk: travel.id,
companyFk: travelConfig.companyFk
}, myOptions);
if (tx) await tx.commit();
return entry;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,51 @@
const models = require('vn-loopback/server/server').models;
describe('entry addFromBuy()', () => {
const ctx = {req: {accessToken: {userId: 18}}};
it('should change the printedStickers of an existent buy', async() => {
const id = 1;
const item = 1;
const buy = 1;
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const currentBuy = await models.Buy.findById(buy, {fields: ['printedStickers']}, options);
const printedStickers = currentBuy.printedStickers + 10;
ctx.args = {id, item, printedStickers};
const newBuy = await models.Entry.addFromBuy(ctx, options);
expect(newBuy.printedStickers).toEqual(printedStickers);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should create for an entry without a concrete item a new buy', async() => {
const id = 8;
const item = 1;
const printedStickers = 10;
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const emptyBuy = await models.Buy.findOne({where: {entryFk: id}}, options);
ctx.args = {id, item, printedStickers};
const newBuy = await models.Entry.addFromBuy(ctx, options);
expect(emptyBuy).toEqual(null);
expect(newBuy.entryFk).toEqual(id);
expect(newBuy.printedStickers).toEqual(printedStickers);
expect(newBuy.itemFk).toEqual(item);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,49 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('entry addFromPackaging()', () => {
const supplier = 442;
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 49},
http: {
req: {
headers: {origin: 'http://localhost'},
},
},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx,
});
});
it('should create an incoming travel', async() => {
const ctx = {args: {isTravelReception: true, supplier}};
const tx = await models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const entry = await models.Entry.addFromPackaging(ctx, options);
const travelConfig = await models.TravelConfig.findOne({}, options);
const travel = await models.Travel.findOne({order: 'id DESC'}, options);
expect(new Date(travel.shipped).getDate()).toEqual(yesterday.getDate());
expect(new Date(travel.landed).getDate()).toEqual(today.getDate());
expect(travel.agencyModeFk).toEqual(travelConfig.agencyFk);
expect(travel.warehouseInFk).toEqual(travelConfig.warehouseOutFk);
expect(travel.warehouseOutFk).toEqual(travelConfig.warehouseInFk);
expect(entry.supplierFk).toEqual(supplier);
expect(entry.travelFk).toEqual(travel.id);
expect(entry.companyFk).toEqual(travelConfig.companyFk);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -5,6 +5,9 @@
"Buy": { "Buy": {
"dataSource": "vn" "dataSource": "vn"
}, },
"BuyConfig": {
"dataSource": "vn"
},
"ItemMatchProperties": { "ItemMatchProperties": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,18 @@
{
"name": "BuyConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "buyConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"showLastBuy": {
"type": "number"
}
}
}

View File

@ -39,6 +39,9 @@
"packageValue": { "packageValue": {
"type": "number" "type": "number"
}, },
"price1": {
"type": "number"
},
"price2": { "price2": {
"type": "number" "type": "number"
}, },
@ -47,7 +50,44 @@
}, },
"weight": { "weight": {
"type": "number" "type": "number"
},
"printedStickers": {
"type": "number"
},
"dispatched": {
"type": "number"
},
"isIgnored": {
"type": "boolean"
},
"containerFk": {
"type": "number"
},
"location": {
"type": "number"
},
"minPrice": {
"type": "number"
},
"isChecked": {
"type": "boolean"
},
"isPickedOff": {
"type": "boolean"
},
"created": {
"type": "date"
},
"ektFk": {
"type": "number"
},
"itemOriginalFk": {
"type": "number"
},
"editorFk": {
"type": "number"
} }
}, },
"relations": { "relations": {
"entry": { "entry": {
@ -64,6 +104,16 @@
"type": "belongsTo", "type": "belongsTo",
"model": "Packaging", "model": "Packaging",
"foreignKey": "packageFk" "foreignKey": "packageFk"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"delivery": {
"type": "belongsTo",
"model": "Delivery",
"foreignKey": "deliveryFk"
} }
} }
} }

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "entryLog" "table": "entryLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

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

View File

@ -5,57 +5,5 @@
"mysql": { "mysql": {
"table": "invoiceInLog" "table": "invoiceInLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": [
"creationDate DESC",
"id DESC"
]
} }
} }

View File

@ -2,11 +2,19 @@ module.exports = Self => {
Self.remoteMethod('refund', { Self.remoteMethod('refund', {
description: 'Create refund tickets with sales and services if provided', description: 'Create refund tickets with sales and services if provided',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [
arg: 'ref', {
type: 'string', arg: 'ref',
description: 'The invoice reference' type: 'string',
}], description: 'The invoice reference',
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: { returns: {
type: ['number'], type: ['number'],
root: true root: true
@ -17,7 +25,7 @@ module.exports = Self => {
} }
}); });
Self.refund = async(ref, options) => { Self.refund = async(ref, withWarehouse, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -35,7 +43,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions); const tickets = await models.Ticket.find(filter, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id); const ticketsIds = tickets.map(ticket => ticket.id);
const refundedTickets = await models.Ticket.refund(ticketsIds, myOptions); const refundedTickets = await models.Ticket.refund(ticketsIds, withWarehouse, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -3,6 +3,7 @@ const LoopBackContext = require('loopback-context');
describe('InvoiceOut refund()', () => { describe('InvoiceOut refund()', () => {
const userId = 5; const userId = 5;
const withWarehouse = true;
const activeCtx = { const activeCtx = {
accessToken: {userId: userId}, accessToken: {userId: userId},
}; };
@ -15,7 +16,7 @@ describe('InvoiceOut refund()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
try { try {
const result = await models.InvoiceOut.refund('T1111111', options); const result = await models.InvoiceOut.refund('T1111111', withWarehouse, options);
expect(result).toBeDefined(); expect(result).toBeDefined();

View File

@ -76,14 +76,27 @@
translate> translate>
Show CITES letter Show CITES letter
</vn-item> </vn-item>
<vn-item <vn-item class="dropdown"
ng-click="refundConfirmation.show()" vn-click-stop="refundMenu.show($event, 'left')"
name="refundInvoice"
vn-tooltip="Create a single ticket with all the content of the current invoice" vn-tooltip="Create a single ticket with all the content of the current invoice"
vn-acl="invoicing, claimManager, salesAssistant" vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove" vn-acl-action="remove"
translate> translate>
Refund Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refundInvoiceOut(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refundInvoiceOut(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item> </vn-item>
</vn-list> </vn-list>
</vn-menu> </vn-menu>
@ -97,12 +110,7 @@
on-accept="$ctrl.bookInvoiceOut()" on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?"> question="Are you sure you want to book this invoice?">
</vn-confirm> </vn-confirm>
<vn-confirm <vn-client-descriptor-popover
vn-id="refundConfirmation"
on-accept="$ctrl.refundInvoiceOut()"
question="Are you sure you want to refund this invoice?">
</vn-confirm>
<vn-client-descriptor-popover
vn-id="clientDescriptor"> vn-id="clientDescriptor">
</vn-client-descriptor-popover> </vn-client-descriptor-popover>
@ -148,4 +156,4 @@
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/> <input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button> <button response="accept" translate>Confirm</button>
</tpl-buttons> </tpl-buttons>
</vn-dialog> </vn-dialog>

View File

@ -114,9 +114,9 @@ class Controller extends Section {
}); });
} }
refundInvoiceOut() { refundInvoiceOut(withWarehouse) {
const query = 'InvoiceOuts/refund'; const query = 'InvoiceOuts/refund';
const params = {ref: this.invoiceOut.ref}; const params = {ref: this.invoiceOut.ref, withWarehouse: withWarehouse};
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
const refundTicket = res.data; const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {

View File

@ -13,10 +13,11 @@ InvoiceOut deleted: Factura eliminada
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura? Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura?
InvoiceOut booked: Factura asentada InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
Are you sure you want to refund this invoice?: Estas seguro de querer abonar esta factura? Are you sure you want to refund this invoice?: Estas seguro de querer abonar esta factura?
Create a single ticket with all the content of the current invoice: Crear un ticket unico con todo el contenido de la factura actual Create a single ticket with all the content of the current invoice: Crear un ticket unico con todo el contenido de la factura actual
Regenerate PDF invoice: Regenerar PDF factura Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
The email can't be empty: El correo no puede estar vacío The email can't be empty: El correo no puede estar vacío
The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}" The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}"
Refund...: Abono...

View File

@ -1,4 +1,4 @@
name: botanical name: botanical data
columns: columns:
itemFk: item itemFk: item
genusFk: genus genusFk: genus

View File

@ -1,4 +1,4 @@
name: botánico name: datos botánicos
columns: columns:
itemFk: artículo itemFk: artículo
genusFk: género genusFk: género

View File

@ -35,7 +35,7 @@ columns:
packingOut: packing out packingOut: packing out
hasMinPrice: has min price hasMinPrice: has min price
isFragile: fragile isFragile: fragile
isFloramondo: is floramondo isFloramondo: floramondo
packingShelve: packing shelve packingShelve: packing shelve
isLaid: laid isLaid: laid
inkFk: ink inkFk: ink

View File

@ -35,7 +35,7 @@ columns:
packingOut: empaquetar packingOut: empaquetar
hasMinPrice: tiene precio mínimo hasMinPrice: tiene precio mínimo
isFragile: frágil isFragile: frágil
isFloramondo: es floramondo isFloramondo: floramondo
packingShelve: estantería embalaje packingShelve: estantería embalaje
isLaid: puesto isLaid: puesto
inkFk: tinta inkFk: tinta

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "itemLog" "table": "itemLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -23,7 +23,7 @@ class Controller extends SearchPanel {
addValue() { addValue() {
this.filter.values.push({}); this.filter.values.push({});
setTimeout(() => this.popover.relocate()); setTimeout(() => this.parentPopover.relocate());
} }
changeTag() { changeTag() {
@ -36,7 +36,7 @@ ngModule.vnComponent('vnOrderCatalogSearchPanel', {
controller: Controller, controller: Controller,
bindings: { bindings: {
onSubmit: '&?', onSubmit: '&?',
popover: '<?', parentPopover: '<?',
resultTags: '<?' resultTags: '<?'
} }
}); });

View File

@ -18,7 +18,7 @@
</vn-searchbar> </vn-searchbar>
</vn-portal> </vn-portal>
<vn-order-catalog-view <vn-order-catalog-view
model="model" model="model"
order="$ctrl.order"> order="$ctrl.order">
</vn-order-catalog-view> </vn-order-catalog-view>
<vn-side-menu side="right"> <vn-side-menu side="right">
@ -31,7 +31,7 @@
label="Category"> label="Category">
</vn-autocomplete> </vn-autocomplete>
<vn-one ng-repeat="category in categories"> <vn-one ng-repeat="category in categories">
<vn-icon <vn-icon
ng-class="{'active': $ctrl.categoryId == category.id}" ng-class="{'active': $ctrl.categoryId == category.id}"
icon="{{::category.icon}}" icon="{{::category.icon}}"
vn-tooltip="{{::category.name}}" vn-tooltip="{{::category.name}}"
@ -83,7 +83,7 @@
</div> </div>
</vn-vertical> </vn-vertical>
<vn-vertical class="input vn-pt-md"> <vn-vertical class="input vn-pt-md">
<vn-textfield vn-one <vn-textfield vn-one
vn-id="search" vn-id="search"
ng-keyUp="$ctrl.onSearchByTag($event)" ng-keyUp="$ctrl.onSearchByTag($event)"
label="Search tag"> label="Search tag">
@ -104,20 +104,20 @@
on-close="$ctrl.onPopoverClose()"> on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel <vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)" on-submit="$ctrl.onPanelSubmit($filter)"
popover="popover" parent-popover="popover"
result-tags="$ctrl.resultTags"> result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel> </vn-order-catalog-search-panel>
</vn-popover> </vn-popover>
<div class="chips"> <div class="chips">
<vn-chip <vn-chip
ng-if="$ctrl.itemId" ng-if="$ctrl.itemId"
removable="true" removable="true"
vn-tooltip="Item id" vn-tooltip="Item id"
on-remove="$ctrl.removeItemId()" on-remove="$ctrl.removeItemId()"
class="colored"> class="colored">
<span>Id: {{$ctrl.itemId}}</span> <span>Id: {{$ctrl.itemId}}</span>
</vn-chip> </vn-chip>
<vn-chip <vn-chip
ng-if="$ctrl.itemName" ng-if="$ctrl.itemName"
removable="true" removable="true"
vn-tooltip="Item" vn-tooltip="Item"
@ -130,20 +130,20 @@
<span>{{$ctrl.itemName}}</span> <span>{{$ctrl.itemName}}</span>
</div> </div>
</vn-chip> </vn-chip>
<vn-chip <vn-chip
ng-if="category.selection" ng-if="category.selection"
removable="true" removable="true"
vn-tooltip="Category" vn-tooltip="Category"
on-remove="$ctrl.categoryId = null" on-remove="$ctrl.categoryId = null"
class="colored"> class="colored">
<span translate>{{category.selection.name}}</span> <span translate>{{category.selection.name}}</span>
</vn-chip> </vn-chip>
<vn-chip <vn-chip
ng-if="type.selection" ng-if="type.selection"
removable="true" removable="true"
vn-tooltip="Type" vn-tooltip="Type"
on-remove="$ctrl.typeId = null" on-remove="$ctrl.typeId = null"
class="colored"> class="colored">
<span translate>{{type.selection.name}}</span> <span translate>{{type.selection.name}}</span>
</vn-chip> </vn-chip>
<vn-chip <vn-chip
@ -151,7 +151,7 @@
removable="true" removable="true"
on-remove="$ctrl.remove($index)" on-remove="$ctrl.remove($index)"
vn-tooltip="{{::$ctrl.formatTooltip(tagGroup)}}" vn-tooltip="{{::$ctrl.formatTooltip(tagGroup)}}"
class="colored"> class="colored">
<div> <div>
<span ng-if="::tagGroup.tagFk"> <span ng-if="::tagGroup.tagFk">
<span translate>{{::tagGroup.tagSelection.name}}</span>: <span translate>{{::tagGroup.tagSelection.name}}</span>:
@ -163,4 +163,4 @@
</div> </div>
</vn-chip> </vn-chip>
</div> </div>
</vn-side-menu> </vn-side-menu>

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "routeLog" "table": "routeLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -22,7 +22,7 @@
ng-model="filter.agencyModeFk"> ng-model="filter.agencyModeFk">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-one <vn-autocomplete vn-one
url="Agencies" data="agencies"
label="Agency Agreement" label="Agency Agreement"
show-field="name" show-field="name"
value-field="id" value-field="id"

View File

@ -0,0 +1,11 @@
name: shelving
columns:
id: id
code: code
parkingFk: parking
isPrinted: printed
priority: priority
parked: parked
userFk: user
isSpam: SPAM
isRecyclable: recyclable

View File

@ -0,0 +1,11 @@
name: estantería
columns:
id: id
code: código
parkingFk: parking
isPrinted: impreso
priority: prioridad
parked: aparcado
userFk: usuario
isSpam: SPAM
isRecyclable: reciclable

View File

@ -56,7 +56,7 @@
"type": "number", "type": "number",
"required": false "required": false
}, },
"printerFk": { "mainPrinterFk": {
"type": "number", "type": "number",
"required": false "required": false
}, },
@ -69,4 +69,4 @@
"required": true "required": true
} }
} }
} }

View File

@ -1,58 +1,9 @@
{ {
"name": "ShelvingLog", "name": "ShelvingLog",
"base": "Log", "base": "Log",
"options": { "options": {
"mysql": { "mysql": {
"table": "shelvingLog" "table": "shelvingLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
} }
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -0,0 +1,50 @@
module.exports = Self => {
Self.remoteMethod('getItemsPackaging', {
description: 'Returns the list of items from the supplier of type packing',
accessType: 'READ',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The supplier id',
http: {source: 'path'}
}, {
arg: 'entry',
type: 'number',
required: true,
description: 'The entry id',
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getItemsPackaging`,
verb: 'GET'
}
});
Self.getItemsPackaging = async(id, entry) => {
return Self.rawSql(`
WITH entryTmp AS (
SELECT i.id, SUM(b.quantity) quantity
FROM vn.entry e
JOIN vn.buy b ON b.entryFk = e.id
JOIN vn.supplier s ON s.id = e.supplierFk
JOIN vn.item i ON i.id = b.itemFk
WHERE e.id = ? AND e.supplierFk = ?
GROUP BY i.id
) SELECT i.id, i.name, et.quantity, SUM(b.quantity) quantityTotal
FROM vn.buy b
JOIN vn.item i ON i.id = b.itemFk
JOIN vn.entry e ON e.id = b.entryFk
JOIN vn.supplier s ON s.id = e.supplierFk
JOIN vn.buyConfig bc ON bc.monthsAgo
JOIN vn.travel t ON t.id = e.travelFk
LEFT JOIN entryTmp et ON et.id = i.id
WHERE e.supplierFk = ?
AND i.family IN ('EMB', 'CONT')
AND b.created > (util.VN_CURDATE() - INTERVAL bc.monthsAgo MONTH)
GROUP BY b.itemFk
ORDER BY et.quantity DESC, quantityTotal DESC`, [entry, id, id]);
};
};

View File

@ -0,0 +1,12 @@
const app = require('vn-loopback/server/server');
describe('Supplier getItemsPackaging()', () => {
it('should return a summary of the list of items from a specific supplier', async() => {
const [item] = await app.models.Supplier.getItemsPackaging(1, 1);
expect(item.id).toEqual(1);
expect(item.name).toEqual('Ranged weapon longbow 2m');
expect(item.quantity).toEqual(5000);
expect(item.quantityTotal).toEqual(5100);
});
});

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "supplierLog" "table": "supplierLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -11,6 +11,7 @@ module.exports = Self => {
require('../methods/supplier/campaignMetricsPdf')(Self); require('../methods/supplier/campaignMetricsPdf')(Self);
require('../methods/supplier/campaignMetricsEmail')(Self); require('../methods/supplier/campaignMetricsEmail')(Self);
require('../methods/supplier/newSupplier')(Self); require('../methods/supplier/newSupplier')(Self);
require('../methods/supplier/getItemsPackaging')(Self);
Self.validatesPresenceOf('name', { Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty' message: 'The social name cannot be empty'

View File

@ -12,3 +12,5 @@ columns:
hostFk: PC hostFk: PC
isBox: box isBox: box
itemPackingTypeFk: packing type itemPackingTypeFk: packing type
externalId: external id
stateTypeFk: status

View File

@ -12,3 +12,5 @@ columns:
hostFk: PC hostFk: PC
isBox: caja isBox: caja
itemPackingTypeFk: tipo empaquetado itemPackingTypeFk: tipo empaquetado
externalId: id externo
stateTypeFk: estado

View File

@ -11,6 +11,11 @@ module.exports = Self => {
{ {
arg: 'servicesIds', arg: 'servicesIds',
type: ['number'] type: ['number']
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
} }
], ],
returns: { returns: {
@ -23,7 +28,7 @@ module.exports = Self => {
} }
}); });
Self.refund = async(salesIds, servicesIds, options) => { Self.refund = async(salesIds, servicesIds, withWarehouse, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -65,7 +70,7 @@ module.exports = Self => {
const now = Date.vnNew(); const now = Date.vnNew();
const [firstTicketId] = ticketsIds; const [firstTicketId] = ticketsIds;
const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, myOptions); const refundTicket = await createTicketRefund(firstTicketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions);
for (const sale of sales) { for (const sale of sales) {
const createdSale = await models.Sale.create({ const createdSale = await models.Sale.create({
@ -113,7 +118,7 @@ module.exports = Self => {
} }
}; };
async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, myOptions) { async function createTicketRefund(ticketId, now, refundAgencyMode, refoundZoneId, withWarehouse, myOptions) {
const models = Self.app.models; const models = Self.app.models;
const filter = {include: {relation: 'address'}}; const filter = {include: {relation: 'address'}};
@ -125,7 +130,7 @@ module.exports = Self => {
addressFk: ticket.address().id, addressFk: ticket.address().id,
agencyModeFk: refundAgencyMode.id, agencyModeFk: refundAgencyMode.id,
nickname: ticket.address().nickname, nickname: ticket.address().nickname,
warehouseFk: ticket.warehouseFk, warehouseFk: withWarehouse ? ticket.warehouseFk : null,
companyFk: ticket.companyFk, companyFk: ticket.companyFk,
landed: now, landed: now,
zoneFk: refoundZoneId zoneFk: refoundZoneId

View File

@ -6,8 +6,8 @@ describe('Sale refund()', () => {
const activeCtx = { const activeCtx = {
accessToken: {userId: userId}, accessToken: {userId: userId},
}; };
const servicesIds = [3]; const servicesIds = [3];
const withWarehouse = true;
beforeEach(() => { beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
@ -22,7 +22,7 @@ describe('Sale refund()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const refundedTicket = await models.Sale.refund(salesIds, servicesIds, options); const refundedTicket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
expect(refundedTicket).toBeDefined(); expect(refundedTicket).toBeDefined();
@ -40,7 +40,7 @@ describe('Sale refund()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ticket = await models.Sale.refund(salesIds, servicesIds, options); const ticket = await models.Sale.refund(salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({ const refundedTicket = await models.Ticket.findOne({
where: { where: {

View File

@ -7,6 +7,11 @@ module.exports = Self => {
arg: 'ticketsIds', arg: 'ticketsIds',
type: ['number'], type: ['number'],
required: true required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
} }
], ],
returns: { returns: {
@ -19,7 +24,7 @@ module.exports = Self => {
} }
}); });
Self.refund = async(ticketsIds, options) => { Self.refund = async(ticketsIds, withWarehouse, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -41,7 +46,7 @@ module.exports = Self => {
const services = await models.TicketService.find(filter, myOptions); const services = await models.TicketService.find(filter, myOptions);
const servicesIds = services.map(service => service.id); const servicesIds = services.map(service => service.id);
const refundedTickets = await models.Sale.refund(salesIds, servicesIds, myOptions); const refundedTickets = await models.Sale.refund(salesIds, servicesIds, withWarehouse, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -1,58 +1,9 @@
{ {
"name": "TicketLog", "name": "TicketLog",
"base": "Log", "base": "Log",
"options": { "options": {
"mysql": { "mysql": {
"table": "ticketLog" "table": "ticketLog"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "number"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
} }
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

View File

@ -150,7 +150,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td> <td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td> <td>
<span <span
class="chip {{ticket.classColor}}"> class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState | dashIfEmpty}} {{::ticket.futureState | dashIfEmpty}}
</span> </span>
</td> </td>

View File

@ -102,13 +102,6 @@ export default class Controller extends Section {
return checkedLines; return checkedLines;
} }
stateColor(state) {
if (state === 'OK')
return 'success';
else if (state === 'Libre')
return 'notice';
}
dateRange(value) { dateRange(value) {
const minHour = new Date(value); const minHour = new Date(value);
minHour.setHours(0, 0, 0, 0); minHour.setHours(0, 0, 0, 0);

View File

@ -61,24 +61,6 @@ describe('Component vnTicketAdvance', () => {
}); });
}); });
describe('stateColor()', () => {
it('should return success to the OK tickets', () => {
const ok = controller.stateColor(controller.$.model.data[0].state);
const notOk = controller.stateColor(controller.$.model.data[1].state);
expect(ok).toEqual('success');
expect(notOk).not.toEqual('success');
});
it('should return success to the FREE tickets', () => {
const notFree = controller.stateColor(controller.$.model.data[0].state);
const free = controller.stateColor(controller.$.model.data[1].state);
expect(free).toEqual('notice');
expect(notFree).not.toEqual('notice');
});
});
describe('dateRange()', () => { describe('dateRange()', () => {
it('should return two dates with the hours at the start and end of the given date', () => { it('should return two dates with the hours at the start and end of the given date', () => {
const now = Date.vnNew(); const now = Date.vnNew();

View File

@ -141,12 +141,27 @@
translate> translate>
Recalculate components Recalculate components
</vn-item> </vn-item>
<vn-item <vn-item class="dropdown"
ng-click="refundAllConfirmation.show()" vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant" vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove" vn-acl-action="remove"
vn-tooltip="Create a single ticket with all the content of the current ticket"
translate> translate>
Refund all Refund all...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
ng-click="$ctrl.refund(true)"
translate>
with warehouse
</vn-item>
<vn-item
ng-click="$ctrl.refund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item> </vn-item>
</vn-list> </vn-list>
</vn-menu> </vn-menu>
@ -319,14 +334,6 @@
message="Recalculate components"> message="Recalculate components">
</vn-confirm> </vn-confirm>
<!-- Refund all confirmation dialog -->
<vn-confirm
vn-id="refundAllConfirmation"
on-accept="$ctrl.refund()"
question="Are you sure you want to refund all?"
message="Refund all">
</vn-confirm>
<!-- Client balance popup--> <!-- Client balance popup-->
<vn-client-balance-create <vn-client-balance-create
vn-id="balance-create" vn-id="balance-create"

View File

@ -297,16 +297,17 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Data saved!'))); .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
} }
async refund() { refund(withWarehouse) {
const params = {ticketsIds: [this.id]}; const params = {ticketsIds: [this.id], withWarehouse: withWarehouse};
const query = 'Tickets/refund'; const query = 'Tickets/refund';
return this.$http.post(query, params).then(res => { return this.$http.post(query, params)
const refundTicket = res.data; .then(res => {
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { const refundTicket = res.data;
ticketId: refundTicket.id this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
})); ticketId: refundTicket.id
this.$state.go('ticket.card.sale', {id: refundTicket.id}); }));
}); this.$state.go('ticket.card.sale', {id: refundTicket.id});
});
} }
onSmsSend(sms) { onSmsSend(sms) {

View File

@ -10,7 +10,9 @@ Send CSV: Enviar CSV
Send CSV Delivery Note: Enviar albarán en CSV Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF Send PDF Delivery Note: Enviar albarán en PDF
Show Proforma: Ver proforma Show Proforma: Ver proforma
Refund all: Abonar todo Refund all...: Abonar todo...
with warehouse: con almacén
without warehouse: sin almacén
Invoice sent: Factura enviada Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}" The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente Transfer client: Transferir cliente
@ -18,3 +20,4 @@ SMS Notify changes: SMS Notificar cambios
PDF sent!: ¡PDF enviado! PDF sent!: ¡PDF enviado!
Already exist signed delivery note: Ya existe albarán de entrega firmado Already exist signed delivery note: Ya existe albarán de entrega firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán de entrega? Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán de entrega?
Create a single ticket with all the content of the current ticket: Crea un ticket único con todo el contenido del ticket actual

View File

@ -158,7 +158,7 @@
<td>{{::ticket.futureIpt | dashIfEmpty}}</td> <td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td> <td>
<span <span
class="chip {{ticket.classColor}}"> class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState}} {{::ticket.futureState}}
</span> </span>
</td> </td>

View File

@ -529,11 +529,28 @@
ng-if="$ctrl.isEditable && $ctrl.hasReserves()"> ng-if="$ctrl.isEditable && $ctrl.hasReserves()">
Unmark as reserved Unmark as reserved
</vn-item> </vn-item>
<vn-item translate <vn-item class="dropdown"
name="refund" name="refund"
ng-click="$ctrl.createRefund()" vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant" vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"> vn-acl-action="remove"
Refund translate>
</vn-item> Refund...
<vn-menu vn-id="refundMenu">
<vn-list>
<vn-item
name="refundWithWarehouse"
ng-click="$ctrl.createRefund(true)"
translate>
with warehouse
</vn-item>
<vn-item
name="refundWithoutWarehouse"
ng-click="$ctrl.createRefund(false)"
translate>
without warehouse
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
</vn-menu> </vn-menu>

View File

@ -520,13 +520,12 @@ class Controller extends Section {
}); });
} }
createRefund() { createRefund(withWarehouse) {
const sales = this.selectedValidSales(); const sales = this.selectedValidSales();
if (!sales) return; if (!sales) return;
const salesIds = sales.map(sale => sale.id); const salesIds = sales.map(sale => sale.id);
const params = {salesIds: salesIds, withWarehouse: withWarehouse};
const params = {salesIds: salesIds};
const query = 'Sales/refund'; const query = 'Sales/refund';
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
const refundTicket = res.data; const refundTicket = res.data;

View File

@ -36,10 +36,10 @@ Warehouse: Almacen
Agency: Agencia Agency: Agencia
Shipped: F. envio Shipped: F. envio
Packaging: Encajado Packaging: Encajado
Refund: Abono Refund...: Abono...
Promotion mana: Maná promoción Promotion mana: Maná promoción
Claim mana: Maná reclamación Claim mana: Maná reclamación
History: Historial History: Historial
Do you want to continue?: ¿Desea continuar? Do you want to continue?: ¿Desea continuar?
Claim out of time: Reclamación fuera de plazo Claim out of time: Reclamación fuera de plazo
Do you want to create a claim?: ¿Quieres crear una reclamación? Do you want to create a claim?: ¿Quieres crear una reclamación?

View File

@ -14,6 +14,9 @@
"TravelThermograph": { "TravelThermograph": {
"dataSource": "vn" "dataSource": "vn"
}, },
"TravelConfig": {
"dataSource": "vn"
},
"Temperature": { "Temperature": {
"dataSource": "vn" "dataSource": "vn"
} }

View File

@ -0,0 +1,49 @@
{
"name": "TravelConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "travelConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"warehouseInFk": {
"type": "number"
},
"warehouseOutFk": {
"type": "number"
},
"agencyFk": {
"type": "number"
},
"companyFk": {
"type": "number"
}
},
"relations": {
"warehouseIn": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseInFk"
},
"warehouseOut": {
"type": "belongsTo",
"model": "Warehouse",
"foreignKey": "warehouseOutFk"
},
"agency": {
"type": "belongsTo",
"model": "AgencyMode",
"foreignKey": "agencyFk"
},
"company": {
"type": "belongsTo",
"model": "Company",
"foreignKey": "companyFk"
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": { "mysql": {
"table": "travelLog" "table": "travelLog"
} }
},
"properties": {
"id": {
"id": true,
"type": "number",
"forceId": false
},
"originFk": {
"type": "number",
"required": true
},
"userFk": {
"type": "number"
},
"action": {
"type": "string",
"required": true
},
"changedModel": {
"type": "string"
},
"oldInstance": {
"type": "object"
},
"newInstance": {
"type": "object"
},
"creationDate": {
"type": "date"
},
"changedModelId": {
"type": "string"
},
"changedModelValue": {
"type": "string"
},
"description": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
} }
} }

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