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

This commit is contained in:
Pablo Natek 2023-06-09 07:11:13 +02:00
commit ac67a2606f
182 changed files with 13199 additions and 15228 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/), 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

@ -121,7 +121,8 @@ module.exports = Self => {
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'
}); });
} }
@ -132,7 +133,7 @@ module.exports = Self => {
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) => {

View File

@ -67,7 +67,7 @@ module.exports = Self => {
if (!image) return false; if (!image) return false;
const hasReadRole = models.ImageCollection.hasReadRole(ctx, collection); const hasReadRole = await models.ImageCollection.hasReadRole(ctx, collection);
if (!hasReadRole) if (!hasReadRole)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);

View File

@ -9,10 +9,11 @@ module.exports = Self => {
* @return {boolean} True for user with read privileges * @return {boolean} True for user with read privileges
*/ */
Self.hasReadRole = async(ctx, name, options) => { Self.hasReadRole = async(ctx, name, options) => {
const collection = await Self.findOne({where: {name}}, { const collection = await Self.findOne({
include: { include: {
relation: 'readRole' relation: 'readRole'
} },
where: {name}
}, options); }, options);
return await hasRole(ctx, collection, options); return await hasRole(ctx, collection, options);

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

@ -27,7 +27,6 @@ INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalTyp
('Client', 'summary', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'summary', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateAddress', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'updateAddress', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateFiscalData', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'updateFiscalData', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'updateUser', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'uploadFile', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'uploadFile', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsPdf', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'campaignMetricsPdf', '*', 'ALLOW', 'ROLE', 'employee'),
('Client', 'campaignMetricsEmail', '*', 'ALLOW', 'ROLE', 'employee'), ('Client', 'campaignMetricsEmail', '*', 'ALLOW', 'ROLE', 'employee'),

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

@ -0,0 +1,121 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Ticket', 'editDiscount', 'WRITE', 'ALLOW', 'ROLE', 'salesPerson'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'salesAssistant'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'deliveryBoss'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'buyer'),
('Ticket', 'isRoleAdvanced', '*', 'ALLOW', 'ROLE', 'claimManager'),
('Ticket', 'deleteTicketWithPartPrepared', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'),
('Ticket', 'editZone', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss'),
('State', 'editableStates', 'READ', 'ALLOW', 'ROLE', 'employee'),
('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('State', 'seeEditableStates', 'READ', 'ALLOW', 'ROLE', 'production'),
('State', 'isSomeEditable', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('State', 'isAllEditable', 'READ', 'ALLOW', 'ROLE', 'production'),
('State', 'isAllEditable', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('Agency', 'seeExpired', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('Agency', 'seeExpired', 'READ', 'ALLOW', 'ROLE', 'productionBoss'),
('Claim', 'createAfterDeadline', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Client', 'editAddressLogifloraAllowed', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'),
('Client', 'editFiscalDataWithoutTaxDataCheck', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'),
('Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE', 'ALLOW', 'ROLE', 'salesAssistant'),
('Client', 'editCredit', 'WRITE', 'ALLOW', 'ROLE', 'financialBoss'),
('Client', 'isNotEditableCredit', 'WRITE', 'ALLOW', 'ROLE', 'financialBoss'),
('InvoiceOut', 'canCreatePdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'),
('Supplier', 'editPayMethodCheck', 'WRITE', 'ALLOW', 'ROLE', 'financial'),
('Worker', 'isTeamBoss', 'WRITE', 'ALLOW', 'ROLE', 'teamBoss'),
('Worker', 'forceIsSubordinate', 'READ', 'ALLOW', 'ROLE', 'hr'),
('Claim', 'editState', 'WRITE', 'ALLOW', 'ROLE', 'claimManager');
DELETE FROM `salix`.`ACL`
WHERE
model = 'Claim'
AND property = '*'
AND accessType = '*';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Claim', 'find', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'findById', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'findOne', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'getSummary', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'updateClaim', 'WRITE', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'regularizeClaim', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Claim', 'updateClaimDestination', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Claim', 'downloadFile', 'READ', 'ALLOW', 'ROLE', 'claimManager'),
('Claim', 'deleteById', 'WRITE', 'ALLOW', 'ROLE', 'claimManager'),
('Claim', 'filter', 'READ', 'ALLOW', 'ROLE', 'salesPerson'),
('Claim', 'logs', 'READ', 'ALLOW', 'ROLE', 'claimManager');
DELETE FROM `salix`.`ACL`
WHERE
model = 'Ticket'
AND property = '*'
AND accessType = '*';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Ticket', 'find', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'findById', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'findOne', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'getVolume', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'getTotalVolume', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'summary', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'priceDifference', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'componentUpdate', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'new', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'isEditable', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'setDeleted', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'restore', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'getSales', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'getSalesPersonMana', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'makeInvoice', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'updateEditableTicket', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'updateDiscount', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'transferSales', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'sendSms', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'isLocked', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'freightCost', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'getComponentsSum', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Ticket', 'updateAttributes', 'WRITE', 'ALLOW', 'ROLE', 'delivery'), -- Change Priority in Route tickets
('Ticket', 'deliveryNoteCsv', 'READ', 'ALLOW', 'ROLE', 'employee');
DELETE FROM `salix`.`ACL`
WHERE
model = 'State'
AND property = '*'
AND accessType = 'READ';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('State', 'find', 'READ', 'ALLOW', 'ROLE', 'employee'),
('State', 'findById', 'READ', 'ALLOW', 'ROLE', 'employee'),
('State', 'findOne', 'READ', 'ALLOW', 'ROLE', 'employee');
DELETE FROM `salix`.`ACL`
WHERE
model = 'Worker'
AND property = '*'
AND accessType = 'READ';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Worker', 'find', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'findById', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'findOne', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'getWorkedHours', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'active', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'activeWithRole', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'uploadFile', 'WRITE', 'ALLOW', 'ROLE', 'hr'),
('Worker', 'contracts', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'holidays', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'activeContract', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Worker', 'activeWithInheritedRole', 'READ', 'ALLOW', 'ROLE', 'employee');
DELETE FROM `salix`.`ACL`
WHERE model = 'Client'
AND property = 'updateUser'
AND accessType = '*';

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',
@ -625,6 +627,7 @@ export default {
selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check', selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check',
secondSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(2) vn-check[ng-model="sale.checked"]', secondSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(2) vn-check[ng-model="sale.checked"]',
thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[ng-model="sale.checked"]', thirdSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(3) vn-check[ng-model="sale.checked"]',
fourthSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(4) vn-check[ng-model="sale.checked"]',
deleteSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="delete"]', deleteSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="delete"]',
transferSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="call_split"]', transferSaleButton: 'vn-ticket-sale vn-tool-bar > vn-button[icon="call_split"]',
moveToTicketInput: 'form vn-input-number[ng-model="$ctrl.transfer.ticketId"] input', moveToTicketInput: 'form vn-input-number[ng-model="$ctrl.transfer.ticketId"] input',

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,12 +257,10 @@ 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);
await page.waitToClick(selectors.globalItems.acceptButton); await page.waitToClick(selectors.globalItems.acceptButton);
await page.waitToClick(selectors.globalItems.acceptButton);
await page.waitForState('claim.card.basicData'); await page.waitForState('claim.card.basicData');
}); });
@ -317,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

@ -17,7 +17,7 @@ describe('Claim summary path', () => {
}); });
it('should navigate to the target claim summary section', async() => { it('should navigate to the target claim summary section', async() => {
await page.loginAndModule('employee', 'claim'); await page.loginAndModule('salesPerson', 'claim');
await page.accessToSearchResult(claimId); await page.accessToSearchResult(claimId);
await page.waitForState('claim.card.summary'); await page.waitForState('claim.card.summary');
}); });
@ -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

@ -16,7 +16,7 @@ describe('Claim descriptor path', () => {
}); });
it('should now navigate to the target claim summary section', async() => { it('should now navigate to the target claim summary section', async() => {
await page.loginAndModule('employee', 'claim'); await page.loginAndModule('salesPerson', 'claim');
await page.accessToSearchResult(claimId); await page.accessToSearchResult(claimId);
await page.waitForState('claim.card.summary'); await page.waitForState('claim.card.summary');
}); });

View File

@ -7,11 +7,13 @@ vn-descriptor-content {
.photo { .photo {
position: relative; position: relative;
width: 100%;
text-align: center;
overflow: hidden;
& > img[ng-src] { & > img[ng-src] {
min-height: 16em; min-height: 16em;
display: block; display: block;
max-width: 100%;
height: 256px; height: 256px;
} }

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

@ -171,5 +171,7 @@
"Added observation": "Added observation", "Added observation": "Added observation",
"Comment added to client": "Comment added to client", "Comment added to client": "Comment added to client",
"This ticket is already a refund": "This ticket is already a refund", "This ticket is already a refund": "This ticket is already a refund",
"A claim with that sale already exists": "A claim with that sale already exists" "A claim with that sale already exists": "A claim with that sale already exists",
"Can't transfer claimed sales": "Can't transfer claimed sales",
"Invalid quantity": "Invalid quantity"
} }

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

@ -59,12 +59,14 @@ module.exports = Self => {
const landedPlusWeek = new Date(ticket.landed); const landedPlusWeek = new Date(ticket.landed);
landedPlusWeek.setDate(landedPlusWeek.getDate() + 7); landedPlusWeek.setDate(landedPlusWeek.getDate() + 7);
const hasClaimManagerRole = await models.VnUser.hasRole(userId, 'claimManager', myOptions);
const isClaimable = landedPlusWeek >= Date.vnNew(); const isClaimable = landedPlusWeek >= Date.vnNew();
const canCreateClaimAfterDeadline =
await models.ACL.checkAccessAcl(ctx, 'Claim', 'createAfterDeadline', 'WRITE');
if (ticket.isDeleted) if (ticket.isDeleted)
throw new UserError(`You can't create a claim for a removed ticket`); throw new UserError(`You can't create a claim for a removed ticket`);
if (!isClaimable && !hasClaimManagerRole) if (!isClaimable && !canCreateClaimAfterDeadline)
throw new UserError(`You can't create a claim from a ticket delivered more than seven days ago`); throw new UserError(`You can't create a claim from a ticket delivered more than seven days ago`);
const newClaim = await Self.create({ const newClaim = await Self.create({

View File

@ -46,7 +46,6 @@ module.exports = Self => {
Self.updateClaim = async(ctx, id, options) => { Self.updateClaim = async(ctx, id, options) => {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const args = ctx.args; const args = ctx.args;
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -81,9 +80,9 @@ module.exports = Self => {
if (args.claimStateFk) { if (args.claimStateFk) {
const canEditOldState = await models.ClaimState.isEditable(ctx, claim.claimStateFk, myOptions); const canEditOldState = await models.ClaimState.isEditable(ctx, claim.claimStateFk, myOptions);
const canEditNewState = await models.ClaimState.isEditable(ctx, args.claimStateFk, myOptions); const canEditNewState = await models.ClaimState.isEditable(ctx, args.claimStateFk, myOptions);
const isClaimManager = await models.VnUser.hasRole(userId, 'claimManager', myOptions); const canEditState = await models.ACL.checkAccessAcl(ctx, 'Claim', 'editState', 'WRITE');
if (!canEditOldState || !canEditNewState || changedHasToPickUp && !isClaimManager) if (!canEditOldState || !canEditNewState || changedHasToPickUp && !canEditState)
throw new UserError(`You don't have enough privileges to change that field`); throw new UserError(`You don't have enough privileges to change that field`);
} }

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

@ -87,15 +87,15 @@ module.exports = function(Self) {
Self.updateAddress = async(ctx, clientId, addressId, options) => { Self.updateAddress = async(ctx, clientId, addressId, options) => {
const models = Self.app.models; const models = Self.app.models;
const args = ctx.args; const args = ctx.args;
const userId = ctx.req.accessToken.userId;
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const isSalesAssistant = await models.VnUser.hasRole(userId, 'salesAssistant', myOptions); const canEditAddressLogifloraAllowed =
await models.ACL.checkAccessAcl(ctx, 'Client', 'editAddressLogifloraAllowed');
if (args.isLogifloraAllowed && !isSalesAssistant) if (args.isLogifloraAllowed && !canEditAddressLogifloraAllowed)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
const address = await models.Address.findOne({ const address = await models.Address.findOne({

View File

@ -131,9 +131,10 @@ module.exports = Self => {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
try { try {
const isSalesAssistant = await models.VnUser.hasRole(userId, 'salesAssistant', myOptions); const canEditNotTaxDataChecked =
await models.ACL.checkAccessAcl(ctx, 'Client', 'editFiscalDataWithoutTaxDataCheck', 'WRITE');
const client = await models.Client.findById(clientId, null, myOptions); const client = await models.Client.findById(clientId, null, myOptions);
if (!isSalesAssistant && client.isTaxDataChecked) if (!canEditNotTaxDataChecked && client.isTaxDataChecked)
throw new UserError(`Not enough privileges to edit a client with verified data`); throw new UserError(`Not enough privileges to edit a client with verified data`);
// Sage data validation // Sage data validation
const taxDataChecked = args.isTaxDataChecked; const taxDataChecked = args.isTaxDataChecked;

View File

@ -2,6 +2,7 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateUser', { Self.remoteMethodCtx('updateUser', {
description: 'Updates the user information', description: 'Updates the user information',
accessType: 'WRITE',
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',
@ -32,7 +33,6 @@ module.exports = Self => {
Self.updateUser = async function(ctx, id, options) { Self.updateUser = async function(ctx, id, options) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -45,9 +45,8 @@ module.exports = Self => {
} }
try { try {
const isSalesPerson = await models.VnUser.hasRole(userId, 'salesPerson', myOptions); const canEdit = await models.ACL.checkAccessAcl(ctx, 'Client', 'updateUser', 'WRITE');
if (!canEdit)
if (!isSalesPerson)
throw new UserError(`Not enough privileges to edit a client`); throw new UserError(`Not enough privileges to edit a client`);
const isClient = await models.Client.findById(id, null, myOptions); const isClient = await models.Client.findById(id, null, myOptions);

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

@ -218,9 +218,9 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const loopBackContext = LoopBackContext.getCurrentContext(); const loopBackContext = LoopBackContext.getCurrentContext();
const userId = loopBackContext.active.accessToken.userId; const accessToken = {req: loopBackContext.active.accessToken};
const isSalesAssistant = await models.VnUser.hasRole(userId, 'salesAssistant', ctx.options); const editVerifiedDataWithoutTaxDataChecked = models.ACL.checkAccessAcl(accessToken, 'Client', 'editVerifiedDataWithoutTaxDataCheck', 'WRITE');
const hasChanges = orgData && changes; const hasChanges = orgData && changes;
const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked); const isTaxDataChecked = hasChanges && (changes.isTaxDataChecked || orgData.isTaxDataChecked);
@ -232,8 +232,8 @@ module.exports = Self => {
const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk); const sageTransactionType = hasChanges && (changes.sageTransactionTypeFk || orgData.sageTransactionTypeFk);
const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType; const sageTransactionTypeChanged = hasChanges && orgData.sageTransactionTypeFk != sageTransactionType;
const cantEditVerifiedData = isTaxDataCheckedChanged && !isSalesAssistant; const cantEditVerifiedData = isTaxDataCheckedChanged && !editVerifiedDataWithoutTaxDataChecked;
const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !isSalesAssistant; const cantChangeSageData = (sageTaxTypeChanged || sageTransactionTypeChanged) && !editVerifiedDataWithoutTaxDataChecked;
if (cantEditVerifiedData || cantChangeSageData) if (cantEditVerifiedData || cantChangeSageData)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
@ -401,9 +401,10 @@ module.exports = Self => {
Self.changeCredit = async function changeCredit(ctx, finalState, changes) { Self.changeCredit = async function changeCredit(ctx, finalState, changes) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const accessToken = {req: {accessToken: ctx.options.accessToken} };
const isFinancialBoss = await models.VnUser.hasRole(userId, 'financialBoss', ctx.options); const canEditCredit = await models.ACL.checkAccessAcl(accessToken, 'Client', 'editCredit', 'WRITE');
if (!isFinancialBoss) { if (!canEditCredit) {
const lastCredit = await models.ClientCredit.findOne({ const lastCredit = await models.ClientCredit.findOne({
where: { where: {
clientFk: finalState.id clientFk: finalState.id
@ -412,10 +413,9 @@ module.exports = Self => {
}, ctx.options); }, ctx.options);
const lastAmount = lastCredit && lastCredit.amount; const lastAmount = lastCredit && lastCredit.amount;
const lastWorkerId = lastCredit && lastCredit.workerFk; const lastCreditIsNotEditable = !await models.ACL.checkAccessAcl(accessToken, 'Client', 'isNotEditableCredit', 'WRITE');
const lastWorkerIsFinancialBoss = await models.VnUser.hasRole(lastWorkerId, 'financialBoss', ctx.options);
if (lastAmount == 0 && lastWorkerIsFinancialBoss) if (lastAmount == 0 && lastCreditIsNotEditable)
throw new UserError(`You can't change the credit set to zero from a financialBoss`); throw new UserError(`You can't change the credit set to zero from a financialBoss`);
const creditLimits = await models.ClientCreditLimit.find({ const creditLimits = await models.ClientCreditLimit.find({

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

@ -2,6 +2,11 @@
vn-id="watcher" vn-id="watcher"
data="$ctrl.dms"> data="$ctrl.dms">
</vn-watcher> </vn-watcher>
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form <form
name="form" name="form"
ng-submit="$ctrl.onSubmit()" ng-submit="$ctrl.onSubmit()"
@ -29,7 +34,7 @@
<vn-autocomplete vn-one required="true" <vn-autocomplete vn-one required="true"
label="Warehouse" label="Warehouse"
ng-model="$ctrl.dms.warehouseId" ng-model="$ctrl.dms.warehouseId"
url="Warehouses" data="warehouses"
show-field="name" show-field="name"
value-field="id"> value-field="id">
</vn-autocomplete> </vn-autocomplete>

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,6 +5,11 @@
form="form" form="form"
save="patch"> save="patch">
</vn-watcher> </vn-watcher>
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md"> <form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
@ -152,14 +157,14 @@
<vn-autocomplete <vn-autocomplete
label="Warehouse Out" label="Warehouse Out"
ng-model="$ctrl.travelFilterParams.warehouseOutFk" ng-model="$ctrl.travelFilterParams.warehouseOutFk"
url="Warehouses" data="warehouses"
show-field="name" show-field="name"
value-field="id"> value-field="id">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
label="Warehouse In" label="Warehouse In"
ng-model="$ctrl.travelFilterParams.warehouseInFk" ng-model="$ctrl.travelFilterParams.warehouseInFk"
url="Warehouses" data="warehouses"
show-field="name" show-field="name"
value-field="id"> value-field="id">
</vn-autocomplete> </vn-autocomplete>

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

@ -25,7 +25,6 @@ module.exports = Self => {
Self.createPdf = async function(ctx, id, options) { Self.createPdf = async function(ctx, id, options) {
const models = Self.app.models; const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
if (process.env.NODE_ENV == 'test') if (process.env.NODE_ENV == 'test')
throw new UserError(`Action not allowed on the test environment`); throw new UserError(`Action not allowed on the test environment`);
@ -43,9 +42,9 @@ module.exports = Self => {
try { try {
const invoiceOut = await Self.findById(id, null, myOptions); const invoiceOut = await Self.findById(id, null, myOptions);
const hasInvoicing = await models.VnUser.hasRole(userId, 'invoicing', myOptions); const canCreatePdf = await models.ACL.checkAccessAcl(ctx, 'InvoiceOut', 'canCreatePdf', 'WRITE');
if (invoiceOut.hasPdf && !hasInvoicing) if (invoiceOut.hasPdf && !canCreatePdf)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
await invoiceOut.updateAttributes({ await invoiceOut.updateAttributes({

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', arg: 'ref',
type: 'string', type: 'string',
description: 'The invoice reference' 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,11 +110,6 @@
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-id="refundConfirmation"
on-accept="$ctrl.refundInvoiceOut()"
question="Are you sure you want to refund this invoice?">
</vn-confirm>
<vn-client-descriptor-popover <vn-client-descriptor-popover
vn-id="clientDescriptor"> vn-id="clientDescriptor">
</vn-client-descriptor-popover> </vn-client-descriptor-popover>

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

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

@ -1,3 +1,8 @@
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<vn-descriptor-content <vn-descriptor-content
module="item" module="item"
description="$ctrl.item.name" description="$ctrl.item.name"
@ -108,7 +113,9 @@
<vn-autocomplete <vn-autocomplete
label="Warehouse" label="Warehouse"
ng-model="$ctrl.warehouseFk" ng-model="$ctrl.warehouseFk"
url="Warehouses"> data="warehouses"
show-field="name"
value="id">
</vn-autocomplete> </vn-autocomplete>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>

View File

@ -1,4 +1,9 @@
<div class="search-panel"> <div class="search-panel">
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form id="manifold-form" ng-submit="$ctrl.onSearch()"> <form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg"> <vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield <vn-textfield
@ -35,7 +40,9 @@
vn-one vn-one
label="Warehouse" label="Warehouse"
ng-model="filter.warehouseFk" ng-model="filter.warehouseFk"
url="Warehouses"> data="warehouses"
show-field="name"
value-field="id">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal class="vn-px-lg"> <vn-horizontal class="vn-px-lg">

View File

@ -1,4 +1,9 @@
<div class="search-panel"> <div class="search-panel">
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form id="manifold-form" ng-submit="$ctrl.onSearch()"> <form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg"> <vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield <vn-textfield
@ -83,7 +88,7 @@
vn-one vn-one
label="Warehouse" label="Warehouse"
ng-model="filter.warehouseFk" ng-model="filter.warehouseFk"
url="Warehouses"> data="warehouses">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
vn-one vn-one

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

@ -104,7 +104,7 @@
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>

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

@ -1,4 +1,9 @@
<div class="search-panel"> <div class="search-panel">
<vn-crud-model
auto-load="true"
url="Agencies"
data="agencies">
</vn-crud-model>
<form id="manifold-form" ng-submit="$ctrl.onSearch()"> <form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg"> <vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield <vn-textfield
@ -17,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

@ -1,4 +1,9 @@
<div class="search-panel"> <div class="search-panel">
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses">
</vn-crud-model>
<form id="manifold-form" ng-submit="$ctrl.onSearch()"> <form id="manifold-form" ng-submit="$ctrl.onSearch()">
<vn-horizontal class="vn-px-lg vn-pt-lg"> <vn-horizontal class="vn-px-lg vn-pt-lg">
<vn-textfield <vn-textfield
@ -79,7 +84,7 @@
vn-one vn-one
label="Warehouse" label="Warehouse"
ng-model="filter.warehouseFk" ng-model="filter.warehouseFk"
url="Warehouses" data="warehouses"
show-field="name" show-field="name"
value-field="id"> value-field="id">
</vn-autocomplete> </vn-autocomplete>

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
}, },

View File

@ -5,54 +5,5 @@
"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'
@ -99,18 +100,20 @@ module.exports = Self => {
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
if (ctx.isNewInstance) return; if (ctx.isNewInstance) return;
const loopbackContext = LoopBackContext.getCurrentContext();
const changes = ctx.data || ctx.instance; const changes = ctx.data || ctx.instance;
const orgData = ctx.currentInstance; const orgData = ctx.currentInstance;
const userId = loopbackContext.active.accessToken.userId; const loopBackContext = LoopBackContext.getCurrentContext();
const accessToken = {req: loopBackContext.active.accessToken};
const editPayMethodCheck =
await Self.app.models.ACL.checkAccessAcl(accessToken, 'Supplier', 'editPayMethodCheck', 'WRITE');
const isNotFinancial = !await Self.app.models.VnUser.hasRole(userId, 'financial');
const isPayMethodChecked = changes.isPayMethodChecked || orgData.isPayMethodChecked; const isPayMethodChecked = changes.isPayMethodChecked || orgData.isPayMethodChecked;
const hasChanges = orgData && changes; const hasChanges = orgData && changes;
const isPayMethodCheckedChanged = hasChanges const isPayMethodCheckedChanged = hasChanges
&& orgData.isPayMethodChecked != isPayMethodChecked; && orgData.isPayMethodChecked != isPayMethodChecked;
if (isNotFinancial && isPayMethodCheckedChanged) if (!editPayMethodCheck && isPayMethodCheckedChanged)
throw new UserError('You can not modify is pay method checked'); throw new UserError('You can not modify is pay method checked');
}); });

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