Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 6264-renewToken
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Javier Segarra 2023-11-27 09:48:31 +01:00
commit 901a44cc62
99 changed files with 4686 additions and 3320 deletions

View File

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

View File

@ -1,4 +1,4 @@
FROM debian:bullseye-slim
FROM debian:bookworm-slim
ENV TZ Europe/Madrid
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 \
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \
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/* \
&& npm -g install pm2

View File

@ -49,7 +49,7 @@ module.exports = Self => {
ish.packing,
ish.grouping,
s.isAdded,
s.originalQuantity,
s.originalQuantity,
s.quantity saleQuantity,
iss.quantity reservedQuantity,
SUM(iss.quantity) OVER (PARTITION BY s.id ORDER BY ish.id) accumulatedQuantity,
@ -75,7 +75,7 @@ module.exports = Self => {
LEFT JOIN itemColor ic ON ic.itemFk = s.itemFk
LEFT JOIN origin o ON o.id = i.originFk
WHERE tc.collectionFk = ?
GROUP BY ish.id, p.code, p2.code
GROUP BY s.id, ish.id, p.code, p2.code
ORDER BY pickingOrder;`, [id], myOptions);
if (print)
@ -105,7 +105,7 @@ module.exports = Self => {
LEFT JOIN vn.buy c ON c.itemFk = s.itemFk
LEFT JOIN vn.entry e ON e.id = c.entryFk
LEFT JOIN vn.travel tr ON tr.id = e.travelFk
WHERE s.ticketFk = ?
WHERE s.ticketFk = ?
AND tr.landed >= util.VN_CURDATE() - INTERVAL 1 YEAR`,
[ticketId], myOptions);
ticket.sales = [];

View File

@ -1,133 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('newCollection', {
description: 'Make a new collection of tickets',
accessType: 'WRITE',
accepts: [{
arg: 'collectionFk',
type: 'Number',
required: false,
description: 'The collection id'
}, {
arg: 'sectorFk',
type: 'Number',
required: true,
description: 'The sector of worker'
}, {
arg: 'vWagons',
type: 'Number',
required: true,
description: 'The number of wagons'
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/newCollection`,
verb: 'POST'
}
});
Self.newCollection = async(ctx, collectionFk, sectorFk, vWagons) => {
let query = '';
const userId = ctx.req.accessToken.userId;
if (!collectionFk) {
query = `CALL vn.collectionTrain_newBeta(?,?,?)`;
const [result] = await Self.rawSql(query, [sectorFk, vWagons, userId], {userId});
if (result.length == 0)
throw new Error(`No collections for today`);
collectionFk = result[0].vCollectionFk;
}
query = `CALL vn.collectionTicket_get(?)`;
const [tickets] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionSale_get(?)`;
const [sales] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionPlacement_get(?)`;
const [placements] = await Self.rawSql(query, [collectionFk], {userId});
query = `CALL vn.collectionSticker_print(?,?)`;
await Self.rawSql(query, [collectionFk, sectorFk], {userId});
return makeCollection(tickets, sales, placements, collectionFk);
};
/**
* Returns a collection json
* @param {*} tickets - Request tickets
* @param {*} sales - Request sales
* @param {*} placements - Request placements
* @param {*} collectionFk - Request placements
* @return {Object} Collection JSON
*/
async function makeCollection(tickets, sales, placements, collectionFk) {
let collection = [];
for (let i = 0; i < tickets.length; i++) {
let ticket = {};
ticket['ticketFk'] = tickets[i]['ticketFk'];
ticket['level'] = tickets[i]['level'];
ticket['agencyName'] = tickets[i]['agencyName'];
ticket['warehouseFk'] = tickets[i]['warehouseFk'];
ticket['salesPersonFk'] = tickets[i]['salesPersonFk'];
let ticketSales = [];
for (let x = 0; x < sales.length; x++) {
if (sales[x]['ticketFk'] == ticket['ticketFk']) {
let sale = {};
sale['collectionFk'] = collectionFk;
sale['ticketFk'] = sales[x]['ticketFk'];
sale['saleFk'] = sales[x]['saleFk'];
sale['itemFk'] = sales[x]['itemFk'];
sale['quantity'] = sales[x]['quantity'];
if (sales[x]['quantityPicked'] != null)
sale['quantityPicked'] = sales[x]['quantityPicked'];
else
sale['quantityPicked'] = 0;
sale['longName'] = sales[x]['longName'];
sale['size'] = sales[x]['size'];
sale['color'] = sales[x]['color'];
sale['discount'] = sales[x]['discount'];
sale['price'] = sales[x]['price'];
sale['stems'] = sales[x]['stems'];
sale['category'] = sales[x]['category'];
sale['origin'] = sales[x]['origin'];
sale['clientFk'] = sales[x]['clientFk'];
sale['productor'] = sales[x]['productor'];
sale['reserved'] = sales[x]['reserved'];
sale['isPreviousPrepared'] = sales[x]['isPreviousPrepared'];
sale['isPrepared'] = sales[x]['isPrepared'];
sale['isControlled'] = sales[x]['isControlled'];
let salePlacements = [];
for (let z = 0; z < placements.length; z++) {
if (placements[z]['saleFk'] == sale['saleFk']) {
let placement = {};
placement['saleFk'] = placements[z]['saleFk'];
placement['itemFk'] = placements[z]['itemFk'];
placement['placement'] = placements[z]['placement'];
placement['shelving'] = placements[z]['shelving'];
placement['created'] = placements[z]['created'];
placement['visible'] = placements[z]['visible'];
placement['order'] = placements[z]['order'];
placement['grouping'] = placements[z]['grouping'];
salePlacements.push(placement);
}
}
sale['placements'] = salePlacements;
ticketSales.push(sale);
}
}
ticket['sales'] = ticketSales;
collection.push(ticket);
}
return collection;
}
};

View File

@ -1,12 +0,0 @@
const {models} = require('vn-loopback/server/server');
describe('newCollection()', () => {
it('should return a new collection', async() => {
pending('#3400 analizar que hacer con rutas de back collection');
let ctx = {req: {accessToken: {userId: 1106}}};
let response = await models.Collection.newCollection(ctx, 1, 1, 1);
expect(response.length).toBeGreaterThan(0);
expect(response[0].ticketFk).toEqual(2);
});
});

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
<soap12:Body>
<DeleteEnvio xmlns="http://82.223.6.71:82">
<IdCliente><%= viaexpressConfig.client %></IdCliente>
<Usuario><%= viaexpressConfig.user %></Usuario>
<Password><%= viaexpressConfig.password %></Password>
<etiqueta><%= externalId %></etiqueta>
</DeleteEnvio>
</soap12:Body>
</soap12:Envelope>

View File

@ -0,0 +1,45 @@
const axios = require('axios');
const {DOMParser} = require('xmldom');
module.exports = Self => {
Self.remoteMethod('deleteExpedition', {
description: 'Delete a shipment by providing the expedition ID, interacting with Viaexpress API',
accessType: 'WRITE',
accepts: [{
arg: 'expeditionFk',
type: 'number',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteExpedition`,
verb: 'POST'
}
});
Self.deleteExpedition = async expeditionFk => {
const models = Self.app.models;
const viaexpressConfig = await models.ViaexpressConfig.findOne({
fields: ['url']
});
const renderedXml = await models.ViaexpressConfig.deleteExpeditionRenderer(expeditionFk);
const response = await axios.post(`${viaexpressConfig.url}ServicioVxClientes.asmx`, renderedXml, {
headers: {
'Content-Type': 'application/soap+xml; charset=utf-8'
}
});
const xmlString = response.data;
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
const resultElement = xmlDoc.getElementsByTagName('DeleteEnvioResult')[0];
const result = resultElement.textContent;
return result;
};
};

View File

@ -0,0 +1,44 @@
const fs = require('fs');
const ejs = require('ejs');
module.exports = Self => {
Self.remoteMethod('deleteExpeditionRenderer', {
description: 'Renders the data from an XML',
accessType: 'READ',
accepts: [{
arg: 'expeditionFk',
type: 'number',
required: true
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteExpeditionRenderer`,
verb: 'GET'
}
});
Self.deleteExpeditionRenderer = async expeditionFk => {
const models = Self.app.models;
const viaexpressConfig = await models.ViaexpressConfig.findOne({
fields: ['client', 'user', 'password']
});
const expedition = await models.Expedition.findOne({
fields: ['id', 'externalId'],
where: {id: expeditionFk}
});
const data = {
viaexpressConfig,
externalId: expedition.externalId
};
const template = fs.readFileSync(__dirname + '/deleteExpedition.ejs', 'utf-8');
const renderedXml = ejs.render(template, data);
return renderedXml;
};
};

View File

@ -51,7 +51,7 @@ module.exports = Self => {
}
const validateLogin = await Self.validateLogin(user, password);
await Self.app.models.SignInLog.create({
id: validateLogin.token,
token: validateLogin.token,
userFk: vnUser.id,
ip: ctx.req.ip
});

View File

@ -12,8 +12,21 @@ describe('VnUser Sign-in()', () => {
},
args: {}
};
const {VnUser, AccessToken} = models;
const {VnUser, AccessToken, SignInLog} = models;
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() => {
let login = await VnUser.signIn(unAuthCtx, 'salesAssistant', 'nightmare');
let accessToken = await AccessToken.findById(login.token);

View File

@ -1,6 +1,5 @@
module.exports = Self => {
require('../methods/collection/getCollection')(Self);
require('../methods/collection/newCollection')(Self);
require('../methods/collection/getSectors')(Self);
require('../methods/collection/setSaleQuantity')(Self);
require('../methods/collection/previousLabel')(Self);

View File

@ -1,4 +1,6 @@
module.exports = Self => {
require('../methods/viaexpress-config/internationalExpedition')(Self);
require('../methods/viaexpress-config/renderer')(Self);
require('../methods/viaexpress-config/deleteExpedition')(Self);
require('../methods/viaexpress-config/deleteExpeditionRenderer')(Self);
};

View File

@ -124,17 +124,20 @@ module.exports = function(Self) {
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) {
const loginInfo = Object.assign({password}, Self.userUses(user));
const token = await Self.login(loginInfo, 'user');
const userToken = await token.user.get();
if (userToken.username.toLowerCase() !== user.toLowerCase()) {
console.error('ERROR!!! - Signin with other user', userToken, user);
throw new UserError('Try again');
}
Self.signInValidate(user, userToken);
try {
await Self.app.models.Account.sync(userToken.name, password);

View File

@ -2,17 +2,18 @@
--
-- 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` varchar(10) NOT NULL ,
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,
PRIMARY KEY (`id`),
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,26 @@
DELETE FROM `salix`.`ACL`
WHERE
model = 'Route'
AND property = '*'
AND accessType = 'READ';
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Route', 'find', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'findById', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'findOne', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getRoutesByWorker', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'canViewAllRoute', 'READ', 'ALLOW', 'ROLE', 'deliveryBoss'),
('Route', 'cmr', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'downloadCmrsZip', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'downloadZip', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'filter', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getByWorker', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getDeliveryPoint', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getExternalCmrs', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getSuggestedTickets', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getTickets', 'READ', 'ALLOW', 'ROLE', 'employee'),
('Route', 'guessPriority', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Route', 'insertTicket', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Route', 'getDeliveryPoint', 'READ', 'ALLOW', 'ROLE', 'deliveryBoss'),
('Route', 'summary', 'READ', 'ALLOW', 'ROLE', 'employee');

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
('1', '11');
INSERT INTO `vn`.`packagingConfig`(`upperGap`)
VALUES
('10');
UPDATE `account`.`role` SET id = 100 WHERE id = 0;
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`)
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;
INSERT INTO `account`.`account`(`id`)
@ -87,9 +83,15 @@ INSERT INTO `vn`.`educationLevel` (`id`, `name`)
(1, 'ESTUDIOS PRIMARIOS COMPLETOS'),
(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`)
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 = 20 WHERE id = 1 OR id = 9;
@ -549,15 +551,6 @@ INSERT INTO `vn`.`supplierActivity`(`code`, `name`)
('flowerPlants', 'Wholesale of flowers and plants'),
('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`)
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'),
@ -568,6 +561,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'),
(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`)
VALUES
(1, 1, 123121212, 654789123, 'supplier1@email.es', 'observation1', 'the boss'),
@ -966,6 +968,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),
('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`)
VALUES
(1, 'En reparto', 'ON DELIVERY'),
@ -2887,7 +2893,9 @@ INSERT INTO `vn`.`report` (`id`, `name`, `paperSizeFk`, `method`)
INSERT INTO `vn`.`payDemDetail` (`id`, `detail`)
VALUES
(1, 1);
(1, 1),
(2, 20),
(7, 1);
INSERT INTO `vn`.`workerConfig` (`id`, `businessUpdated`, `roleFk`, `payMethodFk`, `businessTypeFk`)
VALUES

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -145,6 +145,7 @@ export default {
adController: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adController"]',
adUser: 'vn-account-samba vn-textfield[ng-model="$ctrl.config.adUser"]',
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"]',
save: 'vn-account-samba vn-submit'
},

View File

@ -22,7 +22,7 @@ describe('Travel basic data path', () => {
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();
lastMonth.setMonth(lastMonth.getMonth() - 1);
@ -30,6 +30,23 @@ describe('Travel basic data path', () => {
await page.waitToClick(selectors.travelBasicData.save);
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');
});
@ -39,7 +56,7 @@ describe('Travel basic data path', () => {
await page.waitToClick(selectors.travelBasicData.undoChanges);
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() => {

View File

@ -23,18 +23,6 @@ describe('Account Accounts path', () => {
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() => {
await page.loginAndModule('sysadmin', 'account');
await page.accessToSection('account.accounts');

View File

@ -20,8 +20,9 @@ describe('Account Samba path', () => {
await page.waitToClick(selectors.accountSamba.checkEnable);
await page.write(selectors.accountSamba.adDomain, '1234');
await page.write(selectors.accountSamba.adController, '1234');
await page.write(selectors.accountSamba.adUser, 'nightmare');
await page.write(selectors.accountSamba.adPassword, 'sysadmin');
await page.write(selectors.accountSamba.adUser, '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.save);

View File

@ -197,5 +197,8 @@
"Booking completed": "Booking complete",
"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",
"Try again": "Try again"
}
"Try again": "Try again",
"keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries",
"It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}"
}

View File

@ -327,5 +327,7 @@
"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"
}
"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",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}"
}

View File

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

View File

@ -5,7 +5,7 @@ const crypto = require('crypto');
const nthash = require('smbhash').nthash;
module.exports = Self => {
const shouldSync = process.env.NODE_ENV === 'production';
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() {
return await Self.findOne({
@ -140,6 +140,7 @@ module.exports = Self => {
try {
if (shouldSync)
await client.del(dn);
// eslint-disable-next-line no-console
console.log(` -> User '${userName}' removed from LDAP`);
} catch (e) {
if (e.name !== 'NoSuchObjectError') throw e;

View File

@ -27,8 +27,7 @@ module.exports = Self => {
const [row] = await Self.rawSql(
`SELECT COUNT(*) AS nRows
FROM mysql.user
WHERE User = ?
AND Host = ?`,
WHERE User = ? AND Host = ?`,
[mysqlUser, this.userHost]
);
let userExists = row.nRows > 0;
@ -38,8 +37,7 @@ module.exports = Self => {
const [row] = await Self.rawSql(
`SELECT Priv AS priv
FROM mysql.global_priv
WHERE User = ?
AND Host = ?`,
WHERE User = ? AND Host = ?`,
[mysqlUser, this.userHost]
);
const priv = row && JSON.parse(row.priv);
@ -88,10 +86,18 @@ module.exports = Self => {
else
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 ?@?',
[role, mysqlUser, this.userHost]);
} else {

View File

@ -1,6 +1,6 @@
const ldap = require('../util/ldapjs-extra');
const ssh = require('node-ssh');
const execFile = require('child_process').execFile;
/**
* Summary of userAccountControl flags:
@ -11,6 +11,8 @@ const UserAccountControlFlags = {
};
module.exports = Self => {
const shouldSync = process.env.NODE_ENV !== 'test';
Self.getSynchronizer = async function() {
return await Self.findOne({
fields: [
@ -19,6 +21,7 @@ module.exports = Self => {
'adController',
'adUser',
'adPassword',
'userDn',
'verifyCert'
]
});
@ -26,88 +29,123 @@ module.exports = Self => {
Object.assign(Self.prototype, {
async init() {
let sshClient = new ssh.NodeSSH();
await sshClient.connect({
host: this.adController,
username: this.adUser,
password: this.adPassword
});
const baseDn = this.adDomain
.split('.')
.map(part => `dc=${part}`)
.join(',');
const bindDn = `cn=${this.adUser},cn=Users,${baseDn}`;
let adUser = `cn=${this.adUser},${this.usersDn()}`;
let adClient = ldap.createClient({
const adClient = ldap.createClient({
url: `ldaps://${this.adController}:636`,
tlsOptions: {rejectUnauthorized: this.verifyCert}
});
await adClient.bind(adUser, this.adPassword);
await adClient.bind(bindDn, this.adPassword);
Object.assign(this, {
sshClient,
adClient
adClient,
fullUsersDn: `${this.userDn},${baseDn}`,
bindDn
});
},
async deinit() {
await this.sshClient.dispose();
await this.adClient.unbind();
},
usersDn() {
let dnBase = this.adDomain
.split('.')
.map(part => `dc=${part}`)
.join(',');
return `cn=Users,${dnBase}`;
async sambaTool(command, args = []) {
let authArgs = [
'--URL', `ldaps://${this.adController}`,
'--simple-bind-dn', this.bindDn,
'--password', this.adPassword
];
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 syncUser(userName, info, password) {
let {sshClient} = this;
let sambaUser = await this.adClient.searchOne(this.usersDn(), {
async getAdUser(userName) {
const sambaUser = await this.adClient.searchOne(this.fullUsersDn, {
scope: 'sub',
attributes: ['userAccountControl'],
attributes: [
'dn',
'userAccountControl',
'uidNumber',
'accountExpires',
'mail'
],
filter: `(&(objectClass=user)(sAMAccountName=${userName}))`
});
let isEnabled = sambaUser
&& !(sambaUser.userAccountControl & UserAccountControlFlags.ACCOUNTDISABLE);
if (process.env.NODE_ENV === 'test')
return;
if (sambaUser) {
for (const intProp of ['uidNumber', 'userAccountControl']) {
if (sambaUser[intProp] != null)
sambaUser[intProp] = parseInt(sambaUser[intProp]);
}
}
return sambaUser;
},
async syncUser(userName, info, password) {
let sambaUser = await this.getAdUser(userName);
let entry;
if (info.hasAccount) {
if (!sambaUser) {
await sshClient.exec('samba-tool user create', [
userName,
'--uid-number', `${info.uidNumber}`,
'--mail-address', info.corporateMail,
await this.sambaTool('user', [
'create', userName,
'--userou', this.userDn,
'--random-password'
]);
await sshClient.exec('samba-tool user setexpiry', [
userName,
'--noexpiry'
]);
await sshClient.exec('mkhomedir_helper', [
userName,
'0027'
]);
}
if (!isEnabled) {
await sshClient.exec('samba-tool user enable', [
userName
]);
sambaUser = await this.getAdUser(userName);
}
if (password) {
await sshClient.exec('samba-tool user setpassword', [
userName,
await this.sambaTool('user', [
'setpassword', userName,
'--newpassword', password
]);
}
} else if (isEnabled) {
await sshClient.exec('samba-tool user disable', [
userName
]);
entry = {
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`);
}
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) {
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',
attributes: ['sAMAccountName'],
filter: `(&(objectClass=user)(${filter}))`
};
await this.adClient.searchForeach(this.usersDn(), opts,
await this.adClient.searchForeach(this.fullUsersDn, opts,
o => usersToSync.add(o.sAMAccountName));
}
});

View File

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

View File

@ -8,13 +8,20 @@
},
"properties": {
"id": {
"type": "number",
"id": true,
"type": "string"
"description": "Identifier"
},
"token": {
"required": true,
"type": "string",
"description": "Token's user"
},
"creationDate": {
"type": "date"
"type": "date"
},
"userFk": {
"required": true,
"type": "number"
},
"ip": {

View File

@ -12,40 +12,40 @@
<vn-card class="vn-pa-lg" vn-focus>
<vn-vertical>
<vn-textfield
label="Homedir base"
label="Homedir base"
ng-model="$ctrl.config.homedir"
rule="AccountConfig">
</vn-textfield>
<vn-textfield
label="Shell"
label="Shell"
ng-model="$ctrl.config.shell"
rule="AccountConfig">
</vn-textfield>
<vn-input-number
label="User and role base id"
label="User and role base id"
ng-model="$ctrl.config.idBase"
rule="AccountConfig">
</vn-input-number>
<vn-horizontal>
<vn-input-number
label="Min"
label="Min"
ng-model="$ctrl.config.min"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Max"
label="Max"
ng-model="$ctrl.config.max"
rule="AccountConfig">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number
label="Warn"
label="Warn"
ng-model="$ctrl.config.warn"
rule="AccountConfig">
</vn-input-number>
<vn-input-number
label="Inact"
label="Inact"
ng-model="$ctrl.config.inact"
rule="AccountConfig">
</vn-input-number>
@ -61,10 +61,6 @@
label="Synchronize all"
ng-click="$ctrl.onSynchronizeAll()">
</vn-button>
<vn-button
label="Synchronize user"
ng-click="syncUser.show()">
</vn-button>
<vn-button
label="Synchronize roles"
ng-click="$ctrl.onSynchronizeRoles()">
@ -77,25 +73,3 @@
</vn-button>
</vn-button-bar>
</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 Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
export default class Controller extends Section {
onSynchronizeAll() {
@ -8,27 +7,10 @@ export default class Controller extends Section {
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() {
this.$http.patch(`RoleInherits/sync`)
.then(() => this.vnApp.showSuccess(this.$t('Roles synchronized!')));
}
onSyncClose() {
this.syncUser = '';
this.syncPassword = '';
}
}
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
User and role base id: Id base usuarios y roles
Synchronize all: Sincronizar todo
Synchronize user: Sincronizar usuario
Synchronize roles: Sincronizar roles
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
@ -12,5 +11,4 @@ Users synchronized!: ¡Usuarios sincronizados!
Username: Nombre de usuario
Synchronize: Sincronizar
Please enter the username: Por favor introduce el nombre de usuario
User synchronized!: ¡Usuario sincronizado!
Roles synchronized!: ¡Roles sincronizados!

View File

@ -67,6 +67,15 @@
translate>
Deactivate user
</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-body>
<div class="attributes">
@ -153,6 +162,32 @@
<button response="accept" translate>Change password</button>
</tpl-buttons>
</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-user-summary user="$ctrl.user"></vn-user-summary>
</vn-popup>

View File

@ -120,6 +120,20 @@ class Controller extends Descriptor {
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', {

View File

@ -22,6 +22,10 @@ Old password: Contraseña antigua
New password: Nueva contraseña
Repeat password: Repetir contraseña
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!
Password requirements: >
La contraseña debe tener al menos {{ length }} caracteres de longitud,

View File

@ -12,7 +12,7 @@
<vn-card class="vn-pa-lg" vn-focus>
<vn-vertical>
<vn-check
label="Enable synchronization"
label="Enable synchronization"
ng-model="watcher.hasData">
</vn-check>
</vn-vertical>
@ -20,28 +20,33 @@
ng-if="watcher.hasData"
class="vn-mt-md">
<vn-textfield
label="AD domain"
label="AD domain"
ng-model="$ctrl.config.adDomain"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="Domain controller"
label="Domain controller"
ng-model="$ctrl.config.adController"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD user"
label="AD user"
ng-model="$ctrl.config.adUser"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="AD password"
label="AD password"
ng-model="$ctrl.config.adPassword"
type="password"
rule="SambaConfig">
</vn-textfield>
<vn-textfield
label="User DN (without domain part)"
ng-model="$ctrl.config.userDn"
rule="SambaConfig">
</vn-textfield>
<vn-check
label="Verify certificate"
label="Verify certificate"
ng-model="$ctrl.config.verifyCert">
</vn-check>
</vn-vertical>
@ -63,4 +68,4 @@
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>
</form>

View File

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

View File

@ -82,12 +82,15 @@ module.exports = Self => {
if (args.zoneFk) {
let stmts = [];
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`);
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);
}

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

@ -29,15 +29,18 @@ module.exports = Self => {
SELECT iit.*,
SUM(iidd.amount) totalDueDay
FROM vn.invoiceIn ii
LEFT JOIN (SELECT SUM(iit.taxableBase) totalTaxableBase,
CAST(SUM(iit.taxableBase * (1 + (ti.PorcentajeIva / 100))) AS DECIMAL(10,2)) totalVat
LEFT JOIN (
SELECT SUM(iit.taxableBase) totalTaxableBase,
CAST(
SUM(IFNULL(iit.taxableBase * (1 + (ti.PorcentajeIva / 100)), iit.taxableBase))
AS DECIMAL(10, 2)
) totalVat
FROM vn.invoiceInTax iit
LEFT JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE iit.invoiceInFk = ?) iit ON TRUE
WHERE iit.invoiceInFk = ?
) iit ON TRUE
LEFT JOIN vn.invoiceInDueDay iidd ON iidd.invoiceInFk = ii.id
WHERE
ii.id = ?`, [id, id]);
WHERE ii.id = ?`, [id, id]);
return result;
};
};

View File

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

View File

@ -162,61 +162,69 @@ module.exports = Self => {
const stmts = [];
let stmt;
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.filter');
stmts.push(`SET @_optimizer_search_depth = @@optimizer_search_depth`);
stmts.push(`SET SESSION optimizer_search_depth = 0`);
stmt = new ParameterizedSQL(
`CREATE TEMPORARY TABLE tmp.filter
stmt = new ParameterizedSQL(`
CREATE OR REPLACE TEMPORARY TABLE tmp.filter
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT
t.id,
t.shipped,
CAST(DATE(t.shipped) AS CHAR) AS shippedDate,
t.nickname,
t.refFk,
t.routeFk,
t.warehouseFk,
t.clientFk,
t.totalWithoutVat,
t.totalWithVat,
io.id AS invoiceOutId,
a.provinceFk,
p.name AS province,
w.name AS warehouse,
am.name AS agencyMode,
am.id AS agencyModeFk,
st.name AS state,
wk.lastName AS salesPerson,
ts.stateFk AS stateFk,
ts.alertLevel AS alertLevel,
ts.code AS alertLevelCode,
u.name AS userName,
c.salesPersonFk,
c.credit,
z.hour AS zoneLanding,
z.name AS zoneName,
z.id AS zoneFk,
st.classColor,
TIME_FORMAT(t.shipped, '%H:%i') AS preparationHour,
TIME_FORMAT(z.hour, '%H:%i') AS theoreticalhour,
TIME_FORMAT(zed.etc, '%H:%i') AS practicalHour
FROM ticket t
LEFT JOIN invoiceOut io ON t.refFk = io.ref
LEFT JOIN zone z ON z.id = t.zoneFk
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN province p ON p.id = a.provinceFk
LEFT JOIN warehouse w ON w.id = t.warehouseFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.id
LEFT JOIN zoneEstimatedDelivery zed ON zed.zoneFk = t.zoneFk`);
SELECT t.id,
t.shipped,
CAST(DATE(t.shipped) AS CHAR) shippedDate,
t.nickname,
t.refFk,
t.routeFk,
t.warehouseFk,
t.clientFk,
t.totalWithoutVat,
t.totalWithVat,
io.id invoiceOutId,
a.provinceFk,
p.name province,
w.name warehouse,
am.name agencyMode,
am.id agencyModeFk,
st.name state,
wk.lastName salesPerson,
ts.stateFk stateFk,
ts.alertLevel alertLevel,
ts.code alertLevelCode,
u.name userName,
c.salesPersonFk,
c.credit,
z.hour zoneLanding,
z.name zoneName,
z.id zoneFk,
st.classColor,
TIME_FORMAT(t.shipped, '%H:%i') preparationHour,
TIME_FORMAT(z.hour, '%H:%i') theoreticalhour,
TIME_FORMAT(zed.etc, '%H:%i') practicalHour
FROM ticket t
LEFT JOIN invoiceOut io ON t.refFk = io.ref
LEFT JOIN zone z ON z.id = t.zoneFk
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN province p ON p.id = a.provinceFk
LEFT JOIN warehouse w ON w.id = t.warehouseFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
LEFT JOIN state st ON st.id = ts.stateFk
LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.id
LEFT JOIN (
SELECT zoneFk,
CAST(
IFNULL(zoneClosureHour, zoneHour) +
SUM(IF(hasToRecalcPrice, volume, 0)) * 60 /
GREATEST(IFNULL(m3, 0), IFNULL(minSpeed, 0))
AS time
) etc
FROM zoneEstimatedDelivery
GROUP BY zoneFk
) zed ON zed.zoneFk = t.zoneFk
`);
if (args.orderFk) {
stmt.merge({

View File

@ -139,46 +139,65 @@ module.exports = Self => {
filter = mergeFilters(filter, {where});
const stmts = [];
let stmt;
stmts.push(`SET @_optimizer_search_depth = @@optimizer_search_depth`);
stmts.push(`SET SESSION optimizer_search_depth = 0`);
stmt = new ParameterizedSQL(
`CREATE OR REPLACE TEMPORARY TABLE tmp.filter
(INDEX (id))
ENGINE = MEMORY
SELECT
o.id,
o.total,
o.date_send landed,
o.date_make created,
o.customer_id clientFk,
o.agency_id agencyModeFk,
o.address_id addressFk,
o.company_id companyFk,
o.source_app sourceApp,
o.confirmed isConfirmed,
c.name clientName,
c.salesPersonFk,
u.nickname workerNickname,
u.name name,
co.code companyCode,
zed.zoneFk,
zed.hourTheoretical,
zed.hourEffective,
am.name AS agencyName
FROM hedera.order o
LEFT JOIN address a ON a.id = o.address_id
LEFT JOIN agencyMode am ON am.id = o.agency_id
LEFT JOIN client c ON c.id = o.customer_id
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.id
LEFT JOIN company co ON co.id = o.company_id
LEFT JOIN orderTicket ot ON ot.orderFk = o.id
LEFT JOIN ticket t ON t.id = ot.ticketFk
LEFT JOIN zoneEstimatedDelivery zed ON zed.zoneFk = t.zoneFk`);
stmt = new ParameterizedSQL(`
CREATE OR REPLACE TEMPORARY TABLE tmp.filter
(INDEX (id))
ENGINE = MEMORY
SELECT o.id,
o.total,
o.date_send landed,
o.date_make created,
o.customer_id clientFk,
o.agency_id agencyModeFk,
o.address_id addressFk,
o.company_id companyFk,
o.source_app sourceApp,
o.confirmed isConfirmed,
c.name clientName,
c.salesPersonFk,
u.nickname workerNickname,
u.name name,
co.code companyCode,
zed.zoneFk,
zed.hourTheoretical,
zed.hourEffective,
am.name agencyName
FROM hedera.order o
LEFT JOIN address a ON a.id = o.address_id
LEFT JOIN agencyMode am ON am.id = o.agency_id
LEFT JOIN client c ON c.id = o.customer_id
LEFT JOIN worker wk ON wk.id = c.salesPersonFk
LEFT JOIN account.user u ON u.id = wk.id
LEFT JOIN company co ON co.id = o.company_id
LEFT JOIN orderTicket ot ON ot.orderFk = o.id
LEFT JOIN ticket t ON t.id = ot.ticketFk
LEFT JOIN (
SELECT zoneFk,
CAST(
util.VN_CURDATE() +
INTERVAL HOUR(IFNULL(zoneClosureHour, zoneHour)) * 60 +
MINUTE(IFNULL(zoneClosureHour, zoneHour)) MINUTE
AS time
) hourTheoretical,
CAST(
IFNULL(zoneClosureHour, zoneHour) +
SUM(IF(hasToRecalcPrice, volume, 0)) * 60 /
GREATEST(IFNULL(m3, 0), IFNULL(minSpeed, 0))
AS time
) hourEffective
FROM zoneEstimatedDelivery
GROUP BY zoneFk
) zed ON zed.zoneFk = t.zoneFk
`);
stmt.merge(conn.makeWhere(filter.where));
stmt.merge(`GROUP BY id`);
stmt.merge(conn.makePagination(filter));
stmts.push(stmt);
stmts.push(`SET SESSION optimizer_search_depth = @_optimizer_search_depth`);
stmt = new ParameterizedSQL(`SELECT * FROM tmp.filter`);
stmt.merge(conn.makeOrderBy(filter.order));

View File

@ -105,7 +105,7 @@ module.exports = Self => {
}
});
filter = mergeFilters(ctx.args.filter, {where});
filter = mergeFilters(filter, {where});
let stmts = [];
let stmt;
@ -129,9 +129,11 @@ module.exports = Self => {
r.description,
am.name agencyName,
u.name AS workerUserName,
v.numberPlate AS vehiclePlateNumber
v.numberPlate AS vehiclePlateNumber,
Date_format(r.time, '%H:%i') hour
FROM route r
LEFT JOIN agencyMode am ON am.id = r.agencyModeFk
LEFT JOIN agency a ON a.id = am.agencyFk
LEFT JOIN vehicle v ON v.id = r.vehicleFk
LEFT JOIN worker w ON w.id = r.workerFk
LEFT JOIN account.user u ON u.id = w.id`

View File

@ -0,0 +1,65 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethodCtx('getByWorker', {
description: 'Return the routes by worker',
accessType: 'READ',
returns: {
type: ['object'],
root: true
},
http: {
path: `/getByWorker`,
verb: 'GET'
}
});
Self.getByWorker = async ctx => {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const canViewAll = await models.ACL.checkAccessAcl(ctx, 'Route', 'canViewAllRoute', 'READ');
let filterGrant = {};
if (canViewAll) {
const userConfig = await models.UserConfig.getUserConfig(ctx, myOptions);
filterGrant = {
where: {'a.warehouseFk': userConfig.warehouseFk}
};
} else {
filterGrant = {
where: {'r.workerFk': userId}
};
}
const currentDate = Date.vnNew();
currentDate.setHours(0, 0, 0, 0);
const nextDay = Date.vnNew();
nextDay.setDate(currentDate.getDate() + 1);
const filter = {
where: {
and: [
{
or: [
{'r.created': currentDate},
{'r.created': nextDay}
]
}
]
},
order: [
'r.created ASC',
'r.time ASC',
'am.name ASC'
]
};
const result = await Self.filter(ctx, mergeFilters(filter, filterGrant));
return result;
};
};

View File

@ -1,7 +1,7 @@
module.exports = Self => {
Self.remoteMethod('getDeliveryPoint', {
description: 'get the deliveryPoint address',
accessType: 'WRITE',
accessType: 'READ',
accepts: {
arg: 'vehicleId',
type: 'number',

View File

@ -1,7 +1,7 @@
module.exports = Self => {
Self.remoteMethodCtx('guessPriority', {
description: 'Changes automatically the priority of the tickets in a route',
accessType: 'READ',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
@ -15,7 +15,7 @@ module.exports = Self => {
},
http: {
path: `/:id/guessPriority`,
verb: 'GET'
verb: 'PATCH'
}
});

View File

@ -3,7 +3,7 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('insertTicket', {
description: 'Check if the ticket can be insert into the route and insert it',
accessType: 'READ',
accessType: 'WRITE',
accepts: [{
arg: 'routeId',
type: 'number',

View File

@ -0,0 +1,36 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('route getByWorker()', () => {
const userId = 56;
const activeCtx = {
accessToken: {userId: userId},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
const ctx = {req: activeCtx};
beforeAll(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should return routes assigned to the worker', async() => {
const result = await app.models.Route.getByWorker(ctx);
expect(result.every(route => route.workerFk === userId)).toBe(true);
});
it('should return all routes if user has canViewAllRoute permission', async() => {
// Simular que el usuario tiene permiso para ver todas las rutas
spyOn(app.models.ACL, 'checkAccessAcl').and.returnValue(Promise.resolve(true));
const result = await app.models.Route.getByWorker(ctx);
expect(result.some(route => route.workerFk != userId)).toBe(true);
});
});

View File

@ -17,6 +17,7 @@ module.exports = Self => {
require('../methods/route/cmr')(Self);
require('../methods/route/getExternalCmrs')(Self);
require('../methods/route/downloadCmrsZip')(Self);
require('../methods/route/getByWorker')(Self);
Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000'
@ -31,5 +32,5 @@ module.exports = Self => {
const routeMaxKm = 1000;
if (routeTotalKm > routeMaxKm || this.kmStart > this.kmEnd)
err();
};
}
};

View File

@ -120,7 +120,7 @@ class Controller extends Section {
guessPriority() {
let query = `Routes/${this.$params.id}/guessPriority/`;
this.$http.get(query).then(() => {
this.$http.patch(query).then(() => {
this.vnApp.showSuccess(this.$t('Order changed'));
this.$.model.refresh();
});

View File

@ -209,22 +209,6 @@ describe('Route', () => {
});
});
describe('guessPriority()', () => {
it('should perform a GET query then call both refresh and showSuccess methods', () => {
jest.spyOn(controller.$.model, 'refresh');
jest.spyOn(controller.vnApp, 'showSuccess');
controller.$params = {id: 99};
const url = `Routes/${controller.$params.id}/guessPriority/`;
$httpBackend.expectGET(url).respond('ok');
controller.guessPriority();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Order changed');
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
describe('onDrop()', () => {
it('should call the insert method when dragging a ticket number', () => {
jest.spyOn(controller, 'insert');

View File

@ -1,6 +1,7 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('deleteExpeditions', {
Self.remoteMethodCtx('deleteExpeditions', {
description: 'Delete the selected expeditions',
accessType: 'WRITE',
accepts: [{
@ -9,44 +10,59 @@ module.exports = Self => {
required: true,
description: 'The expeditions ids to delete'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/deleteExpeditions`,
verb: 'POST'
},
returns: {
type: ['object'],
root: true
}
});
Self.deleteExpeditions = async(expeditionIds, options) => {
Self.deleteExpeditions = async(ctx, expeditionIds) => {
const models = Self.app.models;
const myOptions = {};
let tx;
const $t = ctx.req.__;
const notDeletedExpeditions = [];
const deletedExpeditions = [];
if (typeof options == 'object')
Object.assign(myOptions, options);
for (let expeditionId of expeditionIds) {
const filter = {
fields: [],
where: {
id: expeditionId
},
include: [
{
relation: 'agencyMode',
scope: {
fields: ['code'],
}
}
]
};
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
const expedition = await models.Expedition.findOne(filter);
const {code} = expedition.agencyMode();
try {
const promises = [];
for (let expeditionId of expeditionIds) {
const deletedExpedition = models.Expedition.destroyById(expeditionId, myOptions);
promises.push(deletedExpedition);
if (code && code.toLowerCase().substring(0, 10) == 'viaexpress') {
const isDeleted = await models.ViaexpressConfig.deleteExpedition(expeditionId);
if (isDeleted === 'true') {
const deletedExpedition = await models.Expedition.destroyById(expeditionId);
deletedExpeditions.push(deletedExpedition);
} else notDeletedExpeditions.push(expeditionId);
} else {
const deletedExpedition = await models.Expedition.destroyById(expeditionId);
deletedExpeditions.push(deletedExpedition);
}
const deletedExpeditions = await Promise.all(promises);
if (tx) await tx.commit();
return deletedExpeditions;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
if (notDeletedExpeditions.length) {
throw new UserError(
$t(`It was not able to remove the next expeditions:`, {expeditions: notDeletedExpeditions.join()})
);
}
return deletedExpeditions;
};
};

View File

@ -2,17 +2,16 @@ const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('ticket deleteExpeditions()', () => {
let ctx;
beforeAll(async() => {
const activeCtx = {
ctx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
req: {
headers: {origin: 'http://localhost'}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
active: ctx
});
});
@ -23,7 +22,7 @@ describe('ticket deleteExpeditions()', () => {
const options = {transaction: tx};
const expeditionIds = [12, 13];
const result = await models.Expedition.deleteExpeditions(expeditionIds, options);
const result = await models.Expedition.deleteExpeditions(ctx, expeditionIds, options);
expect(result.length).toEqual(2);

View File

@ -3,33 +3,30 @@ const LoopBackContext = require('loopback-context');
describe('ticket addSale()', () => {
const ticketId = 13;
beforeAll(async() => {
const activeCtx = {
accessToken: {userId: 9},
function getActiveCtx(userId) {
return {
accessToken: {userId},
http: {
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() => {
const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(9);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
try {
const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 4;
const quantity = 10;
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() => {
const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(18);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const itemId = 11;
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() => {
const tx = await models.Ticket.beginTransaction({});
const activeCtx = getActiveCtx(9);
const ctx = activeCtx.http;
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
const notEditableTicketId = 1;
const itemId = 4;
const quantity = 10;

View File

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

View File

@ -20,6 +20,9 @@
},
"counter": {
"type": "number"
},
"externalId": {
"type": "string"
}
},
"relations": {
@ -30,7 +33,7 @@
},
"agencyMode": {
"type": "belongsTo",
"model": "agency-mode",
"model": "AgencyMode",
"foreignKey": "agencyModeFk"
},
"worker": {

View File

@ -37,17 +37,8 @@ class Controller extends SearchPanel {
}
applyFilters(param) {
if (typeof this.filter.scopeDays === 'number') {
const today = Date.vnNew();
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});
}
if (this.filter?.search)
delete this.filter.scopeDays;
this.model.applyFilter({}, this.filter)
.then(() => {

View File

@ -11,28 +11,24 @@
"id": true,
"type": "number"
},
"hourTheoretical": {
"zoneClosureHour": {
"type": "date"
},
"totalVolume": {
"zoneHour": {
"type": "date"
},
"volume": {
"type": "number"
},
"remainingVolume": {
"hasToRecalcPrice": {
"type": "boolean"
},
"m3": {
"type": "number"
},
"speed": {
"minSpeed": {
"type": "number"
},
"hourEffective": {
"type": "date"
},
"minutesLess": {
"type": "date"
},
"etc": {
"type": "date"
}
},
"relations": {
"zone": {

4
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

@ -39,19 +39,5 @@ module.exports = {
const data = String(id);
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);
},
},
};