Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4658-createWorker

This commit is contained in:
Alex Moreno 2022-11-11 12:48:32 +01:00
commit 0f2c0fc490
314 changed files with 16129 additions and 17194 deletions

12
.vscode/settings.json vendored
View File

@ -2,7 +2,13 @@
{ {
// Carácter predeterminado de final de línea. // Carácter predeterminado de final de línea.
"files.eol": "\n", "files.eol": "\n",
"editor.codeActionsOnSave": { "editor.bracketPairColorization.enabled": true,
"source.fixAll.eslint": true "editor.guides.bracketPairs": true,
} "editor.formatOnSave": true,
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": [
"javascript",
"json"
]
} }

View File

@ -1,32 +1,43 @@
FROM debian:stretch-slim FROM debian:bullseye-slim
ENV TZ Europe/Madrid ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
# NodeJs
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
gnupg2 \ gnupg2 \
libfontconfig lftp \ && curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \ && apt-get install -y --no-install-recommends nodejs \
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ && npm install -g npm@8.19.2
libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ # Puppeteer
libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& curl -sL https://deb.nodesource.com/setup_14.x | bash - \ RUN apt-get update \
&& apt-get install -y --no-install-recommends \ && apt-get install -y --no-install-recommends \
nodejs \ libfontconfig lftp xvfb gconf-service libasound2 libatk1.0-0 libc6 \
&& apt-get purge -y --auto-remove \ libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 \
gnupg2 \ libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 \
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2 && npm -g install pm2
# Salix
WORKDIR /salix WORKDIR /salix
COPY print/package.json print/package-lock.json print/
RUN npm --prefix ./print install --omit=dev ./print
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
COPY loopback/package.json loopback/ COPY loopback/package.json loopback/
COPY print/package.json print/ RUN npm install --omit=dev
RUN npm install --only=prod
RUN npm --prefix ./print install --only=prod ./print
COPY loopback loopback COPY loopback loopback
COPY back back COPY back back

View File

@ -47,20 +47,22 @@ module.exports = Self => {
for (let dms of dmsToDelete) { for (let dms of dmsToDelete) {
const pathHash = DmsContainer.getHash(dms.id); const pathHash = DmsContainer.getHash(dms.id);
const dmsContainer = await DmsContainer.container(pathHash); const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
try { try {
const dstFile = path.join(dmsContainer.client.root, pathHash, dms.file);
await fs.unlink(dstFile); await fs.unlink(dstFile);
} catch (err) { } catch (err) {
continue; if (err.code != 'ENOENT')
throw err;
} }
await dms.destroy(myOptions);
const dstFolder = path.join(dmsContainer.client.root, pathHash); const dstFolder = path.join(dmsContainer.client.root, pathHash);
try { try {
await fs.rmdir(dstFolder); await fs.rmdir(dstFolder);
} catch (err) { } catch (err) {
continue; continue;
} }
await dms.destroy(myOptions);
} }
}; };
}; };

View File

@ -1,12 +1,13 @@
const jsdom = require('jsdom'); const jsdom = require('jsdom');
const mysql = require('mysql'); const mysql = require('mysql');
const FormData = require('form-data');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('closeTicket', { Self.remoteMethodCtx('closeTicket', {
description: 'Close tickets without response from the user', description: 'Close tickets without response from the user',
accessType: 'READ', accessType: 'READ',
returns: { returns: {
type: 'Object', type: 'object',
root: true root: true
}, },
http: { http: {
@ -54,9 +55,9 @@ module.exports = Self => {
}); });
}); });
await requestToken(); await getRequestToken();
async function requestToken() { async function getRequestToken() {
const response = await fetch(ostUri); const response = await fetch(ostUri);
const result = response.headers.get('set-cookie'); const result = response.headers.get('set-cookie');
@ -93,24 +94,45 @@ module.exports = Self => {
await close(token, secondCookie); await close(token, secondCookie);
} }
async function getLockCode(token, secondCookie, ticketId) {
const ostUri = `${config.host}/ajax.php/lock/ticket/${ticketId}`;
const params = {
method: 'POST',
headers: {
'X-CSRFToken': token,
'Cookie': secondCookie
}
};
const response = await fetch(ostUri, params);
const body = await response.text();
const json = JSON.parse(body);
return json.code;
}
async function close(token, secondCookie) { async function close(token, secondCookie) {
for (const ticketId of ticketsId) { for (const ticketId of ticketsId) {
const ostUri = `${config.host}/ajax.php/tickets/${ticketId}/status`; const lockCode = await getLockCode(token, secondCookie, ticketId);
const data = { let form = new FormData();
status_id: config.newStatusId, form.append('__CSRFToken__', token);
comments: config.comment, form.append('id', ticketId);
undefined: config.action form.append('a', config.responseType);
}; form.append('lockCode', lockCode);
form.append('from_email_id', config.fromEmailId);
form.append('reply-to', config.replyTo);
form.append('cannedResp', 0);
form.append('response', config.comment);
form.append('signature', 'none');
form.append('reply_status_id', config.newStatusId);
const ostUri = `${config.host}/tickets.php?id=${ticketId}`;
const params = { const params = {
method: 'POST', method: 'POST',
body: new URLSearchParams(data), body: form,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-CSRFToken': token,
'Cookie': secondCookie 'Cookie': secondCookie
} }
}; };
return fetch(ostUri, params); return fetch(ostUri, params);
} }
} }

View File

@ -27,9 +27,6 @@
"newStatusId": { "newStatusId": {
"type": "number" "type": "number"
}, },
"action": {
"type": "string"
},
"day": { "day": {
"type": "number" "type": "number"
}, },
@ -47,6 +44,15 @@
}, },
"portDb": { "portDb": {
"type": "number" "type": "number"
},
"responseType": {
"type": "string"
},
"fromEmailId": {
"type": "number"
},
"replyTo": {
"type": "string"
} }
} }
} }

View File

@ -1,5 +0,0 @@
ALTER TABLE `vn`.`itemType` CHANGE `transaction` transaction__ tinyint(4) DEFAULT 0 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE location location__ varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;
ALTER TABLE `vn`.`itemType` CHANGE hasComponents hasComponents__ tinyint(1) DEFAULT 1 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE warehouseFk warehouseFk__ smallint(6) unsigned DEFAULT 60 NOT NULL;
ALTER TABLE `vn`.`itemType` CHANGE compression compression__ decimal(5,2) DEFAULT 1.00 NULL;

View File

@ -13,8 +13,4 @@ CREATE TABLE `vn`.`osTicketConfig` (
`passwordDb` varchar(100) COLLATE utf8mb3_unicode_ci DEFAULT NULL, `passwordDb` varchar(100) COLLATE utf8mb3_unicode_ci DEFAULT NULL,
`portDb` int(11) DEFAULT NULL, `portDb` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `vn`.`osTicketConfig`(`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `action`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`)
VALUES
(0, 'https://cau.verdnatura.es/scp', NULL, NULL, 'open', 3, 'Cerrar', 60, 'Este CAU se ha cerrado automáticamente', NULL, NULL, NULL, NULL);

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Business', '*', '*', 'ALLOW', 'ROLE', 'hr');

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Sale', 'usesMana', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1,8 @@
ALTER TABLE `vn`.`osTicketConfig` DROP COLUMN `action`;
ALTER TABLE `vn`.`osTicketConfig` ADD responseType varchar(100) NULL;
ALTER TABLE `vn`.`osTicketConfig` ADD fromEmailId INT NULL;
ALTER TABLE `vn`.`osTicketConfig` ADD replyTo varchar(100) NULL;
UPDATE `vn`.`osTicketConfig`
SET responseType='reply', fromEmailId=5, replyTo='all'
WHERE id=0;

View File

@ -0,0 +1,9 @@
CREATE TABLE `vn`.`zipConfig` (
`id` double(10,2) NOT NULL,
`maxSize` int(11) DEFAULT NULL COMMENT 'in MegaBytes',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('ZipConfig', '*', '*', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`claimConfig` DROP COLUMN `pickupContact`;

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ItemShelving', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('ItemShelving', '*', 'WRITE', 'ALLOW', 'ROLE', 'production');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('ItemShelvingPlacementSupplyStock', '*', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`workerTimeControlMail` CHANGE emailResponse reason text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL NULL;

View File

@ -0,0 +1,54 @@
DROP PROCEDURE IF EXISTS `vn`.`zone_getPostalCode`;
DELIMITER $$
$$
CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`zone_getPostalCode`(vSelf INT)
BEGIN
/**
* Devuelve los códigos postales incluidos en una zona
*/
DECLARE vGeoFk INT DEFAULT NULL;
DROP TEMPORARY TABLE IF EXISTS tmp.zoneNodes;
CREATE TEMPORARY TABLE tmp.zoneNodes (
geoFk INT,
name VARCHAR(100),
parentFk INT,
sons INT,
isChecked BOOL DEFAULT 0,
zoneFk INT,
PRIMARY KEY zoneNodesPk (zoneFk, geoFk),
INDEX(geoFk))
ENGINE = MEMORY;
CALL zone_getLeaves2(vSelf, NULL , NULL);
UPDATE tmp.zoneNodes zn
SET isChecked = 0
WHERE parentFk IS NULL;
myLoop: LOOP
SET vGeoFk = NULL;
SELECT geoFk INTO vGeoFk
FROM tmp.zoneNodes zn
WHERE NOT isChecked
LIMIT 1;
CALL zone_getLeaves2(vSelf, vGeoFk, NULL);
UPDATE tmp.zoneNodes
SET isChecked = TRUE
WHERE geoFk = vGeoFk;
IF vGeoFk IS NULL THEN
LEAVE myLoop;
END IF;
END LOOP;
DELETE FROM tmp.zoneNodes
WHERE sons > 0;
SELECT zn.geoFk, zn.name
FROM tmp.zoneNodes zn
JOIN zone z ON z.id = zn.zoneFk;
END$$
DELIMITER ;

View File

View File

@ -47,12 +47,8 @@ module.exports = class Docker {
if (ci) network = `--network="${networkName}"`; if (ci) network = `--network="${networkName}"`;
log('Starting container...'); log('Starting container...');
const container = await this.execP(` const container = await this.execP(
docker run \ `docker run ${network} --env RUN_CHOWN=${runChown} -d ${dockerArgs} salix-db`);
${network} \
--env RUN_CHOWN=${runChown} \
-d ${dockerArgs} salix-db
`);
this.id = container.stdout.trim(); this.id = container.stdout.trim();
try { try {

View File

@ -353,48 +353,48 @@ INSERT INTO `vn`.`clientConfig`(`riskTolerance`, `maxCreditRows`)
INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`) INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `phone`, `mobile`, `isActive`, `clientFk`, `agencyModeFk`, `longitude`, `latitude`, `isEqualizated`, `isDefaultAddress`)
VALUES VALUES
(1, 'Bruce Wayne', '1007 Mountain Drive, Gotham', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 1), (1, 'Bruce Wayne', '1007 Mountain Drive, Gotham', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 1),
(2, 'Petter Parker', '20 Ingram Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 1), (2, 'Petter Parker', '20 Ingram Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 1),
(3, 'Clark Kent', '344 Clinton Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 1), (3, 'Clark Kent', '344 Clinton Street', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 1),
(4, 'Tony Stark', '10880 Malibu Point', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 1), (4, 'Tony Stark', '10880 Malibu Point', 'Gotham', 46460, 1, 1111111111, 222222222, 1 , 1104, 2, NULL, NULL, 0, 1),
(5, 'Max Eisenhardt', 'Unknown Whereabouts', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 1), (5, 'Max Eisenhardt', 'Unknown Whereabouts', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 1),
(6, 'DavidCharlesHaller', 'Evil hideout', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 1), (6, 'DavidCharlesHaller', 'Evil hideout', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 1),
(7, 'Hank Pym', 'Anthill', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 1), (7, 'Hank Pym', 'Anthill', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 1),
(8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1), (8, 'Charles Xavier', '3800 Victory Pkwy, Cincinnati, OH 45207, USA', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 1),
(9, 'Bruce Banner', 'Somewhere in New York', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 1), (9, 'Bruce Banner', 'Somewhere in New York', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 1),
(10, 'Jessica Jones', 'NYCC 2015 Poster', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 1), (10, 'Jessica Jones', 'NYCC 2015 Poster', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 1),
(11, 'Missing', 'The space', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1111, 10, NULL, NULL, 0, 1), (11, 'Missing', 'The space', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1111, 10, NULL, NULL, 0, 1),
(12, 'Trash', 'New York city', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1112, 10, NULL, NULL, 0, 1), (12, 'Trash', 'New York city', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1112, 10, NULL, NULL, 0, 1),
(101, 'Somewhere in Thailand', 'address 01', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (101, 'Somewhere in Thailand', 'address 01', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(102, 'Somewhere in Poland', 'address 02', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0), (102, 'Somewhere in Poland', 'address 02', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(103, 'Somewhere in Japan', 'address 03', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0), (103, 'Somewhere in Japan', 'address 03', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(104, 'Somewhere in Spain', 'address 04', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0), (104, 'Somewhere in Spain', 'address 04', 'Gotham', 46460, 1, 3333333333, 444444444, 1, 1109, 2, NULL, NULL, 0, 0),
(105, 'Somewhere in Potugal', 'address 05', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0), (105, 'Somewhere in Potugal', 'address 05', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(106, 'Somewhere in UK', 'address 06', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0), (106, 'Somewhere in UK', 'address 06', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(107, 'Somewhere in Valencia', 'address 07', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0), (107, 'Somewhere in Valencia', 'address 07', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(108, 'Somewhere in Gotham', 'address 08', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0), (108, 'Somewhere in Gotham', 'address 08', 'Gotham', 46460, 1, 5555555555, 666666666, 1, 1109, 2, NULL, NULL, 0, 0),
(109, 'Somewhere in London', 'address 09', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (109, 'Somewhere in London', 'address 09', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(110, 'Somewhere in Algemesi', 'address 10', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (110, 'Somewhere in Algemesi', 'address 10', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(111, 'Somewhere in Carlet', 'address 11', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (111, 'Somewhere in Carlet', 'address 11', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(112, 'Somewhere in Campanar', 'address 12', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (112, 'Somewhere in Campanar', 'address 12', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(113, 'Somewhere in Malilla', 'address 13', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (113, 'Somewhere in Malilla', 'address 13', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(114, 'Somewhere in France', 'address 14', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (114, 'Somewhere in France', 'address 14', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(115, 'Somewhere in Birmingham', 'address 15', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (115, 'Somewhere in Birmingham', 'address 15', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(116, 'Somewhere in Scotland', 'address 16', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (116, 'Somewhere in Scotland', 'address 16', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(117, 'Somewhere in nowhere', 'address 17', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (117, 'Somewhere in nowhere', 'address 17', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(118, 'Somewhere over the rainbow', 'address 18', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (118, 'Somewhere over the rainbow', 'address 18', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(119, 'Somewhere in Alberic', 'address 19', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (119, 'Somewhere in Alberic', 'address 19', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(120, 'Somewhere in Montortal', 'address 20', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0), (120, 'Somewhere in Montortal', 'address 20', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1109, 2, NULL, NULL, 0, 0),
(121, 'the bat cave', 'address 21', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 0), (121, 'the bat cave', 'address 21', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1101, 2, NULL, NULL, 0, 0),
(122, 'NY roofs', 'address 22', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 0), (122, 'NY roofs', 'address 22', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1102, 2, NULL, NULL, 0, 0),
(123, 'The phone box', 'address 23', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 0), (123, 'The phone box', 'address 23', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1103, 2, NULL, NULL, 0, 0),
(124, 'Stark tower Gotham', 'address 24', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 0), (124, 'Stark tower Gotham', 'address 24', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1104, 2, NULL, NULL, 0, 0),
(125, 'The plastic cell', 'address 25', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 0), (125, 'The plastic cell', 'address 25', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1105, 2, NULL, NULL, 0, 0),
(126, 'Many places', 'address 26', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 0), (126, 'Many places', 'address 26', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1106, 2, NULL, NULL, 0, 0),
(127, 'Your pocket', 'address 27', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 0), (127, 'Your pocket', 'address 27', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1107, 2, NULL, NULL, 0, 0),
(128, 'Cerebro', 'address 28', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0), (128, 'Cerebro', 'address 28', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Gotham', 46460, 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 0), (129, 'Luke Cages Bar', 'address 29', 'Gotham', 'EC170150', 1, 1111111111, 222222222, 1, 1110, 2, NULL, NULL, 0, 0),
(130, 'Non valid address', 'address 30', 'Gotham', 46460, 1, 1111111111, 222222222, 0, 1101, 2, NULL, NULL, 0, 0); (130, 'Non valid address', 'address 30', 'Gotham', 46460, 1, 1111111111, 222222222, 0, 1101, 2, NULL, NULL, 0, 0);
INSERT INTO `vn`.`address`( `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `isActive`, `clientFk`, `agencyModeFk`, `isDefaultAddress`) INSERT INTO `vn`.`address`( `nickname`, `street`, `city`, `postalCode`, `provinceFk`, `isActive`, `clientFk`, `agencyModeFk`, `isDefaultAddress`)
SELECT name, CONCAT(name, 'Street'), 'GOTHAM', 46460, 1, 1, id, 2, 1 SELECT name, CONCAT(name, 'Street'), 'GOTHAM', 46460, 1, 1, id, 2, 1
@ -918,7 +918,7 @@ INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`)
(3, 'Perdida', 'LOST'); (3, 'Perdida', 'LOST');
INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `isBox`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`) INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`)
VALUES VALUES
(1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'), (1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'),
(2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL), (2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL),
@ -1778,10 +1778,10 @@ INSERT INTO `vn`.`claimEnd`(`id`, `saleFk`, `claimFk`, `workerFk`, `claimDestina
(1, 31, 4, 21, 2), (1, 31, 4, 21, 2),
(2, 32, 3, 21, 3); (2, 32, 3, 21, 3);
INSERT INTO `vn`.`claimConfig`(`id`, `pickupContact`, `maxResponsibility`) INSERT INTO `vn`.`claimConfig`(`id`, `maxResponsibility`)
VALUES VALUES
(1, 'Contact description', 50), (1, 50),
(2, 'Contact description', 30); (2, 30);
INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRate`, `priceIncreasing`, `packingRate`) INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRate`, `priceIncreasing`, `packingRate`)
VALUES VALUES
@ -1791,7 +1791,7 @@ INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRa
(1104, 2500, 150.00, 0.02, 0.10, 1.00); (1104, 2500, 150.00, 0.02, 0.10, 1.00);
INSERT INTO vn.claimRma (`id`, `code`, `created`, `workerFk`) INSERT INTO vn.claimRma (`id`, `code`, `created`, `workerFk`)
VALUES VALUES
(1, '02676A049183', DEFAULT, 1106), (1, '02676A049183', DEFAULT, 1106),
(2, '02676A049183', DEFAULT, 1106), (2, '02676A049183', DEFAULT, 1106),
(3, '02676A049183', DEFAULT, 1107), (3, '02676A049183', DEFAULT, 1107),
@ -2047,6 +2047,7 @@ INSERT INTO `vn`.`zoneIncluded` (`zoneFk`, `geoFk`, `isIncluded`)
(8, 4, 0), (8, 4, 0),
(8, 5, 0), (8, 5, 0),
(8, 1, 1), (8, 1, 1),
(9, 7, 1),
(10, 14, 1); (10, 14, 1);
INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `dated`) INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `dated`)
@ -2266,12 +2267,16 @@ INSERT INTO `vn`.`zoneEvent`(`zoneFk`, `type`, `started`, `ended`)
VALUES VALUES
(9, 'range', DATE_ADD(util.VN_CURDATE(), INTERVAL -1 YEAR), DATE_ADD(util.VN_CURDATE(), INTERVAL +1 YEAR)); (9, 'range', DATE_ADD(util.VN_CURDATE(), INTERVAL -1 YEAR), DATE_ADD(util.VN_CURDATE(), INTERVAL +1 YEAR));
INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`) INSERT INTO `vn`.`workerTimeControl`(`userFk`, `timed`, `manual`, `direction`, `isSendMail`)
VALUES VALUES
(1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in'), (1106, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle'), (1106, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle'), (1106, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 0),
(1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out'); (1106, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 0),
(1107, CONCAT(util.VN_CURDATE(), ' 07:00'), TRUE, 'in', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 10:00'), TRUE, 'middle', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 10:20'), TRUE, 'middle', 1),
(1107, CONCAT(util.VN_CURDATE(), ' 14:50'), TRUE, 'out', 1);
INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `code`) INSERT INTO `vn`.`dmsType`(`id`, `name`, `path`, `readRoleFk`, `writeRoleFk`, `code`)
VALUES VALUES
@ -2710,3 +2715,7 @@ INSERT INTO `vn`.`ticketCollection` (`ticketFk`, `collectionFk`, `created`, `lev
UPDATE `account`.`user` UPDATE `account`.`user`
SET `hasGrant` = 1 SET `hasGrant` = 1
WHERE `id` = 66; WHERE `id` = 66;
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');

View File

@ -27518,7 +27518,7 @@ CREATE TABLE `expedition` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
`agencyModeFk` int(11) NOT NULL, `agencyModeFk` int(11) NOT NULL,
`ticketFk` int(10) NOT NULL, `ticketFk` int(10) NOT NULL,
`isBox` int(11) DEFAULT 1 COMMENT 'Este campo realmente en un campo itemFk, haciendo referencia al artículo que nos va a facturar el proveedor de transporte.\nSe debería llamar freightItemFk', `freightItemFk` int(11) DEFAULT 1 COMMENT 'Este campo realmente en un campo itemFk, haciendo referencia al artículo que nos va a facturar el proveedor de transporte.\nSe debería llamar freightItemFk',
`created` timestamp NULL DEFAULT current_timestamp(), `created` timestamp NULL DEFAULT current_timestamp(),
`isRefund__` bit(1) DEFAULT b'0' COMMENT 'Deprecado 01/06/2022', `isRefund__` bit(1) DEFAULT b'0' COMMENT 'Deprecado 01/06/2022',
`isPickUp__` bit(1) DEFAULT b'0' COMMENT 'Deprecado 01/06/2022', `isPickUp__` bit(1) DEFAULT b'0' COMMENT 'Deprecado 01/06/2022',
@ -27534,7 +27534,7 @@ CREATE TABLE `expedition` (
`hasNewRoute` bit(1) NOT NULL DEFAULT b'0', `hasNewRoute` bit(1) NOT NULL DEFAULT b'0',
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
KEY `index1` (`agencyModeFk`), KEY `index1` (`agencyModeFk`),
KEY `index2` (`isBox`), KEY `index2` (`freightItemFk`),
KEY `index3` (`created`), KEY `index3` (`created`),
KEY `index4` (`ticketFk`), KEY `index4` (`ticketFk`),
KEY `expedition_fk3_idx` (`packagingFk`), KEY `expedition_fk3_idx` (`packagingFk`),
@ -27567,7 +27567,7 @@ BEGIN
DECLARE vShipFk INT; DECLARE vShipFk INT;
IF NEW.isBox > 0 THEN IF NEW.freightItemFk > 0 THEN
UPDATE ticket SET packages = nz(packages) + 1 WHERE id = NEW.ticketFk; UPDATE ticket SET packages = nz(packages) + 1 WHERE id = NEW.ticketFk;
@ -27638,7 +27638,7 @@ DELIMITER ;;
BEGIN BEGIN
UPDATE ticket t UPDATE ticket t
SET packages = (SELECT COUNT(counter)-1 SET packages = (SELECT COUNT(counter)-1
FROM expedition e WHERE e.ticketFk = OLD.ticketFk and e.isBox) FROM expedition e WHERE e.ticketFk = OLD.ticketFk and e.freightItemFk)
WHERE t.id = OLD.ticketFk; WHERE t.id = OLD.ticketFk;
END */;; END */;;
@ -36287,7 +36287,7 @@ CREATE TABLE `sorter` (
`created` datetime NOT NULL, `created` datetime NOT NULL,
`routeFk` int(10) unsigned NOT NULL, `routeFk` int(10) unsigned NOT NULL,
`ticketFk` int(10) NOT NULL, `ticketFk` int(10) NOT NULL,
`isBox` int(11) DEFAULT 1, `freightItemFk` int(11) DEFAULT 1,
`itemFk` int(11) DEFAULT NULL, `itemFk` int(11) DEFAULT NULL,
`width` decimal(10,2) DEFAULT 0.00, `width` decimal(10,2) DEFAULT 0.00,
`depth` decimal(10,2) DEFAULT 0.00, `depth` decimal(10,2) DEFAULT 0.00,
@ -44956,7 +44956,7 @@ BEGIN
SELECT SUM((t.zonePrice - t.zoneBonus) * ebv.ratio) INTO deliveryPrice SELECT SUM((t.zonePrice - t.zoneBonus) * ebv.ratio) INTO deliveryPrice
FROM vn.ticket t FROM vn.ticket t
LEFT JOIN expedition e ON e.ticketFk = t.id LEFT JOIN expedition e ON e.ticketFk = t.id
JOIN expeditionBoxVol ebv ON ebv.boxFk = e.isBox JOIN expeditionBoxVol ebv ON ebv.boxFk = e.freightItemFk
WHERE t.id = vTicketFk; WHERE t.id = vTicketFk;
END IF; END IF;
@ -46492,7 +46492,7 @@ BEGIN
LEFT JOIN item i ON i.id = b.itemFk LEFT JOIN item i ON i.id = b.itemFk
LEFT JOIN itemType it ON it.id = i.typeFk LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN packaging p ON p.id = b.packageFk AND NOT p.isBox LEFT JOIN packaging p ON p.id = b.packageFk AND NOT p.freightItemFk
JOIN volumeConfig vc ON TRUE JOIN volumeConfig vc ON TRUE
WHERE b.id = vSelf; WHERE b.id = vSelf;
@ -53229,7 +53229,7 @@ BEGIN
INNER JOIN vn.ticketState ts ON ts.ticketFk = exp.ticketFk INNER JOIN vn.ticketState ts ON ts.ticketFk = exp.ticketFk
LEFT JOIN vn.address a ON t.addressFk = a.id LEFT JOIN vn.address a ON t.addressFk = a.id
LEFT JOIN vn.warehouse w ON t.warehouseFk = w.id LEFT JOIN vn.warehouse w ON t.warehouseFk = w.id
WHERE t.routeFk = vRouteFk AND exp.isBox > 0; WHERE t.routeFk = vRouteFk AND exp.freightItemFk > 0;
END ;; END ;;
DELIMITER ; DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ; /*!50003 SET sql_mode = @saved_sql_mode */ ;
@ -53760,7 +53760,7 @@ BEGIN
GROUP BY sub.ticketFk GROUP BY sub.ticketFk
) sub2 ON sub2.ticketFk = t.id ) sub2 ON sub2.ticketFk = t.id
LEFT JOIN expeditionStateType est ON est.id = e.stateTypeFk LEFT JOIN expeditionStateType est ON est.id = e.stateTypeFk
WHERE t.routeFk = vRouteFk AND e.isBox <> FALSE WHERE t.routeFk = vRouteFk AND e.freightItemFk <> FALSE
ORDER BY r.created, t.priority DESC; ORDER BY r.created, t.priority DESC;
END ;; END ;;
DELIMITER ; DELIMITER ;

View File

@ -1,37 +0,0 @@
const app = require('vn-loopback/server/server');
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
// #1885
xdescribe('order_confirmWithUser()', () => {
it('should confirm an order', async() => {
let stmts = [];
let stmt;
stmts.push('START TRANSACTION');
let params = {
orderFk: 10,
userId: 9
};
// problema: la funcion order_confirmWithUser tiene una transacción, por tanto esta nunca hace rollback
stmt = new ParameterizedSQL('CALL hedera.order_confirmWithUser(?, ?)', [
params.orderFk,
params.userId
]);
stmts.push(stmt);
stmt = new ParameterizedSQL('SELECT confirmed FROM hedera.order WHERE id = ?', [
params.orderFk
]);
let orderIndex = stmts.push(stmt) - 1;
stmts.push('ROLLBACK');
let sql = ParameterizedSQL.join(stmts, ';');
let result = await app.models.Ticket.rawStmt(sql);
savedDescription = result[orderIndex][0].confirmed;
expect(savedDescription).toBeTruthy();
});
});

View File

@ -394,11 +394,18 @@ export default {
intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]', intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Intrastat"]',
originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]', originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Origin"]',
buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]', buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Buyer"]',
densityCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Density"]',
openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]',
advancedSearchItemType: 'vn-item-search-panel vn-autocomplete[ng-model="filter.typeFk"]',
advancedSearchButton: 'vn-item-search-panel button[type=submit]',
advancedSmartTableButton: 'vn-item-index vn-button[icon="search"]',
advancedSmartTableGrouping: 'vn-item-index vn-textfield[name=grouping]',
weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]', weightByPieceCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check[label="Weight/Piece"]',
saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button' saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button'
}, },
itemFixedPrice: { itemFixedPrice: {
add: 'vn-fixed-price vn-icon-button[icon="add_circle"]', add: 'vn-fixed-price vn-icon-button[icon="add_circle"]',
firstItemID: 'vn-fixed-price tr:nth-child(2) vn-autocomplete[ng-model="price.itemFk"]',
fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)', fourthFixedPrice: 'vn-fixed-price tr:nth-child(5)',
fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]', fourthItemID: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.itemFk"]',
fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]', fourthWarehouse: 'vn-fixed-price tr:nth-child(5) vn-autocomplete[ng-model="price.warehouseFk"]',
@ -408,7 +415,8 @@ export default {
fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]', fourthMinPrice: 'vn-fixed-price tr:nth-child(5) > td:nth-child(6) > vn-input-number[ng-model="price.minPrice"]',
fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]', fourthStarted: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.started"]',
fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]', fourthEnded: 'vn-fixed-price tr:nth-child(5) vn-date-picker[ng-model="price.ended"]',
fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]' fourthDeleteIcon: 'vn-fixed-price tr:nth-child(5) > td:nth-child(9) > vn-icon-button[icon="delete"]',
orderColumnId: 'vn-fixed-price th[field="itemFk"]'
}, },
itemCreateView: { itemCreateView: {
temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]', temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]',
@ -596,7 +604,14 @@ export default {
submitNotesButton: 'button[type=submit]' submitNotesButton: 'button[type=submit]'
}, },
ticketExpedition: { ticketExpedition: {
thirdExpeditionRemoveButton: 'vn-ticket-expedition vn-table div > vn-tbody > vn-tr:nth-child(3) > vn-td:nth-child(1) > vn-icon-button[icon="delete"]', firstSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(1) vn-check[ng-model="expedition.checked"]',
thirdSaleCheckbox: 'vn-ticket-expedition vn-tr:nth-child(3) vn-check[ng-model="expedition.checked"]',
deleteExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="delete"]',
moveExpeditionButton: 'vn-ticket-expedition vn-tool-bar > vn-button[icon="keyboard_arrow_down"]',
moreMenuWithoutRoute: 'vn-item[name="withoutRoute"]',
moreMenuWithRoute: 'vn-item[name="withRoute"]',
newRouteId: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newRoute"]',
saveButton: '.vn-dialog.shown [response="accept"]',
expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr' expeditionRow: 'vn-ticket-expedition vn-table vn-tbody > vn-tr'
}, },
ticketPackages: { ticketPackages: {
@ -1100,7 +1115,8 @@ export default {
anyBuyLine: 'vn-entry-summary tr.dark-row' anyBuyLine: 'vn-entry-summary tr.dark-row'
}, },
entryBasicData: { entryBasicData: {
reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.ref"]', reference: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.reference"]',
invoiceNumber: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.invoiceNumber"]',
notes: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.notes"]', notes: 'vn-entry-basic-data vn-textfield[ng-model="$ctrl.entry.notes"]',
observations: 'vn-entry-basic-data vn-textarea[ng-model="$ctrl.entry.observation"]', observations: 'vn-entry-basic-data vn-textarea[ng-model="$ctrl.entry.observation"]',
supplier: 'vn-entry-basic-data vn-autocomplete[ng-model="$ctrl.entry.supplierFk"]', supplier: 'vn-entry-basic-data vn-autocomplete[ng-model="$ctrl.entry.supplierFk"]',

View File

@ -0,0 +1,77 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('SmartTable SearchBar integration', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('salesPerson', 'item');
await page.waitToClick(selectors.globalItems.searchButton);
});
afterAll(async() => {
await browser.close();
});
describe('as filters', () => {
it('should search by type in searchBar', async() => {
await page.waitToClick(selectors.itemsIndex.openAdvancedSearchButton);
await page.autocompleteSearch(selectors.itemsIndex.advancedSearchItemType, 'Anthurium');
await page.waitToClick(selectors.itemsIndex.advancedSearchButton);
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 3);
});
it('should search by grouping in smartTable', async() => {
await page.waitToClick(selectors.itemsIndex.advancedSmartTableButton);
await page.write(selectors.itemsIndex.advancedSmartTableGrouping, '1');
await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
it('should now reload page and have same results', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
await page.waitForNumberOfElements(selectors.itemsIndex.searchResult, 2);
});
});
describe('as orders', () => {
it('should order by first id', async() => {
await page.loginAndModule('developer', 'item');
await page.accessToSection('item.fixedPrice');
await page.doSearch();
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('1');
});
it('should order by last id', async() => {
await page.waitToClick(selectors.itemFixedPrice.orderColumnId);
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
it('should reload page and have same order', async() => {
await page.reload({
waitUntil: 'networkidle2'
});
const result = await page.waitToGetProperty(selectors.itemFixedPrice.firstItemID, 'value');
expect(result).toEqual('13');
});
});
});

View File

@ -18,7 +18,8 @@ describe('Ticket expeditions and log path', () => {
}); });
it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => { it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdExpeditionRemoveButton); await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.deleteExpeditionButton);
await page.waitToClick(selectors.globalItems.acceptButton); await page.waitToClick(selectors.globalItems.acceptButton);
await page.reloadSection('ticket.card.expedition'); await page.reloadSection('ticket.card.expedition');

View File

@ -0,0 +1,50 @@
import selectors from '../../helpers/selectors.js';
import getBrowser from '../../helpers/puppeteer';
describe('Ticket expeditions', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('production', 'ticket');
await page.accessToSearchResult('1');
await page.accessToSection('ticket.card.expedition');
});
afterAll(async() => {
await browser.close();
});
it(`should move one expedition to new ticket withoute route`, async() => {
await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithoutRoute);
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
it(`should move one expedition to new ticket with route`, async() => {
await page.waitToClick(selectors.ticketExpedition.firstSaleCheckbox);
await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton);
await page.waitToClick(selectors.ticketExpedition.moreMenuWithRoute);
await page.write(selectors.ticketExpedition.newRouteId, '1');
await page.waitToClick(selectors.ticketExpedition.saveButton);
await page.waitForState('ticket.card.summary');
await page.accessToSection('ticket.card.expedition');
await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {});
const result = await page
.countElement(selectors.ticketExpedition.expeditionRow);
expect(result).toEqual(1);
});
});

View File

@ -19,6 +19,7 @@ describe('Entry basic data path', () => {
it('should edit the basic data', async() => { it('should edit the basic data', async() => {
await page.write(selectors.entryBasicData.reference, 'new movement 8'); await page.write(selectors.entryBasicData.reference, 'new movement 8');
await page.write(selectors.entryBasicData.invoiceNumber, 'new movement 8');
await page.write(selectors.entryBasicData.notes, 'new notes'); await page.write(selectors.entryBasicData.notes, 'new notes');
await page.write(selectors.entryBasicData.observations, ' edited'); await page.write(selectors.entryBasicData.observations, ' edited');
await page.autocompleteSearch(selectors.entryBasicData.supplier, 'Plants nick'); await page.autocompleteSearch(selectors.entryBasicData.supplier, 'Plants nick');
@ -45,6 +46,13 @@ describe('Entry basic data path', () => {
expect(result).toEqual('new movement 8'); expect(result).toEqual('new movement 8');
}); });
it('should confirm the invoiceNumber was edited', async() => {
await page.reloadSection('entry.card.basicData');
const result = await page.waitToGetProperty(selectors.entryBasicData.invoiceNumber, 'value');
expect(result).toEqual('new movement 8');
});
it('should confirm the note was edited', async() => { it('should confirm the note was edited', async() => {
const result = await page.waitToGetProperty(selectors.entryBasicData.notes, 'value'); const result = await page.waitToGetProperty(selectors.entryBasicData.notes, 'value');

View File

@ -99,6 +99,18 @@ export default class CrudModel extends ModelProxy {
return this.refresh(); return this.refresh();
} }
/**
* Applies a new filter to the model.
*
* @param {Object} params Custom parameters
* @return {Promise} The request promise
*/
applyParams(params) {
this.userParams = params;
return this.refresh();
}
removeFilter() { removeFilter() {
return this.applyFilter(null, null); return this.applyFilter(null, null);
} }

View File

@ -3,7 +3,7 @@
ng-transclude="prepend" ng-transclude="prepend"
class="prepend"> class="prepend">
</div> </div>
<div class="infix"> <div class="infix" >
<div class="fix prefix"></div> <div class="fix prefix"></div>
<div class="control"> <div class="control">
<section <section
@ -13,8 +13,7 @@
</section> </section>
<input <input
type="file" type="file"
accept="{{$ctrl.accept}}" accept="{{$ctrl.accept}}">
style="display: none;">
</input> </input>
</div> </div>
<div class="fix suffix"></div> <div class="fix suffix"></div>

View File

@ -4,4 +4,13 @@
.value { .value {
cursor: pointer; cursor: pointer;
} }
.control {
& > input[type=file] {
opacity: 0;
}
& > section {
position: absolute;
bottom: 0;
}
}
} }

View File

@ -139,8 +139,12 @@ export default class Searchbar extends Component {
} }
removeParam(index) { removeParam(index) {
const field = this.params[index].key;
this.filterSanitizer(field);
this.params.splice(index, 1); this.params.splice(index, 1);
this.doSearch(this.fromBar(), 'bar'); this.toRemove = field;
this.doSearch(this.fromBar(), 'removeBar');
} }
fromBar() { fromBar() {
@ -163,7 +167,7 @@ export default class Searchbar extends Component {
let keys = Object.keys(filter); let keys = Object.keys(filter);
keys.forEach(key => { keys.forEach(key => {
if (key == 'search') return; if (key == 'search' || key == 'tableQ' || key == 'tableOrder') return;
let value = filter[key]; let value = filter[key];
let chip; let chip;
@ -198,6 +202,7 @@ export default class Searchbar extends Component {
let promise = this.onSearch({$params: filter}); let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve(); promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data)); promise.then(data => this.onFilter(filter, source, data));
this.toBar(filter);
} }
onFilter(filter, source, data) { onFilter(filter, source, data) {
@ -238,8 +243,11 @@ export default class Searchbar extends Component {
} else { } else {
state = this.searchState; state = this.searchState;
if (filter) if (filter) {
if (this.tableQ)
filter.tableQ = this.tableQ;
params = {q: JSON.stringify(filter)}; params = {q: JSON.stringify(filter)};
}
if (this.$state.is(state)) if (this.$state.is(state))
opts = {location: 'replace'}; opts = {location: 'replace'};
} }
@ -247,6 +255,12 @@ export default class Searchbar extends Component {
this.filter = filter; this.filter = filter;
if (source == 'removeBar') {
delete params[this.toRemove];
delete this.model.userParams[this.toRemove];
this.model.refresh();
}
if (!filter && this.model) if (!filter && this.model)
this.model.clear(); this.model.clear();
if (source != 'state') if (source != 'state')
@ -269,9 +283,14 @@ export default class Searchbar extends Component {
this.model.clear(); this.model.clear();
return; return;
} }
if (Object.keys(filter).length === 0) {
this.filterSanitizer('search');
if (this.model.userParams)
delete this.model.userParams['search'];
}
let where = null; let where = null;
let params = null; let params = {};
if (this.exprBuilder) { if (this.exprBuilder) {
where = buildFilter(filter, where = buildFilter(filter,
@ -283,9 +302,89 @@ export default class Searchbar extends Component {
params = this.fetchParams({$params: params}); params = this.fetchParams({$params: params});
} }
this.tableQ = null;
const hasParams = this.$params.q && Object.keys(JSON.parse(this.$params.q)).length;
if (hasParams) {
const stateFilter = JSON.parse(this.$params.q);
for (let param in stateFilter) {
if (param != 'tableQ' && param != 'orderQ')
this.filterSanitizer(param);
}
for (let param in this.suggestedFilter) {
this.filterSanitizer(param);
delete stateFilter[param];
}
this.tableQ = stateFilter.tableQ;
for (let param in stateFilter.tableQ)
params[param] = stateFilter.tableQ[param];
Object.assign(stateFilter, params);
return this.model.applyParams(params)
.then(() => this.model.data);
}
return this.model.applyFilter(where ? {where} : null, params) return this.model.applyFilter(where ? {where} : null, params)
.then(() => this.model.data); .then(() => this.model.data);
} }
filterSanitizer(field) {
if (!field) return;
const userFilter = this.model.userFilter;
const userParams = this.model.userParams;
const where = userFilter && userFilter.where;
if (this.model.userParams)
delete this.model.userParams[field];
if (this.exprBuilder) {
const param = this.exprBuilder({
param: field,
value: null
});
if (param) [field] = Object.keys(param);
}
if (!where) return;
const whereKeys = Object.keys(where);
for (let key of whereKeys) {
removeProp(where, field, key);
if (Object.keys(where).length == 0)
delete userFilter.where;
}
function removeProp(obj, targetProp, prop) {
if (prop == targetProp)
delete obj[prop];
if (prop === 'and' || prop === 'or' && obj[prop]) {
const arrayCopy = obj[prop].slice();
for (let param of arrayCopy) {
const [key] = Object.keys(param);
const index = obj[prop].findIndex(param => {
return Object.keys(param)[0] == key;
});
if (key == targetProp)
obj[prop].splice(index, 1);
if (param[key] instanceof Array)
removeProp(param, field, key);
if (Object.keys(param).length == 0)
obj[prop].splice(index, 1);
}
if (obj[prop].length == 0)
delete obj[prop];
}
}
return {userFilter, userParams};
}
} }
ngModule.vnComponent('vnSearchbar', { ngModule.vnComponent('vnSearchbar', {

View File

@ -6,7 +6,7 @@ describe('Component vnSearchbar', () => {
let $state; let $state;
let $params; let $params;
let $scope; let $scope;
let filter = {id: 1, search: 'needle'}; const filter = {id: 1, search: 'needle'};
beforeEach(ngModule('vnCore', $stateProvider => { beforeEach(ngModule('vnCore', $stateProvider => {
$stateProvider $stateProvider
@ -70,8 +70,8 @@ describe('Component vnSearchbar', () => {
describe('filter() setter', () => { describe('filter() setter', () => {
it(`should update the bar params and search`, () => { it(`should update the bar params and search`, () => {
let withoutHours = new Date(2000, 1, 1); const withoutHours = new Date(2000, 1, 1);
let withHours = new Date(withoutHours.getTime()); const withHours = new Date(withoutHours.getTime());
withHours.setHours(12, 30, 15, 10); withHours.setHours(12, 30, 15, 10);
controller.filter = { controller.filter = {
@ -83,8 +83,8 @@ describe('Component vnSearchbar', () => {
myObjectProp: {myProp: 1} myObjectProp: {myProp: 1}
}; };
let chips = {}; const chips = {};
for (let param of controller.params || []) for (const param of controller.params || [])
chips[param.key] = param.chip; chips[param.key] = param.chip;
expect(controller.searchString).toBe('needle'); expect(controller.searchString).toBe('needle');
@ -172,13 +172,22 @@ describe('Component vnSearchbar', () => {
describe('removeParam()', () => { describe('removeParam()', () => {
it(`should remove the parameter from the filter`, () => { it(`should remove the parameter from the filter`, () => {
jest.spyOn(controller, 'doSearch'); jest.spyOn(controller, 'doSearch');
controller.model = {
refresh: jest.fn(),
userParams: {
id: 1
}
};
controller.model.applyParams = jest.fn().mockReturnValue(Promise.resolve());
jest.spyOn(controller.model, 'applyParams');
controller.filter = filter; controller.filter = filter;
controller.removeParam(0); controller.removeParam(0);
expect(controller.doSearch).toHaveBeenCalledWith({ expect(controller.doSearch).toHaveBeenCalledWith({
search: 'needle' search: 'needle'
}, 'bar'); }, 'removeBar');
}); });
}); });
@ -199,7 +208,7 @@ describe('Component vnSearchbar', () => {
it(`should go to the summary state when one result`, () => { it(`should go to the summary state when one result`, () => {
jest.spyOn($state, 'go'); jest.spyOn($state, 'go');
let data = [{id: 1}]; const data = [{id: 1}];
controller.baseState = 'foo'; controller.baseState = 'foo';
controller.onFilter(filter, 'any', data); controller.onFilter(filter, 'any', data);
@ -214,7 +223,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply(); $scope.$apply();
jest.spyOn($state, 'go'); jest.spyOn($state, 'go');
let data = [{id: 1}]; const data = [{id: 1}];
controller.baseState = 'foo'; controller.baseState = 'foo';
controller.onFilter(filter, 'any', data); controller.onFilter(filter, 'any', data);
@ -229,7 +238,7 @@ describe('Component vnSearchbar', () => {
$scope.$apply(); $scope.$apply();
jest.spyOn($state, 'go'); jest.spyOn($state, 'go');
let data = [{id: 1}]; const data = [{id: 1}];
controller.baseState = 'foo'; controller.baseState = 'foo';
controller.onFilter(filter, 'any', data); controller.onFilter(filter, 'any', data);
@ -247,7 +256,7 @@ describe('Component vnSearchbar', () => {
controller.onFilter(filter, 'any'); controller.onFilter(filter, 'any');
$scope.$apply(); $scope.$apply();
let queryParams = {q: JSON.stringify(filter)}; const queryParams = {q: JSON.stringify(filter)};
expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined); expect($state.go).toHaveBeenCalledWith('search.state', queryParams, undefined);
expect(controller.filter).toEqual(filter); expect(controller.filter).toEqual(filter);

View File

@ -103,3 +103,4 @@
</div> </div>
</tpl-body> </tpl-body>
</vn-popover> </vn-popover>

View File

@ -15,9 +15,17 @@ export default class SmartTable extends Component {
this.$inputsScope; this.$inputsScope;
this.columns = []; this.columns = [];
this.autoSave = false; this.autoSave = false;
this.autoState = true;
this.transclude(); this.transclude();
} }
$onChanges() {
if (this.model) {
this.defaultFilter();
this.defaultOrder();
}
}
$onDestroy() { $onDestroy() {
const styleElement = document.querySelector('style[id="smart-table"]'); const styleElement = document.querySelector('style[id="smart-table"]');
if (this.$.css && styleElement) if (this.$.css && styleElement)
@ -47,10 +55,8 @@ export default class SmartTable extends Component {
set model(value) { set model(value) {
this._model = value; this._model = value;
if (value) { if (value)
this.$.model = value; this.$.model = value;
this.defaultOrder();
}
} }
getDefaultViewConfig() { getDefaultViewConfig() {
@ -160,8 +166,36 @@ export default class SmartTable extends Component {
} }
} }
defaultFilter() {
if (this.disabledTableFilter || !this.$params.q) return;
const stateFilter = JSON.parse(this.$params.q).tableQ;
if (!stateFilter || !this.exprBuilder) return;
const columns = this.columns.map(column => column.field);
this.displaySearch();
if (!this.$inputsScope.searchProps)
this.$inputsScope.searchProps = {};
for (let param in stateFilter) {
if (columns.includes(param)) {
const whereParams = {[param]: stateFilter[param]};
Object.assign(this.$inputsScope.searchProps, whereParams);
this.addFilter(param, stateFilter[param]);
}
}
}
defaultOrder() { defaultOrder() {
const order = this.model.order; if (this.disabledTableOrder) return;
let stateOrder;
if (this.$params.q)
stateOrder = JSON.parse(this.$params.q).tableOrder;
const order = stateOrder ? stateOrder : this.model.order;
if (!order) return; if (!order) return;
const orderFields = order.split(', '); const orderFields = order.split(', ');
@ -195,6 +229,9 @@ export default class SmartTable extends Component {
this.setPriority(column.element, priority); this.setPriority(column.element, priority);
} }
} }
this.model.order = order;
this.refresh();
} }
registerColumns() { registerColumns() {
@ -395,28 +432,54 @@ export default class SmartTable extends Component {
} }
searchByColumn(field) { searchByColumn(field) {
const searchCriteria = this.$inputsScope.searchProps[field];
const emptySearch = searchCriteria === '' || searchCriteria == null;
const filters = this.filterSanitizer(field); const filters = this.filterSanitizer(field);
if (filters && filters.userFilter) if (filters && filters.userFilter)
this.model.userFilter = filters.userFilter; this.model.userFilter = filters.userFilter;
if (!emptySearch) this.addFilter(field, this.$inputsScope.searchProps[field]);
this.addFilter(field, this.$inputsScope.searchProps[field]); }
else this.model.refresh();
searchPropsSanitizer() {
if (!this.$inputsScope || !this.$inputsScope.searchProps) return null;
let searchProps = this.$inputsScope.searchProps;
const searchPropsArray = Object.entries(searchProps);
searchProps = searchPropsArray.filter(
([key, value]) => value && value != ''
);
return Object.fromEntries(searchProps);
} }
addFilter(field, value) { addFilter(field, value) {
let where = {[field]: value}; if (value == '') value = null;
if (this.exprBuilder) { let stateFilter = {tableQ: {}};
where = buildFilter(where, (param, value) => if (this.$params.q) {
this.exprBuilder({param, value}) stateFilter = JSON.parse(this.$params.q);
); if (!stateFilter.tableQ)
stateFilter.tableQ = {};
delete stateFilter.tableQ[field];
} }
this.model.addFilter({where}); const whereParams = {[field]: value};
if (value) {
let where = {[field]: value};
if (this.exprBuilder) {
where = buildFilter(whereParams, (param, value) =>
this.exprBuilder({param, value})
);
}
this.model.addFilter({where});
}
const searchProps = this.searchPropsSanitizer();
Object.assign(stateFilter.tableQ, searchProps);
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
} }
applySort() { applySort() {
@ -426,7 +489,18 @@ export default class SmartTable extends Component {
if (order) if (order)
this.model.order = order; this.model.order = order;
this.model.refresh(); let stateFilter = {tableOrder: {}};
if (this.$params.q) {
stateFilter = JSON.parse(this.$params.q);
if (!stateFilter.tableOrder)
stateFilter.tableOrder = {};
}
stateFilter.tableOrder = order;
const params = {q: JSON.stringify(stateFilter)};
this.$state.go(this.$state.current.name, params, {location: 'replace'});
this.refresh();
} }
filterSanitizer(field) { filterSanitizer(field) {
@ -535,6 +609,8 @@ ngModule.vnComponent('smartTable', {
autoSave: '<?', autoSave: '<?',
exprBuilder: '&?', exprBuilder: '&?',
defaultNewData: '&?', defaultNewData: '&?',
options: '<?' options: '<?',
disabledTableFilter: '<?',
disabledTableOrder: '<?',
} }
}); });

View File

@ -9,6 +9,11 @@ describe('Component smartTable', () => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$element = $compile(`<smart-table></smart-table>`)($rootScope); $element = $compile(`<smart-table></smart-table>`)($rootScope);
controller = $element.controller('smartTable'); controller = $element.controller('smartTable');
controller.model = {
refresh: jest.fn().mockReturnValue(new Promise(resolve => resolve())),
addFilter: jest.fn(),
userParams: {}
};
})); }));
afterEach(() => { afterEach(() => {
@ -83,7 +88,7 @@ describe('Component smartTable', () => {
describe('defaultOrder', () => { describe('defaultOrder', () => {
it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => { it('should insert a new object to the controller sortCriteria with a sortType value of "ASC"', () => {
const element = document.createElement('div'); const element = document.createElement('div');
controller.model = {order: 'id'}; controller.model.order = 'id';
controller.columns = [ controller.columns = [
{field: 'id', element: element}, {field: 'id', element: element},
{field: 'test1', element: element}, {field: 'test1', element: element},
@ -101,7 +106,8 @@ describe('Component smartTable', () => {
it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => { it('should add new entries to the controller sortCriteria with a sortType values of "ASC" and "DESC"', () => {
const element = document.createElement('div'); const element = document.createElement('div');
controller.model = {order: 'test1, id DESC'}; controller.model.order = 'test1, id DESC';
controller.columns = [ controller.columns = [
{field: 'id', element: element}, {field: 'id', element: element},
{field: 'test1', element: element}, {field: 'test1', element: element},
@ -125,8 +131,6 @@ describe('Component smartTable', () => {
describe('addFilter()', () => { describe('addFilter()', () => {
it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => { it('should call the model addFilter() with a basic where filter if exprBuilder() was not received', () => {
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue'); controller.addFilter('myField', 'myValue');
const expectedFilter = { const expectedFilter = {
@ -140,7 +144,6 @@ describe('Component smartTable', () => {
it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => { it('should call the model addFilter() with a built where filter resultant of exprBuilder()', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'}); controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.model = {addFilter: jest.fn()};
controller.addFilter('myField', 'myValue'); controller.addFilter('myField', 'myValue');
@ -155,35 +158,48 @@ describe('Component smartTable', () => {
}); });
describe('applySort()', () => { describe('applySort()', () => {
it('should call the model refresh() without making changes on the model order', () => { it('should call the $state go and model refresh without making changes on the model order', () => {
controller.model = {refresh: jest.fn()}; controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.applySort(); controller.applySort();
expect(controller.model.order).toBeUndefined(); expect(controller.model.order).toBeUndefined();
expect(controller.model.refresh).toHaveBeenCalled(); expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
}); });
it('should call the model.refresh() after setting model order according to the controller sortCriteria', () => { it('should call the $state go and model refresh after setting model order according to the controller sortCriteria', () => {
controller.model = {refresh: jest.fn()};
const orderBy = {field: 'myField', sortType: 'ASC'}; const orderBy = {field: 'myField', sortType: 'ASC'};
controller.$state = {
go: jest.fn(),
current: {
name: 'section'
}
};
jest.spyOn(controller, 'refresh');
controller.sortCriteria = [orderBy]; controller.sortCriteria = [orderBy];
controller.applySort(); controller.applySort();
expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`); expect(controller.model.order).toEqual(`${orderBy.field} ${orderBy.sortType}`);
expect(controller.model.refresh).toHaveBeenCalled(); expect(controller.$state.go).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
}); });
}); });
describe('filterSanitizer()', () => { describe('filterSanitizer()', () => {
it('should remove the where filter after leaving no fields in it', () => { it('should remove the where filter after leaving no fields in it', () => {
controller.model = { controller.model.userFilter = {
userFilter: { where: {fieldToRemove: 'valueToRemove'}
where: {fieldToRemove: 'valueToRemove'}
},
userParams: {}
}; };
controller.model.userParams = {};
const result = controller.filterSanitizer('fieldToRemove'); const result = controller.filterSanitizer('fieldToRemove');
@ -193,23 +209,21 @@ describe('Component smartTable', () => {
}); });
it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => { it('should remove the where filter after leaving no fields and "empty ands/ors" in it', () => {
controller.model = { controller.model.userFilter = {
userFilter: { where: {
where: { and: [
and: [ {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {
{ or: [
or: [ {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, ]
] }
} ]
] }
} },
}, controller.model.userParams = {};
userParams: {}
};
const result = controller.filterSanitizer('aFieldToRemove'); const result = controller.filterSanitizer('aFieldToRemove');
@ -219,24 +233,22 @@ describe('Component smartTable', () => {
}); });
it('should not remove the where filter after leaving no empty "ands/ors" in it', () => { it('should not remove the where filter after leaving no empty "ands/ors" in it', () => {
controller.model = { controller.model.userFilter = {
userFilter: { where: {
where: { and: [
and: [ {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {
{ or: [
or: [ {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, {aFieldToRemove: 'aValueToRemove'},
{aFieldToRemove: 'aValueToRemove'}, ]
] }
} ],
], or: [{dontKillMe: 'thanks'}]
or: [{dontKillMe: 'thanks'}] }
}
},
userParams: {}
}; };
controller.model.userParams = {};
const result = controller.filterSanitizer('aFieldToRemove'); const result = controller.filterSanitizer('aFieldToRemove');
@ -249,7 +261,7 @@ describe('Component smartTable', () => {
describe('saveAll()', () => { describe('saveAll()', () => {
it('should throw an error if there are no changes to save in the model', () => { it('should throw an error if there are no changes to save in the model', () => {
jest.spyOn(controller.vnApp, 'showError'); jest.spyOn(controller.vnApp, 'showError');
controller.model = {isChanged: false}; controller.model.isChanged = false;
controller.saveAll(); controller.saveAll();
expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save'); expect(controller.vnApp.showError).toHaveBeenCalledWith('No changes to save');
@ -258,10 +270,8 @@ describe('Component smartTable', () => {
it('should call the showSuccess() if there are changes to save in the model', done => { it('should call the showSuccess() if there are changes to save in the model', done => {
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
controller.model = { controller.model.save = jest.fn().mockReturnValue(Promise.resolve());
save: jest.fn().mockReturnValue(Promise.resolve()), controller.model.isChanged = true;
isChanged: true
};
controller.saveAll().then(() => { controller.saveAll().then(() => {
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!'); expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
@ -269,4 +279,43 @@ describe('Component smartTable', () => {
}).catch(done.fail); }).catch(done.fail);
}); });
}); });
describe('defaultFilter()', () => {
it('should call model refresh and model addFilter with filter', () => {
controller.exprBuilder = jest.fn().mockReturnValue({builtField: 'builtValue'});
controller.$params = {
q: '{"tableQ": {"fieldName":"value"}}'
};
controller.columns = [
{field: 'fieldName'}
];
controller.$inputsScope = {
searchProps: {}
};
jest.spyOn(controller, 'refresh');
controller.defaultFilter();
expect(controller.model.addFilter).toHaveBeenCalled();
expect(controller.refresh).toHaveBeenCalled();
});
});
describe('searchPropsSanitizer()', () => {
it('should searchProps sanitize', () => {
controller.$inputsScope = {
searchProps: {
filterOne: '1',
filterTwo: ''
}
};
const searchPropsExpected = {
filterOne: '1'
};
const newSearchProps = controller.searchPropsSanitizer();
expect(newSearchProps).toEqual(searchPropsExpected);
});
});
}); });

View File

@ -24,10 +24,11 @@
} }
}, },
"node_modules/@uirouter/angularjs": { "node_modules/@uirouter/angularjs": {
"version": "1.0.29", "version": "1.0.30",
"license": "MIT", "resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.30.tgz",
"integrity": "sha512-qkc3RFZc91S5K0gc/QVAXc9LGDPXjR04vDgG/11j8+yyZEuQojXxKxdLhKIepiPzqLmGRVqzBmBc27gtqaEeZg==",
"dependencies": { "dependencies": {
"@uirouter/core": "6.0.7" "@uirouter/core": "6.0.8"
}, },
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
@ -37,15 +38,18 @@
} }
}, },
"node_modules/@uirouter/core": { "node_modules/@uirouter/core": {
"version": "6.0.7", "version": "6.0.8",
"license": "MIT", "resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.8.tgz",
"integrity": "sha512-Gc/BAW47i4L54p8dqYCJJZuv2s3tqlXQ0fvl6Zp2xrblELPVfxmjnc0eurx3XwfQdaqm3T6uls6tQKkof/4QMw==",
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/angular": { "node_modules/angular": {
"version": "1.8.2", "version": "1.8.3",
"license": "MIT" "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==",
"deprecated": "For the actively supported Angular, see https://www.npmjs.com/package/@angular/core. AngularJS support has officially ended. For extended AngularJS support options, see https://goo.gle/angularjs-path-forward."
}, },
"node_modules/angular-animate": { "node_modules/angular-animate": {
"version": "1.8.2", "version": "1.8.2",
@ -62,8 +66,9 @@
} }
}, },
"node_modules/angular-translate": { "node_modules/angular-translate": {
"version": "2.18.4", "version": "2.19.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.19.0.tgz",
"integrity": "sha512-Z/Fip5uUT2N85dPQ0sMEe1JdF5AehcDe4tg/9mWXNDVU531emHCg53ZND9Oe0dyNiGX5rWcJKmsL1Fujus1vGQ==",
"dependencies": { "dependencies": {
"angular": "^1.8.0" "angular": "^1.8.0"
}, },
@ -72,10 +77,11 @@
} }
}, },
"node_modules/angular-translate-loader-partial": { "node_modules/angular-translate-loader-partial": {
"version": "2.18.4", "version": "2.19.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.19.0.tgz",
"integrity": "sha512-NnMw13LMV4bPQmJK7/pZOZAnPxe0M5OtUHchADs5Gye7V7feonuEnrZ8e1CKhBlv9a7IQyWoqcBa4Lnhg8gk5w==",
"dependencies": { "dependencies": {
"angular-translate": "~2.18.4" "angular-translate": "~2.19.0"
} }
}, },
"node_modules/argparse": { "node_modules/argparse": {
@ -119,8 +125,9 @@
} }
}, },
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.1", "version": "2.29.4",
"license": "MIT", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -164,16 +171,22 @@
}, },
"dependencies": { "dependencies": {
"@uirouter/angularjs": { "@uirouter/angularjs": {
"version": "1.0.29", "version": "1.0.30",
"resolved": "https://registry.npmjs.org/@uirouter/angularjs/-/angularjs-1.0.30.tgz",
"integrity": "sha512-qkc3RFZc91S5K0gc/QVAXc9LGDPXjR04vDgG/11j8+yyZEuQojXxKxdLhKIepiPzqLmGRVqzBmBc27gtqaEeZg==",
"requires": { "requires": {
"@uirouter/core": "6.0.7" "@uirouter/core": "6.0.8"
} }
}, },
"@uirouter/core": { "@uirouter/core": {
"version": "6.0.7" "version": "6.0.8",
"resolved": "https://registry.npmjs.org/@uirouter/core/-/core-6.0.8.tgz",
"integrity": "sha512-Gc/BAW47i4L54p8dqYCJJZuv2s3tqlXQ0fvl6Zp2xrblELPVfxmjnc0eurx3XwfQdaqm3T6uls6tQKkof/4QMw=="
}, },
"angular": { "angular": {
"version": "1.8.2" "version": "1.8.3",
"resolved": "https://registry.npmjs.org/angular/-/angular-1.8.3.tgz",
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw=="
}, },
"angular-animate": { "angular-animate": {
"version": "1.8.2" "version": "1.8.2"
@ -185,15 +198,19 @@
} }
}, },
"angular-translate": { "angular-translate": {
"version": "2.18.4", "version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate/-/angular-translate-2.19.0.tgz",
"integrity": "sha512-Z/Fip5uUT2N85dPQ0sMEe1JdF5AehcDe4tg/9mWXNDVU531emHCg53ZND9Oe0dyNiGX5rWcJKmsL1Fujus1vGQ==",
"requires": { "requires": {
"angular": "^1.8.0" "angular": "^1.8.0"
} }
}, },
"angular-translate-loader-partial": { "angular-translate-loader-partial": {
"version": "2.18.4", "version": "2.19.0",
"resolved": "https://registry.npmjs.org/angular-translate-loader-partial/-/angular-translate-loader-partial-2.19.0.tgz",
"integrity": "sha512-NnMw13LMV4bPQmJK7/pZOZAnPxe0M5OtUHchADs5Gye7V7feonuEnrZ8e1CKhBlv9a7IQyWoqcBa4Lnhg8gk5w==",
"requires": { "requires": {
"angular-translate": "~2.18.4" "angular-translate": "~2.19.0"
} }
}, },
"argparse": { "argparse": {
@ -222,7 +239,9 @@
} }
}, },
"moment": { "moment": {
"version": "2.29.1" "version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}, },
"oclazyload": { "oclazyload": {
"version": "0.6.3" "version": "0.6.3"

View File

@ -51,6 +51,7 @@ Entries: Entradas
Users: Usuarios Users: Usuarios
Suppliers: Proveedores Suppliers: Proveedores
Monitors: Monitores Monitors: Monitores
Shelvings: Carros
# Common # Common

View File

@ -134,6 +134,7 @@
"Password does not meet requirements": "Password does not meet requirements", "Password does not meet requirements": "Password does not meet requirements",
"You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies", "You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies",
"Not enough privileges to edit a client": "Not enough privileges to edit a client", "Not enough privileges to edit a client": "Not enough privileges to edit a client",
"Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*",
"You don't have grant privilege": "You don't have grant privilege", "You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user" "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user"
} }

View File

@ -153,6 +153,7 @@
"Email already exists": "Email already exists", "Email already exists": "Email already exists",
"User already exists": "User already exists", "User already exists": "User already exists",
"Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral",
"Record of hours week": "Registro de horas semana {{week}} año {{year}} ",
"Created absence": "El empleado <strong>{{author}}</strong> ha añadido una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> para el día {{dated}}.", "Created absence": "El empleado <strong>{{author}}</strong> ha añadido una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> para el día {{dated}}.",
"Deleted absence": "El empleado <strong>{{author}}</strong> ha eliminado una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> del día {{dated}}.", "Deleted absence": "El empleado <strong>{{author}}</strong> ha eliminado una ausencia de tipo '{{absenceType}}' a <a href='{{{workerUrl}}}'><strong>{{employee}}</strong></a> del día {{dated}}.",
"I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})", "I have deleted the ticket id": "He eliminado el ticket id [{{id}}]({{{url}}})",
@ -236,6 +237,11 @@
"Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador", "Modifiable user details only by an administrator": "Detalles de usuario modificables solo por un administrador",
"Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador", "Modifiable password only via recovery or by an administrator": "Contraseña modificable solo a través de la recuperación o por un administrador",
"Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente", "Not enough privileges to edit a client": "No tienes suficientes privilegios para editar un cliente",
"You don't have grant privilege": "No tienes privilegios para dar privilegios", "This route does not exists": "Esta ruta no existe",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario" "Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*",
"You don't have grant privilege": "No tienes privilegios para dar privilegios",
"You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario",
"Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio"
} }

View File

@ -5,6 +5,8 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash; const nthash = require('smbhash').nthash;
module.exports = Self => { module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
fields: [ fields: [
@ -30,6 +32,7 @@ module.exports = Self => {
}, },
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
let { let {
client, client,
accountConfig accountConfig
@ -130,13 +133,14 @@ module.exports = Self => {
})); }));
} }
if (changes.length) if (shouldSync && changes.length)
await client.modify(dn, changes); await client.modify(dn, changes);
} else } else if (shouldSync)
await client.add(dn, newEntry); await client.add(dn, newEntry);
} else { } else {
try { try {
await client.del(dn); if (shouldSync)
await client.del(dn);
console.log(` -> User '${userName}' removed from LDAP`); console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) { } catch (e) {
if (e.name !== 'NoSuchObjectError') throw e; if (e.name !== 'NoSuchObjectError') throw e;
@ -196,17 +200,19 @@ module.exports = Self => {
for (let group of groups) { for (let group of groups) {
try { try {
let dn = `cn=${group},${groupDn}`; let dn = `cn=${group},${groupDn}`;
await client.modify(dn, new ldap.Change({ if (shouldSync) {
operation, await client.modify(dn, new ldap.Change({
modification: {memberUid: userName} operation,
})); modification: {memberUid: userName}
}));
}
} catch (err) { } catch (err) {
if (err.name !== 'NoSuchObjectError') if (err.name !== 'NoSuchObjectError')
throw err; throw err;
} }
} }
} }
await applyOperations(deleteGroups, 'delete'); await applyOperations(deleteGroups, 'delete');
await applyOperations(addGroups, 'add'); await applyOperations(addGroups, 'add');
}, },
@ -266,8 +272,10 @@ module.exports = Self => {
filter: 'objectClass=posixGroup' filter: 'objectClass=posixGroup'
}; };
let reqs = []; let reqs = [];
await client.searchForeach(this.groupDn, opts, await client.searchForeach(this.groupDn, opts, object => {
o => reqs.push(client.del(o.dn))); if (shouldSync)
reqs.push(client.del(object.dn));
});
await Promise.all(reqs); await Promise.all(reqs);
// Recreate roles // Recreate roles
@ -291,7 +299,8 @@ module.exports = Self => {
} }
let dn = `cn=${role.name},${this.groupDn}`; let dn = `cn=${role.name},${this.groupDn}`;
reqs.push(client.add(dn, newEntry)); if (shouldSync)
reqs.push(client.add(dn, newEntry));
} }
await Promise.all(reqs); await Promise.all(reqs);
} }

View File

@ -60,16 +60,19 @@ module.exports = Self => {
return `cn=Users,${dnBase}`; return `cn=Users,${dnBase}`;
}, },
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
let {sshClient} = this; let {sshClient} = this;
let sambaUser = await this.adClient.searchOne(this.usersDn(), { let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub', scope: 'sub',
attributes: ['userAccountControl'], attributes: ['userAccountControl'],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))` filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
}); });
let isEnabled = sambaUser let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE); && !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
if (info.hasAccount) { if (info.hasAccount) {
if (!sambaUser) { if (!sambaUser) {

View File

@ -9,7 +9,7 @@ module.exports = Self => {
arg: 'id', arg: 'id',
type: 'number', type: 'number',
required: true, required: true,
description: 'The client id', description: 'The claim id',
http: {source: 'path'} http: {source: 'path'}
}, },
{ {
@ -42,6 +42,11 @@ module.exports = Self => {
}); });
Self.claimPickupEmail = async ctx => { Self.claimPickupEmail = async ctx => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate
const origin = ctx.req.headers.origin;
const args = Object.assign({}, ctx.args); const args = Object.assign({}, ctx.args);
const params = { const params = {
recipient: args.recipient, recipient: args.recipient,
@ -52,6 +57,34 @@ module.exports = Self => {
for (const param in args) for (const param in args)
params[param] = args[param]; params[param] = args[param];
const claim = await models.Claim.findById(args.id, {
fields: ['id', 'clientFk'],
include: {
relation: 'client',
scope: {
fields: ['name', 'salesPersonFk']
}
}
});
const message = $t('Claim pickup order sent', {
claimId: args.id,
clientName: claim.client().name,
claimUrl: `${origin}/#!/claim/${args.id}/summary`,
});
const salesPersonId = claim.client().salesPersonFk;
if (salesPersonId)
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
await models.ClaimLog.create({
originFk: args.id,
userFk: userId,
action: 'insert',
description: 'Claim-pickup-order sent',
changedModel: 'Mail'
});
const email = new Email('claim-pickup-order', params); const email = new Email('claim-pickup-order', params);
return email.send(); return email.send();

View File

@ -19,7 +19,7 @@
readonly="true"> readonly="true">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Created" label="Created"
field="::$ctrl.claim.created | date:'yyyy-MM-dd HH:mm'" field="::$ctrl.claim.created | date:'yyyy-MM-dd HH:mm'"
readonly="true"> readonly="true">
</vn-textfield> </vn-textfield>
@ -56,7 +56,7 @@
label="Pick up" label="Pick up"
ng-model="$ctrl.claim.hasToPickUp" ng-model="$ctrl.claim.hasToPickUp"
vn-acl="claimManager" vn-acl="claimManager"
info="When checked will notify to the salesPerson"> title="{{'When checked will notify to the salesPerson' | translate}}">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>

View File

@ -5,5 +5,5 @@ Responsability: Responsabilidad
Company: Empresa Company: Empresa
Sales/Client: Comercial/Cliente Sales/Client: Comercial/Cliente
Pick up: Recoger Pick up: Recoger
When checked will notify a pickup to the salesPerson: Cuando se marque enviará una notificación de recogida al comercial When checked will notify to the salesPerson: Cuando se marque enviará una notificación de recogida al comercial
Packages received: Bultos recibidos Packages received: Bultos recibidos

View File

@ -73,7 +73,7 @@
{ {
"url": "/note", "url": "/note",
"state": "claim.card.note", "state": "claim.card.note",
"component": "ui-view", "component": "ui-view",
"abstract": true, "abstract": true,
"acl": ["salesPerson"] "acl": ["salesPerson"]
}, },
@ -105,7 +105,7 @@
"acl": ["claimManager"] "acl": ["claimManager"]
}, },
{ {
"url": "/action", "url": "/action?q",
"state": "claim.card.action", "state": "claim.card.action",
"component": "vn-claim-action", "component": "vn-claim-action",
"description": "Action", "description": "Action",
@ -131,4 +131,4 @@
"acl": ["claimManager"] "acl": ["claimManager"]
} }
] ]
} }

View File

@ -25,16 +25,23 @@
</vn-button-menu> </vn-button-menu>
</h5> </h5>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-auto>
<vn-label-value <h4>
label="Created" <a
value="{{$ctrl.summary.claim.created | date: 'dd/MM/yyyy'}}"> ui-sref="claim.card.basicData({id:$ctrl.claim.id})"
</vn-label-value> target="_self">
<vn-label-value <span translate vn-tooltip="Go to">Basic data</span>
label="State" </a>
value="{{$ctrl.summary.claim.claimState.description}}"> </h4>
</vn-label-value> <vn-label-value
<vn-label-value label="Created"
value="{{$ctrl.summary.claim.created | date: 'dd/MM/yyyy'}}">
</vn-label-value>
<vn-label-value
label="State"
value="{{$ctrl.summary.claim.claimState.description}}">
</vn-label-value>
<vn-label-value
label="Salesperson" label="Salesperson"
value="{{$ctrl.summary.claim.client.salesPersonUser.name}}"> value="{{$ctrl.summary.claim.client.salesPersonUser.name}}">
</vn-label-value> </vn-label-value>
@ -42,16 +49,23 @@
label="Attended by" label="Attended by"
value="{{$ctrl.summary.claim.worker.user.nickname}}"> value="{{$ctrl.summary.claim.worker.user.nickname}}">
</vn-label-value> </vn-label-value>
</vn-one> <vn-check
class="vn-mr-md"
label="Pick up"
ng-model="$ctrl.summary.claim.hasToPickUp"
title="{{'When checked will notify to the salesPerson' | translate}}"
disabled="true">
</vn-check>
</vn-auto>
<vn-auto> <vn-auto>
<h4 ng-show="$ctrl.isSalesPerson && $ctrl.summary.observations.length"> <h4 ng-show="$ctrl.isSalesPerson && $ctrl.summary.observations.length">
<a <a
ui-sref="claim.card.note.index({id:$ctrl.claim.id})" ui-sref="claim.card.note.index({id:$ctrl.claim.id})"
target="_self"> target="_self">
<span translate vn-tooltip="Go to">Observations</span> <span translate vn-tooltip="Go to">Observations</span>
</a> </a>
</h4> </h4>
<h4 <h4
ng-show="!$ctrl.isSalesPerson && $ctrl.summary.observations.length" ng-show="!$ctrl.isSalesPerson && $ctrl.summary.observations.length"
translate> translate>
Observations Observations
@ -70,13 +84,13 @@
</vn-auto> </vn-auto>
<vn-auto> <vn-auto>
<h4 ng-show="$ctrl.isSalesPerson"> <h4 ng-show="$ctrl.isSalesPerson">
<a <a
ui-sref="claim.card.detail({id:$ctrl.claim.id})" ui-sref="claim.card.detail({id:$ctrl.claim.id})"
target="_self"> target="_self">
<span translate vn-tooltip="Go to">Detail</span> <span translate vn-tooltip="Go to">Detail</span>
</a> </a>
</h4> </h4>
<h4 <h4
ng-show="!$ctrl.isSalesPerson" ng-show="!$ctrl.isSalesPerson"
translate> translate>
Detail Detail
@ -98,7 +112,7 @@
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="saleClaimed in $ctrl.summary.salesClaimed"> <vn-tr ng-repeat="saleClaimed in $ctrl.summary.salesClaimed">
<vn-td number> <vn-td number>
<span <span
ng-click="itemDescriptor.show($event, saleClaimed.sale.itemFk, saleClaimed.sale.id)" ng-click="itemDescriptor.show($event, saleClaimed.sale.itemFk, saleClaimed.sale.id)"
class="link"> class="link">
{{::saleClaimed.sale.itemFk | zeroFill:6}} {{::saleClaimed.sale.itemFk | zeroFill:6}}
@ -111,7 +125,7 @@
<vn-td number>{{::saleClaimed.sale.price | currency: 'EUR':2}}</vn-td> <vn-td number>{{::saleClaimed.sale.price | currency: 'EUR':2}}</vn-td>
<vn-td number>{{::saleClaimed.sale.discount}} %</vn-td> <vn-td number>{{::saleClaimed.sale.discount}} %</vn-td>
<vn-td number> <vn-td number>
{{saleClaimed.sale.quantity * saleClaimed.sale.price * {{saleClaimed.sale.quantity * saleClaimed.sale.price *
((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}} ((100 - saleClaimed.sale.discount) / 100) | currency: 'EUR':2}}
</vn-td> </vn-td>
</vn-tr> </vn-tr>
@ -123,7 +137,7 @@
<h4 translate>Photos</h4> <h4 translate>Photos</h4>
<vn-horizontal class="photo-list"> <vn-horizontal class="photo-list">
<section class="photo" ng-repeat="photo in photos"> <section class="photo" ng-repeat="photo in photos">
<section class="image" on-error-src <section class="image" on-error-src
ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}" ng-style="{'background': 'url(' + $ctrl.getImagePath(photo.dmsFk) + ')'}"
zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}" zoom-image="{{$ctrl.getImagePath(photo.dmsFk)}}"
ng-if="photo.dms.contentType != 'video/mp4'"> ng-if="photo.dms.contentType != 'video/mp4'">
@ -137,13 +151,13 @@
</vn-auto> </vn-auto>
<vn-auto> <vn-auto>
<h4 ng-show="$ctrl.isClaimManager"> <h4 ng-show="$ctrl.isClaimManager">
<a <a
ui-sref="claim.card.development({id:$ctrl.claim.id})" ui-sref="claim.card.development({id:$ctrl.claim.id})"
target="_self"> target="_self">
<span translate vn-tooltip="Go to">Development</span> <span translate vn-tooltip="Go to">Development</span>
</a> </a>
</h4> </h4>
<h4 <h4
translate translate
ng-show="!$ctrl.isClaimManager"> ng-show="!$ctrl.isClaimManager">
Development Development
@ -165,8 +179,8 @@
<vn-td>{{::development.claimResult.description}}</vn-td> <vn-td>{{::development.claimResult.description}}</vn-td>
<vn-td>{{::development.claimResponsible.description}}</vn-td> <vn-td>{{::development.claimResponsible.description}}</vn-td>
<vn-td expand> <vn-td expand>
<span <span
class="link" class="link"
ng-click="workerDescriptor.show($event, development.workerFk)"> ng-click="workerDescriptor.show($event, development.workerFk)">
{{::development.worker.user.nickname}} {{::development.worker.user.nickname}}
</span> </span>
@ -179,21 +193,21 @@
</vn-auto> </vn-auto>
<vn-auto> <vn-auto>
<h4 ng-show="$ctrl.isClaimManager"> <h4 ng-show="$ctrl.isClaimManager">
<a <a
ui-sref="claim.card.action({id:$ctrl.claim.id})" ui-sref="claim.card.action({id:$ctrl.claim.id})"
target="_self"> target="_self">
<span translate vn-tooltip="Go to">Action</span> <span translate vn-tooltip="Go to">Action</span>
</a> </a>
</h4> </h4>
<h4 <h4
translate translate
ng-show="!$ctrl.isClaimManager"> ng-show="!$ctrl.isClaimManager">
Action Action
</h4> </h4>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>
<vn-range <vn-range
vn-one vn-one
disabled="true" disabled="true"
label="Responsability" label="Responsability"
min-label="Company" min-label="Company"
@ -224,14 +238,14 @@
<vn-tbody> <vn-tbody>
<vn-tr ng-repeat="action in $ctrl.summary.actions"> <vn-tr ng-repeat="action in $ctrl.summary.actions">
<vn-td number> <vn-td number>
<span <span
ng-click="itemDescriptor.show($event, action.sale.itemFk, action.sale.id)" ng-click="itemDescriptor.show($event, action.sale.itemFk, action.sale.id)"
class="link"> class="link">
{{::action.sale.itemFk | zeroFill:6}} {{::action.sale.itemFk | zeroFill:6}}
</span> </span>
</vn-td> </vn-td>
<vn-td number> <vn-td number>
<span <span
ng-click="ticketDescriptor.show($event, action.sale.ticket.id)" ng-click="ticketDescriptor.show($event, action.sale.ticket.id)"
class="link"> class="link">
{{::action.sale.ticket.id}} {{::action.sale.ticket.id}}
@ -258,9 +272,9 @@
vn-id="item-descriptor" vn-id="item-descriptor"
warehouse-fk="$ctrl.vnConfig.warehouseFk"> warehouse-fk="$ctrl.vnConfig.warehouseFk">
</vn-item-descriptor-popover> </vn-item-descriptor-popover>
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="worker-descriptor"> vn-id="worker-descriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
<vn-ticket-descriptor-popover <vn-ticket-descriptor-popover
vn-id="ticket-descriptor"> vn-id="ticket-descriptor">
</vn-ticket-descriptor-popover> </vn-ticket-descriptor-popover>

View File

@ -0,0 +1,147 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
const buildFilter = require('vn-loopback/util/filter').buildFilter;
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('filter', {
description: 'Find all clients matched by the filter',
accessType: 'READ',
accepts: [
{
arg: 'filter',
type: 'object',
},
{
arg: 'search',
type: 'string',
description: `If it's and integer searchs by id, otherwise it searchs by name`,
},
{
arg: 'name',
type: 'string',
description: 'The client name',
},
{
arg: 'salesPersonFk',
type: 'number',
},
{
arg: 'fi',
type: 'string',
description: 'The client fiscal id',
},
{
arg: 'socialName',
type: 'string',
},
{
arg: 'city',
type: 'string',
},
{
arg: 'postcode',
type: 'string',
},
{
arg: 'provinceFk',
type: 'number',
},
{
arg: 'email',
type: 'string',
},
{
arg: 'phone',
type: 'string',
},
{
arg: 'zoneFk',
type: 'number',
},
],
returns: {
type: ['object'],
root: true
},
http: {
path: `/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter, options) => {
const conn = Self.dataSource.connector;
const myOptions = {};
const postalCode = [];
const args = ctx.args;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (args.zoneFk) {
query = `CALL vn.zone_getPostalCode(?)`;
const [geos] = await Self.rawSql(query, [args.zoneFk]);
for (let geo of geos)
postalCode.push(geo.name);
}
const where = buildFilter(ctx.args, (param, value) => {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {'c.id': {inq: value}}
: {'c.name': {like: `%${value}%`}};
case 'name':
case 'salesPersonFk':
case 'fi':
case 'socialName':
case 'city':
case 'postcode':
case 'provinceFk':
case 'email':
case 'phone':
param = `c.${param}`;
return {[param]: value};
case 'zoneFk':
param = 'a.postalCode';
return {[param]: {inq: postalCode}};
}
});
filter = mergeFilters(filter, {where});
const stmts = [];
const stmt = new ParameterizedSQL(
`SELECT
DISTINCT c.id,
c.name,
c.fi,
c.socialName,
c.phone,
c.city,
c.postcode,
c.email,
c.isActive,
c.isFreezed,
p.id AS provinceFk,
p.name AS province,
u.id AS salesPersonFk,
u.name AS salesPerson
FROM client c
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN province p ON p.id = c.provinceFk
JOIN vn.address a ON a.clientFk = c.id
`
);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(conn.makePagination(filter));
const clientsIndex = stmts.push(stmt) - 1;
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql, myOptions);
return clientsIndex === 0 ? result : result[clientsIndex];
};
};

View File

@ -0,0 +1,199 @@
const {models} = require('vn-loopback/server/server');
describe('client filter()', () => {
it('should return the clients matching the filter with a limit of 20 rows', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {}};
const filter = {limit: '8'};
const result = await models.Client.filter(ctx, filter, options);
expect(result.length).toEqual(8);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the search argument with his name', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {search: 'Bruce Wayne'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the search argument with his id', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {search: '1101'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the client "Bruce Wayne" matching the name argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {name: 'Bruce Wayne'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstResult = result[0];
expect(result.length).toEqual(1);
expect(firstResult.name).toEqual('Bruce Wayne');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "salesPersonFk" argument', async() => {
const tx = await models.Client.beginTransaction({});
const salesPersonId = 18;
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {salesPersonFk: salesPersonId}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(5);
expect(randomResultClient.salesPersonFk).toEqual(salesPersonId);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "fi" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {fi: '251628698'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const firstClient = result[0];
expect(result.length).toEqual(1);
expect(firstClient.name).toEqual('Max Eisenhardt');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "city" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {city: 'Gotham'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(20);
expect(randomResultClient.city.toLowerCase()).toEqual('gotham');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "postcode" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {postcode: '46460'}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
const randomIndex = Math.floor(Math.random() * result.length);
const randomResultClient = result[randomIndex];
expect(result.length).toBeGreaterThanOrEqual(20);
expect(randomResultClient.postcode).toEqual('46460');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the clients matching the "zoneFk" argument', async() => {
const tx = await models.Client.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}, args: {zoneFk: 9}};
const filter = {};
const result = await models.Client.filter(ctx, filter, options);
expect(result.length).toBe(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,40 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('balanceCompensationEmail', {
description: 'Sends the debit balances compensation email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'Number',
required: true,
description: 'The receipt id',
http: { source: 'path' }
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/:id/balance-compensation-email',
verb: 'POST'
}
});
Self.balanceCompensationEmail = async (ctx, id) => {
const models = Self.app.models;
const receipt = await models.Receipt.findById(id, {fields: ['clientFk']});
const client = await models.Client.findById(receipt.clientFk, {fields:['email']});
const email = new Email('balance-compensation', {
lang: ctx.req.getLocale(),
recipient: client.email+',administracion@verdnatura.es',
id
});
return email.send();
};
};

View File

@ -0,0 +1,50 @@
const { Report } = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('balanceCompensationPdf', {
description: 'Returns the the debit balances compensation pdf',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The receipt id',
http: { source: 'path' }
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/balance-compensation-pdf',
verb: 'GET'
}
});
Self.balanceCompensationPdf = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {lang: ctx.req.getLocale()};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const report = new Report('balance-compensation', params);
const stream = await report.toPdfStream();
return [stream, 'application/pdf', `filename="doc-${id}.pdf"`];
};
};

View File

@ -8,6 +8,9 @@
"BankEntity": { "BankEntity": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Business": {
"dataSource": "vn"
},
"BusinessType": { "BusinessType": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,27 @@
{
"name": "Business",
"base": "VnModel",
"options": {
"mysql": {
"table": "business"
}
},
"properties": {
"id": {
"type": "number",
"id": true
}
},
"relations": {
"worker": {
"type": "belongsTo",
"model": "Worker",
"foreignKey": "workerFk"
},
"department": {
"type": "belongsTo",
"model": "Department",
"foreignKey": "departmentFk"
}
}
}

View File

@ -47,4 +47,5 @@ module.exports = Self => {
require('../methods/client/incotermsAuthorizationHtml')(Self); require('../methods/client/incotermsAuthorizationHtml')(Self);
require('../methods/client/incotermsAuthorizationEmail')(Self); require('../methods/client/incotermsAuthorizationEmail')(Self);
require('../methods/client/consumptionSendQueued')(Self); require('../methods/client/consumptionSendQueued')(Self);
require('../methods/client/filter')(Self);
}; };

View File

@ -2,6 +2,8 @@ const LoopBackContext = require('loopback-context');
module.exports = function(Self) { module.exports = function(Self) {
require('../methods/receipt/filter')(Self); require('../methods/receipt/filter')(Self);
require('../methods/receipt/balanceCompensationEmail')(Self);
require('../methods/receipt/balanceCompensationPdf')(Self);
require('../methods/receipt/receiptPdf')(Self); require('../methods/receipt/receiptPdf')(Self);
Self.validateBinded('amountPaid', isNotZero, { Self.validateBinded('amountPaid', isNotZero, {

View File

@ -121,9 +121,22 @@
</vn-icon-button> </vn-icon-button>
</a> </a>
</vn-td> </vn-td>
</vn-tr> <vn-td center shrink ng-if="!balance.isInvoice">
</vn-tbody> <vn-icon-button
</vn-table> vn-dialog="send_compensation"
icon="outgoing_mail"
title="{{'Send compensation' | translate}}">
</vn-icon-button>
</vn-td>
<vn-confirm
vn-id="send_compensation"
on-accept="$ctrl.sendEmail(balance)"
question="Notify compensation"
message="Send compensation">
</vn-confirm>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card> </vn-card>
</vn-data-viewer> </vn-data-viewer>
</div> </div>

View File

@ -2,8 +2,9 @@ import ngModule from '../../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
class Controller extends Section { class Controller extends Section {
constructor($element, $) { constructor($element, $, vnEmail) {
super($element, $); super($element, $);
this.vnEmail = vnEmail;
this.filter = { this.filter = {
include: { include: {
relation: 'company', relation: 'company',
@ -54,45 +55,49 @@ class Controller extends Section {
} }
})).then(() => this.getBalances()); })).then(() => this.getBalances());
} }
getCurrentBalance() { getCurrentBalance() {
const clientRisks = this.$.riskModel.data; const clientRisks = this.$.riskModel.data;
const selectedCompany = this.companyId; const selectedCompany = this.companyId;
const currentBalance = clientRisks.find(balance => { const currentBalance = clientRisks.find(balance => {
return balance.companyFk === selectedCompany; return balance.companyFk === selectedCompany;
}); });
return currentBalance && currentBalance.amount; return currentBalance && currentBalance.amount;
} }
getBalances() { getBalances() {
const balances = this.$.model.data; const balances = this.$.model.data;
balances.forEach((balance, index) => { balances.forEach((balance, index) => {
if (index === 0) if (index === 0)
balance.balance = this.getCurrentBalance(); balance.balance = this.getCurrentBalance();
if (index > 0) { if (index > 0) {
let previousBalance = balances[index - 1]; let previousBalance = balances[index - 1];
balance.balance = previousBalance.balance - (previousBalance.debit - previousBalance.credit); balance.balance = previousBalance.balance - (previousBalance.debit - previousBalance.credit);
} }
}); });
} }
showInvoiceOutDescriptor(event, balance) { showInvoiceOutDescriptor(event, balance) {
if (!balance.isInvoice) return; if (!balance.isInvoice) return;
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
this.$.invoiceOutDescriptor.show(event.target, balance.id); this.$.invoiceOutDescriptor.show(event.target, balance.id);
} }
changeDescription(balance) { changeDescription(balance) {
const params = {description: balance.description}; const params = {description: balance.description};
const endpoint = `Receipts/${balance.id}`; const endpoint = `Receipts/${balance.id}`;
this.$http.patch(endpoint, params) this.$http.patch(endpoint, params)
.then(() => this.vnApp.showSuccess(this.$t('Data saved!'))); .then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
}
sendEmail(balance) {
return this.vnEmail.send(`Receipts/${balance.id}/balance-compensation-email`);
} }
} }
Controller.$inject = ['$element', '$scope']; Controller.$inject = ['$element', '$scope', 'vnEmail'];
ngModule.vnComponent('vnClientBalanceIndex', { ngModule.vnComponent('vnClientBalanceIndex', {
template: require('./index.html'), template: require('./index.html'),

View File

@ -8,4 +8,6 @@ Havings: Haber
Balance: Balance Balance: Balance
Total by company: Total por empresa Total by company: Total por empresa
Download PDF: Descargar PDF Download PDF: Descargar PDF
BILL: N/FRA {{ref}} Send compensation: Enviar compensación
BILL: N/FRA {{ref}}
Notify compensation: ¿Desea informar de la compensación al cliente por correo?

View File

@ -1,6 +1,6 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="Clients" url="Clients/filter"
order="id DESC" order="id DESC"
limit="8" limit="8"
data="clients"> data="clients">
@ -10,8 +10,7 @@
vn-focus vn-focus
panel="vn-client-search-panel" panel="vn-client-search-panel"
info="Search client by id or name" info="Search client by id or name"
model="model" model="model">
expr-builder="$ctrl.exprBuilder(param, value)">
</vn-searchbar> </vn-searchbar>
</vn-portal> </vn-portal>
<vn-portal slot="menu"> <vn-portal slot="menu">

View File

@ -2,32 +2,6 @@ import ngModule from '../module';
import ModuleMain from 'salix/components/module-main'; import ModuleMain from 'salix/components/module-main';
export default class Client extends ModuleMain { export default class Client extends ModuleMain {
exprBuilder(param, value) {
switch (param) {
case 'search':
return /^\d+$/.test(value)
? {id: value}
: {or: [{name: {like: `%${value}%`}}, {socialName: {like: `%${value}%`}}]};
case 'phone':
return {
or: [
{phone: value},
{mobile: value}
]
};
case 'name':
case 'socialName':
case 'city':
case 'email':
return {[param]: {like: `%${value}%`}};
case 'id':
case 'fi':
case 'postcode':
case 'provinceFk':
case 'salesPersonFk':
return {[param]: value};
}
}
} }
ngModule.vnComponent('vnClient', { ngModule.vnComponent('vnClient', {

View File

@ -20,7 +20,7 @@
{"state": "client.card.credit.index", "icon": "credit_card"}, {"state": "client.card.credit.index", "icon": "credit_card"},
{"state": "client.card.greuge.index", "icon": "work"}, {"state": "client.card.greuge.index", "icon": "work"},
{"state": "client.card.balance.index", "icon": "icon-invoice"}, {"state": "client.card.balance.index", "icon": "icon-invoice"},
{"state": "client.card.recovery.index", "icon": "icon-recovery"}, {"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"}, {"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"}, {"state": "client.card.log", "icon": "history"},
{ {
@ -37,7 +37,7 @@
{"state": "client.card.unpaid", "icon": "icon-defaulter"} {"state": "client.card.unpaid", "icon": "icon-defaulter"}
] ]
} }
] ]
}, },
"keybindings": [ "keybindings": [
{"key": "c", "state": "client.index"} {"key": "c", "state": "client.index"}
@ -147,7 +147,7 @@
{ {
"url": "/note", "url": "/note",
"state": "client.card.note", "state": "client.card.note",
"component": "ui-view", "component": "ui-view",
"abstract": true "abstract": true
}, },
{ {
@ -236,7 +236,7 @@
"client": "$ctrl.client" "client": "$ctrl.client"
} }
}, },
{ {
"url": "/create?payed&companyFk&bankFk&payedAmount", "url": "/create?payed&companyFk&bankFk&payedAmount",
"state": "client.card.balance.create", "state": "client.card.balance.create",
"component": "vn-client-balance-create", "component": "vn-client-balance-create",
@ -406,13 +406,13 @@
} }
}, },
{ {
"url": "/defaulter", "url": "/defaulter?q",
"state": "client.defaulter", "state": "client.defaulter",
"component": "vn-client-defaulter", "component": "vn-client-defaulter",
"description": "Defaulter" "description": "Defaulter"
}, },
{ {
"url" : "/notification", "url" : "/notification?q",
"state": "client.notification", "state": "client.notification",
"component": "vn-client-notification", "component": "vn-client-notification",
"description": "Notifications" "description": "Notifications"
@ -424,7 +424,7 @@
"description": "Unpaid" "description": "Unpaid"
}, },
{ {
"url": "/extended-list", "url": "/extended-list?q",
"state": "client.extendedList", "state": "client.extendedList",
"component": "vn-client-extended-list", "component": "vn-client-extended-list",
"description": "Extended list" "description": "Extended list"

View File

@ -58,6 +58,13 @@
value-field="id" value-field="id"
label="Province"> label="Province">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete
ng-model="filter.zoneFk"
url="Zones"
show-field="name"
value-field="id"
label="Zone">
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield

View File

@ -154,7 +154,8 @@ module.exports = Self => {
e.id, e.id,
e.supplierFk, e.supplierFk,
e.dated, e.dated,
e.ref, e.ref reference,
e.ref invoiceNumber,
e.isBooked, e.isBooked,
e.isExcludedFromAvailable, e.isExcludedFromAvailable,
e.notes, e.notes,

View File

@ -12,10 +12,15 @@ module.exports = Self => {
http: {source: 'path'} http: {source: 'path'}
}, },
{ {
arg: 'ref', arg: 'reference',
type: 'string', type: 'string',
description: 'The buyed boxes ids', description: 'The buyed boxes ids',
}, },
{
arg: 'invoiceNumber',
type: 'string',
description: 'The registered invoice number',
},
{ {
arg: 'observation', arg: 'observation',
type: 'string', type: 'string',
@ -63,7 +68,8 @@ module.exports = Self => {
await entry.updateAttributes({ await entry.updateAttributes({
observation: args.observation, observation: args.observation,
ref: args.ref reference: args.reference,
invoiceNumber: args.invoiceNumber
}, myOptions); }, myOptions);
const travel = entry.travel(); const travel = entry.travel();

View File

@ -15,13 +15,15 @@ describe('entry import()', () => {
}); });
it('should import the buy rows', async() => { it('should import the buy rows', async() => {
const expectedRef = '1, 2'; const expectedReference = '1, 2';
const expectedInvoiceNumber = '1, 2';
const expectedObservation = '123456'; const expectedObservation = '123456';
const ctx = { const ctx = {
req: activeCtx, req: activeCtx,
args: { args: {
observation: expectedObservation, observation: expectedObservation,
ref: expectedRef, reference: expectedReference,
invoiceNumber: expectedInvoiceNumber,
buys: [ buys: [
{ {
itemFk: 1, itemFk: 1,
@ -58,7 +60,8 @@ describe('entry import()', () => {
}, options); }, options);
expect(updatedEntry.observation).toEqual(expectedObservation); expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef); expect(updatedEntry.reference).toEqual(expectedReference);
expect(updatedEntry.invoiceNumber).toEqual(expectedInvoiceNumber);
expect(entryBuys.length).toEqual(4); expect(entryBuys.length).toEqual(4);
await tx.rollback(); await tx.rollback();

View File

@ -18,8 +18,17 @@
"dated": { "dated": {
"type": "date" "type": "date"
}, },
"ref": { "reference": {
"type": "string" "type": "string",
"mysql": {
"columnName": "ref"
}
},
"invoiceNumber": {
"type": "string",
"mysql": {
"columnName": "ref"
}
}, },
"isBooked": { "isBooked": {
"type": "boolean" "type": "boolean"

View File

@ -48,7 +48,7 @@
<vn-textfield <vn-textfield
vn-one vn-one
label="Reference" label="Reference"
ng-model="$ctrl.entry.ref" ng-model="$ctrl.entry.reference"
rule rule
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
@ -61,17 +61,25 @@
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textarea <vn-textfield
vn-one vn-one
label="Observation" label="Invoice number"
ng-model="$ctrl.entry.observation" ng-model="$ctrl.entry.invoiceNumber"
rule> rule
</vn-textarea> vn-focus>
</vn-textfield>
<vn-autocomplete
url="Companies"
label="Company"
show-field="code"
value-field="id"
ng-model="$ctrl.entry.companyFk">
</vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
label="Currency" label="Currency"
ng-model="$ctrl.entry.currencyFk" ng-model="$ctrl.entry.currencyFk"
url="Currencies" url="Currencies"
show-field="code" show-field="code"
@ -84,13 +92,14 @@
ng-model="$ctrl.entry.commission" ng-model="$ctrl.entry.commission"
rule> rule>
</vn-input-number> </vn-input-number>
<vn-autocomplete </vn-horizontal>
url="Companies" <vn-horizontal>
label="Company" <vn-textarea
show-field="code" vn-one
value-field="id" label="Observation"
ng-model="$ctrl.entry.companyFk"> ng-model="$ctrl.entry.observation"
</vn-autocomplete> rule>
</vn-textarea>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check

View File

@ -91,7 +91,7 @@
url="Packagings" url="Packagings"
show-field="id" show-field="id"
value-field="id" value-field="id"
where="{isBox: true}" where="{freightItemFk: true}"
ng-model="buy.packageFk" ng-model="buy.packageFk"
on-change="$ctrl.saveBuy(buy)"> on-change="$ctrl.saveBuy(buy)">
</vn-autocomplete> </vn-autocomplete>

View File

@ -12,6 +12,7 @@
<vn-th field="id" number>Id</vn-th> <vn-th field="id" number>Id</vn-th>
<vn-th field="landed" center expand>Landed</vn-th> <vn-th field="landed" center expand>Landed</vn-th>
<vn-th>Reference</vn-th> <vn-th>Reference</vn-th>
<vn-th>Invoice number</vn-th>
<vn-th field="supplierFk">Supplier</vn-th> <vn-th field="supplierFk">Supplier</vn-th>
<vn-th field="isBooked" center>Booked</vn-th> <vn-th field="isBooked" center>Booked</vn-th>
<vn-th field="isConfirmed" center>Confirmed</vn-th> <vn-th field="isConfirmed" center>Confirmed</vn-th>
@ -45,7 +46,8 @@
{{::entry.landed | date:'dd/MM/yyyy'}} {{::entry.landed | date:'dd/MM/yyyy'}}
</span> </span>
</vn-td> </vn-td>
<vn-td expand>{{::entry.ref}}</vn-td> <vn-td expand>{{::entry.reference}}</vn-td>
<vn-td expand>{{::entry.invoiceNumber}}</vn-td>
<vn-td expand>{{::entry.supplierName}}</vn-td> <vn-td expand>{{::entry.supplierName}}</vn-td>
<vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td> <vn-td center><vn-check ng-model="entry.isBooked" disabled="true"></vn-check></vn-td>
<vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td> <vn-td center><vn-check ng-model="entry.isConfirmed" disabled="true"></vn-check></vn-td>

View File

@ -14,4 +14,5 @@ Booked: Contabilizada
Is inventory: Inventario Is inventory: Inventario
Notes: Notas Notes: Notas
Status: Estado Status: Estado
Selection: Selección Selection: Selección
Invoice number: Núm. factura

View File

@ -13,9 +13,16 @@
<vn-textfield <vn-textfield
vn-one vn-one
label="Reference" label="Reference"
ng-model="filter.ref"> ng-model="filter.reference">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
vn-one
label="Invoice number"
ng-model="filter.invoiceNumber">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one vn-one
label="Travel" label="Travel"
ng-model="filter.travelFk"> ng-model="filter.travelFk">

View File

@ -5,4 +5,5 @@ From: Desde
To: Hasta To: Hasta
Agency: Agencia Agency: Agencia
Warehouse: Almacén Warehouse: Almacén
Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias Search entry by id or a suppliers by name or alias: Buscar entrada por id o proveedores por nombre y alias
Invoice number: Núm. factura

View File

@ -27,7 +27,10 @@
value="{{$ctrl.entryData.company.code}}"> value="{{$ctrl.entryData.company.code}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Reference" <vn-label-value label="Reference"
value="{{$ctrl.entryData.ref}}"> value="{{$ctrl.entryData.reference}}">
</vn-label-value>
<vn-label-value label="Invoice number"
value="{{$ctrl.entryData.invoiceNumber}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Notes" <vn-label-value label="Notes"
value="{{$ctrl.entryData.notes}}"> value="{{$ctrl.entryData.notes}}">

View File

@ -8,4 +8,4 @@ Minimum price: Precio mínimo
Buys: Compras Buys: Compras
Travel: Envio Travel: Envio
Go to the entry: Ir a la entrada Go to the entry: Ir a la entrada
Invoice number: Núm. factura

View File

@ -0,0 +1,53 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInEmail', {
description: 'Sends the invoice in email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoice id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
},
{
arg: 'recipientId',
type: 'number',
description: 'The recipient id to send to the recipient preferred language',
required: false
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/:id/invoice-in-email',
verb: 'POST'
}
});
Self.invoiceInEmail = async ctx => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoiceIn', params);
return email.send();
};
};

View File

@ -0,0 +1,50 @@
const {Report} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInPdf', {
description: 'Returns the invoiceIn pdf',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/invoice-in-pdf',
verb: 'GET'
}
});
Self.invoiceInPdf = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {lang: ctx.req.getLocale()};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const report = new Report('invoiceIn', params);
const stream = await report.toPdfStream();
return [stream, 'application/pdf', `filename="doc-${id}.pdf"`];
};
};

View File

@ -4,4 +4,6 @@ module.exports = Self => {
require('../methods/invoice-in/clone')(Self); require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self); require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self); require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
}; };

View File

@ -94,6 +94,11 @@
"model": "Supplier", "model": "Supplier",
"foreignKey": "supplierFk" "foreignKey": "supplierFk"
}, },
"supplierContact": {
"type": "hasMany",
"model": "SupplierContact",
"foreignKey": "supplierFk"
},
"currency": { "currency": {
"type": "belongsTo", "type": "belongsTo",
"model": "Currency", "model": "Currency",

View File

@ -8,6 +8,14 @@ class Controller extends ModuleCard {
{ {
relation: 'supplier' relation: 'supplier'
}, },
{
relation: 'supplierContact',
scope: {
where: {
email: {neq: null}
}
}
},
{ {
relation: 'invoiceInDueDay' relation: 'invoiceInDueDay'
}, },

View File

@ -1,5 +1,5 @@
<vn-descriptor-content <vn-descriptor-content
module="invoiceIn" module="invoiceIn"
description="$ctrl.invoiceIn.supplierRef" description="$ctrl.invoiceIn.supplierRef"
summary="$ctrl.$.summary"> summary="$ctrl.$.summary">
<slot-menu> <slot-menu>
@ -10,7 +10,6 @@
translate> translate>
To book To book
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
vn-acl="administrative" vn-acl="administrative"
@ -26,6 +25,16 @@
translate> translate>
Clone Invoice Clone Invoice
</vn-item> </vn-item>
<vn-item
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
</vn-item>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -37,7 +46,7 @@
</vn-label-value> </vn-label-value>
<vn-label-value label="Supplier"> <vn-label-value label="Supplier">
<span ng-click="supplierDescriptor.show($event, $ctrl.invoiceIn.supplier.id)" class="link"> <span ng-click="supplierDescriptor.show($event, $ctrl.invoiceIn.supplier.id)" class="link">
{{$ctrl.invoiceIn.supplier.nickname}} {{$ctrl.invoiceIn.supplier.nickname}}
</span> </span>
</vn-label-value> </vn-label-value>
</div> </div>
@ -57,9 +66,9 @@
icon="icon-invoice-in"> icon="icon-invoice-in">
</vn-quick-link> </vn-quick-link>
</div> </div>
</div> </div>
</slot-body> </slot-body>
</vn-descriptor-content> </vn-descriptor-content>
<vn-confirm <vn-confirm
@ -75,11 +84,29 @@
<vn-supplier-descriptor-popover <vn-supplier-descriptor-popover
vn-id="supplierDescriptor"> vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover> </vn-supplier-descriptor-popover>
<vn-confirm <vn-confirm
vn-id="confirm-toBookAnyway" vn-id="confirm-toBookAnyway"
message="Are you sure you want to book this invoice?" message="Are you sure you want to book this invoice?"
on-accept="$ctrl.onAcceptToBook()"> on-accept="$ctrl.onAcceptToBook()">
</vn-confirm> </vn-confirm>
<vn-popup vn-id="summary"> <vn-popup vn-id="summary">
<vn-invoice-in-summary invoice-in="$ctrl.invoiceIn"></vn-invoice-in-summary> <vn-invoice-in-summary invoice-in="$ctrl.invoiceIn"></vn-invoice-in-summary>
</vn-popup> </vn-popup>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -96,6 +96,20 @@ class Controller extends Descriptor {
.then(() => this.$state.reload()) .then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked'))); .then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
} }
showPdfInvoice() {
this.vnReport.show(`InvoiceIns/${this.id}/invoice-in-pdf`);
}
sendPdfInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.send(`InvoiceIns/${this.entity.id}/invoice-in-email`, {
recipient: $data.email,
recipientId: this.entity.supplier.id
});
}
} }
ngModule.vnComponent('vnInvoiceInDescriptor', { ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -19,3 +19,5 @@ To book: Contabilizar
Total amount: Total importe Total amount: Total importe
Total net: Total neto Total net: Total neto
Total stems: Total tallos Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF

View File

@ -0,0 +1,55 @@
const JSZip = require('jszip');
const fs = require('fs-extra');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('downloadZip', {
description: 'Download a zip file with multiple invoices pdfs',
accessType: 'READ',
accepts: [
{
arg: 'ids',
type: ['number'],
description: 'The invoice ids'
}
],
returns: {
arg: 'base64',
type: 'string',
root: true
},
http: {
path: '/downloadZip',
verb: 'POST'
}
});
Self.downloadZip = async function(ctx, ids, options) {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const zip = new JSZip();
let totalSize = 0;
const zipConfig = await models.ZipConfig.findOne(null, myOptions);
for (let id of ids) {
if (zipConfig && totalSize > zipConfig.maxSize) throw new UserError('Files are too large');
const invoiceOutPdf = await models.InvoiceOut.download(ctx, id, myOptions);
const fileName = extractFileName(invoiceOutPdf[2]);
const body = invoiceOutPdf[0];
const sizeInBytes = (await fs.promises.stat(body.path)).size;
const sizeInMegabytes = sizeInBytes / (1024 * 1024);
totalSize += sizeInMegabytes;
zip.file(fileName, body);
}
const base64 = await zip.generateAsync({type: 'base64'});
return base64;
};
function extractFileName(str) {
const matches = str.match(/"(.*?)"/);
return matches ? matches[1] : str;
}
};

View File

@ -0,0 +1,53 @@
const models = require('vn-loopback/server/server').models;
const UserError = require('vn-loopback/util/user-error');
describe('InvoiceOut downloadZip()', () => {
const userId = 9;
const invoiceIds = [1, 2];
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
it('should return part of link to dowloand the zip', async() => {
const tx = await models.Order.beginTransaction({});
try {
const options = {transaction: tx};
const result = await models.InvoiceOut.downloadZip(ctx, invoiceIds, options);
expect(result).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return an error if the size of the files is too large', async() => {
const tx = await models.Order.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const zipConfig = {
maxSize: 0
};
await models.ZipConfig.create(zipConfig, options);
await models.InvoiceOut.downloadZip(ctx, invoiceIds, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toEqual(new UserError(`Files are too large`));
});
});

View File

@ -22,5 +22,8 @@
}, },
"TaxType": { "TaxType": {
"dataSource": "vn" "dataSource": "vn"
},
"ZipConfig": {
"dataSource": "vn"
} }
} }

View File

@ -3,6 +3,7 @@ module.exports = Self => {
require('../methods/invoiceOut/summary')(Self); require('../methods/invoiceOut/summary')(Self);
require('../methods/invoiceOut/getTickets')(Self); require('../methods/invoiceOut/getTickets')(Self);
require('../methods/invoiceOut/download')(Self); require('../methods/invoiceOut/download')(Self);
require('../methods/invoiceOut/downloadZip')(Self);
require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/delete')(Self);
require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self); require('../methods/invoiceOut/createPdf')(Self);

View File

@ -0,0 +1,25 @@
{
"name": "ZipConfig",
"base": "VnModel",
"options": {
"mysql": {
"table": "zipConfig"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"maxSize": {
"type": "number"
}
},
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -4,10 +4,24 @@
<vn-data-viewer <vn-data-viewer
model="model" model="model"
class="vn-w-lg"> class="vn-w-lg">
<vn-card class="vn-pa-lg">
<vn-button
disabled="$ctrl.totalChecked == 0"
vn-click-stop="$ctrl.openPdf()"
icon="cloud_download"
title="Download PDF"
vn-tooltip="Download PDF">
</vn-button>
</vn-card>
<vn-card> <vn-card>
<vn-table model="model"> <vn-table model="model">
<vn-thead> <vn-thead>
<vn-tr> <vn-tr>
<vn-th shrink>
<vn-multi-check
model="model">
</vn-multi-check>
</vn-th>
<vn-th field="ref">Reference</vn-th> <vn-th field="ref">Reference</vn-th>
<vn-th field="issued" expand>Issued</vn-th> <vn-th field="issued" expand>Issued</vn-th>
<vn-th field="amount" number>Amount</vn-th> <vn-th field="amount" number>Amount</vn-th>
@ -23,6 +37,12 @@
<a ng-repeat="invoiceOut in model.data" <a ng-repeat="invoiceOut in model.data"
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="invoiceOut.card.summary({id: {{::invoiceOut.id}}})"> ui-sref="invoiceOut.card.summary({id: {{::invoiceOut.id}}})">
<vn-td>
<vn-check
ng-model="invoiceOut.checked"
vn-click-stop>
</vn-check>
</vn-td>
<vn-td>{{::invoiceOut.ref | dashIfEmpty}}</vn-td> <vn-td>{{::invoiceOut.ref | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::invoiceOut.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td> <vn-td shrink>{{::invoiceOut.issued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td number>{{::invoiceOut.amount | currency: 'EUR': 2 | dashIfEmpty}}</vn-td> <vn-td number>{{::invoiceOut.amount | currency: 'EUR': 2 | dashIfEmpty}}</vn-td>
@ -36,15 +56,6 @@
<vn-td expand>{{::invoiceOut.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td> <vn-td expand>{{::invoiceOut.created | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td>{{::invoiceOut.companyCode | dashIfEmpty}}</vn-td> <vn-td>{{::invoiceOut.companyCode | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::invoiceOut.dued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td> <vn-td shrink>{{::invoiceOut.dued | date:'dd/MM/yyyy' | dashIfEmpty}}</vn-td>
<vn-td shrink>
<vn-icon-button
ng-show="invoiceOut.hasPdf"
vn-click-stop="$ctrl.openPdf(invoiceOut.id)"
icon="cloud_download"
title="Download PDF"
vn-tooltip="Download PDF">
</vn-icon-button>
</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button <vn-icon-button
vn-click-stop="$ctrl.preview(invoiceOut)" vn-click-stop="$ctrl.preview(invoiceOut)"

View File

@ -2,14 +2,41 @@ import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section { export default class Controller extends Section {
get checked() {
const rows = this.$.model.data || [];
const checkedRows = [];
for (let row of rows) {
if (row.checked)
checkedRows.push(row.id);
}
return checkedRows;
}
get totalChecked() {
return this.checked.length;
}
preview(invoiceOut) { preview(invoiceOut) {
this.selectedInvoiceOut = invoiceOut; this.selectedInvoiceOut = invoiceOut;
this.$.summary.show(); this.$.summary.show();
} }
openPdf(id) { openPdf() {
let url = `api/InvoiceOuts/${id}/download?access_token=${this.vnToken.token}`; if (this.checked.length <= 1) {
window.open(url, '_blank'); const [invoiceOutId] = this.checked;
const url = `api/InvoiceOuts/${invoiceOutId}/download?access_token=${this.vnToken.token}`;
window.open(url, '_blank');
} else {
const invoiceOutIds = this.checked;
const params = {
ids: invoiceOutIds
};
this.$http.post(`InvoiceOuts/downloadZip`, params)
.then(res => {
location.href = 'data:application/zip;base64,' + res.data;
});
}
} }
} }

View File

@ -6,3 +6,4 @@ Minimum: Minimo
Maximum: Máximo Maximum: Máximo
Global invoicing: Facturación global Global invoicing: Facturación global
Manual invoicing: Facturación manual Manual invoicing: Facturación manual
Files are too large: Los archivos son demasiado grandes

View File

@ -0,0 +1,51 @@
module.exports = Self => {
Self.remoteMethod('deleteItemShelvings', {
description: 'Deletes the selected item shelvings',
accessType: 'WRITE',
accepts: [{
arg: 'itemShelvingIds',
type: ['number'],
required: true,
description: 'The itemShelving ids to delete'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteItemShelvings`,
verb: 'POST'
}
});
Self.deleteItemShelvings = async(itemShelvingIds, options) => {
const models = Self.app.models;
const myOptions = {};
let tx;
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const promises = [];
for (let itemShelvingId of itemShelvingIds) {
const itemShelvingToDelete = models.ItemShelving.destroyById(itemShelvingId, myOptions);
promises.push(itemShelvingToDelete);
}
const deletedItemShelvings = await Promise.all(promises);
if (tx) await tx.commit();
return deletedItemShelvings;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,21 @@
const models = require('vn-loopback/server/server').models;
describe('ItemShelving deleteItemShelvings()', () => {
it('should return the deleted itemShelvings', async() => {
const tx = await models.Order.beginTransaction({});
try {
const options = {transaction: tx};
const itemShelvingIds = [1, 2];
const result = await models.ItemShelving.deleteItemShelvings(itemShelvingIds, options);
expect(result.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -53,6 +53,9 @@
"ItemShelvingSale": { "ItemShelvingSale": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ItemShelvingPlacementSupplyStock": {
"dataSource": "vn"
},
"ItemImageQueue": { "ItemImageQueue": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,36 @@
{
"name": "ItemShelvingPlacementSupplyStock",
"base": "VnModel",
"options": {
"mysql": {
"table": "itemShelvingPlacementSupplyStock"
}
},
"properties": {
"itemShelvingFk": {
"type": "number",
"id": true
},
"created": {
"type": "date"
},
"itemFk": {
"type": "number"
},
"longName": {
"type": "string"
},
"parking": {
"type": "string"
},
"shelving": {
"type": "string"
},
"packing": {
"type": "number"
},
"stock": {
"type": "number"
}
}
}

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/item-shelving/deleteItemShelvings')(Self);
};

View File

@ -23,9 +23,9 @@
</vn-portal> </vn-portal>
<div class="vn-w-xl"> <div class="vn-w-xl">
<vn-card> <vn-card>
<smart-table <smart-table
model="model" model="model"
options="$ctrl.smartTableOptions" options="$ctrl.smartTableOptions"
expr-builder="$ctrl.exprBuilder(param, value)"> expr-builder="$ctrl.exprBuilder(param, value)">
<slot-table> <slot-table>
<table> <table>
@ -34,18 +34,18 @@
<th field="itemFk"> <th field="itemFk">
<span translate>Item ID</span> <span translate>Item ID</span>
</th> </th>
<th field="itemName"> <th field="name">
<span translate>Description</span> <span translate>Description</span>
</th> </th>
<th field="warehouseFk"> <th field="warehouseFk">
<span translate>Warehouse</span> <span translate>Warehouse</span>
</th> </th>
<th <th
field="rate2" field="rate2"
vn-tooltip="Price By Unit"> vn-tooltip="Price By Unit">
<span translate>P.P.U.</span> <span translate>P.P.U.</span>
</th> </th>
<th <th
field="rate3" field="rate3"
vn-tooltip="Price By Package"> vn-tooltip="Price By Package">
<span translate>P.P.P.</span> <span translate>P.P.P.</span>
@ -170,7 +170,7 @@
</vn-icon-button> </vn-icon-button>
</div> </div>
</slot-table> </slot-table>
</smart-table> </smart-table>
</vn-card> </vn-card>
</div> </div>
<vn-item-descriptor-popover <vn-item-descriptor-popover
@ -182,4 +182,4 @@
on-accept="$ctrl.removePrice($data.$index)" on-accept="$ctrl.removePrice($data.$index)"
question="Are you sure you want to continue?" question="Are you sure you want to continue?"
message="This row will be removed"> message="This row will be removed">
</vn-confirm> </vn-confirm>

View File

@ -12,14 +12,6 @@ export default class Controller extends Section {
}, },
defaultSearch: true, defaultSearch: true,
columns: [ columns: [
{
field: 'itemName',
autocomplete: {
url: 'Items',
showField: 'name',
valueField: 'id'
}
},
{ {
field: 'warehouseFk', field: 'warehouseFk',
autocomplete: { autocomplete: {
@ -105,8 +97,8 @@ export default class Controller extends Section {
exprBuilder(param, value) { exprBuilder(param, value) {
switch (param) { switch (param) {
case 'itemName': case 'name':
return {'i.id': value}; return {'i.name': {like: `%${value}%`}};
case 'itemFk': case 'itemFk':
case 'warehouseFk': case 'warehouseFk':
case 'rate2': case 'rate2':

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