Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 5701-update_to_mariadb10.7.7

This commit is contained in:
Alex Moreno 2023-06-09 08:43:39 +02:00
commit 50ced36338
135 changed files with 12903 additions and 15077 deletions

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/),
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
### 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
-
- (General -> Permisos) Mejorada seguridad
### Fixed
-
@ -24,13 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- (Tickets -> Crear Factura) Al facturar se envia automáticamente el pdf al cliente
- (Artículos -> Histórico) Filtro para mostrar lo anterior al inventario
- (Trabajadores -> Nuevo trabajador) Permite elegir el método de pago
### Changed
- (Trabajadores -> Nuevo trabajador) Los clientes se crean sin 'TR' pero se añade tipo de negocio 'Trabajador'
### Fixed
- (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 \
gnupg2 \
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 \
&& npm install -g npm@8.19.2
&& npm install -g npm@9.6.6
# Puppeteer

8
Jenkinsfile vendored
View File

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

View File

@ -1,9 +1,9 @@
LOAD DATA LOCAL INFILE ?
INTO TABLE `edi`.`item`
CHARACTER SET ascii
FIELDS TERMINATED BY ';'
LINES TERMINATED BY '\n' (@col1, @col2, @col3, @col4, @col5, @col6, @col7, @col8, @col9, @col10, @col11, @col12)
SET
id = @col2,
SET id = @col2,
product_name = @col4,
name = @col5,
plant_id = @col7,
@ -11,3 +11,4 @@ LOAD DATA LOCAL INFILE ?
entry_date = STR_TO_DATE(@col10, '%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')

View File

@ -3,236 +3,237 @@ const path = require('path');
const fs = require('fs-extra');
module.exports = Self => {
Self.remoteMethodCtx('updateData', {
description: 'Updates schema data from external provider',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
http: {
path: `/updateData`,
verb: 'POST'
}
});
Self.remoteMethodCtx('updateData', {
description: 'Updates schema data from external provider',
accessType: 'WRITE',
returns: {
type: 'object',
root: true
},
http: {
path: `/updateData`,
verb: 'POST'
}
});
Self.updateData = async() => {
const models = Self.app.models;
Self.updateData = async() => {
const models = Self.app.models;
// Get files checksum
const tx = await Self.beginTransaction({});
// Get files checksum
const tx = await Self.beginTransaction({});
try {
const options = {transaction: tx};
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
try {
const options = {transaction: tx};
const files = await Self.rawSql('SELECT name, checksum, keyValue FROM edi.fileConfig', null, options);
const updatableFiles = [];
for (const file of files) {
const fileChecksum = await getChecksum(file);
const updatableFiles = [];
for (const file of files) {
const fileChecksum = await getChecksum(file);
if (file.checksum != fileChecksum) {
updatableFiles.push({
name: file.name,
checksum: fileChecksum
});
} else
console.debug(`File already updated, skipping...`);
}
if (file.checksum != fileChecksum) {
updatableFiles.push({
name: file.name,
checksum: fileChecksum
});
} else
console.debug(`File already updated, skipping...`);
}
if (updatableFiles.length === 0)
return false;
if (updatableFiles.length === 0)
return false;
// Download files
const container = await models.TempContainer.container('edi');
const tempPath = path.join(container.client.root, container.name);
// Download files
const container = await models.TempContainer.container('edi');
const tempPath = path.join(container.client.root, container.name);
let remoteFile;
let tempDir;
let tempFile;
let remoteFile;
let tempDir;
let tempFile;
const fileNames = updatableFiles.map(file => file.name);
const fileNames = updatableFiles.map(file => file.name);
const tables = await Self.rawSql(`
SELECT fileName, toTable, file
FROM edi.tableConfig
WHERE file IN (?)`, [fileNames], options);
const tables = await Self.rawSql(`
SELECT fileName, toTable, file
FROM edi.tableConfig
WHERE file IN (?)`, [fileNames], options);
for (const table of tables) {
const fileName = table.file;
for (const table of tables) {
const fileName = table.file;
remoteFile = `codes/${fileName}.ZIP`;
tempDir = `${tempPath}/${fileName}`;
tempFile = `${tempPath}/${fileName}.zip`;
remoteFile = `codes/${fileName}.ZIP`;
tempDir = `${tempPath}/${fileName}`;
tempFile = `${tempPath}/${fileName}.zip`;
try {
await fs.readFile(tempFile);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`Downloading file ${fileName}...`);
const downloadOutput = await downloadFile(remoteFile, tempFile);
if (downloadOutput.error)
continue;
}
}
try {
await fs.readFile(tempFile);
} catch (error) {
if (error.code === 'ENOENT') {
console.debug(`Downloading file ${fileName}...`);
const downloadOutput = await downloadFile(remoteFile, tempFile);
if (downloadOutput.error)
continue;
}
}
await extractFile(fileName, tempFile, tempDir);
await extractFile(fileName, tempFile, tempDir);
console.debug(`Updating table ${table.toTable}...`);
await dumpData(tempDir, table, options);
}
console.debug(`Updating table ${table.toTable}...`);
await dumpData(tempDir, table, options);
}
// Update files checksum
for (const file of updatableFiles) {
console.log(`Updating file ${file.name} checksum...`);
await Self.rawSql(`
UPDATE edi.fileConfig
SET checksum = ?
WHERE name = ?`,
[file.checksum, file.name], options);
}
// Update files checksum
for (const file of updatableFiles) {
console.log(`Updating file ${file.name} checksum...`);
await Self.rawSql(`
UPDATE edi.fileConfig
SET checksum = ?
WHERE name = ?`,
[file.checksum, file.name], options);
}
await tx.commit();
await tx.commit();
// Clean files
try {
console.debug(`Cleaning files...`);
await fs.remove(tempPath);
} catch (error) {
if (error.code !== 'ENOENT')
throw e;
}
// Clean files
try {
console.debug(`Cleaning files...`);
await fs.remove(tempPath);
} catch (error) {
if (error.code !== 'ENOENT')
throw e;
}
return true;
} catch (error) {
await tx.rollback();
throw error;
}
};
return true;
} catch (error) {
await tx.rollback();
throw error;
}
};
let ftpClient;
async function getFtpClient() {
if (!ftpClient) {
const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig');
console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`);
let ftpClient;
async function getFtpClient() {
if (!ftpClient) {
const [ftpConfig] = await Self.rawSql('SELECT host, user, password FROM edi.ftpConfig');
console.debug(`Openning FTP connection to ${ftpConfig.host}...\n`);
const FtpClient = require('ftps');
const FtpClient = require('ftps');
ftpClient = new FtpClient({
host: ftpConfig.host,
username: ftpConfig.user,
password: ftpConfig.password,
procotol: 'ftp'
});
}
ftpClient = new FtpClient({
host: ftpConfig.host,
username: ftpConfig.user,
password: ftpConfig.password,
procotol: 'ftp',
additionalLftpCommands: 'set ssl:verify-certificate no'
});
}
return ftpClient;
}
return ftpClient;
}
async function getChecksum(file) {
const ftpClient = await getFtpClient();
console.debug(`Checking checksum for file ${file.name}...`);
async function getChecksum(file) {
const ftpClient = await getFtpClient();
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) => {
ftpClient.exec((err, response) => {
if (err || response.error) {
console.debug(`Error downloading checksum file... ${response.error}`);
return reject(err);
}
const response = await new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (err || response.error) {
console.debug(`Error downloading checksum file... ${response.error}`);
return reject(err);
}
resolve(response);
});
});
resolve(response);
});
});
if (response && response.data) {
const fileContents = response.data;
const rows = fileContents.split('\n');
const row = rows[4];
const columns = row.split(/\s+/);
if (response && response.data) {
const fileContents = response.data;
const rows = fileContents.split('\n');
const row = rows[4];
const columns = row.split(/\s+/);
let fileChecksum;
if (file.keyValue)
fileChecksum = columns[1];
let fileChecksum;
if (file.keyValue)
fileChecksum = columns[1];
if (!file.keyValue)
fileChecksum = columns[0];
if (!file.keyValue)
fileChecksum = columns[0];
return fileChecksum;
}
}
return fileChecksum;
}
}
async function downloadFile(remoteFile, tempFile) {
const ftpClient = await getFtpClient();
async function downloadFile(remoteFile, tempFile) {
const ftpClient = await getFtpClient();
ftpClient.get(remoteFile, tempFile);
ftpClient.get(remoteFile, tempFile);
return new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (err || response.error) {
console.debug(`Error downloading file... ${response.error}`);
return reject(err);
}
return new Promise((resolve, reject) => {
ftpClient.exec((err, response) => {
if (err || response.error) {
console.debug(`Error downloading file... ${response.error}`);
return reject(err);
}
resolve(response);
});
});
}
resolve(response);
});
});
}
async function extractFile(fileName, tempFile, tempDir) {
const JSZip = require('jszip');
async function extractFile(fileName, tempFile, tempDir) {
const JSZip = require('jszip');
try {
await fs.mkdir(tempDir);
console.debug(`Extracting file ${fileName}...`);
} catch (error) {
if (error.code !== 'EEXIST')
throw e;
}
try {
await fs.mkdir(tempDir);
console.debug(`Extracting file ${fileName}...`);
} catch (error) {
if (error.code !== 'EEXIST')
throw e;
}
const fileStream = await fs.readFile(tempFile);
if (fileStream) {
const zip = new JSZip();
const zipContents = await zip.loadAsync(fileStream);
const fileStream = await fs.readFile(tempFile);
if (fileStream) {
const zip = new JSZip();
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) {
const fileContent = await zip.file(fileName).async('nodebuffer');
const dest = path.join(tempDir, fileName);
await fs.writeFile(dest, fileContent);
}
}
}
for (const fileName of fileNames) {
const fileContent = await zip.file(fileName).async('nodebuffer');
const dest = path.join(tempDir, fileName);
await fs.writeFile(dest, fileContent);
}
}
}
async function dumpData(tempDir, table, options) {
const toTable = table.toTable;
const baseName = table.fileName;
async function dumpData(tempDir, table, options) {
const toTable = table.toTable;
const baseName = table.fileName;
console.log(`Emptying table ${toTable}...`);
const tableName = `edi.${toTable}`;
await Self.rawSql(`DELETE FROM ??`, [tableName]);
console.log(`Emptying table ${toTable}...`);
const tableName = `edi.${toTable}`;
await Self.rawSql(`DELETE FROM ??`, [tableName]);
const dirFiles = await fs.readdir(tempDir);
const files = dirFiles.filter(file => file.startsWith(baseName));
const dirFiles = await fs.readdir(tempDir);
const files = dirFiles.filter(file => file.startsWith(baseName));
for (const file of files) {
console.log(`Dumping data from file ${file}...`);
for (const file of files) {
console.log(`Dumping data from file ${file}...`);
const templatePath = path.join(__dirname, `./sql/${toTable}.sql`);
const sqlTemplate = await fs.readFile(templatePath, 'utf8');
const filePath = path.join(tempDir, file);
const templatePath = path.join(__dirname, `./sql/${toTable}.sql`);
const sqlTemplate = await fs.readFile(templatePath, 'utf8');
const filePath = path.join(tempDir, file);
await Self.rawSql(sqlTemplate, [filePath], options);
await Self.rawSql(`
UPDATE edi.tableConfig
SET updated = ?
WHERE fileName = ?
`, [Date.vnNew(), baseName], options);
}
await Self.rawSql(sqlTemplate, [filePath], options);
await Self.rawSql(`
UPDATE edi.tableConfig
SET updated = ?
WHERE fileName = ?
`, [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 `label` int(11) unsigned NOT NULL;
ALTER TABLE `vn`.`wagon` ADD CONSTRAINT `wagon_type` FOREIGN KEY (`typeFk`) REFERENCES `wagonType` (`id`) ON UPDATE CASCADE;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('WagonType', '*', '*', 'ALLOW', 'ROLE', 'productionAssi'),
@ -70,3 +68,4 @@ INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `pri
('WagonType', 'createWagonType', '*', 'ALLOW', 'ROLE', 'productionAssi'),
('WagonType', 'deleteWagonType', '*', '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 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;
-- Cuando se apruebe el PR quitar y poner en redmine para hacerse manualmente
UPDATE `vn`.`workerConfig`
SET payMethodFk = 4
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),
(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`)
VALUES
(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`,
`comment`, `relevancy`, `image`, `subName`, `minPrice`, `stars`, `family`, `isFloramondo`, `genericFk`, `itemPackingTypeFk`, `hasMinPrice`, `packingShelve`, `weightByPiece`)
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),
(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),
@ -2729,6 +2731,7 @@ INSERT INTO `util`.`notification` (`id`, `name`, `description`)
VALUES
(1, 'print-email', 'notification fixture one'),
(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');
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
@ -2886,6 +2889,10 @@ INSERT INTO `vn`.`wagonTypeTray` (`id`, `typeFk`, `height`, `colorFk`)
(2, 1, 50, 2),
(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
* @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 vInvCalculated INT;

View File

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

View File

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

View File

@ -595,6 +595,8 @@ export default {
moreMenuUpdateDiscount: 'vn-item[name="discount"]',
moreMenuRecalculatePrice: 'vn-item[name="calculatePrice"]',
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',
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',

View File

@ -64,6 +64,6 @@ describe('SmartTable SearchBar integration', () => {
await page.reload({
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() => {
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');
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() => {
await page.loginAndModule('salesAssistant', 'ticket');
await page.accessToSearchResult('16');
await page.accessToSearchResult('17');
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.moreMenu);
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.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() => {
await page.accessToSearchResult('16');
await page.accessToSection('ticket.card.sale');
await page.waitToClick(selectors.ticketSales.firstSaleCheckbox);
await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketSales.moreMenu);
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() => {
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() => {

View File

@ -1,8 +1,7 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642'
xdescribe('Ticket Future path', () => {
describe('Ticket Future path', () => {
let browser;
let page;
let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Future path', () => {
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.clearInput(selectors.ticketFuture.warehouseFk);
await page.waitToClick(selectors.ticketFuture.submit);
@ -43,69 +42,58 @@ xdescribe('Ticket Future path', () => {
message = await page.waitForSnackbar();
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 getBrowser from '../../helpers/puppeteer';
// 'https:// redmine.verdnatura.es/issues/5642'
xdescribe('Ticket Advance path', () => {
describe('Ticket Advance path', () => {
let browser;
let page;
let httpRequest;
@ -22,7 +21,7 @@ xdescribe('Ticket Advance path', () => {
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.clearInput(selectors.ticketAdvance.warehouseFk);
@ -44,45 +43,37 @@ xdescribe('Ticket Advance path', () => {
message = await page.waitForSnackbar();
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() => {
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);
const visible = await page.isVisible(selectors.claimSummary.itemDescriptorPopover);

View File

@ -1,4 +1,11 @@
@import "./variables";
@import "./effects";
@mixin mobile {
@media screen and (max-width: $mobile-width) {
@content;
}
}
@mixin browser($browser) {
html[data-browser*="#{$browser}"] & {
@content;

View File

@ -1,5 +1,3 @@
@import "./util";
$font-size: 11pt;
$menu-width: 256px;
$topbar-height: 56px;

View File

@ -1,4 +1,4 @@
@import "variables";
@import "util";
@keyframes fadein {
from {
@ -16,7 +16,7 @@ vn-background {
background-color: black;
z-index: 14;
@media screen and (max-width: $mobile-width) {
@include mobile {
&.shown {
display: block;
opacity: .3;

View File

@ -1,4 +1,4 @@
@import "effects";
@import "util";
vn-layout {
& > vn-topbar {
@ -134,7 +134,7 @@ vn-layout {
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
& > vn-topbar {
& > .start > .logo {
display: none;

View File

@ -16,7 +16,7 @@
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-sm vn-px-sm">
class="vn-w-sm vn-px-sm vn-pb-xl">
<div class="change vn-mb-sm" ng-repeat="log in $ctrl.logs">
<div class="left">
<vn-avatar class="vn-mt-xs"
@ -33,17 +33,6 @@
</div>
<vn-card class="detail">
<div class="header vn-pa-sm">
<div
class="action-date text-secondary text-caption vn-mr-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
<vn-icon
class="action vn-mr-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
{{::$ctrl.relativeDate(log.creationDate)}}
</div>
<div class="action-model">
<span class="model-name"
ng-if="::$ctrl.showModelName && log.changedModel"
@ -52,13 +41,27 @@
{{::log.changedModelI18n}}
</span>
</div>
<div
class="action-date text-secondary text-caption vn-ml-sm"
title="{{::log.creationDate | date:'dd/MM/yyyy HH:mm:ss'}}">
{{::$ctrl.relativeDate(log.creationDate)}}
<vn-icon
class="action vn-ml-xs"
ng-class="::$ctrl.actionsClass[log.action]"
icon="{{::$ctrl.actionsIcon[log.action]}}"
translate-attr="::{title: $ctrl.actionsText[log.action]}">
</vn-icon>
</div>
</div>
<div
class="model vn-pb-sm vn-px-sm"
title="{{::log.changedModelValue}}"
ng-if="::log.changedModelId || log.changedModelValue">
<div class="model vn-pb-sm vn-px-sm"
ng-if="::$ctrl.showModelName">
<span class="model-id" ng-if="::log.changedModelId">#{{::log.changedModelId}}</span>
<span class="model-value">{{::log.changedModelValue}}</span>
<vn-icon
icon="filter_alt"
translate-attr="{title: 'Show all record changes'}"
ng-click="$ctrl.filterByEntity(log)">
</vn-icon>
<span class="model-value" title="{{::log.changedModelValue}}">{{::log.changedModelValue}}</span>
</div>
<div class="changes vn-pa-sm"
ng-class="{expanded: log.expand}"
@ -75,16 +78,16 @@
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</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>
<div ng-if="log.expand" class="expanded-json">
<div ng-repeat="prop in ::log.props">
<span class="json-field" title="{{::prop.name}}">
{{::prop.nameI18n}}:
</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'">
<vn-json-value value="::prop.old"></vn-json-value>
<vn-log-value val="::prop.old"></vn-log-value>
</span>
</div>
</div>
@ -96,6 +99,13 @@
</div>
</div>
</vn-data-viewer>
<vn-float-button
ng-if="model.userFilter"
icon="filter_alt_off"
translate-attr="{title: 'Quit filter'}"
ng-click="$ctrl.resetFilter()"
fixed-bottom-right>
</vn-float-button>
<vn-side-menu side="right">
<form vn-vertical
ng-model-options="{updateOn: 'change blur'}"
@ -163,12 +173,17 @@
data="$ctrl.models"
class="changed-model">
</vn-autocomplete>
<!-- FIXME: Cannot use LIKE with JSON columns
<vn-textfield
label="Changes"
ng-model="filter.changes">
<append>
<vn-icon
icon="info_outline"
vn-tooltip="Search by changes"
pointer>
</vn-icon>
</append>
</vn-textfield>
-->
<vn-vertical>
<vn-check
label="Creates"
@ -195,18 +210,6 @@
label="To"
ng-model="filter.to">
</vn-date-picker>
<vn-button-bar vn-vertical>
<vn-button
label="Filter"
ng-click="$ctrl.applyFilter(filter)">
</vn-button>
<vn-button
label="Reset"
class="flat"
ng-click="$ctrl.resetFilter()"
ng-if="model.userFilter">
</vn-button>
</vn-button-bar>
</form>
</vn-side-menu>
<vn-worker-descriptor-popover vn-id="workerDescriptor">

View File

@ -64,29 +64,47 @@ export default class Controller extends Section {
set logs(value) {
this._logs = value;
if (!value) return;
const empty = {};
const validations = window.validations;
const castJsonValue = this.castJsonValue;
for (const log of value) {
const oldValues = log.oldInstance || empty;
const newValues = log.newInstance || empty;
const notDelete = log.action != 'delete';
const olds = (notDelete ? log.oldInstance : null) || empty;
const vals = (notDelete ? log.newInstance : log.oldInstance) || empty;
const locale = validations[log.changedModel]?.locale || empty;
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)];
log.props = [];
for (const prop of props) {
if (prop.endsWith('$')) continue;
log.props.push({
name: prop,
nameI18n: firstUpper(locale.columns?.[prop]) || prop,
old: this.castJsonValue(oldValues[prop]),
new: this.castJsonValue(newValues[prop])
old: getVal(olds, prop),
val: getVal(vals, prop)
});
}
log.props.sort(
(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() {
@ -113,10 +131,6 @@ export default class Controller extends Section {
: value;
}
mainVal(prop, action) {
return action == 'delete' ? prop.old : prop.new;
}
relativeDate(dateVal) {
if (dateVal == null) return '';
const date = new Date(dateVal);
@ -150,14 +164,15 @@ export default class Controller extends Section {
if (value == null || value == '') return null;
switch (prop) {
case 'search':
const or = [{changedModelId: value}];
if (!/^[0-9]+$/.test(value))
or.push({changedModelValue: {like: `%${value}%`}});
return {or};
const or = [];
if (/^\s*[0-9]+\s*$/.test(value))
return {changedModelId: value.trim()};
else
return {changedModelValue: {like: `%${value}%`}};
case 'changes':
return {or: [
{oldInstance: {like: `%${value}%`}},
{newInstance: {like: `%${value}%`}},
{oldJson: {like: `%${value}%`}},
{newJson: {like: `%${value}%`}},
{description: {like: `%${value}%`}}
]};
case 'who':
@ -206,6 +221,14 @@ export default class Controller extends Section {
return this.$.model.applyFilter(lbFilter);
}
filterByEntity(log) {
this.$.filter = {
who: 'all',
search: log.changedModelId,
changedModel: log.changedModel
};
}
searchUser(search) {
if (/^[0-9]+$/.test(search)) {
return {id: search};
@ -238,3 +261,12 @@ ngModule.vnComponent('vnLog', {
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
Search: Buscar
Search by id or concept: Buscar por identificador o concepto
Search by changes: |
Buscar por cambios. Los atributos deben buscarse por su nombre interno,
para obtenerlo situar el cursor sobre el atributo.
Entity: Entidad
Action: Acción
Author: Autor
@ -13,9 +16,12 @@ Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
All: Todo
System: Sistema
Details: Detalles
note: nota
Changes: Cambios
today: hoy
yesterday: ayer
Show all record changes: Mostrar todos los cambios realizados en el registro
Quit filter: Quitar filtro

View File

@ -1,5 +1,4 @@
@import "variables";
@import "effects";
@import "util";
vn-log {
.change {
@ -77,7 +76,7 @@ vn-log {
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 1.4em;
font-size: 18px;
&.notice {
background-color: $color-notice-medium
@ -98,7 +97,22 @@ vn-log {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-height: 18px;
& > vn-icon {
@extend %clickable-light;
vertical-align: middle;
padding: 2px;
margin: -2px;
font-size: 18px;
color: $color-font-secondary;
float: right;
display: none;
@include mobile {
display: initial;
}
}
& > .model-value {
font-style: italic;
}
@ -107,6 +121,9 @@ vn-log {
font-size: .9rem;
}
}
&:hover > .model > vn-icon {
display: initial;
}
}
}
.changes {
@ -144,3 +161,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
});
}
vnConfig.initialize();
}
set lang(value) {

View File

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

View File

@ -1,5 +1,4 @@
@import "./variables";
@import "./effects";
@import "./util";
form vn-horizontal {
align-items: center;
@ -22,7 +21,7 @@ form vn-horizontal {
}
}
@media screen and (max-width: $mobile-width) {
@include mobile {
flex-direction: column;
align-items: initial;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,54 +5,5 @@
"mysql": {
"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": {
"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": {
"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']
}
},
{
relation: 'businessType',
scope: {
fields: ['description']
}
},
{
relation: 'account',
scope: {

View File

@ -5,54 +5,5 @@
"mysql": {
"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",
"options": {
"mysql": {
"table": "payMethod"
"table": "payMethod"
}
},
"properties": {

View File

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

View File

@ -8,3 +8,4 @@ Client invoices list: Listado de facturas del cliente
Pay method: Forma de pago
Unpaid Dated: "Fecha: {{dated | date:'dd/MM/yyyy'}}"
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": {
"dataSource": "vn"
},
"BuyConfig": {
"dataSource": "vn"
},
"ItemMatchProperties": {
"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": {
"type": "number"
},
"price1": {
"type": "number"
},
"price2": {
"type": "number"
},
@ -47,7 +50,44 @@
},
"weight": {
"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": {
"entry": {
@ -64,6 +104,16 @@
"type": "belongsTo",
"model": "Packaging",
"foreignKey": "packageFk"
},
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"delivery": {
"type": "belongsTo",
"model": "Delivery",
"foreignKey": "deliveryFk"
}
}
}

View File

@ -5,54 +5,5 @@
"mysql": {
"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/lastItemBuys')(Self);
require('../methods/entry/entryOrderPdf')(Self);
require('../methods/entry/addFromPackaging')(Self);
require('../methods/entry/addFromBuy')(Self);
Self.observe('before save', async function(ctx, options) {
if (ctx.isNewInstance) return;

View File

@ -5,57 +5,5 @@
"mysql": {
"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', {
description: 'Create refund tickets with sales and services if provided',
accessType: 'WRITE',
accepts: [{
arg: 'ref',
type: 'string',
description: 'The invoice reference'
}],
accepts: [
{
arg: 'ref',
type: 'string',
description: 'The invoice reference',
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
returns: {
type: ['number'],
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 myOptions = {};
let tx;
@ -35,7 +43,7 @@ module.exports = Self => {
const tickets = await models.Ticket.find(filter, myOptions);
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();

View File

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

View File

@ -76,14 +76,27 @@
translate>
Show CITES letter
</vn-item>
<vn-item
ng-click="refundConfirmation.show()"
name="refundInvoice"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-tooltip="Create a single ticket with all the content of the current invoice"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
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-list>
</vn-menu>
@ -97,11 +110,6 @@
on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?">
</vn-confirm>
<vn-confirm
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-client-descriptor-popover>

View File

@ -114,9 +114,9 @@ class Controller extends Section {
});
}
refundInvoiceOut() {
refundInvoiceOut(withWarehouse) {
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 => {
const refundTicket = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {

View File

@ -20,3 +20,4 @@ Regenerate PDF invoice: Regenerar PDF factura
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 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:
itemFk: item
genusFk: genus

View File

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

View File

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

View File

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

View File

@ -5,54 +5,5 @@
"mysql": {
"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() {
this.filter.values.push({});
setTimeout(() => this.popover.relocate());
setTimeout(() => this.parentPopover.relocate());
}
changeTag() {
@ -36,7 +36,7 @@ ngModule.vnComponent('vnOrderCatalogSearchPanel', {
controller: Controller,
bindings: {
onSubmit: '&?',
popover: '<?',
parentPopover: '<?',
resultTags: '<?'
}
});

View File

@ -104,7 +104,7 @@
on-close="$ctrl.onPopoverClose()">
<vn-order-catalog-search-panel
on-submit="$ctrl.onPanelSubmit($filter)"
popover="popover"
parent-popover="popover"
result-tags="$ctrl.resultTags">
</vn-order-catalog-search-panel>
</vn-popover>

View File

@ -5,54 +5,5 @@
"mysql": {
"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">
</vn-autocomplete>
<vn-autocomplete vn-one
url="Agencies"
data="agencies"
label="Agency Agreement"
show-field="name"
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",
"required": false
},
"printerFk": {
"mainPrinterFk": {
"type": "number",
"required": false
},

View File

@ -1,58 +1,9 @@
{
"name": "ShelvingLog",
"name": "ShelvingLog",
"base": "Log",
"options": {
"mysql": {
"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"
"options": {
"mysql": {
"table": "shelvingLog"
}
},
"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": {
"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/campaignMetricsEmail')(Self);
require('../methods/supplier/newSupplier')(Self);
require('../methods/supplier/getItemsPackaging')(Self);
Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty'

View File

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

View File

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

View File

@ -11,6 +11,11 @@ module.exports = Self => {
{
arg: 'servicesIds',
type: ['number']
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
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 myOptions = {};
let tx;
@ -65,7 +70,7 @@ module.exports = Self => {
const now = Date.vnNew();
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) {
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 filter = {include: {relation: 'address'}};
@ -125,7 +130,7 @@ module.exports = Self => {
addressFk: ticket.address().id,
agencyModeFk: refundAgencyMode.id,
nickname: ticket.address().nickname,
warehouseFk: ticket.warehouseFk,
warehouseFk: withWarehouse ? ticket.warehouseFk : null,
companyFk: ticket.companyFk,
landed: now,
zoneFk: refoundZoneId

View File

@ -6,8 +6,8 @@ describe('Sale refund()', () => {
const activeCtx = {
accessToken: {userId: userId},
};
const servicesIds = [3];
const withWarehouse = true;
beforeEach(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
@ -22,7 +22,7 @@ describe('Sale refund()', () => {
try {
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();
@ -40,7 +40,7 @@ describe('Sale refund()', () => {
try {
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({
where: {

View File

@ -7,6 +7,11 @@ module.exports = Self => {
arg: 'ticketsIds',
type: ['number'],
required: true
},
{
arg: 'withWarehouse',
type: 'boolean',
required: true
}
],
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 myOptions = {};
let tx;
@ -41,7 +46,7 @@ module.exports = Self => {
const services = await models.TicketService.find(filter, myOptions);
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();

View File

@ -1,58 +1,9 @@
{
"name": "TicketLog",
"name": "TicketLog",
"base": "Log",
"options": {
"mysql": {
"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"
"options": {
"mysql": {
"table": "ticketLog"
}
},
"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>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState | dashIfEmpty}}
</span>
</td>

View File

@ -102,13 +102,6 @@ export default class Controller extends Section {
return checkedLines;
}
stateColor(state) {
if (state === 'OK')
return 'success';
else if (state === 'Libre')
return 'notice';
}
dateRange(value) {
const minHour = new Date(value);
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()', () => {
it('should return two dates with the hours at the start and end of the given date', () => {
const now = Date.vnNew();

View File

@ -141,12 +141,27 @@
translate>
Recalculate components
</vn-item>
<vn-item
ng-click="refundAllConfirmation.show()"
<vn-item class="dropdown"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
vn-tooltip="Create a single ticket with all the content of the current ticket"
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-list>
</vn-menu>
@ -319,14 +334,6 @@
message="Recalculate components">
</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-->
<vn-client-balance-create
vn-id="balance-create"

View File

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

View File

@ -10,7 +10,9 @@ Send CSV: Enviar CSV
Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF
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
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente
@ -18,3 +20,4 @@ SMS Notify changes: SMS Notificar cambios
PDF sent!: ¡PDF enviado!
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?
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>
<span
class="chip {{ticket.classColor}}">
class="chip {{ticket.futureClassColor}}">
{{::ticket.futureState}}
</span>
</td>

View File

@ -529,11 +529,28 @@
ng-if="$ctrl.isEditable && $ctrl.hasReserves()">
Unmark as reserved
</vn-item>
<vn-item translate
name="refund"
ng-click="$ctrl.createRefund()"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove">
Refund
</vn-item>
<vn-item class="dropdown"
name="refund"
vn-click-stop="refundMenu.show($event, 'left')"
vn-acl="invoicing, claimManager, salesAssistant"
vn-acl-action="remove"
translate>
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>

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