Merge branch 'dev' into 6005-backupLabeler
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Pablo Natek 2023-11-22 09:38:20 +00:00
commit 8dae2919e8
96 changed files with 5686 additions and 3380 deletions

View File

@ -10,5 +10,9 @@
"eslint.format.enable": true, "eslint.format.enable": true,
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
} },
"cSpell.words": [
"salix",
"fdescribe"
]
} }

View File

@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2348.01] - 2023-11-30
### Added
- (Ticket -> Adelantar) Permite mover lineas sin generar negativos
- (Ticket -> Adelantar) Permite modificar la fecha de los tickets
### Changed
### Fixed
- (Ticket -> RocketChat) Arreglada detección de cambios
## [2346.01] - 2023-11-16 ## [2346.01] - 2023-11-16
### Added ### Added

View File

@ -1,4 +1,4 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
ENV TZ Europe/Madrid ENV TZ Europe/Madrid
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
@ -25,7 +25,13 @@ RUN apt-get update \
libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \ libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \ libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \ libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \ fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
# Extra dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
samba-common-bin samba-dsdb-modules\
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& npm -g install pm2 && npm -g install pm2

View File

@ -8,7 +8,7 @@ Salix is also the scientific name of a beautifull tree! :)
Required applications. Required applications.
* Node.js >= 16.x LTS * Node.js
* Docker * Docker
* Git * Git
@ -17,20 +17,7 @@ You will need to install globally the following items.
$ sudo npm install -g jest gulp-cli $ sudo npm install -g jest gulp-cli
``` ```
For the usage of jest --watch on macOs. ## Installing dependencies and launching
```
$ brew install watchman
```
* [watchman](https://facebook.github.io/watchman/)
## Linux Only Prerequisites
Your user must be on the docker group to use it so you will need to run this command:
```
$ sudo usermod -a -G docker yourusername
```
## Getting Started // Installing
Pull from repository. Pull from repository.
@ -76,29 +63,6 @@ In Visual Studio Code we use the ESLint extension.
ext install dbaeumer.vscode-eslint ext install dbaeumer.vscode-eslint
``` ```
Gitlens for visualization of code authorship
```
ext install eamodio.gitlens
```
Spanish language pack
```
ext install ms-ceintl.vscode-language-pack-es
```
### Recommended extensions
Material icon Theme
```
ext install pkief.material-icon-theme
```
Material UI Themes
```
ext install equinusocio.vsc-material-theme
```
## Built With ## Built With
* [angularjs](https://angularjs.org/) * [angularjs](https://angularjs.org/)

View File

@ -0,0 +1,54 @@
module.exports = Self => {
Self.remoteMethod('getList', {
description: 'Get list of the available and active notification subscriptions',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
description: 'User to modify',
http: {source: 'path'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/getList`,
verb: 'GET'
}
});
Self.getList = async(id, options) => {
const activeNotificationsMap = new Map();
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const availableNotificationsMap = await Self.getAvailable(id, myOptions);
const activeNotifications = await Self.app.models.NotificationSubscription.find({
fields: ['id', 'notificationFk'],
include: {relation: 'notification'},
where: {userFk: id}
}, myOptions);
for (active of activeNotifications) {
activeNotificationsMap.set(active.notificationFk, {
id: active.id,
notificationFk: active.notificationFk,
name: active.notification().name,
description: active.notification().description,
active: true
});
availableNotificationsMap.delete(active.notificationFk);
}
return {
active: [...activeNotificationsMap.entries()],
available: [...availableNotificationsMap.entries()]
};
};
};

View File

@ -0,0 +1,13 @@
const models = require('vn-loopback/server/server').models;
describe('NotificationSubscription getList()', () => {
it('should return a list of available and active notifications of a user', async() => {
const userId = 9;
const {active, available} = await models.NotificationSubscription.getList(userId);
const notifications = await models.Notification.find({});
const totalAvailable = notifications.length - active.length;
expect(active.length).toEqual(2);
expect(available.length).toEqual(totalAvailable);
});
});

View File

@ -49,8 +49,13 @@ module.exports = Self => {
if (vnUser.twoFactor) if (vnUser.twoFactor)
throw new ForbiddenError(null, 'REQUIRES_2FA'); throw new ForbiddenError(null, 'REQUIRES_2FA');
} }
const validateLogin = await Self.validateLogin(user, password);
return Self.validateLogin(user, password); await Self.app.models.SignInLog.create({
token: validateLogin.token,
userFk: vnUser.id,
ip: ctx.req.ip
});
return validateLogin;
}; };
Self.passExpired = async vnUser => { Self.passExpired = async vnUser => {

View File

@ -12,8 +12,21 @@ describe('VnUser Sign-in()', () => {
}, },
args: {} args: {}
}; };
const {VnUser, AccessToken} = models; const {VnUser, AccessToken, SignInLog} = models;
describe('when credentials are correct', () => { describe('when credentials are correct', () => {
it('should return the token if user uses email', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant@mydomain.com', 'nightmare');
let accessToken = await AccessToken.findById(login.token);
let ctx = {req: {accessToken: accessToken}};
let signInLog = await SignInLog.find({where: {token: accessToken.id}});
expect(signInLog.length).toEqual(1);
expect(signInLog[0].userFk).toEqual(accessToken.userId);
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id);
});
it('should return the token', async() => { it('should return the token', async() => {
let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare'); let login = await VnUser.signIn(unauthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token); let accessToken = await AccessToken.findById(login.token);

View File

@ -1,62 +1,74 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
require('../methods/notification/getList')(Self);
Self.observe('before save', async function(ctx) { Self.observe('before save', async function(ctx) {
await checkModifyPermission(ctx);
});
Self.observe('before delete', async function(ctx) {
await checkModifyPermission(ctx);
});
async function checkModifyPermission(ctx) {
const models = Self.app.models; const models = Self.app.models;
const instance = ctx.instance;
const userId = ctx.options.accessToken.userId; const userId = ctx.options.accessToken.userId;
const user = await ctx.instance.userFk;
const modifiedUser = await getUserToModify(null, user, models);
if (userId != modifiedUser.id && userId != modifiedUser.bossFk) let notificationFk;
throw new UserError('You dont have permission to modify this user'); let workerId;
});
Self.remoteMethod('deleteNotification', { if (instance) {
description: 'Deletes a notification subscription', notificationFk = instance.notificationFk;
accepts: [ workerId = instance.userFk;
{ } else {
arg: 'ctx', const notificationSubscription = await models.NotificationSubscription.findById(ctx.where.id);
type: 'object', notificationFk = notificationSubscription.notificationFk;
http: {source: 'context'} workerId = notificationSubscription.userFk;
},
{
arg: 'notificationId',
type: 'number',
required: true
},
],
returns: {
type: 'object',
root: true
},
http: {
verb: 'POST',
path: '/deleteNotification'
} }
});
Self.deleteNotification = async function(ctx, notificationId) { const worker = await models.Worker.findById(workerId, {fields: ['id', 'bossFk']});
const models = Self.app.models; const available = await Self.getAvailable(workerId);
const user = ctx.req.accessToken.userId; const hasAcl = available.has(notificationFk);
const modifiedUser = await getUserToModify(notificationId, null, models);
if (user != modifiedUser.id && user != modifiedUser.bossFk) if (!hasAcl || (userId != worker.id && userId != worker.bossFk))
throw new UserError('You dont have permission to modify this user'); throw new UserError('The notification subscription of this worker cant be modified');
await models.NotificationSubscription.destroyById(notificationId);
};
async function getUserToModify(notificationId, userFk, models) {
let userToModify = userFk;
if (notificationId) {
const subscription = await models.NotificationSubscription.findById(notificationId);
userToModify = subscription.userFk;
}
return await models.Worker.findOne({
fields: ['id', 'bossFk'],
where: {
id: userToModify
}
});
} }
Self.getAvailable = async function(userId, options) {
const availableNotificationsMap = new Map();
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const roles = await models.RoleMapping.find({
fields: ['roleId'],
where: {principalId: userId}
}, myOptions);
const availableNotifications = await models.NotificationAcl.find({
fields: ['notificationFk', 'roleFk'],
include: {relation: 'notification'},
where: {
roleFk: {
inq: roles.map(role => role.roleId),
},
}
}, myOptions);
for (available of availableNotifications) {
availableNotificationsMap.set(available.notificationFk, {
id: null,
notificationFk: available.notificationFk,
name: available.notification().name,
description: available.notification().description,
active: false
});
}
return availableNotificationsMap;
};
}; };

View File

@ -1,74 +1,126 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('loopback model NotificationSubscription', () => { describe('loopback model NotificationSubscription', () => {
it('Should fail to delete a notification if the user is not editing itself or a subordinate', async() => { it('should fail to add a notification subscription if the worker doesnt have ACLs', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; await models.NotificationSubscription.create({notificationFk: 1, userFk: 62}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to add a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 1}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
});
it('should fail to delete a notification subscription if the user isnt editing itself or subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 2; const notificationSubscriptionId = 2;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
let error;
try {
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
} catch (e) {
error = e;
}
expect(error.message).toContain('You dont have permission to modify this user');
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
expect(error.message).toEqual('The notification subscription of this worker cant be modified');
}); });
it('Should delete a notification if the user is editing itself', async() => { it('should add a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({}); const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx, accessToken: {userId: 9}};
const user = 9; await models.NotificationSubscription.create({notificationFk: 2, userFk: 9}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing itself', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
const notificationSubscriptionId = 6;
await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should add a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.NotificationSubscription.create({notificationFk: 1, userFk: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a notification subscription if the user is editing a subordinate', async() => {
const tx = await models.NotificationSubscription.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 19}};
const notificationSubscriptionId = 4; const notificationSubscriptionId = 4;
const ctx = {req: {accessToken: {userId: user}}}; await models.NotificationSubscription.destroyAll({id: notificationSubscriptionId}, options);
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; error = e;
} }
});
it('Should delete a notification if the user is editing a subordinate', async() => { expect(error).toBeUndefined();
const tx = await models.NotificationSubscription.beginTransaction({});
try {
const options = {transaction: tx};
const user = 9;
const notificationSubscriptionId = 5;
const ctx = {req: {accessToken: {userId: user}}};
const notification = await models.NotificationSubscription.findById(notificationSubscriptionId);
await models.NotificationSubscription.deleteNotification(ctx, notification.id, options);
const deletedNotification = await models.NotificationSubscription.findById(notificationSubscriptionId);
expect(deletedNotification).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
}); });
}); });

View File

@ -2,6 +2,7 @@ const vnModel = require('vn-loopback/common/models/vn-model');
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const ForbiddenError = require('vn-loopback/util/forbiddenError'); const ForbiddenError = require('vn-loopback/util/forbiddenError');
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) { module.exports = function(Self) {
vnModel(Self); vnModel(Self);
@ -92,7 +93,11 @@ module.exports = function(Self) {
}; };
Self.on('resetPasswordRequest', async function(info) { Self.on('resetPasswordRequest', async function(info) {
const url = await Self.app.models.Url.getUrl(); const loopBackContext = LoopBackContext.getCurrentContext();
const httpCtx = {req: loopBackContext.active};
const httpRequest = httpCtx.req.http.req;
const headers = httpRequest.headers;
const origin = headers.origin;
const defaultHash = '/reset-password?access_token=$token$'; const defaultHash = '/reset-password?access_token=$token$';
const recoverHashes = { const recoverHashes = {
@ -108,7 +113,7 @@ module.exports = function(Self) {
const params = { const params = {
recipient: info.email, recipient: info.email,
lang: user.lang, lang: user.lang,
url: url.slice(0, -1) + recoverHash url: origin + '/#!' + recoverHash
}; };
const options = Object.assign({}, info.options); const options = Object.assign({}, info.options);
@ -119,12 +124,21 @@ module.exports = function(Self) {
return email.send(); return email.send();
}); });
Self.signInValidate = (user, userToken) => {
const [[key, value]] = Object.entries(Self.userUses(user));
if (userToken[key].toLowerCase().trim() !== value.toLowerCase().trim()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
};
Self.validateLogin = async function(user, password) { Self.validateLogin = async function(user, password) {
let loginInfo = Object.assign({password}, Self.userUses(user)); const loginInfo = Object.assign({password}, Self.userUses(user));
token = await Self.login(loginInfo, 'user'); const token = await Self.login(loginInfo, 'user');
const userToken = await token.user.get(); const userToken = await token.user.get();
Self.signInValidate(user, userToken);
try { try {
await Self.app.models.Account.sync(userToken.name, password); await Self.app.models.Account.sync(userToken.name, password);
} catch (err) { } catch (err) {
@ -220,8 +234,11 @@ module.exports = function(Self) {
class Mailer { class Mailer {
async send(verifyOptions, cb) { async send(verifyOptions, cb) {
const url = new URL(verifyOptions.verifyHref);
if (process.env.NODE_ENV) url.port = '';
const params = { const params = {
url: verifyOptions.verifyHref, url: url.href,
recipient: verifyOptions.to recipient: verifyOptions.to
}; };

View File

@ -74,7 +74,7 @@ BEGIN
clientFk, clientFk,
dued, dued,
companyFk, companyFk,
cplusInvoiceType477Fk siiTypeInvoiceOutFk
) )
SELECT SELECT
1, 1,

View File

@ -96,7 +96,7 @@ BEGIN
clientFk, clientFk,
dued, dued,
companyFk, companyFk,
cplusInvoiceType477Fk siiTypeInvoiceOutFk
) )
SELECT SELECT
1, 1,

View File

@ -96,7 +96,7 @@ BEGIN
clientFk, clientFk,
dued, dued,
companyFk, companyFk,
cplusInvoiceType477Fk siiTypeInvoiceOutFk
) )
SELECT SELECT
1, 1,

View File

@ -1,6 +1,6 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES VALUES
('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), ('CplusRectificationType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('CplusInvoiceType477', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), ('SiiTypeInvoiceOut', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'), ('InvoiceCorrectionType', '*', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative'); ('InvoiceOut', 'transferInvoice', 'WRITE', 'ALLOW', 'ROLE', 'administrative');

View File

@ -0,0 +1 @@
CALL `account`.`role_sync`();

View File

@ -0,0 +1,20 @@
--
-- Table structure for table `signInLog`
-- Description: log to debug cross-login error
--
DROP TABLE IF EXISTS `account`.`signInLog`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `account`.`signInLog` (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`token` varchar(255) NOT NULL ,
`userFk` int(10) unsigned DEFAULT NULL,
`creationDate` timestamp NULL DEFAULT current_timestamp(),
`ip` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
KEY `userFk` (`userFk`),
CONSTRAINT `signInLog_ibfk_1` FOREIGN KEY (`userFk`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
);

View File

@ -0,0 +1,2 @@
ALTER TABLE account.sambaConfig
ADD userDn varchar(255) NOT NULL COMMENT 'Base DN for users without domain DN part';

View File

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -59,10 +59,6 @@ INSERT IGNORE INTO `vn`.`greugeConfig`(`id`, `freightPickUpPrice`)
VALUES VALUES
('1', '11'); ('1', '11');
INSERT INTO `vn`.`packagingConfig`(`upperGap`)
VALUES
('10');
UPDATE `account`.`role` SET id = 100 WHERE id = 0; UPDATE `account`.`role` SET id = 100 WHERE id = 0;
INSERT INTO `account`.`roleConfig`(`id`, `mysqlPassword`, `rolePrefix`, `userPrefix`, `userHost`, `tplUser`) INSERT INTO `account`.`roleConfig`(`id`, `mysqlPassword`, `rolePrefix`, `userPrefix`, `userHost`, `tplUser`)
@ -73,7 +69,7 @@ CALL `account`.`role_sync`;
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `lang`, `image`, `password`) INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `role`,`active`,`email`, `lang`, `image`, `password`)
SELECT id, name, CONCAT(name, 'Nick'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2' SELECT id, name, CONCAT(name, 'Nick'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd', '$2b$10$UzQHth.9UUQ1T5aiQJ21lOU0oVlbxoqH4PFM9V8T90KNSAcg0eEL2'
FROM `account`.`role` WHERE id <> 20 FROM `account`.`role`
ORDER BY id; ORDER BY id;
INSERT INTO `account`.`account`(`id`) INSERT INTO `account`.`account`(`id`)
@ -87,9 +83,15 @@ INSERT INTO `vn`.`educationLevel` (`id`, `name`)
(1, 'ESTUDIOS PRIMARIOS COMPLETOS'), (1, 'ESTUDIOS PRIMARIOS COMPLETOS'),
(2, 'ENSEÑANZAS DE BACHILLERATO'); (2, 'ENSEÑANZAS DE BACHILLERATO');
INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `bossFk`)
SELECT id,UPPER(LPAD(role, 3, '0')), name, name, NULL
FROM `account`.`user`
WHERE `id` = 9;
INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `bossFk`) INSERT INTO `vn`.`worker`(`id`,`code`, `firstName`, `lastName`, `bossFk`)
SELECT id,UPPER(LPAD(role, 3, '0')), name, name, 9 SELECT id,UPPER(LPAD(role, 3, '0')), name, name, 9
FROM `account`.`user`; FROM `account`.`user`
WHERE `id` <> 9;
UPDATE `vn`.`worker` SET bossFk = NULL WHERE id = 20; UPDATE `vn`.`worker` SET bossFk = NULL WHERE id = 20;
UPDATE `vn`.`worker` SET bossFk = 20 WHERE id = 1 OR id = 9; UPDATE `vn`.`worker` SET bossFk = 20 WHERE id = 1 OR id = 9;
@ -368,7 +370,7 @@ INSERT INTO `vn`.`client`(`id`,`name`,`fi`,`socialName`,`contact`,`street`,`city
(1105, 'Max Eisenhardt', '251628698', 'MAGNETO', 'Rogue', 'UNKNOWN WHEREABOUTS', 'Gotham', 46460, 1111111111, 222222222, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 300, 8, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1, 'florist','normal'), (1105, 'Max Eisenhardt', '251628698', 'MAGNETO', 'Rogue', 'UNKNOWN WHEREABOUTS', 'Gotham', 46460, 1111111111, 222222222, 1, 'MaxEisenhardt@mydomain.com', NULL, 0, 1234567890, 0, 3, 1, 300, 8, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 18, 0, 1, 'florist','normal'),
(1106, 'DavidCharlesHaller', '53136686Q', 'LEGION', 'Charles Xavier', 'CITY OF NEW YORK, NEW YORK, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'), (1106, 'DavidCharlesHaller', '53136686Q', 'LEGION', 'Charles Xavier', 'CITY OF NEW YORK, NEW YORK, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'DavidCharlesHaller@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 0, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
(1107, 'Hank Pym', '09854837G', 'ANT MAN', 'Hawk', 'ANTHILL, SAN FRANCISCO, CALIFORNIA', 'Gotham', 46460, 1111111111, 222222222, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'), (1107, 'Hank Pym', '09854837G', 'ANT MAN', 'Hawk', 'ANTHILL, SAN FRANCISCO, CALIFORNIA', 'Gotham', 46460, 1111111111, 222222222, 1, 'HankPym@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
(1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1, 'florist','normal'), (1108, 'Charles Xavier', '22641921P', 'PROFESSOR X', 'Beast', '3800 VICTORY PKWY, CINCINNATI, OH 45207, USA', 'Gotham', 46460, 1111111111, 222222222, 1, 'CharlesXavier@mydomain.com', NULL, 0, 1234567890, 0, 5, 1, 300, 13, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 1, 1, NULL, 0, 0, 19, 0, 1, 'florist','normal'),
(1109, 'Bruce Banner', '16104829E', 'HULK', 'Black widow', 'SOMEWHERE IN NEW YORK', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 9, 0, 1, 'florist','normal'), (1109, 'Bruce Banner', '16104829E', 'HULK', 'Black widow', 'SOMEWHERE IN NEW YORK', 'Gotham', 46460, 1111111111, 222222222, 1, 'BruceBanner@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, 9, 0, 1, 'florist','normal'),
(1110, 'Jessica Jones', '58282869H', 'JESSICA JONES', 'Luke Cage', 'NYCC 2015 POSTER', 'Gotham', 46460, 1111111111, 222222222, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1, 'florist','normal'), (1110, 'Jessica Jones', '58282869H', 'JESSICA JONES', 'Luke Cage', 'NYCC 2015 POSTER', 'Gotham', 46460, 1111111111, 222222222, 1, 'JessicaJones@mydomain.com', NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 1, 1, 0, 0, NULL, 0, 0, NULL, 0, 1, 'florist','normal'),
(1111, 'Missing', NULL, 'MISSING MAN', 'Anton', 'THE SPACE, UNIVERSE FAR AWAY', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, 0, 1, 0, NULL, 1, 0, NULL, 0, 1, 'others','normal'), (1111, 'Missing', NULL, 'MISSING MAN', 'Anton', 'THE SPACE, UNIVERSE FAR AWAY', 'Gotham', 46460, 1111111111, 222222222, 1, NULL, NULL, 0, 1234567890, 0, 1, 1, 300, 1, 1, NULL, 10, 5, util.VN_CURDATE(), 1, 5, 1, 1, 1, '0000-00-00', 4, 0, 1, 0, NULL, 1, 0, NULL, 0, 1, 'others','normal'),
@ -406,7 +408,7 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(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, 5, 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),
@ -438,7 +440,7 @@ INSERT INTO `vn`.`address`(`id`, `nickname`, `street`, `city`, `postalCode`, `pr
(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, 5, 1111111111, 222222222, 1, 1108, 2, NULL, NULL, 0, 0),
(129, 'Luke Cages Bar', 'address 29', 'Gotham', 'EC170150', 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);
@ -550,15 +552,6 @@ INSERT INTO `vn`.`supplierActivity`(`code`, `name`)
('flowerPlants', 'Wholesale of flowers and plants'), ('flowerPlants', 'Wholesale of flowers and plants'),
('vegetablesFruits', 'Fruit and vegetable trade'); ('vegetablesFruits', 'Fruit and vegetable trade');
INSERT INTO `vn`.`supplierAddress`(`id`, `supplierFk`, `nickname`, `street`, `provinceFk`, `postalCode`, `city`, `phone`, `mobile`)
VALUES
(1, 1, 'Ace Chemicals', 'The Midtown', 1, '46000', 'Gotham', '111111111', '222222222'),
(2, 1, 'Arkham Asylum', 'Grand Avenue', 1, '46000', 'Gotham', '111111111', '222222222'),
(3, 2, 'Wayne Tower', 'Grand Avenue', 1, '46000', 'Gotham', '111111111', '222222222'),
(4, 2, 'Bank of Gotham', 'Founders Island', 1, '46000', 'Gotham', '111111111', '222222222'),
(5, 442, 'GCR building', 'Bristol district', 1, '46000', 'Gotham', '111111111', '222222222'),
(6, 442, 'The Gotham Tonight building', 'Bristol district', 1, '46000', 'Gotham', '111111111', '222222222');
INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`) INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`, `commission`, `created`, `isActive`, `street`, `city`, `provinceFk`, `postCode`, `payMethodFk`, `payDemFk`, `payDay`, `taxTypeSageFk`, `withholdingSageFk`, `transactionTypeSageFk`, `workerFk`, `supplierActivityFk`, `isPayMethodChecked`, `healthRegister`)
VALUES VALUES
(1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'), (1, 'Plants SL', 'Plants nick', 4100000001, 1, '06089160W', 0, util.VN_CURDATE(), 1, 'supplier address 1', 'PONTEVEDRA', 1, 15214, 1, 1, 15, 4, 1, 1, 18, 'flowerPlants', 1, '400664487V'),
@ -569,6 +562,15 @@ INSERT INTO `vn`.`supplier`(`id`, `name`, `nickname`,`account`,`countryFk`,`nif`
(791, 'Bros SL', 'Bros nick', 5115000791, 1, '37718083S', 0, util.VN_CURDATE(), 1, 'supplier address 7', 'ASGARD', 3, 46600, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'), (791, 'Bros SL', 'Bros nick', 5115000791, 1, '37718083S', 0, util.VN_CURDATE(), 1, 'supplier address 7', 'ASGARD', 3, 46600, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'),
(1381, 'Ornamentales', 'Ornamentales', 7185001381, 1, '07972486L', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V'); (1381, 'Ornamentales', 'Ornamentales', 7185001381, 1, '07972486L', 0, util.VN_CURDATE(), 1, 'supplier address 4', 'GOTHAM', 1, 43022, 1, 2, 15, 6, 9, 3, 18, 'complements', 1, '400664487V');
INSERT INTO `vn`.`supplierAddress`(`id`, `supplierFk`, `nickname`, `street`, `provinceFk`, `postalCode`, `city`, `phone`, `mobile`)
VALUES
(1, 1, 'Ace Chemicals', 'The Midtown', 1, '46000', 'Gotham', '111111111', '222222222'),
(2, 1, 'Arkham Asylum', 'Grand Avenue', 1, '46000', 'Gotham', '111111111', '222222222'),
(3, 2, 'Wayne Tower', 'Grand Avenue', 1, '46000', 'Gotham', '111111111', '222222222'),
(4, 2, 'Bank of Gotham', 'Founders Island', 1, '46000', 'Gotham', '111111111', '222222222'),
(5, 442, 'GCR building', 'Bristol district', 1, '46000', 'Gotham', '111111111', '222222222'),
(6, 442, 'The Gotham Tonight building', 'Bristol district', 1, '46000', 'Gotham', '111111111', '222222222');
INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`) INSERT INTO `vn`.`supplierContact`(`id`, `supplierFk`, `phone`, `mobile`, `email`, `observation`, `name`)
VALUES VALUES
(1, 1, 123121212, 654789123, 'supplier1@email.es', 'observation1', 'the boss'), (1, 1, 123121212, 654789123, 'supplier1@email.es', 'observation1', 'the boss'),
@ -967,6 +969,10 @@ INSERT INTO `vn`.`packaging`(`id`, `volume`, `width`, `height`, `depth`, `isPack
('cc', 1640038.00, 56.00, 220.00, 128.00, 1, util.VN_CURDATE(), 15, 90.00), ('cc', 1640038.00, 56.00, 220.00, 128.00, 1, util.VN_CURDATE(), 15, 90.00),
('pallet 100', 2745600.00, 100.00, 220.00, 120.00, 1, util.VN_CURDATE(), 16, 0.00); ('pallet 100', 2745600.00, 100.00, 220.00, 120.00, 1, util.VN_CURDATE(), 16, 0.00);
INSERT INTO `vn`.`packagingConfig`(`upperGap`, `defaultSmallPackageFk`, `defaultBigPackageFk`)
VALUES
('10', 1, 'pallet 100');
INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`) INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`)
VALUES VALUES
(1, 'En reparto', 'ON DELIVERY'), (1, 'En reparto', 'ON DELIVERY'),
@ -2791,6 +2797,11 @@ INSERT IGNORE INTO `util`.`notification` (`id`, `name`, `description`)
INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`) INSERT INTO `util`.`notificationAcl` (`notificationFk`, `roleFk`)
VALUES VALUES
(1, 9), (1, 9),
(1, 1),
(2, 1),
(3, 9),
(4, 1),
(5, 9),
(6, 9); (6, 9);
INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`) INSERT INTO `util`.`notificationQueue` (`id`, `notificationFk`, `params`, `authorFk`, `status`, `created`)
@ -2803,6 +2814,8 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`)
VALUES VALUES
(1, 1109), (1, 1109),
(1, 1110), (1, 1110),
(2, 1110),
(4, 1110),
(2, 1109), (2, 1109),
(1, 9), (1, 9),
(1, 3), (1, 3),
@ -2883,7 +2896,9 @@ INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
INSERT INTO `vn`.`payDemDetail` (`id`, `detail`) INSERT INTO `vn`.`payDemDetail` (`id`, `detail`)
VALUES VALUES
(1, 1); (1, 1),
(2, 20),
(7, 1);
INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk`, `businessTypeFk`) INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk`, `businessTypeFk`)
VALUES VALUES

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,12 @@ TABLES=(
alertLevel alertLevel
bookingPlanner bookingPlanner
businessType businessType
cplusInvoiceType472 siiTypeInvoiceIn
cplusInvoiceType477 siiTypeInvoiceOut
cplusRectificationType cplusRectificationType
cplusSubjectOp cplusSubjectOp
cplusTaxBreak cplusTaxBreak
cplusTrascendency472 siiTrascendencyInvoiceIn
claimResponsible claimResponsible
claimReason claimReason
claimRedelivery claimRedelivery
@ -68,6 +68,8 @@ TABLES=(
volumeConfig volumeConfig
workCenter workCenter
companyI18n companyI18n
workerTimeControlError
silexACL
) )
dump_tables ${TABLES[@]} dump_tables ${TABLES[@]}

View File

@ -145,6 +145,7 @@ export default {
adController: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adController"]', adController: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adController"]',
adUser: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adUser"]', adUser: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adUser"]',
adPassword: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adPassword"]', adPassword: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adPassword"]',
userDn: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.userDn"]',
verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]', verifyCert: 'vn-account-samba vn-check[ng-model="$ctrl.config.verifyCert"]',
save: 'vn-account-samba vn-submit' save: 'vn-account-samba vn-submit'
}, },
@ -722,7 +723,7 @@ export default {
isFullMovable: 'vn-check[ng-model="filter.isFullMovable"]', isFullMovable: 'vn-check[ng-model="filter.isFullMovable"]',
warehouseFk: 'vn-autocomplete[label="Warehouse"]', warehouseFk: 'vn-autocomplete[label="Warehouse"]',
tableButtonSearch: 'vn-button[vn-tooltip="Search"]', tableButtonSearch: 'vn-button[vn-tooltip="Search"]',
moveButton: 'vn-button[vn-tooltip="Advance tickets"]', moveButton: 'vn-button[vn-tooltip="Advance tickets with negatives"]',
acceptButton: '.vn-confirm.shown button[response="accept"]', acceptButton: '.vn-confirm.shown button[response="accept"]',
firstCheck: 'tbody > tr:nth-child(2) > td > vn-check', firstCheck: 'tbody > tr:nth-child(2) > td > vn-check',
tableId: 'vn-textfield[name="id"]', tableId: 'vn-textfield[name="id"]',

View File

@ -22,7 +22,7 @@ describe('Travel basic data path', () => {
await page.waitForState('travel.card.basicData'); await page.waitForState('travel.card.basicData');
}); });
it('should set a wrong delivery date then receive an error on submit', async() => { it('should throw error if try move a travel with entries', async() => {
const lastMonth = Date.vnNew(); const lastMonth = Date.vnNew();
lastMonth.setMonth(lastMonth.getMonth() - 1); lastMonth.setMonth(lastMonth.getMonth() - 1);
@ -30,6 +30,23 @@ describe('Travel basic data path', () => {
await page.waitToClick(selectors.travelBasicData.save); await page.waitToClick(selectors.travelBasicData.save);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Cannot past travels with entries');
});
it('should set a wrong delivery date then receive an error on submit', async() => {
await page.loginAndModule('buyer', 'travel');
await page.write(selectors.travelIndex.generalSearchFilter, '4');
await page.keyboard.press('Enter');
await page.accessToSection('travel.card.basicData');
await page.waitForState('travel.card.basicData');
const lastMonth = Date.vnNew();
lastMonth.setMonth(lastMonth.getMonth() - 2);
await page.pickDate(selectors.travelBasicData.deliveryDate, lastMonth);
await page.waitToClick(selectors.travelBasicData.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Landing cannot be lesser than shipment'); expect(message.text).toContain('Landing cannot be lesser than shipment');
}); });
@ -39,7 +56,7 @@ describe('Travel basic data path', () => {
await page.waitToClick(selectors.travelBasicData.undoChanges); await page.waitToClick(selectors.travelBasicData.undoChanges);
const result = await page.waitToGetProperty(selectors.travelBasicData.reference, 'value'); const result = await page.waitToGetProperty(selectors.travelBasicData.reference, 'value');
expect(result).toEqual('third travel'); expect(result).toEqual('fourth travel');
}); });
it('should now edit the whole form then save', async() => { it('should now edit the whole form then save', async() => {

View File

@ -23,18 +23,6 @@ describe('Account Accounts path', () => {
expect(message.text).toContain('Roles synchronized!'); expect(message.text).toContain('Roles synchronized!');
}); });
it('should sync user', async() => {
await page.waitToClick(selectors.accountAccounts.syncUser);
await page.write(selectors.accountAccounts.syncUserName, 'sysadmin');
await page.write(selectors.accountAccounts.syncUserPassword, 'nightmare');
await page.waitToClick(selectors.accountAccounts.buttonAccept);
const message = await page.waitForSnackbar();
expect(message.text).toContain('User synchronized!');
});
it('should relogin', async() => { it('should relogin', async() => {
await page.loginAndModule('sysadmin', 'account'); await page.loginAndModule('sysadmin', 'account');
await page.accessToSection('account.accounts'); await page.accessToSection('account.accounts');

View File

@ -20,8 +20,9 @@ describe('Account Samba path', () => {
await page.waitToClick(selectors.accountSamba.checkEnable); await page.waitToClick(selectors.accountSamba.checkEnable);
await page.write(selectors.accountSamba.adDomain, '1234'); await page.write(selectors.accountSamba.adDomain, '1234');
await page.write(selectors.accountSamba.adController, '1234'); await page.write(selectors.accountSamba.adController, '1234');
await page.write(selectors.accountSamba.adUser, 'nightmare'); await page.write(selectors.accountSamba.adUser, 'sysadmin');
await page.write(selectors.accountSamba.adPassword, 'sysadmin'); await page.write(selectors.accountSamba.adPassword, 'nightmare');
await page.write(selectors.accountSamba.userDn, 'testDn');
await page.waitToClick(selectors.accountSamba.verifyCert); await page.waitToClick(selectors.accountSamba.verifyCert);
await page.waitToClick(selectors.accountSamba.save); await page.waitToClick(selectors.accountSamba.save);

View File

@ -0,0 +1,28 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.execute = async(ctx, type, query, params, options) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
params = params ?? [];
const myOptions = {userId: ctx.req.accessToken.userId};
if (typeof options == 'object')
Object.assign(myOptions, options);
const chain = query.split(' ')[1];
const [canExecute] = await models.ProcsPriv.rawSql(
'SELECT account.user_hasRoutinePriv(?,?,?)',
[type, chain, userId],
myOptions);
if (!Object.values(canExecute)[0]) throw new UserError(`You don't have enough privileges`, 'ACCESS_DENIED');
const argString = params.map(() => '?').join(',');
const response = await models.ProcsPriv.rawSql(query + `(${argString})`, params, myOptions);
if (!Array.isArray(response)) return;
return response[0];
};
};

View File

@ -0,0 +1,41 @@
module.exports = Self => {
Self.remoteMethodCtx('executeFunc', {
description: 'Return result of function',
accessType: 'EXECUTE',
accepts: [
{
arg: 'routine',
type: 'string',
description: 'The routine name',
required: true,
http: {source: 'path'}
},
{
arg: 'schema',
type: 'string',
description: 'The routine schema',
required: true,
},
{
arg: 'params',
type: ['any'],
description: 'The params array',
},
],
returns: {
type: 'any',
root: true
},
http: {
path: `/:routine/execute-func`,
verb: 'POST'
}
});
Self.executeFunc = async(ctx, routine, schema, params, options) => {
const query = `SELECT ${schema}.${routine}`;
const response = await Self.execute(ctx, 'FUNCTION', query, params, options);
return Object.values(response)[0];
};
};

View File

@ -0,0 +1,39 @@
module.exports = Self => {
Self.remoteMethodCtx('executeProc', {
description: 'Return result of procedure',
accessType: 'EXECUTE',
accepts: [
{
arg: 'routine',
type: 'string',
description: 'The routine name',
required: true,
http: {source: 'path'}
},
{
arg: 'schema',
type: 'string',
description: 'The routine schema',
required: true,
},
{
arg: 'params',
type: ['any'],
description: 'The params array',
},
],
returns: {
type: 'any',
root: true
},
http: {
path: `/:routine/execute-proc`,
verb: 'POST'
}
});
Self.executeProc = async(ctx, routine, schema, params, options) => {
const query = `CALL ${schema}.${routine}`;
return Self.execute(ctx, 'PROCEDURE', query, params, options);
};
};

View File

@ -0,0 +1,161 @@
const models = require('vn-loopback/server/server').models;
describe('Application execute()/executeProc()/executeFunc()', () => {
const userWithoutPrivileges = 1;
const userWithPrivileges = 9;
const userWithInheritedPrivileges = 120;
let tx;
function getCtx(userId) {
return {
req: {
accessToken: {userId},
headers: {origin: 'http://localhost'}
}
};
}
beforeEach(async() => {
tx = await models.Application.beginTransaction({});
const options = {transaction: tx};
await models.Application.rawSql(`
CREATE OR REPLACE PROCEDURE vn.myProcedure(vMyParam INT)
BEGIN
SELECT vMyParam myParam, t.*
FROM ticket t
LIMIT 2;
END
`, null, options);
await models.Application.rawSql(`
CREATE OR REPLACE FUNCTION bs.myFunction(vMyParam INT) RETURNS int(11)
BEGIN
RETURN vMyParam;
END
`, null, options);
await models.Application.rawSql(`
GRANT EXECUTE ON PROCEDURE vn.myProcedure TO developer;
GRANT EXECUTE ON FUNCTION bs.myFunction TO developer;
`, null, options);
});
it('should throw error when execute procedure and not have privileges', async() => {
const ctx = getCtx(userWithoutPrivileges);
let error;
try {
const options = {transaction: tx};
await models.Application.execute(
ctx,
'PROCEDURE',
'CALL vn.myProcedure',
[1],
options
);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
it('should execute procedure and get data', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.execute(
ctx,
'PROCEDURE',
'CALL vn.myProcedure',
[1],
options
);
expect(response.length).toEqual(2);
expect(response[0].myParam).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
describe('Application executeProc()', () => {
it('should execute procedure and get data (executeProc)', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeProc(
ctx,
'myProcedure',
'vn',
[1],
options
);
expect(response.length).toEqual(2);
expect(response[0].myParam).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
describe('Application executeFunc()', () => {
it('should execute function and get data', async() => {
const ctx = getCtx(userWithPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeFunc(
ctx,
'myFunction',
'bs',
[1],
options
);
expect(response).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should execute function and get data with user with inherited privileges', async() => {
const ctx = getCtx(userWithInheritedPrivileges);
try {
const options = {transaction: tx};
const response = await models.Application.executeFunc(
ctx,
'myFunction',
'bs',
[1],
options
);
expect(response).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

@ -2,4 +2,7 @@
module.exports = function(Self) { module.exports = function(Self) {
require('../methods/application/status')(Self); require('../methods/application/status')(Self);
require('../methods/application/post')(Self); require('../methods/application/post')(Self);
require('../methods/application/execute')(Self);
require('../methods/application/executeProc')(Self);
require('../methods/application/executeFunc')(Self);
}; };

View File

@ -0,0 +1,44 @@
{
"name": "ProcsPriv",
"base": "VnModel",
"options": {
"mysql": {
"table": "mysql.procs_priv"
}
},
"properties": {
"name": {
"id": 1,
"type": "string",
"mysql": {
"columnName": "Routine_name"
}
},
"schema": {
"id": 3,
"type": "string",
"mysql": {
"columnName": "Db"
}
},
"role": {
"type": "string",
"mysql": {
"columnName": "user"
}
},
"type": {
"id": 2,
"type": "string",
"mysql": {
"columnName": "Routine_type"
}
},
"host": {
"type": "string",
"mysql": {
"columnName": "Host"
}
}
}
}

View File

@ -196,6 +196,8 @@
"Negative basis of tickets: 23": "Negative basis of tickets: 23", "Negative basis of tickets: 23": "Negative basis of tickets: 23",
"Booking completed": "Booking complete", "Booking completed": "Booking complete",
"The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation", "The ticket is in preparation": "The ticket [{{ticketId}}]({{{ticketUrl}}}) of the sales person {{salesPersonId}} is in preparation",
"You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets" "You can only add negative amounts in refund tickets": "You can only add negative amounts in refund tickets",
"Try again": "Try again",
"keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries"
} }

View File

@ -224,7 +224,7 @@
"date in the future": "Fecha en el futuro", "date in the future": "Fecha en el futuro",
"reference duplicated": "Referencia duplicada", "reference duplicated": "Referencia duplicada",
"This ticket is already a refund": "Este ticket ya es un abono", "This ticket is already a refund": "Este ticket ya es un abono",
"isWithoutNegatives": "isWithoutNegatives", "isWithoutNegatives": "Sin negativos",
"routeFk": "routeFk", "routeFk": "routeFk",
"Can't change the password of another worker": "No se puede cambiar la contraseña de otro trabajador", "Can't change the password of another worker": "No se puede cambiar la contraseña de otro trabajador",
"No hay un contrato en vigor": "No hay un contrato en vigor", "No hay un contrato en vigor": "No hay un contrato en vigor",
@ -321,9 +321,12 @@
"Select a different client": "Seleccione un cliente distinto", "Select a different client": "Seleccione un cliente distinto",
"Fill all the fields": "Rellene todos los campos", "Fill all the fields": "Rellene todos los campos",
"The response is not a PDF": "La respuesta no es un PDF", "The response is not a PDF": "La respuesta no es un PDF",
"Ticket without Route": "Ticket sin ruta",
"Booking completed": "Reserva completada", "Booking completed": "Reserva completada",
"The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación", "The ticket is in preparation": "El ticket [{{ticketId}}]({{{ticketUrl}}}) del comercial {{salesPersonId}} está en preparación",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mímina", "Incoterms data for consignee is missing": "Faltan los datos de los Incoterms para el consignatario",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mímina" "The notification subscription of this worker cant be modified": "La subscripción a la notificación de este trabajador no puede ser modificada",
"User disabled": "Usuario desactivado",
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mínima",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas"
} }

View File

@ -49,5 +49,13 @@
}, },
"Container": { "Container": {
"dataSource": "vn" "dataSource": "vn"
},
"ProcsPriv": {
"dataSource": "vn",
"options": {
"mysql": {
"table": "mysql.procs_priv"
}
}
} }
} }

View File

@ -1,3 +1,4 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('sync', { Self.remoteMethod('sync', {
@ -32,9 +33,13 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const user = await models.VnUser.findOne({ const user = await models.VnUser.findOne({
fields: ['id'], fields: ['id', 'password'],
where: {name: userName} where: {name: userName}
}, myOptions); }, myOptions);
if (user && password && !await user.hasPassword(password))
throw new ForbiddenError('Wrong password');
const isSync = !await models.UserSync.exists(userName, myOptions); const isSync = !await models.UserSync.exists(userName, myOptions);
if (!force && isSync && user) return; if (!force && isSync && user) return;
@ -42,4 +47,3 @@ module.exports = Self => {
await models.UserSync.destroyById(userName, myOptions); await models.UserSync.destroyById(userName, myOptions);
}; };
}; };

View File

@ -35,6 +35,9 @@
"SambaConfig": { "SambaConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },
"SignInLog": {
"dataSource": "vn"
},
"Sip": { "Sip": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -5,7 +5,7 @@ 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 === 'production'; const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() { Self.getSynchronizer = async function() {
return await Self.findOne({ return await Self.findOne({
@ -140,6 +140,7 @@ module.exports = Self => {
try { try {
if (shouldSync) if (shouldSync)
await client.del(dn); await client.del(dn);
// eslint-disable-next-line no-console
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;

View File

@ -27,8 +27,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows `SELECT COUNT(*) AS nRows
FROM mysql.user FROM mysql.user
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
let userExists = row.nRows > 0; let userExists = row.nRows > 0;
@ -38,8 +37,7 @@ module.exports = Self => {
const [row] = await Self.rawSql( const [row] = await Self.rawSql(
`SELECT Priv AS priv `SELECT Priv AS priv
FROM mysql.global_priv FROM mysql.global_priv
WHERE User = ? WHERE User = ? AND Host = ?`,
AND Host = ?`,
[mysqlUser, this.userHost] [mysqlUser, this.userHost]
); );
const priv = row && JSON.parse(row.priv); const priv = row && JSON.parse(row.priv);
@ -88,10 +86,18 @@ module.exports = Self => {
else else
throw err; throw err;
} }
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, this.userHost]);
if (role) { const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows
FROM mysql.user
WHERE User = ? AND Host = ''`,
[role]
);
const roleExists = row.nRows > 0;
if (roleExists) {
await Self.rawSql('GRANT ? TO ?@?',
[role, mysqlUser, this.userHost]);
await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?', await Self.rawSql('SET DEFAULT ROLE ? FOR ?@?',
[role, mysqlUser, this.userHost]); [role, mysqlUser, this.userHost]);
} else { } else {

View File

@ -1,6 +1,6 @@
const ldap = require('../util/ldapjs-extra'); const ldap = require('../util/ldapjs-extra');
const ssh = require('node-ssh'); const execFile = require('child_process').execFile;
/** /**
* Summary of userAccountControl flags: * Summary of userAccountControl flags:
@ -11,6 +11,8 @@ const UserAccountControlFlags = {
}; };
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: [
@ -19,6 +21,7 @@ module.exports = Self => {
'adController', 'adController',
'adUser', 'adUser',
'adPassword', 'adPassword',
'userDn',
'verifyCert' 'verifyCert'
] ]
}); });
@ -26,88 +29,123 @@ module.exports = Self => {
Object.assign(Self.prototype, { Object.assign(Self.prototype, {
async init() { async init() {
let sshClient = new ssh.NodeSSH(); const baseDn = this.adDomain
await sshClient.connect({ .split('.')
host: this.adController, .map(part => `dc=${part}`)
username: this.adUser, .join(',');
password: this.adPassword const bindDn = `cn=${this.adUser},cn=Users,${baseDn}`;
});
let adUser = `cn=${this.adUser},${this.usersDn()}`; const adClient = ldap.createClient({
let adClient = ldap.createClient({
url: `ldaps://${this.adController}:636`, url: `ldaps://${this.adController}:636`,
tlsOptions: {rejectUnauthorized: this.verifyCert} tlsOptions: {rejectUnauthorized: this.verifyCert}
}); });
await adClient.bind(adUser, this.adPassword); await adClient.bind(bindDn, this.adPassword);
Object.assign(this, { Object.assign(this, {
sshClient, adClient,
adClient fullUsersDn: `${this.userDn},${baseDn}`,
bindDn
}); });
}, },
async deinit() { async deinit() {
await this.sshClient.dispose();
await this.adClient.unbind(); await this.adClient.unbind();
}, },
usersDn() { async sambaTool(command, args = []) {
let dnBase = this.adDomain let authArgs = [
.split('.') '--URL', `ldaps://${this.adController}`,
.map(part => `dc=${part}`) '--simple-bind-dn', this.bindDn,
.join(','); '--password', this.adPassword
return `cn=Users,${dnBase}`; ];
if (!this.verifyCert)
authArgs.push('--option', 'tls verify peer = no_check');
const allArgs = [command].concat(
args, authArgs
);
if (!shouldSync) return;
return await new Promise((resolve, reject) => {
execFile('samba-tool', allArgs, (err, stdout, stderr) => {
if (err)
reject(err);
else
resolve({stdout, stderr});
});
});
},
async getAdUser(userName) {
const sambaUser = await this.adClient.searchOne(this.fullUsersDn, {
scope: 'sub',
attributes: [
'dn',
'userAccountControl',
'uidNumber',
'accountExpires',
'mail'
],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
if (sambaUser) {
for (const intProp of ['uidNumber', 'userAccountControl']) {
if (sambaUser[intProp] != null)
sambaUser[intProp] = parseInt(sambaUser[intProp]);
}
}
return sambaUser;
}, },
async syncUser(userName, info, password) { async syncUser(userName, info, password) {
let {sshClient} = this; let sambaUser = await this.getAdUser(userName);
let entry;
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
scope: 'sub',
attributes: ['userAccountControl'],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
if (info.hasAccount) { if (info.hasAccount) {
if (!sambaUser) { if (!sambaUser) {
await sshClient.exec('samba-tool user create', [ await this.sambaTool('user', [
userName, 'create', userName,
'--uid-number', `${info.uidNumber}`, '--userou', this.userDn,
'--mail-address', info.corporateMail,
'--random-password' '--random-password'
]); ]);
await sshClient.exec('samba-tool user setexpiry', [ sambaUser = await this.getAdUser(userName);
userName,
'--noexpiry'
]);
await sshClient.exec('mkhomedir_helper', [
userName,
'0027'
]);
}
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [
userName
]);
} }
if (password) { if (password) {
await sshClient.exec('samba-tool user setpassword', [ await this.sambaTool('user', [
userName, 'setpassword', userName,
'--newpassword', password '--newpassword', password
]); ]);
} }
} else if (isEnabled) {
await sshClient.exec('samba-tool user disable', [ entry = {
userName userAccountControl: sambaUser.userAccountControl
]); & ~UserAccountControlFlags.ACCOUNTDISABLE,
uidNumber: info.uidNumber,
accountExpires: 0,
mail: info.corporateMail
};
} else if (sambaUser) {
entry = {
userAccountControl: sambaUser.userAccountControl
| UserAccountControlFlags.ACCOUNTDISABLE
};
// eslint-disable-next-line no-console
console.log(` -> User '${userName}' disabled on Samba`); console.log(` -> User '${userName}' disabled on Samba`);
} }
if (sambaUser && entry) {
const changes = [];
for (const prop in entry) {
if (sambaUser[prop] == entry[prop]) continue;
changes.push(new ldap.Change({
operation: 'replace',
modification: {
[prop]: entry[prop]
}
}));
}
if (changes.length && shouldSync)
await this.adClient.modify(sambaUser.dn, changes);
}
}, },
/** /**
@ -117,14 +155,15 @@ module.exports = Self => {
*/ */
async getUsers(usersToSync) { async getUsers(usersToSync) {
const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803'; const LDAP_MATCHING_RULE_BIT_AND = '1.2.840.113556.1.4.803';
let filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}:=${UserAccountControlFlags.ACCOUNTDISABLE})`; const filter = `!(userAccountControl:${LDAP_MATCHING_RULE_BIT_AND}`
+ `:=${UserAccountControlFlags.ACCOUNTDISABLE})`;
let opts = { const opts = {
scope: 'sub', scope: 'sub',
attributes: ['sAMAccountName'], attributes: ['sAMAccountName'],
filter: `(&(objectClass=user)(${filter}))` filter: `(&(objectClass=user)(${filter}))`
}; };
await this.adClient.searchForeach(this.usersDn(), opts, await this.adClient.searchForeach(this.fullUsersDn, opts,
o => usersToSync.add(o.sAMAccountName)); o => usersToSync.add(o.sAMAccountName));
} }
}); });

View File

@ -28,6 +28,10 @@
"adPassword": { "adPassword": {
"type": "string" "type": "string"
}, },
"userDn": {
"type": "string",
"required": true
},
"verifyCert": { "verifyCert": {
"type": "boolean" "type": "boolean"
} }

View File

@ -0,0 +1,41 @@
{
"name": "SignInLog",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.signInLog"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"token": {
"required": true,
"type": "string",
"description": "Token's user"
},
"creationDate": {
"type": "date"
},
"userFk": {
"required": true,
"type": "number"
},
"ip": {
"type": "string"
}
},
"relations": {
"user": {
"type": "belongsTo",
"model": "VnUser",
"foreignKey": "userFk"
}
},
"scope": {
"order": ["creationDate DESC", "id DESC"]
}
}

View File

@ -61,10 +61,6 @@
label="Synchronize all" label="Synchronize all"
ng-click="$ctrl.onSynchronizeAll()"> ng-click="$ctrl.onSynchronizeAll()">
</vn-button> </vn-button>
<vn-button
label="Synchronize user"
ng-click="syncUser.show()">
</vn-button>
<vn-button <vn-button
label="Synchronize roles" label="Synchronize roles"
ng-click="$ctrl.onSynchronizeRoles()"> ng-click="$ctrl.onSynchronizeRoles()">
@ -77,25 +73,3 @@
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onUserSync()"
on-close="$ctrl.onSyncClose()">
<tpl-body>
<vn-textfield
label="Username"
ng-model="$ctrl.syncUser"
vn-focus>
</vn-textfield>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
info="If password is not specified, just user attributes are synchronized">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,6 +1,5 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section { export default class Controller extends Section {
onSynchronizeAll() { onSynchronizeAll() {
@ -8,27 +7,10 @@ export default class Controller extends Section {
this.$http.patch(`Accounts/syncAll`); this.$http.patch(`Accounts/syncAll`);
} }
onUserSync() {
if (!this.syncUser)
throw new UserError('Please enter the username');
let params = {
password: this.syncPassword,
force: true
};
return this.$http.patch(`Accounts/${this.syncUser}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
}
onSynchronizeRoles() { onSynchronizeRoles() {
this.$http.patch(`RoleInherits/sync`) this.$http.patch(`RoleInherits/sync`)
.then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!'))); .then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!')));
} }
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
} }
ngModule.component('vnAccountAccounts', { ngModule.component('vnAccountAccounts', {

View File

@ -3,7 +3,6 @@ Homedir base: Directorio base para carpetas de usuario
Shell: Intérprete de línea de comandos Shell: Intérprete de línea de comandos
User and role base id: Id base usuarios y roles User and role base id: Id base usuarios y roles
Synchronize all: Sincronizar todo Synchronize all: Sincronizar todo
Synchronize user: Sincronizar usuario
Synchronize roles: Sincronizar roles Synchronize roles: Sincronizar roles
If password is not specified, just user attributes are synchronized: >- If password is not specified, just user attributes are synchronized: >-
Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
@ -12,5 +11,4 @@ Users synchronized!: ¡Usuarios sincronizados!
Username: Nombre de usuario Username: Nombre de usuario
Synchronize: Sincronizar Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario Please enter the username: Por favor introduce el nombre de usuario
User synchronized!: ¡Usuario sincronizado!
Roles synchronized!: ¡Roles sincronizados! Roles synchronized!: ¡Roles sincronizados!

View File

@ -67,6 +67,15 @@
translate> translate>
Deactivate user Deactivate user
</vn-item> </vn-item>
<vn-item
ng-if="$ctrl.user.active"
ng-click="syncUser.show()"
name="synchronizeUser"
vn-acl="it"
vn-acl-action="remove"
translate>
Synchronize
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -153,6 +162,32 @@
<button response="accept" translate>Change password</button> <button response="accept" translate>Change password</button>
</tpl-buttons> </tpl-buttons>
</vn-dialog> </vn-dialog>
<vn-dialog
vn-id="syncUser"
on-accept="$ctrl.onSync()"
on-close="$ctrl.onSyncClose()">
<tpl-title ng-translate>
Do you want to synchronize user?
</tpl-title>
<tpl-body>
<vn-check
label="Synchronize password"
ng-model="$ctrl.shouldSyncPassword"
info="If password is not specified, just user attributes are synchronized"
vn-focus>
</vn-check>
<vn-textfield
label="Password"
ng-model="$ctrl.syncPassword"
type="password"
ng-if="$ctrl.shouldSyncPassword">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Synchronize</button>
</tpl-buttons>
</vn-dialog>
<vn-popup vn-id="summary"> <vn-popup vn-id="summary">
<vn-user-summary user="$ctrl.user"></vn-user-summary> <vn-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup> </vn-popup>

View File

@ -120,6 +120,20 @@ class Controller extends Descriptor {
this.vnApp.showSuccess(this.$t(message)); this.vnApp.showSuccess(this.$t(message));
}); });
} }
onSync() {
const params = {force: true};
if (this.shouldSyncPassword)
params.password = this.syncPassword;
return this.$http.patch(`Accounts/${this.user.name}/sync`, params)
.then(() => this.vnApp.showSuccess(this.$t('User synchronized!')));
}
onSyncClose() {
this.shouldSyncPassword = false;
this.syncPassword = undefined;
}
} }
ngModule.component('vnUserDescriptor', { ngModule.component('vnUserDescriptor', {

View File

@ -22,6 +22,10 @@ Old password: Contraseña antigua
New password: Nueva contraseña New password: Nueva contraseña
Repeat password: Repetir contraseña Repeat password: Repetir contraseña
Password changed succesfully!: ¡Contraseña modificada correctamente! Password changed succesfully!: ¡Contraseña modificada correctamente!
Synchronize: Sincronizar
Do you want to synchronize user?: ¿Quieres sincronizar el usuario?
Synchronize password: Sincronizar contraseña
User synchronized!: ¡Usuario sincronizado!
Role changed succesfully!: ¡Rol modificado correctamente! Role changed succesfully!: ¡Rol modificado correctamente!
Password requirements: > Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud, La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

@ -40,6 +40,11 @@
type="password" type="password"
rule="SambaConfig"> rule="SambaConfig">
</vn-textfield> </vn-textfield>
<vn-textfield
label="User DN (without domain part)"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
<vn-check <vn-check
label="Verify certificate" label="Verify certificate"
ng-model="$ctrl.config.verifyCert"> ng-model="$ctrl.config.verifyCert">

View File

@ -3,6 +3,7 @@ Domain controller: Controlador de dominio
AD domain: Dominio AD AD domain: Dominio AD
AD user: Usuario AD AD user: Usuario AD
AD password: Contraseña AD AD password: Contraseña AD
User DN (without domain part): DN usuarios (sin la parte del dominio)
Verify certificate: Verificar certificado Verify certificate: Verificar certificado
Test connection: Probar conexión Test connection: Probar conexión
Samba connection established!: ¡Conexión con Samba establecida! Samba connection established!: ¡Conexión con Samba establecida!

View File

@ -82,12 +82,15 @@ module.exports = Self => {
if (args.zoneFk) { if (args.zoneFk) {
let stmts = []; let stmts = [];
stmts.push(new ParameterizedSQL('CALL vn.zone_getPostalCode(?)', [args.zoneFk])); stmts.push(new ParameterizedSQL('CALL vn.zone_getPostalCode(?)', [args.zoneFk]));
stmts.push(`SELECT name FROM tmp.zoneNodes`); stmts.push(`
SELECT zn.geoFk, zn.name
FROM tmp.zoneNodes zn
JOIN zone z ON z.id = zn.zoneFk`);
stmts.push(`DROP TEMPORARY TABLE tmp.zoneNodes`); stmts.push(`DROP TEMPORARY TABLE tmp.zoneNodes`);
const sql = ParameterizedSQL.join(stmts, ';'); const sql = ParameterizedSQL.join(stmts, ';');
const [results] = await conn.executeStmt(sql); const results = await conn.executeStmt(sql);
for (let result of results) for (let result of results[1])
postalCode.push(result.name); postalCode.push(result.name);
} }

View File

@ -158,7 +158,7 @@
}, },
"user": { "user": {
"type": "belongsTo", "type": "belongsTo",
"model": "Account", "model": "VnUser",
"foreignKey": "id" "foreignKey": "id"
}, },
"payMethod": { "payMethod": {

View File

@ -0,0 +1,37 @@
module.exports = Self => {
Self.remoteMethodCtx('new', {
description: 'Creates a new invoiceIn due day',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
},
http: {
path: '/new',
verb: 'POST'
}
});
Self.new = async(ctx, id, options) => {
let tx;
const myOptions = {userId: ctx.req.accessToken.userId};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
await Self.rawSql(`CALL vn.invoiceInDueDay_calculate(?)`, [id], myOptions);
if (tx) await tx.commit();
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,45 @@
const LoopBackContext = require('loopback-context');
const models = require('vn-loopback/server/server').models;
describe('invoiceInDueDay new()', () => {
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should correctly create a new due day', async() => {
const userId = 9;
const invoiceInFk = 6;
const ctx = {
req: {
accessToken: {userId: userId},
}
};
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
await models.InvoiceInDueDay.destroyAll({
invoiceInFk
}, options);
await models.InvoiceInDueDay.new(ctx, invoiceInFk, options);
const result = await models.InvoiceInDueDay.find({where: {invoiceInFk}}, options);
expect(result).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -0,0 +1,3 @@
module.exports = Self => {
require('../methods/invoice-in-due-day/new')(Self);
};

View File

@ -27,7 +27,7 @@ describe('InvoiceOut tranferInvoice()', () => {
ref: 'T4444444', ref: 'T4444444',
newClientFk: 1, newClientFk: 1,
cplusRectificationId: 1, cplusRectificationId: 1,
cplusInvoiceType477Id: 1, siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1 invoiceCorrectionTypeId: 1
}; };
ctx.args = args; ctx.args = args;
@ -52,7 +52,7 @@ describe('InvoiceOut tranferInvoice()', () => {
ref: 'T1111111', ref: 'T1111111',
newClientFk: 1101, newClientFk: 1101,
cplusRectificationId: 1, cplusRectificationId: 1,
cplusInvoiceType477Id: 1, siiTypeInvoiceOutId: 1,
invoiceCorrectionTypeId: 1 invoiceCorrectionTypeId: 1
}; };
ctx.args = args; ctx.args = args;

View File

@ -27,7 +27,7 @@ module.exports = Self => {
required: true required: true
}, },
{ {
arg: 'cplusInvoiceType477Id', arg: 'siiTypeInvoiceOutId',
type: 'number', type: 'number',
required: true required: true
}, },
@ -93,7 +93,7 @@ module.exports = Self => {
correctingFk: invoiceId, correctingFk: invoiceId,
correctedFk: args.id, correctedFk: args.id,
cplusRectificationTypeFk: args.cplusRectificationId, cplusRectificationTypeFk: args.cplusRectificationId,
cplusInvoiceType477Fk: args.cplusInvoiceType477Id, siiTypeInvoiceOutFk: args.siiTypeInvoiceOutId,
invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId
}, myOptions); }, myOptions);

View File

@ -41,7 +41,7 @@
"InvoiceCorrection": { "InvoiceCorrection": {
"dataSource": "vn" "dataSource": "vn"
}, },
"CplusInvoiceType477": { "SiiTypeInvoiceOut": {
"dataSource": "vn" "dataSource": "vn"
} }
} }

View File

@ -18,7 +18,7 @@
"cplusRectificationTypeFk": { "cplusRectificationTypeFk": {
"type": "number" "type": "number"
}, },
"cplusInvoiceType477Fk": { "siiTypeInvoiceOutFk": {
"type": "number" "type": "number"
}, },
"invoiceCorrectionTypeFk": { "invoiceCorrectionTypeFk": {

View File

@ -1,9 +1,9 @@
{ {
"name": "CplusInvoiceType477", "name": "SiiTypeInvoiceOut",
"base": "VnModel", "base": "VnModel",
"options": { "options": {
"mysql": { "mysql": {
"table": "cplusInvoiceType477" "table": "siiTypeInvoiceOut"
} }
}, },
"properties": { "properties": {

View File

@ -6,8 +6,8 @@
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"
url="CplusInvoiceType477s" url="SiiTypeInvoiceOuts"
data="cplusInvoiceType477"> data="siiTypeInvoiceOut">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"
@ -223,11 +223,11 @@
<vn-autocomplete <vn-autocomplete
vn-one vn-one
vn-id="cplusInvoiceType" vn-id="cplusInvoiceType"
data="cplusInvoiceType477" data="siiTypeInvoiceOut"
show-field="description" show-field="description"
value-field="id" value-field="id"
required="true" required="true"
ng-model="$ctrl.cplusInvoiceType477" ng-model="$ctrl.siiTypeInvoiceOut"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}" search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
label="Class"> label="Class">
</vn-autocomplete> </vn-autocomplete>

View File

@ -132,7 +132,7 @@ class Controller extends Section {
ref: this.invoiceOut.ref, ref: this.invoiceOut.ref,
newClientFk: this.invoiceOut.client.id, newClientFk: this.invoiceOut.client.id,
cplusRectificationId: this.cplusRectificationType, cplusRectificationId: this.cplusRectificationType,
cplusInvoiceType477Id: this.cplusInvoiceType477, siiTypeInvoiceOutId: this.siiTypeInvoiceOut,
invoiceCorrectionTypeId: this.invoiceCorrectionType invoiceCorrectionTypeId: this.invoiceCorrectionType
}; };
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => { this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {

View File

@ -7,7 +7,7 @@ export default class Controller extends Section {
super($element, $); super($element, $);
this.vnReport = vnReport; this.vnReport = vnReport;
const now = new Date(); const now = Date.vnNew();
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
this.params = { this.params = {

View File

@ -20,8 +20,7 @@ module.exports = Self => {
}, },
{ {
arg: 'addressFk', arg: 'addressFk',
type: 'Number', type: 'Any',
required: true,
description: 'The address id' description: 'The address id'
}], }],
http: { http: {

View File

@ -1,6 +1,6 @@
{ {
"name": "ItemShelving", "name": "ItemShelving",
"base": "VnModel", "base": "Loggable",
"options": { "options": {
"mysql": { "mysql": {
"table": "itemShelving" "table": "itemShelving"

View File

@ -64,7 +64,10 @@ module.exports = Self => {
const sale = await models.Sale.findById(id, filter, myOptions); const sale = await models.Sale.findById(id, filter, myOptions);
const oldQuantity = sale.quantity; const oldQuantity = sale.quantity;
const result = await sale.updateAttributes({quantity: newQuantity}, myOptions); const result = await sale.updateAttributes({
quantity: newQuantity,
originalQuantity: newQuantity
}, myOptions);
const salesPerson = sale.ticket().client().salesPersonUser(); const salesPerson = sale.ticket().client().salesPersonUser();
if (salesPerson) { if (salesPerson) {

View File

@ -32,31 +32,30 @@ module.exports = Self => {
throw new UserError('You cannot close tickets for today'); throw new UserError('You cannot close tickets for today');
const tickets = await Self.rawSql(` const tickets = await Self.rawSql(`
SELECT SELECT t.id,
t.id, t.clientFk,
t.clientFk, t.companyFk,
t.companyFk, c.name clientName,
c.name clientName, c.email recipient,
c.email recipient, c.salesPersonFk,
c.salesPersonFk, c.isToBeMailed,
c.isToBeMailed, c.hasToInvoice,
c.hasToInvoice, co.hasDailyInvoice,
co.hasDailyInvoice, eu.email salesPersonEmail,
eu.email salesPersonEmail t.addressFk
FROM ticket t FROM ticket t
JOIN agencyMode am ON am.id = t.agencyModeFk JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission
JOIN ticketState ts ON ts.ticketFk = t.id JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered')) WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code != 'delivered'))
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY) AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY) AND util.dayEnd(?)
AND util.dayEnd(?) AND t.refFk IS NULL
AND t.refFk IS NULL GROUP BY t.id
GROUP BY t.id
`, [toDate, toDate]); `, [toDate, toDate]);
await closure(ctx, Self, tickets); await closure(ctx, Self, tickets);

View File

@ -5,177 +5,178 @@ const config = require('vn-print/core/config');
const storage = require('vn-print/core/storage'); const storage = require('vn-print/core/storage');
module.exports = async function(ctx, Self, tickets, reqArgs = {}) { module.exports = async function(ctx, Self, tickets, reqArgs = {}) {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
if (tickets.length == 0) return; if (tickets.length == 0) return;
const failedtickets = []; const failedtickets = [];
for (const ticket of tickets) { for (const ticket of tickets) {
try { try {
await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId}); await Self.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id], {userId});
const [invoiceOut] = await Self.rawSql(` const [invoiceOut] = await Self.rawSql(`
SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued SELECT io.id, io.ref, io.serial, cny.code companyCode, io.issued
FROM ticket t FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk JOIN invoiceOut io ON io.ref = t.refFk
JOIN company cny ON cny.id = io.companyFk JOIN company cny ON cny.id = io.companyFk
WHERE t.id = ? WHERE t.id = ?
`, [ticket.id]); `, [ticket.id]);
const mailOptions = { const mailOptions = {
overrideAttachments: true, overrideAttachments: true,
attachments: [] attachments: []
}; };
const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed; const isToBeMailed = ticket.recipient && ticket.salesPersonFk && ticket.isToBeMailed;
if (invoiceOut) { if (invoiceOut) {
const args = { const args = {
reference: invoiceOut.ref, reference: invoiceOut.ref,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const invoiceReport = new Report('invoice', args); const invoiceReport = new Report('invoice', args);
const stream = await invoiceReport.toPdfStream(); const stream = await invoiceReport.toPdfStream();
const issued = invoiceOut.issued; const issued = invoiceOut.issued;
const year = issued.getFullYear().toString(); const year = issued.getFullYear().toString();
const month = (issued.getMonth() + 1).toString(); const month = (issued.getMonth() + 1).toString();
const day = issued.getDate().toString(); const day = issued.getDate().toString();
const fileName = `${year}${invoiceOut.ref}.pdf`; const fileName = `${year}${invoiceOut.ref}.pdf`;
// Store invoice // Store invoice
await storage.write(stream, { await storage.write(stream, {
type: 'invoice', type: 'invoice',
path: `${year}/${month}/${day}`, path: `${year}/${month}/${day}`,
fileName: fileName fileName: fileName
}); });
await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId}); await Self.rawSql('UPDATE invoiceOut SET hasPdf = true WHERE id = ?', [invoiceOut.id], {userId});
if (isToBeMailed) { if (isToBeMailed) {
const invoiceAttachment = { const invoiceAttachment = {
filename: fileName, filename: fileName,
content: stream content: stream
}; };
if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') { if (invoiceOut.serial == 'E' && invoiceOut.companyCode == 'VNL') {
const exportation = new Report('exportation', args); const exportation = new Report('exportation', args);
const stream = await exportation.toPdfStream(); const stream = await exportation.toPdfStream();
const fileName = `CITES-${invoiceOut.ref}.pdf`; const fileName = `CITES-${invoiceOut.ref}.pdf`;
mailOptions.attachments.push({ mailOptions.attachments.push({
filename: fileName, filename: fileName,
content: stream content: stream
}); });
} }
mailOptions.attachments.push(invoiceAttachment); mailOptions.attachments.push(invoiceAttachment);
const email = new Email('invoice', args); const email = new Email('invoice', args);
await email.send(mailOptions); await email.send(mailOptions);
} }
} else if (isToBeMailed) { } else if (isToBeMailed) {
const args = { const args = {
id: ticket.id, id: ticket.id,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail
}; };
const email = new Email('delivery-note-link', args); const email = new Email('delivery-note-link', args);
await email.send(); await email.send();
} }
// Incoterms authorization // Incoterms authorization
const [{firstOrder}] = await Self.rawSql(` const [{firstOrder}] = await Self.rawSql(`
SELECT COUNT(*) as firstOrder SELECT COUNT(*) as firstOrder
FROM ticket t FROM ticket t
JOIN client c ON c.id = t.clientFk JOIN client c ON c.id = t.clientFk
WHERE t.clientFk = ? WHERE t.clientFk = ?
AND NOT t.isDeleted AND NOT t.isDeleted
AND c.isVies AND c.isVies
`, [ticket.clientFk]); `, [ticket.clientFk]);
if (firstOrder == 1) { if (firstOrder == 1) {
const args = { const args = {
id: ticket.clientFk, id: ticket.clientFk,
companyId: ticket.companyFk, companyId: ticket.companyFk,
recipientId: ticket.clientFk, recipientId: ticket.clientFk,
recipient: ticket.recipient, recipient: ticket.recipient,
replyTo: ticket.salesPersonEmail replyTo: ticket.salesPersonEmail,
}; addressId: ticket.addressFk
};
const email = new Email('incoterms-authorization', args); const email = new Email('incoterms-authorization', args);
await email.send(); await email.send();
const [sample] = await Self.rawSql( const [sample] = await Self.rawSql(
`SELECT id `SELECT id
FROM sample FROM sample
WHERE code = 'incoterms-authorization' WHERE code = 'incoterms-authorization'
`); `);
await Self.rawSql(` await Self.rawSql(`
INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?) INSERT INTO clientSample (clientFk, typeFk, companyFk) VALUES(?, ?, ?)
`, [ticket.clientFk, sample.id, ticket.companyFk], {userId}); `, [ticket.clientFk, sample.id, ticket.companyFk], {userId});
}; }
} catch (error) { } catch (error) {
// Domain not found // Domain not found
if (error.responseCode == 450) if (error.responseCode == 450)
return invalidEmail(ticket); return invalidEmail(ticket);
// Save tickets on a list of failed ids // Save tickets on a list of failed ids
failedtickets.push({ failedtickets.push({
id: ticket.id, id: ticket.id,
stacktrace: error stacktrace: error
}); });
} }
} }
// Send email with failed tickets // Send email with failed tickets
if (failedtickets.length > 0) { if (failedtickets.length > 0) {
let body = 'This following tickets have failed:<br/><br/>'; let body = 'This following tickets have failed:<br/><br/>';
for (const ticket of failedtickets) { for (const ticket of failedtickets) {
body += `Ticket: <strong>${ticket.id}</strong> body += `Ticket: <strong>${ticket.id}</strong>
<br/> <strong>${ticket.stacktrace}</strong><br/><br/>`; <br/> <strong>${ticket.stacktrace}</strong><br/><br/>`;
} }
smtp.send({ smtp.send({
to: config.app.reportEmail, to: config.app.reportEmail,
subject: '[API] Nightly ticket closure report', subject: '[API] Nightly ticket closure report',
html: body html: body
}); });
} }
async function invalidEmail(ticket) { async function invalidEmail(ticket) {
await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [ await Self.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk ticket.clientFk
], {userId}); ], {userId});
const oldInstance = `{"email": "${ticket.recipient}"}`; const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`; const newInstance = `{"email": ""}`;
await Self.rawSql(` await Self.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance) INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [ VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk, ticket.clientFk,
oldInstance, oldInstance,
newInstance newInstance
], {userId}); ], {userId});
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong> const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong> al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>
porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta porque la dirección de email <strong>"${ticket.recipient}"</strong> no es correcta
o no está disponible.<br/><br/> o no está disponible.<br/><br/>
Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente. Para evitar que se repita este error, se ha eliminado la dirección de email de la ficha del cliente.
Actualiza la dirección de email con una correcta.`; Actualiza la dirección de email con una correcta.`;
smtp.send({ smtp.send({
to: ticket.salesPersonEmail, to: ticket.salesPersonEmail,
subject: 'No se ha podido enviar el albarán', subject: 'No se ha podido enviar el albarán',
html: body html: body
}); });
} }
}; };

View File

@ -75,8 +75,7 @@ module.exports = Self => {
{ {
arg: 'option', arg: 'option',
type: 'number', type: 'number',
description: 'Action id', description: 'Action id'
required: true
}, },
{ {
arg: 'isWithoutNegatives', arg: 'isWithoutNegatives',
@ -88,7 +87,18 @@ module.exports = Self => {
arg: 'withWarningAccept', arg: 'withWarningAccept',
type: 'boolean', type: 'boolean',
description: 'Has pressed in confirm message', description: 'Has pressed in confirm message',
}], },
{
arg: 'newTicket',
type: 'number',
description: 'Ticket id you want to move the lines to, if applicable',
},
{
arg: 'keepPrice',
type: 'boolean',
description: 'If prices should be maintained',
}
],
returns: { returns: {
type: ['object'], type: ['object'],
root: true root: true
@ -141,12 +151,18 @@ module.exports = Self => {
const salesNewTicketLength = salesNewTicket.length; const salesNewTicketLength = salesNewTicket.length;
if (salesNewTicketLength && sales.length != salesNewTicketLength) { if (salesNewTicketLength && sales.length != salesNewTicketLength) {
const newTicket = await models.Ticket.transferSales(ctx, args.id, null, salesNewTicket, myOptions); const newTicket = await models.Ticket.transferSales(
ctx,
args.id,
args.newTicket,
salesNewTicket,
myOptions
);
args.id = newTicket.id; args.id = newTicket.id;
} }
} }
const originalTicket = await models.Ticket.findOne({ const ticketToChange = await models.Ticket.findOne({
where: {id: args.id}, where: {id: args.id},
fields: [ fields: [
'id', 'id',
@ -179,33 +195,38 @@ module.exports = Self => {
}, },
] ]
}, myOptions); }, myOptions);
const originalTicket = JSON.parse(JSON.stringify(ticketToChange));
const ticketChanges = {
clientFk: args.clientFk,
nickname: args.nickname,
agencyModeFk: args.agencyModeFk,
addressFk: args.addressFk,
zoneFk: args.zoneFk,
warehouseFk: args.warehouseFk,
companyFk: args.companyFk,
shipped: args.shipped,
landed: args.landed,
isDeleted: args.isDeleted
};
args.routeFk = null; let response;
if (args.keepPrice) {
ticketChanges.routeFk = null;
response = await ticketToChange.updateAttributes(ticketChanges, myOptions);
} else {
const hasToBeUnrouted = true;
response = await Self.rawSql(
'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[args.id].concat(Object.values(ticketChanges), [hasToBeUnrouted, args.option]),
myOptions
);
}
if (args.isWithoutNegatives === false) delete args.isWithoutNegatives; if (args.isWithoutNegatives === false) delete args.isWithoutNegatives;
const updatedTicket = Object.assign({}, args); const updatedTicket = Object.assign({}, args);
delete updatedTicket.ctx; delete updatedTicket.ctx;
delete updatedTicket.option; delete updatedTicket.option;
// Force to unroute ticket if (ticketToChange.addressFk != updatedTicket.addressFk && args.id) {
const hasToBeUnrouted = true;
const query = 'CALL vn.ticket_componentMakeUpdate(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
const res = await Self.rawSql(query, [
args.id,
args.clientFk,
args.nickname,
args.agencyModeFk,
args.addressFk,
args.zoneFk,
args.warehouseFk,
args.companyFk,
args.shipped,
args.landed,
args.isDeleted,
hasToBeUnrouted,
args.option
], myOptions);
if (originalTicket.addressFk != updatedTicket.addressFk && args.id) {
await models.TicketObservation.destroyAll({ await models.TicketObservation.destroyAll({
ticketFk: args.id ticketFk: args.id
}, myOptions); }, myOptions);
@ -232,10 +253,15 @@ module.exports = Self => {
const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0; const hasChanges = Object.keys(changes.old).length > 0 || Object.keys(changes.new).length > 0;
if (hasChanges) { if (hasChanges) {
delete changes.old.client;
delete changes.old.address;
delete changes.new.client;
delete changes.new.address;
const oldProperties = await loggable.translateValues(Self, changes.old); const oldProperties = await loggable.translateValues(Self, changes.old);
const newProperties = await loggable.translateValues(Self, changes.new); const newProperties = await loggable.translateValues(Self, changes.new);
const salesPersonId = originalTicket.client().salesPersonFk; const salesPersonId = ticketToChange.client().salesPersonFk;
if (salesPersonId) { if (salesPersonId) {
const url = await Self.app.models.Url.getUrl(); const url = await Self.app.models.Url.getUrl();
@ -256,10 +282,10 @@ module.exports = Self => {
} }
} }
res.id = args.id; response.id = args.id;
if (tx) await tx.commit(); if (tx) await tx.commit();
return res; return response;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;

View File

@ -49,7 +49,6 @@ module.exports = function(Self) {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
let serial;
try { try {
const ticketToInvoice = await Self.rawSql(` const ticketToInvoice = await Self.rawSql(`
SELECT id SELECT id
@ -60,7 +59,7 @@ module.exports = function(Self) {
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, },
fields: ['id', 'clientFk'] fields: ['id', 'clientFk', 'addressFk']
}, myOptions); }, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions); await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions);
@ -72,13 +71,19 @@ module.exports = function(Self) {
throw new UserError(`This client can't be invoiced`); throw new UserError(`This client can't be invoiced`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`; const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`;
const [result] = await Self.rawSql(query, [ const [{serial}] = await Self.rawSql(query, [
clientId, clientId,
companyFk, companyFk,
invoiceType, invoiceType,
], myOptions); ], myOptions);
serial = result.serial;
const invoiceOutSerial = await models.InvoiceOutSerial.findById(serial);
if (invoiceOutSerial?.taxAreaFk == 'WORLD') {
const address = await models.Address.findById(firstTicket.addressFk);
if (!address || !address.customsAgentFk || !address.incotermsFk)
throw new UserError('Incoterms data for consignee is missing');
}
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions); await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions); const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);

View File

@ -3,33 +3,30 @@ const LoopBackContext = require('loopback-context');
describe('ticket addSale()', () => { describe('ticket addSale()', () => {
const ticketId = 13; const ticketId = 13;
beforeAll(async() => { function getActiveCtx(userId) {
const activeCtx = { return {
accessToken: {userId: 9}, accessToken: {userId},
http: { http: {
req: { req: {
headers: {origin: 'http://localhost'} accessToken: {userId},
headers: {origin: 'http://localhost'},
__: () => {}
} }
} }
}; };
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ }
active: activeCtx
});
});
it('should create a new sale for the ticket with id 13', async() => { it('should create a new sale for the ticket with id 13', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(9);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 4; const itemId = 4;
const quantity = 10; const quantity = 10;
const newSale = await models.Ticket.addSale(ctx, ticketId, itemId, quantity, options); const newSale = await models.Ticket.addSale(ctx, ticketId, itemId, quantity, options);
@ -45,19 +42,16 @@ describe('ticket addSale()', () => {
it('should not be able to add a sale if the item quantity is not available', async() => { it('should not be able to add a sale if the item quantity is not available', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(18);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let error; let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 11; const itemId = 11;
const quantity = 10; const quantity = 10;
@ -74,18 +68,16 @@ describe('ticket addSale()', () => {
it('should not be able to add a sale if the ticket is not editable', async() => { it('should not be able to add a sale if the ticket is not editable', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(9);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let error; let error;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const notEditableTicketId = 1; const notEditableTicketId = 1;
const itemId = 4; const itemId = 4;
const quantity = 10; const quantity = 10;

View File

@ -209,4 +209,112 @@ describe('ticket componentUpdate()', () => {
throw e; throw e;
} }
}); });
describe('componentUpdate() keepPrice', () => {
it('should change shipped and keep price', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const saleId = 27;
const originDate = today;
const newDate = tomorrow;
const ticketID = 14;
newDate.setHours(0, 0, 0, 0, 0);
originDate.setHours(0, 0, 0, 0, 0);
const args = {
id: ticketID,
clientFk: 1104,
agencyModeFk: 2,
addressFk: 4,
zoneFk: 9,
warehouseFk: 1,
companyFk: 442,
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
isWithoutNegatives: false,
keepPrice: true
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const beforeSale = await models.Sale.findById(saleId, null, options);
await models.Ticket.componentUpdate(ctx, options);
const afterSale = await models.Sale.findById(saleId, null, options);
expect(beforeSale.price).toEqual(afterSale.price);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should change shipped and not keep price', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const saleId = 27;
const originDate = today;
const newDate = tomorrow;
const ticketID = 14;
newDate.setHours(0, 0, 0, 0, 0);
originDate.setHours(0, 0, 0, 0, 0);
const args = {
id: ticketID,
clientFk: 1104,
agencyModeFk: 2,
addressFk: 4,
zoneFk: 9,
warehouseFk: 1,
companyFk: 442,
shipped: newDate,
landed: tomorrow,
isDeleted: false,
option: 1,
isWithoutNegatives: false,
keepPrice: false
};
const ctx = {
args: args,
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'},
__: value => {
return value;
}
}
};
const beforeSale = await models.Sale.findById(saleId, null, options);
await models.Ticket.componentUpdate(ctx, options);
const afterSale = await models.Sale.findById(saleId, null, options);
expect(beforeSale.price).not.toEqual(afterSale.price);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
}); });

View File

@ -9,7 +9,7 @@ describe('ticket getSalesPersonMana()', () => {
const mana = await models.Ticket.getSalesPersonMana(1, options); const mana = await models.Ticket.getSalesPersonMana(1, options);
expect(mana).toEqual(124); expect(mana).toEqual(73);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -52,4 +52,31 @@ describe('ticket makeInvoice()', () => {
throw e; throw e;
} }
}); });
it('should throw an error when invoicing a client without incoterms', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticketsId = [18];
await models.Ticket.rawSql(`
DROP TEMPORARY TABLE IF EXISTS tmp.ticketToInvoice;
CREATE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketsId], options);
await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`Incoterms data for consignee is missing`);
});
}); });

View File

@ -21,10 +21,17 @@
expr-builder="$ctrl.exprBuilder(param, value)" expr-builder="$ctrl.exprBuilder(param, value)"
> >
<slot-actions> <slot-actions>
<vn-button disabled="$ctrl.checked.length === 0" <vn-button
disabled="$ctrl.checked.length === 0"
icon="keyboard_double_arrow_left" icon="keyboard_double_arrow_left"
ng-click="moveTicketsAdvance.show($event)" ng-click="moveTicketsAdvance.show($event)"
vn-tooltip="Advance tickets"> vn-tooltip="Advance tickets with negatives">
</vn-button>
<vn-button
disabled="$ctrl.checked.length === 0"
icon="alt_route"
ng-click="splitTickets.show($event)"
vn-tooltip="Advance tickets without negatives">
</vn-button> </vn-button>
</slot-actions> </slot-actions>
<slot-table> <slot-table>
@ -32,8 +39,14 @@
<thead> <thead>
<tr second-header> <tr second-header>
<td></td> <td></td>
<th colspan="7" translate>Destination</th> <th colspan="7">
<th colspan="9" translate>Origin</th> <span translate>Destination</span>
{{model.userParams.dateToAdvance| date: 'dd/MM/yyyy'}}
</th>
<th colspan="11">
<span translate>Origin</span>
{{model.userParams.dateFuture | date: 'dd/MM/yyyy'}}
</th>
</tr> </tr>
<tr> <tr>
<th shrink> <th shrink>
@ -48,9 +61,6 @@
<th field="id"> <th field="id">
<span translate>ID</span> <span translate>ID</span>
</th> </th>
<th field="shipped">
<span translate>Date</span>
</th>
<th field="ipt" title="{{'Item Packing Type' | translate}}"> <th field="ipt" title="{{'Item Packing Type' | translate}}">
<span>IPT</span> <span>IPT</span>
</th> </th>
@ -69,9 +79,6 @@
<th separator field="futureId"> <th separator field="futureId">
<span translate>ID</span> <span translate>ID</span>
</th> </th>
<th field="futureShipped">
<span translate>Date</span>
</th>
<th field="futureIpt" title="{{'Item Packing Type' | translate}}"> <th field="futureIpt" title="{{'Item Packing Type' | translate}}">
<span>IPT</span> <span>IPT</span>
</th> </th>
@ -105,7 +112,7 @@
</td> </td>
<td> <td>
<vn-icon <vn-icon
ng-show="ticket.futureAgency !== ticket.agency" ng-show="ticket.futureAgency !== ticket.agency && ticket.agency"
icon="icon-agency-term" icon="icon-agency-term"
title="{{$ctrl.agencies(ticket.futureAgency, ticket.agency)}}"> title="{{$ctrl.agencies(ticket.futureAgency, ticket.agency)}}">
</vn-icon> </vn-icon>
@ -114,12 +121,7 @@
<span <span
ng-click="ticketDescriptor.show($event, ticket.id)" ng-click="ticketDescriptor.show($event, ticket.id)"
class="link"> class="link">
{{::ticket.id | dashIfEmpty}} {{::ticket.id}}
</span>
</td>
<td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.shipped)}}">
{{::ticket.shipped | date: 'dd/MM/yyyy'}}
</span> </span>
</td> </td>
<td>{{::ticket.ipt | dashIfEmpty}}</td> <td>{{::ticket.ipt | dashIfEmpty}}</td>
@ -135,7 +137,7 @@
<span <span
class="chip {{$ctrl.totalPriceColor(ticket.totalWithVat)}}" class="chip {{$ctrl.totalPriceColor(ticket.totalWithVat)}}"
title="{{$ctrl.totalPriceTitle(ticket.totalWithVat) | translate}}"> title="{{$ctrl.totalPriceTitle(ticket.totalWithVat) | translate}}">
{{::(ticket.totalWithVat ? ticket.totalWithVat : 0) | currency: 'EUR': 2}} {{::ticket.totalWithVat | currency: 'EUR': 2}}
</span> </span>
</td> </td>
<td separator> <td separator>
@ -145,11 +147,6 @@
{{::ticket.futureId | dashIfEmpty}} {{::ticket.futureId | dashIfEmpty}}
</span> </span>
</td> </td>
<td shrink-date>
<span class="chip {{$ctrl.compareDate(ticket.futureShipped)}}">
{{::ticket.futureShipped | date: 'dd/MM/yyyy'}}
</span>
</td>
<td>{{::ticket.futureIpt | dashIfEmpty}}</td> <td>{{::ticket.futureIpt | dashIfEmpty}}</td>
<td> <td>
<span <span
@ -181,6 +178,38 @@
question="{{$ctrl.confirmationMessage}}" question="{{$ctrl.confirmationMessage}}"
message="Advance tickets"> message="Advance tickets">
</vn-confirm> </vn-confirm>
<vn-confirm
vn-id="splitTickets"
on-accept="$ctrl.splitTickets()"
question="{{$ctrl.confirmationMessage}}"
message="Advance tickets (without negatives)">
</vn-confirm>
<!-- Split progress -->
<vn-dialog
vn-id="splitProgress"
message="Progress">
<tpl-body class="split-progress">
<vn-vertical>
<vn-spinner
enable="splitProgress.enable">
</vn-spinner>
<vn-label-value
label="Total progress"
value="[{{$ctrl.progress.length}}/{{::$ctrl.checked.length}}]">
</vn-label-value>
<span class="vn-mt-md" ng-if="$ctrl.splitErrors.length" translate>Error list</span>
<vn-vertical ng-repeat="error in $ctrl.splitErrors">
<vn-label-value
label="{{error.id}}"
value="{{error.reason}}">
</vn-label-value>
</vn-vertical>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}" ng-click="splitProgress.cancel = true"/>
</tpl-buttons>
</vn-dialog>
<vn-ticket-descriptor-popover <vn-ticket-descriptor-popover
vn-id="ticketDescriptor"> vn-id="ticketDescriptor">
</vn-ticket-descriptor-popover> </vn-ticket-descriptor-popover>

View File

@ -75,20 +75,6 @@ export default class Controller extends Section {
}); });
} }
compareDate(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let comparation = today - timeTicket;
if (comparation == 0)
return 'warning';
if (comparation < 0)
return 'success';
}
get checked() { get checked() {
const tickets = this.$.model.data || []; const tickets = this.$.model.data || [];
const checkedLines = []; const checkedLines = [];
@ -134,9 +120,16 @@ export default class Controller extends Section {
'\n' + this.$t(`Destination agency`, {agency: agency}); '\n' + this.$t(`Destination agency`, {agency: agency});
} }
moveTicketsAdvance() { async moveTicketsAdvance() {
let ticketsToMove = []; let ticketsToMove = [];
this.checked.forEach(ticket => { for (const ticket of this.checked) {
if (!ticket.id) {
try {
const {query, params} = await this.requestComponentUpdate(ticket, false);
this.$http.post(query, params);
} catch (e) {}
continue;
}
ticketsToMove.push({ ticketsToMove.push({
originId: ticket.futureId, originId: ticket.futureId,
destinationId: ticket.id, destinationId: ticket.id,
@ -144,15 +137,105 @@ export default class Controller extends Section {
destinationShipped: ticket.shipped, destinationShipped: ticket.shipped,
workerFk: ticket.workerFk workerFk: ticket.workerFk
}); });
}); }
const params = {tickets: ticketsToMove}; const params = {tickets: ticketsToMove};
return this.$http.post('Tickets/merge', params) return this.$http.post('Tickets/merge', params)
.then(() => { .then(() => {
this.$.model.refresh(); this.refresh();
this.vnApp.showSuccess(this.$t('Success')); if (ticketsToMove.length)
this.vnApp.showSuccess(this.$t('Success', {tickets: ticketsToMove.length}));
}); });
} }
async getLanded(params) {
const query = `Agencies/getLanded`;
return this.$http.get(query, {params}).then(res => {
if (res.data)
return res.data;
return this.vnApp.showError(
this.$t(`No delivery zone available for this landing date`)
);
});
}
async splitTickets() {
this.progress = [];
this.splitErrors = [];
this.$.splitTickets.hide();
this.$.splitProgress.enable = true;
this.$.splitProgress.cancel = false;
this.$.splitProgress.show();
for (const ticket of this.checked) {
if (this.$.splitProgress.cancel) break;
try {
const {query, params} = await this.requestComponentUpdate(ticket, true);
this.$http.post(query, params)
.catch(e => {
this.splitErrors.push({id: ticket.futureId, reason: e.message});
})
.finally(() => this.progressAdd(ticket.futureId));
} catch (e) {
this.splitErrors.push({id: ticket.futureId, reason: e.message});
this.progressAdd(ticket.futureId);
}
}
}
progressAdd(ticketId) {
this.progress.push(ticketId);
if (this.progress.length == this.checked.length) {
this.$.splitProgress.enable = false;
this.refresh();
if ((this.progress.length - this.splitErrors.length) > 0) {
this.vnApp.showSuccess(this.$t('Success', {
tickets: this.progress.length - this.splitErrors.length
}));
}
}
}
async requestComponentUpdate(ticket, isWithoutNegatives) {
const query = `tickets/${ticket.futureId}/componentUpdate`;
if (!ticket.landed) {
const newLanded = await this.getLanded({
shipped: this.$.model.userParams.dateToAdvance,
addressFk: ticket.addressFk,
agencyModeFk: ticket.agencyModeFk,
warehouseFk: ticket.warehouseFk
});
if (!newLanded)
throw new Error(this.$t(`No delivery zone available for this landing date`));
ticket.landed = newLanded.landed;
ticket.zoneFk = newLanded.zoneFk;
}
const params = {
clientFk: ticket.clientFk,
nickname: ticket.nickname,
agencyModeFk: ticket.agencyModeFk ?? ticket.futureAgencyModeFk,
addressFk: ticket.addressFk,
zoneFk: ticket.zoneFk ?? ticket.futureZoneFk,
warehouseFk: ticket.warehouseFk,
companyFk: ticket.companyFk,
shipped: this.$.model.userParams.dateToAdvance,
landed: ticket.landed,
isDeleted: false,
isWithoutNegatives,
newTicket: ticket.id ?? undefined,
keepPrice: true
};
return {query, params};
}
refresh() {
this.$.model.refresh();
this.$checkAll = null;
}
exprBuilder(param, value) { exprBuilder(param, value) {
switch (param) { switch (param) {
case 'id': case 'id':

View File

@ -24,31 +24,6 @@ describe('Component vnTicketAdvance', () => {
}]; }];
})); }));
describe('compareDate()', () => {
it('should return warning when the date is the present', () => {
let today = Date.vnNew();
let result = controller.compareDate(today);
expect(result).toEqual('warning');
});
it('should return sucess when the date is in the future', () => {
let futureDate = Date.vnNew();
futureDate = futureDate.setDate(futureDate.getDate() + 10);
let result = controller.compareDate(futureDate);
expect(result).toEqual('success');
});
it('should return undefined when the date is in the past', () => {
let pastDate = Date.vnNew();
pastDate = pastDate.setDate(pastDate.getDate() - 10);
let result = controller.compareDate(pastDate);
expect(result).toEqual(undefined);
});
});
describe('checked()', () => { describe('checked()', () => {
it('should return an array of checked tickets', () => { it('should return an array of checked tickets', () => {
const result = controller.checked; const result = controller.checked;

View File

@ -1,7 +1,9 @@
Advance tickets: Adelantar tickets Advance tickets: Adelantar tickets
Advance tickets with negatives: Adelantar tickets con negativos
Advance tickets without negatives: Adelantar tickets sin negativos
Search advance tickets by date: Busca tickets para adelantar por fecha Search advance tickets by date: Busca tickets para adelantar por fecha
Advance confirmation: ¿Desea adelantar {{checked}} tickets? Advance confirmation: ¿Desea adelantar {{checked}} tickets?
Success: Tickets movidos correctamente Success: "{{tickets}} Tickets movidos correctamente"
Lines: Líneas Lines: Líneas
Liters: Litros Liters: Litros
Item Packing Type: Encajado Item Packing Type: Encajado
@ -9,3 +11,7 @@ Origin agency: "Agencia origen: {{agency}}"
Destination agency: "Agencia destino: {{agency}}" Destination agency: "Agencia destino: {{agency}}"
Less than 50€: Menor a 50€ Less than 50€: Menor a 50€
Not Movable: No movibles Not Movable: No movibles
Error list: Lista de errores
Progress: Progreso
Total progress: Progreso total
Advance tickets (without negatives): Adelantar tickets (sin negativos)

View File

@ -5,3 +5,12 @@ vn-ticket-advance{
color: #f7931e color: #f7931e
} }
} }
.split-progress {
width: 40em;
}
@media screen and (max-width: 600px) {
.split-progress {
width: 100%;
}
}

View File

@ -114,7 +114,8 @@ class Controller extends Component {
isDeleted: this.ticket.isDeleted, isDeleted: this.ticket.isDeleted,
option: parseInt(this.ticket.option), option: parseInt(this.ticket.option),
isWithoutNegatives: this.ticket.withoutNegatives, isWithoutNegatives: this.ticket.withoutNegatives,
withWarningAccept: this.ticket.withWarningAccept withWarningAccept: this.ticket.withWarningAccept,
keepPrice: false
}; };
this.$http.post(query, params) this.$http.post(query, params)

View File

@ -37,17 +37,8 @@ class Controller extends SearchPanel {
} }
applyFilters(param) { applyFilters(param) {
if (typeof this.filter.scopeDays === 'number') { if (this.filter?.search)
const today = Date.vnNew(); delete this.filter.scopeDays;
const shippedFrom = new Date(today.getTime());
shippedFrom.setDate(today.getDate() - 30);
shippedFrom.setHours(0, 0, 0, 0);
const shippedTo = new Date(today.getTime());
shippedTo.setDate(shippedTo.getDate() + this.filter.scopeDays);
shippedTo.setHours(23, 59, 59, 999);
Object.assign(this.filter, {shippedFrom, shippedTo});
}
this.model.applyFilter({}, this.filter) this.model.applyFilter({}, this.filter)
.then(() => { .then(() => {

View File

@ -61,8 +61,6 @@
</div> </div>
<div ng-transclude="btnTwo"> <div ng-transclude="btnTwo">
<vn-quick-link <vn-quick-link
vn-acl="hr"
vn-acl-action="remove"
tooltip="Go to user" tooltip="Go to user"
state="['account.card.summary', {id: $ctrl.id}]" state="['account.card.summary', {id: $ctrl.id}]"
icon="face"> icon="face">

View File

@ -20,4 +20,5 @@ import './dms/create';
import './dms/edit'; import './dms/edit';
import './note/index'; import './note/index';
import './note/create'; import './note/create';
import './notifications';

View File

@ -0,0 +1,2 @@
<vn-card>
</vn-card>

View File

@ -0,0 +1,21 @@
import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
constructor($element, $) {
super($element, $);
}
async $onInit() {
const url = await this.vnApp.getUrl(`worker/${this.$params.id}/notifications`);
window.open(url).focus();
}
}
ngModule.vnComponent('vnWorkerNotifications', {
template: require('./index.html'),
controller: Controller,
bindings: {
ticket: '<'
}
});

View File

@ -15,6 +15,7 @@
{"state": "worker.card.timeControl", "icon": "access_time"}, {"state": "worker.card.timeControl", "icon": "access_time"},
{"state": "worker.card.calendar", "icon": "icon-calendar"}, {"state": "worker.card.calendar", "icon": "icon-calendar"},
{"state": "worker.card.pda", "icon": "phone_android"}, {"state": "worker.card.pda", "icon": "phone_android"},
{"state": "worker.card.notifications", "icon": "notifications"},
{"state": "worker.card.pbx", "icon": "icon-pbx"}, {"state": "worker.card.pbx", "icon": "icon-pbx"},
{"state": "worker.card.dms.index", "icon": "cloud_upload"}, {"state": "worker.card.dms.index", "icon": "cloud_upload"},
{ {
@ -112,6 +113,14 @@
"params": { "params": {
"worker": "$ctrl.worker" "worker": "$ctrl.worker"
} }
}, {
"url": "/notifications",
"state": "worker.card.notifications",
"component": "vn-worker-notifications",
"description": "Notifications",
"params": {
"worker": "$ctrl.worker"
}
}, { }, {
"url": "/time-control?timestamp", "url": "/time-control?timestamp",
"state": "worker.card.timeControl", "state": "worker.card.timeControl",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "salix-back", "name": "salix-back",
"version": "23.46.01", "version": "23.48.01",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "salix-back", "name": "salix-back",
"version": "23.46.01", "version": "23.48.01",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^1.2.2", "axios": "^1.2.2",

View File

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

View File

@ -56,9 +56,6 @@ html {
margin-left: 35px; margin-left: 35px;
float:left; float:left;
} }
#barcode{
text-align: center;
}
#right { #right {
float: right; float: right;
margin-top: 20px; margin-top: 20px;

View File

@ -9,7 +9,6 @@
</tr> </tr>
<tr> <tr>
<td> <td>
<div v-html="getBarcode(labelData.palletFk)" id="barcode"></div>
<table v-for="labelData in labelsData" class="zoneTable"> <table v-for="labelData in labelsData" class="zoneTable">
<thead> <thead>
<tr v-if="!labelData.isMatch" id="black"> <tr v-if="!labelData.isMatch" id="black">

View File

@ -39,19 +39,5 @@ module.exports = {
const data = String(id); const data = String(id);
return qrcode.toDataURL(data, {margin: 0}); return qrcode.toDataURL(data, {margin: 0});
}, },
getBarcode(id) {
const xmlSerializer = new XMLSerializer();
const document = new DOMImplementation().createDocument('http://www.w3.org/1999/xhtml', 'html', null);
const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
jsBarcode(svgNode, id, {
xmlDocument: document,
format: 'code128',
displayValue: false,
width: 6,
height: 90,
});
return xmlSerializer.serializeToString(svgNode);
},
}, },
}; };

View File

@ -1,7 +1,7 @@
SELECT io.issued, SELECT io.issued,
c.socialName, c.socialName,
c.street postalAddress, c.street postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, c.fi,
io.clientFk, io.clientFk,
c.postcode, c.postcode,
c.city, c.city,
@ -50,7 +50,7 @@ SELECT io.issued,
) sub2 ON TRUE ) sub2 ON TRUE
JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE' JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'WORLD'
JOIN vn.country cty ON cty.id = c.countryFk JOIN vn.country cty ON cty.id = c.countryFk
JOIN vn.payMethod pm ON pm.id = c .payMethodFk JOIN vn.payMethod pm ON pm.id = c .payMethodFk
JOIN vn.company co ON co.id=io.companyFk JOIN vn.company co ON co.id=io.companyFk