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

This commit is contained in:
Vicent Llopis 2023-07-28 07:54:26 +00:00
commit fbc1e28031
109 changed files with 1522 additions and 778 deletions

View File

@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
## [2330.01] - 2023-07-27
### Added
- (Artículos -> Vista Previa) Añadido campo "Plástico reciclado"
- (Rutas -> Troncales) Nueva sección
- (Tickets -> Opciones) Opción establecer peso
- (Clientes -> SMS) Nueva sección
### Changed
- (General -> Iconos) Añadidos nuevos iconos
- (Clientes -> Razón social) Nuevas restricciones por pais
### Fixed
## [2328.01] - 2023-07-13
### Added

View File

@ -1,7 +1,5 @@
const axios = require('axios');
module.exports = Self => {
Self.remoteMethodCtx('checkFile', {
Self.remoteMethod('checkFile', {
description: 'Check if exist docuware file',
accessType: 'READ',
accepts: [
@ -17,12 +15,16 @@ module.exports = Self => {
required: true,
description: 'The fileCabinet name'
},
{
arg: 'filter',
type: 'object',
description: 'The filter'
},
{
arg: 'signed',
type: 'boolean',
required: true,
description: 'If pdf is necessary to be signed'
}
},
],
returns: {
type: 'object',
@ -34,7 +36,7 @@ module.exports = Self => {
}
});
Self.checkFile = async function(ctx, id, fileCabinet, signed) {
Self.checkFile = async function(id, fileCabinet, filter, signed) {
const models = Self.app.models;
const action = 'find';
@ -45,39 +47,34 @@ module.exports = Self => {
}
});
const searchFilter = {
condition: [
{
DBName: docuwareInfo.findById,
Value: [id]
}
],
sortOrder: [
{
Field: 'FILENAME',
Direction: 'Desc'
}
]
};
if (!filter) {
filter = {
condition: [
{
DBName: docuwareInfo.findById,
Value: [id]
}
],
sortOrder: [
{
Field: 'FILENAME',
Direction: 'Desc'
}
]
};
}
if (signed) {
filter.condition.push({
DBName: 'ESTADO',
Value: ['Firmado']
});
}
try {
const options = await Self.getOptions();
const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
const response = await axios.post(
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
searchFilter,
options.headers
);
const [documents] = response.data.Items;
const response = await Self.get(fileCabinet, filter);
const [documents] = response.Items;
if (!documents) return false;
const state = documents.Fields.find(field => field.FieldName == 'ESTADO');
if (signed && state.Item != 'Firmado') return false;
return {id: documents.Id};
} catch (error) {
return false;

View File

@ -1,59 +1,6 @@
const axios = require('axios');
module.exports = Self => {
/**
* Returns the dialog id
*
* @param {string} code - The fileCabinet name
* @param {string} action - The fileCabinet name
* @param {string} fileCabinetId - Optional The fileCabinet name
* @return {number} - The fileCabinet id
*/
Self.getDialog = async(code, action, fileCabinetId) => {
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code: code,
action: action
}
});
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
const options = await Self.getOptions();
if (!process.env.NODE_ENV)
return Math.round();
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
const dialogs = response.data.Dialog;
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
return dialogId;
};
/**
* Returns the fileCabinetId
*
* @param {string} code - The fileCabinet code
* @return {number} - The fileCabinet id
*/
Self.getFileCabinet = async code => {
const options = await Self.getOptions();
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code: code
}
});
if (!process.env.NODE_ENV)
return Math.round();
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
const fileCabinets = fileCabinetResponse.data.FileCabinet;
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
return fileCabinetId;
};
/**
* Returns basic headers
*
@ -75,4 +22,139 @@ module.exports = Self => {
headers
};
};
/**
* Returns the dialog id
*
* @param {string} code - The fileCabinet name
* @param {string} action - The fileCabinet name
* @param {string} fileCabinetId - Optional The fileCabinet name
* @return {number} - The fileCabinet id
*/
Self.getDialog = async(code, action, fileCabinetId) => {
if (!process.env.NODE_ENV)
return Math.floor(Math.random() + 100);
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code,
action
}
});
if (!fileCabinetId) fileCabinetId = await Self.getFileCabinet(code);
const options = await Self.getOptions();
const response = await axios.get(`${options.url}/FileCabinets/${fileCabinetId}/dialogs`, options.headers);
const dialogs = response.data.Dialog;
const dialogId = dialogs.find(dialogs => dialogs.DisplayName === docuwareInfo.dialogName).Id;
return dialogId;
};
/**
* Returns the fileCabinetId
*
* @param {string} code - The fileCabinet code
* @return {number} - The fileCabinet id
*/
Self.getFileCabinet = async code => {
if (!process.env.NODE_ENV)
return Math.floor(Math.random() + 100);
const options = await Self.getOptions();
const docuwareInfo = await Self.app.models.Docuware.findOne({
where: {
code
}
});
const fileCabinetResponse = await axios.get(`${options.url}/FileCabinets`, options.headers);
const fileCabinets = fileCabinetResponse.data.FileCabinet;
const fileCabinetId = fileCabinets.find(fileCabinet => fileCabinet.Name === docuwareInfo.fileCabinetName).Id;
return fileCabinetId;
};
/**
* Returns docuware data
*
* @param {string} code - The fileCabinet code
* @param {object} filter - The filter for docuware
* @param {object} parse - The fields parsed
* @return {object} - The data
*/
Self.get = async(code, filter, parse) => {
if (!process.env.NODE_ENV) return;
const options = await Self.getOptions();
const fileCabinetId = await Self.getFileCabinet(code);
const dialogId = await Self.getDialog(code, 'find', fileCabinetId);
const data = await axios.post(
`${options.url}/FileCabinets/${fileCabinetId}/Query/DialogExpression?dialogId=${dialogId}`,
filter,
options.headers
);
return parser(data.data, parse);
};
/**
* Returns docuware data
*
* @param {string} code - The fileCabinet code
* @param {any} id - The id of docuware
* @param {object} parse - The fields parsed
* @return {object} - The data
*/
Self.getById = async(code, id, parse) => {
if (!process.env.NODE_ENV) return;
const docuwareInfo = await Self.app.models.Docuware.findOne({
fields: ['findById'],
where: {
code,
action: 'find'
}
});
const filter = {
condition: [
{
DBName: docuwareInfo.findById,
Value: [id]
}
]
};
return Self.get(code, filter, parse);
};
/**
* Returns docuware data filtered
*
* @param {array} data - The data
* @param {object} parse - The fields parsed
* @return {object} - The data parsed
*/
function parser(data, parse) {
if (!(data && data.Items)) return data;
const parsed = [];
for (item of data.Items) {
const itemParsed = {};
item.Fields.map(field => {
if (field.ItemElementName.includes('Date')) field.Item = toDate(field.Item);
if (!parse) return itemParsed[field.FieldLabel] = field.Item;
if (parse[field.FieldLabel])
itemParsed[parse[field.FieldLabel]] = field.Item;
});
parsed.push(itemParsed);
}
return parsed;
}
function toDate(value) {
if (!value) return;
return new Date(Number(value.substring(6, 19)));
}
};

View File

@ -3,7 +3,7 @@ const axios = require('axios');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('download', {
Self.remoteMethod('download', {
description: 'Download an docuware PDF',
accessType: 'READ',
accepts: [
@ -16,8 +16,12 @@ module.exports = Self => {
{
arg: 'fileCabinet',
type: 'string',
description: 'The file cabinet',
http: {source: 'path'}
description: 'The file cabinet'
},
{
arg: 'filter',
type: 'object',
description: 'The filter'
}
],
returns: [
@ -36,14 +40,15 @@ module.exports = Self => {
}
],
http: {
path: `/:id/download/:fileCabinet`,
path: `/:id/download`,
verb: 'GET'
}
});
Self.download = async function(ctx, id, fileCabinet) {
Self.download = async function(id, fileCabinet, filter) {
const models = Self.app.models;
const docuwareFile = await models.Docuware.checkFile(ctx, id, fileCabinet, true);
const docuwareFile = await models.Docuware.checkFile(id, fileCabinet, filter);
if (!docuwareFile) throw new UserError('The DOCUWARE PDF document does not exists');
const fileCabinetId = await Self.getFileCabinet(fileCabinet);

View File

@ -1,57 +1,15 @@
const models = require('vn-loopback/server/server').models;
const axios = require('axios');
describe('docuware download()', () => {
const ticketId = 1;
const userId = 9;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const docuwareModel = models.Docuware;
const fileCabinetName = 'deliveryNote';
beforeAll(() => {
spyOn(docuwareModel, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(docuwareModel, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
});
it('should return false if there are no documents', async() => {
const response = {
data: {
Items: []
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve({Items: []}))));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
expect(result).toEqual(false);
});
it('should return false if the document is unsigned', async() => {
const response = {
data: {
Items: [
{
Id: 1,
Fields: [
{
FieldName: 'ESTADO',
Item: 'Unsigned'
}
]
}
]
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
expect(result).toEqual(false);
});
@ -59,23 +17,21 @@ describe('docuware download()', () => {
it('should return the document data', async() => {
const docuwareId = 1;
const response = {
data: {
Items: [
{
Id: docuwareId,
Fields: [
{
FieldName: 'ESTADO',
Item: 'Firmado'
}
]
}
]
}
Items: [
{
Id: docuwareId,
Fields: [
{
FieldName: 'ESTADO',
Item: 'Firmado'
}
]
}
]
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(response)));
spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve(response))));
const result = await models.Docuware.checkFile(ctx, ticketId, fileCabinetName, true);
const result = await models.Docuware.checkFile(ticketId, fileCabinetName, null, true);
expect(result.id).toEqual(docuwareId);
});

View File

@ -0,0 +1,135 @@
const axios = require('axios');
const models = require('vn-loopback/server/server').models;
describe('Docuware core', () => {
beforeAll(() => {
process.env.NODE_ENV = 'testing';
});
afterAll(() => {
delete process.env.NODE_ENV;
});
describe('getOptions()', () => {
it('should return url and headers', async() => {
const result = await models.Docuware.getOptions();
expect(result.url).toBeDefined();
expect(result.headers).toBeDefined();
});
});
describe('getDialog()', () => {
it('should return dialogId', async() => {
const dialogs = {
data: {
Dialog: [
{
DisplayName: 'find',
Id: 'getDialogTest'
}
]
}
};
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
const result = await models.Docuware.getDialog('deliveryNote', 'find', 'randomFileCabinetId');
expect(result).toEqual('getDialogTest');
});
});
describe('getFileCabinet()', () => {
it('should return fileCabinetId', async() => {
const code = 'deliveryNote';
const docuwareInfo = await models.Docuware.findOne({
where: {
code
}
});
const dialogs = {
data: {
FileCabinet: [
{
Name: docuwareInfo.fileCabinetName,
Id: 'getFileCabinetTest'
}
]
}
};
spyOn(axios, 'get').and.returnValue(new Promise(resolve => resolve(dialogs)));
const result = await models.Docuware.getFileCabinet(code);
expect(result).toEqual('getFileCabinetTest');
});
});
describe('get()', () => {
it('should return data without parse', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
id: 1
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const result = await models.Docuware.get('deliveryNote');
expect(result.id).toEqual(1);
});
it('should return data with parse', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
Items: [{
Fields: [
{
ItemElementName: 'integer',
FieldLabel: 'firstRequiredField',
Item: 1
},
{
ItemElementName: 'string',
FieldLabel: 'secondRequiredField',
Item: 'myName'
},
{
ItemElementName: 'integer',
FieldLabel: 'notRequiredField',
Item: 2
}
]
}]
}
};
const parse = {
'firstRequiredField': 'id',
'secondRequiredField': 'name',
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const [result] = await models.Docuware.get('deliveryNote', null, parse);
expect(result.id).toEqual(1);
expect(result.name).toEqual('myName');
expect(result.notRequiredField).not.toBeDefined();
});
});
describe('getById()', () => {
it('should return data', async() => {
spyOn(models.Docuware, 'getFileCabinet').and.returnValue((new Promise(resolve => resolve(Math.random()))));
spyOn(models.Docuware, 'getDialog').and.returnValue((new Promise(resolve => resolve(Math.random()))));
const data = {
data: {
id: 1
}
};
spyOn(axios, 'post').and.returnValue(new Promise(resolve => resolve(data)));
const result = await models.Docuware.getById('deliveryNote', 1);
expect(result.id).toEqual(1);
});
});
});

View File

@ -39,7 +39,7 @@ describe('docuware download()', () => {
spyOn(docuwareModel, 'checkFile').and.returnValue({});
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true}));
const result = await models.Docuware.download(ctx, ticketId, fileCabinetName);
const result = await models.Docuware.download(ticketId, fileCabinetName);
expect(result[1]).toEqual('application/pdf');
expect(result[2]).toEqual(`filename="${ticketId}.pdf"`);

View File

@ -1,68 +0,0 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('addAlias', {
description: 'Add an alias if the user has the grant',
accessType: 'WRITE',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'mailAlias',
type: 'number',
description: 'The new alias for user',
required: true
}
],
http: {
path: `/:id/addAlias`,
verb: 'POST'
}
});
Self.addAlias = async function(ctx, id, mailAlias, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const user = await Self.findById(userId, {fields: ['hasGrant']}, myOptions);
if (!user.hasGrant)
throw new UserError(`You don't have grant privilege`);
const account = await models.Account.findById(userId, {
fields: ['id'],
include: {
relation: 'aliases',
scope: {
fields: ['mailAlias']
}
}
}, myOptions);
const aliases = account.aliases().map(alias => alias.mailAlias);
const hasAlias = aliases.includes(mailAlias);
if (!hasAlias)
throw new UserError(`You cannot assign an alias that you are not assigned to`);
return models.MailAliasAccount.create({
mailAlias: mailAlias,
account: id
}, myOptions);
};
};

View File

@ -1,55 +0,0 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('removeAlias', {
description: 'Remove alias if the user has the grant',
accessType: 'WRITE',
accepts: [
{
arg: 'ctx',
type: 'Object',
http: {source: 'context'}
},
{
arg: 'id',
type: 'number',
required: true,
description: 'The user id',
http: {source: 'path'}
},
{
arg: 'mailAlias',
type: 'number',
description: 'The alias to delete',
required: true
}
],
http: {
path: `/:id/removeAlias`,
verb: 'POST'
}
});
Self.removeAlias = async function(ctx, id, mailAlias, options) {
const models = Self.app.models;
const userId = ctx.req.accessToken.userId;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const canRemoveAlias = await models.ACL.checkAccessAcl(ctx, 'VnUser', 'canRemoveAlias', 'WRITE');
if (userId != id && !canRemoveAlias) throw new UserError(`You don't have grant privilege`);
const mailAliasAccount = await models.MailAliasAccount.findOne({
where: {
mailAlias: mailAlias,
account: id
}
}, myOptions);
await mailAliasAccount.destroy(myOptions);
};
};

View File

@ -18,6 +18,9 @@
},
"expired": {
"type": "date"
},
"supplierAccountFk": {
"type": "number"
}
},
"scope": {

View File

@ -22,6 +22,9 @@
},
"isUeeMember": {
"type": "boolean"
},
"isSocialNameUnique": {
"type": "boolean"
}
},
"relations": {
@ -39,4 +42,4 @@
"permission": "ALLOW"
}
]
}
}

View File

@ -28,5 +28,12 @@
"findById": {
"type": "string"
}
},
"relations": {
"dmsType": {
"type": "belongsTo",
"model": "DmsType",
"foreignKey": "dmsTypeFk"
}
}
}

View File

@ -12,8 +12,6 @@ module.exports = function(Self) {
require('../methods/vn-user/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(Self);
require('../methods/vn-user/addAlias')(Self);
require('../methods/vn-user/removeAlias')(Self);
Self.definition.settings.acls = Self.definition.settings.acls.filter(acl => acl.property !== 'create');

View File

@ -1,11 +0,0 @@
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'addAlias', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'removeAlias', 'WRITE', 'ALLOW', 'ROLE', 'employee');
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('VnUser', 'canRemoveAlias', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

@ -0,0 +1,8 @@
DELETE FROM `salix`.`ACL` WHERE model = 'MailAliasAccount';
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('MailAliasAccount', '*', 'READ', 'ALLOW', 'ROLE', 'employee'),
('MailAliasAccount', 'create', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('MailAliasAccount', 'deleteById', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('MailAliasAccount', 'canEditAlias', 'WRITE', 'ALLOW', 'ROLE', 'itManagement');

View File

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

View File

@ -0,0 +1,15 @@
CREATE TABLE `vn`.`clientSms` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`clientFk` int(11) NOT NULL,
`smsFk` mediumint(8) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `clientSms_FK` (`clientFk`),
KEY `clientSms_FK_1` (`smsFk`),
CONSTRAINT `clientSms_FK` FOREIGN KEY (`clientFk`) REFERENCES `client` (`id`) ON UPDATE CASCADE,
CONSTRAINT `clientSms_FK_1` FOREIGN KEY (`smsFk`) REFERENCES `sms` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('ClientSms', 'find', 'READ', 'ALLOW', 'ROLE', 'employee'),
('ClientSms', 'create', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1 @@
ALTER TABLE `vn`.`company` MODIFY COLUMN sage200Company int(2) DEFAULT 10 NOT NULL;

View File

@ -0,0 +1,3 @@
INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalId`)
VALUES
('Vehicle','sorted','WRITE','ALLOW','employee');

View File

@ -0,0 +1,64 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`itemShelving_inventory`(vParkingFromFk VARCHAR(8), vParkingToFk VARCHAR(8))
BEGIN
/**
* Devuelve un listado de ubicaciones a revisar
*
* @param vParkingFromFk Parking de partida, identificador de parking
* @param vParkingToFk Parking de llegada, identificador de parking
*/
DECLARE vSectorFk INT;
DECLARE vPickingOrderFrom INT;
DECLARE vPickingOrderTo INT;
SELECT p.sectorFk, p.pickingOrder INTO vSectorFk, vPickingOrderFrom
FROM parking p
WHERE p.code = vParkingFromFk COLLATE 'utf8mb3_general_ci';
SELECT p.pickingOrder INTO vPickingOrderTo
FROM parking p
WHERE p.code = vParkingToFk COLLATE 'utf8mb3_general_ci';
CALL visible_getMisfit(vSectorFk);
SELECT ish.id,
p.pickingOrder,
p.code parking,
ish.shelvingFk,
ish.itemFk,
i.longName,
ish.visible,
p.sectorFk,
it.workerFk buyer,
CONCAT('http:',ic.url, '/catalog/1600x900/',i.image) urlImage,
ish.isChecked,
CASE
WHEN s.notPrepared > sm.parked THEN 0
WHEN sm.visible > sm.parked THEN 1
ELSE 2
END priority
FROM itemShelving ish
JOIN item i ON i.id = ish.itemFk
JOIN itemType it ON it.id = i.typeFk
JOIN tmp.stockMisfit sm ON sm.itemFk = ish.itemFk
JOIN shelving sh ON sh.code = ish.shelvingFk
JOIN parking p ON p.id = sh.parkingFk
JOIN (SELECT s.itemFk, sum(s.quantity) notPrepared
FROM sale s
JOIN ticket t ON t.id = s.ticketFk
JOIN warehouse w ON w.id = t.warehouseFk
JOIN config c ON c.mainWarehouseFk = w.id
WHERE t.shipped BETWEEN util.VN_CURDATE()
AND util.dayEnd(util.VN_CURDATE())
AND s.isPicked = FALSE
GROUP BY s.itemFk) s ON s.itemFk = i.id
JOIN hedera.imageConfig ic
WHERE p.pickingOrder BETWEEN vPickingOrderFrom AND vPickingOrderTo
AND p.sectorFk = vSectorFk
ORDER BY p.pickingOrder;
END$$
DELIMITER ;

View File

@ -0,0 +1,2 @@
ALTER TABLE `vn`.`country`
ADD COLUMN `isSocialNameUnique` tinyint(1) NOT NULL DEFAULT 1;

View File

@ -6,5 +6,3 @@ ALTER TABLE `vn`.`roadmap` CHANGE name name varchar(45) CHARACTER SET utf8mb3 CO
ALTER TABLE `vn`.`roadmap` MODIFY COLUMN etd datetime NOT NULL;
ALTER TABLE `vn`.`expeditionTruck` COMMENT='Distintas paradas que hacen los trocales';
ALTER TABLE `vn`.`expeditionTruck` DROP FOREIGN KEY expeditionTruck_FK_2;
ALTER TABLE `vn`.`expeditionTruck` ADD CONSTRAINT expeditionTruck_FK_2 FOREIGN KEY (roadmapFk) REFERENCES vn.roadmap(id) ON DELETE CASCADE ON UPDATE CASCADE;

View File

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`)
VALUES ('Ticket','transferClient','WRITE','ALLOW','ROLE','administrative');

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (`model`,`property`,`accessType`,`permission`,`principalType`,`principalId`)
VALUES ('Ticket','canEditWeekly','WRITE','ALLOW','ROLE','buyer');

View File

@ -0,0 +1,10 @@
ALTER TABLE `vn`.`docuware` ADD dmsTypeFk INT(11) DEFAULT NULL NULL;
ALTER TABLE `vn`.`docuware` ADD CONSTRAINT docuware_FK FOREIGN KEY (dmsTypeFk) REFERENCES `vn`.`dmsType`(id) ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO `vn`.`docuware` (code, fileCabinetName, `action`, dialogName, findById, dmsTypeFk)
VALUES
('hr', 'RRHH', 'find', 'Búsqueda', 'N__DOCUMENTO', NULL); -- set dmsTypeFk 3 when deploy in production
INSERT INTO `salix`.`url` (appName, environment, url)
VALUES
('docuware', 'production', 'https://verdnatura.docuware.cloud/DocuWare/Platform/');

View File

@ -37,7 +37,7 @@ ALTER TABLE `vn`.`ticket` AUTO_INCREMENT = 1;
INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES
('DEFAULT_TOKEN', '1209600', util.VN_CURDATE(), 66);
('DEFAULT_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `salix`.`printConfig` (`id`, `itRecipient`, `incidencesEmail`)
VALUES
@ -2953,3 +2953,8 @@ INSERT INTO `vn`.`invoiceInSerial` (`code`, `description`, `cplusTerIdNifFk`, `t
('E', 'Midgard', 1, 'CEE'),
('R', 'Jotunheim', 1, 'NATIONAL'),
('W', 'Vanaheim', 1, 'WORLD');
INSERT INTO `hedera`.`imageConfig` (`id`, `maxSize`, `useXsendfile`, `url`)
VALUES
(1, 0, 0, 'marvel.com');

View File

@ -77831,7 +77831,7 @@ BEGIN
LEAVE cur1Loop;
END IF;
CALL zone_getLeaves2(vZoneFk, NULL, NULL);
CALL zone_getLeaves(vZoneFk, NULL, NULL, TRUE);
myLoop: LOOP
SET vGeoFk = NULL;
@ -77844,7 +77844,7 @@ BEGIN
LEAVE myLoop;
END IF;
CALL zone_getLeaves2(vZoneFk, vGeoFk, NULL);
CALL zone_getLeaves(vZoneFk, vGeoFk, NULL, TRUE);
UPDATE tmp.zoneNodes
SET isChecked = TRUE
WHERE geoFk = vGeoFk;
@ -78130,55 +78130,58 @@ DELIMITER ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'IGNORE_SPACE,NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `zone_getLeaves`(vSelf INT, vParentFk INT, vSearch VARCHAR(255))
BEGIN
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`zone_getLeaves`(
vSelf INT,
vParentFk INT,
vSearch VARCHAR(255),
vHasInsert BOOL
)
BEGIN
/**
* Devuelve las ubicaciones incluidas en la ruta y que sean hijos de parentFk.
* @param vSelf Id de la zona
* @param vParentFk Id del geo a calcular
* @param vSearch cadena a buscar
* @param vSearch Cadena a buscar
* @param vHasInsert Indica si inserta en tmp.zoneNodes
* Optional @table tmp.zoneNodes(geoFk, name, parentFk, sons, isChecked, zoneFk)
*/
DECLARE vIsNumber BOOL;
DECLARE vIsSearch BOOL DEFAULT vSearch IS NOT NULL AND vSearch != '';
DECLARE vIsSearch BOOL DEFAULT vSearch IS NOT NULL AND vSearch <> '';
DROP TEMPORARY TABLE IF EXISTS tNodes;
CREATE TEMPORARY TABLE tNodes
CREATE OR REPLACE TEMPORARY TABLE tNodes
(UNIQUE (id))
ENGINE = MEMORY
SELECT id
FROM zoneGeo
SELECT id
FROM zoneGeo
LIMIT 0;
IF vIsSearch THEN
SET vIsNumber = vSearch REGEXP '^[0-9]+$';
INSERT INTO tNodes
SELECT id
SELECT id
FROM zoneGeo
WHERE (vIsNumber AND `name` = vSearch)
OR (!vIsNumber AND `name` LIKE CONCAT('%', vSearch, '%'))
LIMIT 1000;
ELSEIF vParentFk IS NULL THEN
INSERT INTO tNodes
SELECT geoFk
SELECT geoFk
FROM zoneIncluded
WHERE zoneFk = vSelf;
END IF;
IF vParentFk IS NULL THEN
DROP TEMPORARY TABLE IF EXISTS tChilds;
CREATE TEMPORARY TABLE tChilds
CREATE OR REPLACE TEMPORARY TABLE tChilds
(INDEX(id))
ENGINE = MEMORY
SELECT id
FROM tNodes;
SELECT id FROM tNodes;
DROP TEMPORARY TABLE IF EXISTS tParents;
CREATE TEMPORARY TABLE tParents
CREATE OR REPLACE TEMPORARY TABLE tParents
(INDEX(id))
ENGINE = MEMORY
SELECT id
FROM zoneGeo
LIMIT 0;
SELECT id FROM zoneGeo LIMIT 0;
myLoop: LOOP
DELETE FROM tParents;
@ -78186,43 +78189,67 @@ BEGIN
SELECT parentFk id
FROM zoneGeo g
JOIN tChilds c ON c.id = g.id
WHERE g.parentFk IS NOT NULL;
WHERE g.parentFk IS NOT NULL;
INSERT IGNORE INTO tNodes
SELECT id
FROM tParents;
IF ROW_COUNT() = 0 THEN
SELECT id FROM tParents;
IF NOT ROW_COUNT() THEN
LEAVE myLoop;
END IF;
DELETE FROM tChilds;
INSERT INTO tChilds
SELECT id
FROM tParents;
SELECT id FROM tParents;
END LOOP;
DROP TEMPORARY TABLE tChilds, tParents;
END IF;
IF !vIsSearch THEN
IF NOT vIsSearch THEN
INSERT IGNORE INTO tNodes
SELECT id
SELECT id
FROM zoneGeo
WHERE parentFk <=> vParentFk;
END IF;
SELECT g.id,
g.name,
g.parentFk,
g.sons,
isIncluded selected
FROM zoneGeo g
JOIN tNodes n ON n.id = g.id
LEFT JOIN zoneIncluded i ON i.geoFk = g.id AND i.zoneFk = vSelf
ORDER BY `depth`, selected DESC, name;
CREATE OR REPLACE TEMPORARY TABLE tZones
SELECT g.id,
g.name,
g.parentFk,
g.sons,
NOT g.sons OR `type` = 'country' isChecked,
i.isIncluded selected,
g.`depth`,
vSelf
FROM zoneGeo g
JOIN tNodes n ON n.id = g.id
LEFT JOIN zoneIncluded i ON i.geoFk = g.id
AND i.zoneFk = vSelf
ORDER BY g.`depth`, selected DESC, g.name;
DROP TEMPORARY TABLE tNodes;
IF vHasInsert THEN
INSERT IGNORE INTO tmp.zoneNodes(geoFk, name, parentFk, sons, isChecked, zoneFk)
SELECT id,
name,
parentFk,
sons,
isChecked,
vSelf
FROM tZones
WHERE selected
OR (selected IS NULL AND vParentFk IS NOT NULL);
ELSE
SELECT id,
name,
parentFk,
sons,
selected
FROM tZones
ORDER BY `depth`, selected DESC, name;
END IF;
DROP TEMPORARY TABLE tNodes, tZones;
END ;;
DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ;
@ -78540,7 +78567,7 @@ BEGIN
INDEX(geoFk))
ENGINE = MEMORY;
CALL zone_getLeaves2(vSelf, NULL , NULL);
CALL zone_getLeaves(vSelf, NULL , NULL, TRUE);
UPDATE tmp.zoneNodes zn
SET isChecked = 0
@ -78553,7 +78580,7 @@ BEGIN
WHERE NOT isChecked
LIMIT 1;
CALL zone_getLeaves2(vSelf, vGeoFk, NULL);
CALL zone_getLeaves(vSelf, vGeoFk, NULL, TRUE);
UPDATE tmp.zoneNodes
SET isChecked = TRUE
WHERE geoFk = vGeoFk;

View File

@ -875,7 +875,7 @@ export default {
},
routeTickets: {
firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-input-number[ng-model="ticket.priority"]',
firstTicketPriority: 'vn-route-tickets vn-tr:nth-child(1) vn-td-editable',
firstTicketCheckbox: 'vn-route-tickets vn-tr:nth-child(1) vn-check',
buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]',
firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]',

View File

@ -18,8 +18,7 @@ describe('Route tickets path', () => {
});
it('should modify the first ticket priority', async() => {
await page.clearInput(selectors.routeTickets.firstTicketPriority);
await page.type(selectors.routeTickets.firstTicketPriority, '9');
await page.writeOnEditableTD(selectors.routeTickets.firstTicketPriority, '9');
await page.keyboard.press('Enter');
const message = await page.waitForSnackbar();

View File

@ -66,7 +66,11 @@ export default class App {
return this.logger.$http.get('Urls/findOne', {filter})
.then(res => {
return res.data.url + route;
if (res && res.data)
return res.data.url + route;
})
.catch(() => {
this.showError('Direction not found');
});
}
}

View File

@ -3,4 +3,5 @@ Could not contact the server: No se ha podido contactar con el servidor, asegura
Please enter your username: Por favor introduce tu nombre de usuario
It seems that the server has fall down: Parece que el servidor se ha caído, espera unos minutos e inténtalo de nuevo
Session has expired: Tu sesión ha expirado, por favor vuelve a iniciar sesión
Access denied: Acción no permitida
Access denied: Acción no permitida
Direction not found: Dirección no encontrada

View File

@ -178,5 +178,9 @@
"The renew period has not been exceeded": "The renew period has not been exceeded",
"You can not use the same password": "You can not use the same password",
"Valid priorities": "Valid priorities: %d",
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}"
"Negative basis of tickets": "Negative basis of tickets: {{ticketsIds}}",
"You don't have enough privileges.": "You don't have enough privileges.",
"This ticket is locked.": "This ticket is locked.",
"This ticket is not editable.": "This ticket is not editable.",
"The ticket doesn't exist.": "The ticket doesn't exist."
}

View File

@ -305,5 +305,11 @@
"The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Valid priorities": "Prioridades válidas: %d",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}",
"You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado"
}
"The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias",
"You cannot assign/remove an alias that you are not assigned to": "No puede asignar/eliminar un alias que no tenga asignado",
"This invoice has a linked vehicle.": "Esta factura tiene un vehiculo vinculado",
"You don't have enough privileges.": "No tienes suficientes permisos.",
"This ticket is locked.": "Este ticket está bloqueado.",
"This ticket is not editable.": "Este ticket no es editable.",
"The ticket doesn't exist.": "No existe el ticket."
}

View File

@ -0,0 +1,55 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.observe('before save', async ctx => {
const changes = ctx.currentInstance || ctx.instance;
await Self.hasGrant(ctx, changes.mailAlias);
});
Self.observe('before delete', async ctx => {
const mailAliasAccount = await Self.findById(ctx.where.id);
await Self.hasGrant(ctx, mailAliasAccount.mailAlias);
});
/**
* Checks if current user has
* grant to add/remove alias
*
* @param {Object} ctx - Request context
* @param {Interger} mailAlias - mailAlias id
* @return {Boolean} True for user with grant
*/
Self.hasGrant = async function(ctx, mailAlias) {
const models = Self.app.models;
const accessToken = {req: {accessToken: ctx.options.accessToken}};
const userId = accessToken.req.accessToken.userId;
const canEditAlias = await models.ACL.checkAccessAcl(accessToken, 'MailAliasAccount', 'canEditAlias', 'WRITE');
if (canEditAlias) return true;
const user = await models.VnUser.findById(userId, {fields: ['hasGrant']});
if (!user.hasGrant)
throw new UserError(`You don't have grant privilege`);
const account = await models.Account.findById(userId, {
fields: ['id'],
include: {
relation: 'aliases',
scope: {
fields: ['mailAlias']
}
}
});
const aliases = account.aliases().map(alias => alias.mailAlias);
const hasAlias = aliases.includes(mailAlias);
if (!hasAlias)
throw new UserError(`You cannot assign/remove an alias that you are not assigned to`);
return true;
};
};

View File

@ -21,11 +21,12 @@ export default class Controller extends Section {
}
onAddClick() {
this.addData = {account: this.$params.id};
this.$.dialog.show();
}
onAddSave() {
return this.$http.post(`VnUsers/${this.$params.id}/addAlias`, this.addData)
return this.$http.post(`MailAliasAccounts`, this.addData)
.then(() => this.refresh())
.then(() => this.vnApp.showSuccess(
this.$t('Subscribed to alias!'))
@ -33,12 +34,11 @@ export default class Controller extends Section {
}
onRemove(row) {
const params = {
mailAlias: row.mailAlias
};
return this.$http.post(`VnUsers/${this.$params.id}/removeAlias`, params)
.then(() => this.refresh())
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
return this.$http.delete(`MailAliasAccounts/${row.id}`)
.then(() => {
this.$.data.splice(this.$.data.indexOf(row), 1);
this.vnApp.showSuccess(this.$t('Unsubscribed from alias!'));
});
}
}

View File

@ -25,9 +25,8 @@ describe('component vnUserAliases', () => {
describe('onAddSave()', () => {
it('should add the new row', () => {
controller.addData = {account: 1};
controller.$params = {id: 1};
$httpBackend.expectPOST('VnUsers/1/addAlias').respond();
$httpBackend.expectPOST('MailAliasAccounts').respond();
$httpBackend.expectGET('MailAliasAccounts').respond('foo');
controller.onAddSave();
$httpBackend.flush();
@ -42,14 +41,12 @@ describe('component vnUserAliases', () => {
{id: 1, alias: 'foo'},
{id: 2, alias: 'bar'}
];
controller.$params = {id: 1};
$httpBackend.expectPOST('VnUsers/1/removeAlias').respond();
$httpBackend.expectGET('MailAliasAccounts').respond(controller.$.data[1]);
$httpBackend.expectDELETE('MailAliasAccounts/1').respond();
controller.onRemove(controller.$.data[0]);
$httpBackend.flush();
expect(controller.$.data).toEqual({id: 2, alias: 'bar'});
expect(controller.$.data).toEqual([{id: 2, alias: 'bar'}]);
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', {
description: 'Change property isEqualizated in all client addresses',
@ -9,6 +11,12 @@ module.exports = function(Self) {
required: true,
description: 'Client id',
http: {source: 'path'}
},
{
arg: 'companyFk',
description: 'The company id',
type: 'number',
required: true
}
],
returns: {
@ -22,18 +30,29 @@ module.exports = function(Self) {
}
});
Self.canBeInvoiced = async(id, options) => {
Self.canBeInvoiced = async(id, companyFk, options) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const client = await models.Client.findById(id, {
fields: ['id', 'isTaxDataChecked', 'hasToInvoice']
fields: ['id', 'isTaxDataChecked', 'hasToInvoice', 'payMethodFk'],
include:
{
relation: 'payMethod',
scope: {
fields: ['code']
}
}
}, myOptions);
const company = await models.Company.findById(companyFk, {fields: ['supplierAccountFk']}, myOptions);
if (client.payMethod().code === 'wireTransfer' && !company.supplierAccountFk)
throw new UserError('The company has not informed the supplier account for bank transfers');
if (client.isTaxDataChecked && client.hasToInvoice)
return true;

View File

@ -7,7 +7,7 @@ module.exports = Self => {
arg: 'id',
type: 'number',
required: true,
description: 'The ticket id',
description: 'The client id',
http: {source: 'path'}
},
{
@ -33,6 +33,12 @@ module.exports = Self => {
Self.sendSms = async(ctx, id, destination, message) => {
const models = Self.app.models;
const sms = await models.Sms.send(ctx, destination, message);
await models.ClientSms.create({
clientFk: id,
smsFk: sms.id
});
return sms;
};
};

View File

@ -4,6 +4,7 @@ const LoopBackContext = require('loopback-context');
describe('client canBeInvoiced()', () => {
const userId = 19;
const clientId = 1101;
const companyId = 442;
const activeCtx = {
accessToken: {userId: userId}
};
@ -23,7 +24,7 @@ describe('client canBeInvoiced()', () => {
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('isTaxDataChecked', false, options);
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, options);
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, companyId, options);
expect(canBeInvoiced).toEqual(false);
@ -43,7 +44,7 @@ describe('client canBeInvoiced()', () => {
const client = await models.Client.findById(clientId, null, options);
await client.updateAttribute('hasToInvoice', false, options);
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, options);
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, companyId, options);
expect(canBeInvoiced).toEqual(false);
@ -60,7 +61,7 @@ describe('client canBeInvoiced()', () => {
try {
const options = {transaction: tx};
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, options);
const canBeInvoiced = await models.Client.canBeInvoiced(clientId, companyId, options);
expect(canBeInvoiced).toEqual(true);

View File

@ -70,11 +70,12 @@ module.exports = Self => {
c.creditInsurance,
d.defaulterSinced,
cn.country,
c.countryFk,
pm.name payMethod
FROM vn.defaulter d
JOIN vn.client c ON c.id = d.clientFk
JOIN vn.country cn ON cn.id = c.countryFk
JOIN vn.payMethod pm ON pm.id = c.payMethodFk
JOIN vn.payMethod pm ON pm.id = c.payMethodFk
LEFT JOIN vn.clientObservation co ON co.clientFk = c.id
LEFT JOIN account.user u ON u.id = c.salesPersonFk
LEFT JOIN account.user uw ON uw.id = co.workerFk

View File

@ -95,6 +95,9 @@
"ClientSample": {
"dataSource": "vn"
},
"ClientSms": {
"dataSource": "vn"
},
"Sms": {
"dataSource": "vn"
},

View File

@ -0,0 +1,26 @@
{
"name": "ClientSms",
"base": "VnModel",
"options": {
"mysql": {
"table": "clientSms"
}
},
"properties": {
"id": {
"type": "number",
"id": true,
"description": "Identifier"
},
"clientFk": {
"type": "number"
}
},
"relations": {
"sms": {
"type": "belongsTo",
"model": "Sms",
"foreignKey": "smsFk"
}
}
}

View File

@ -41,7 +41,18 @@ module.exports = Self => {
});
async function socialNameIsUnique(err, done) {
if (!this.countryFk)
return done();
const filter = {
include: {
relation: 'country',
scope: {
fields: {
isSocialNameUnique: true,
},
},
},
where: {
and: [
{socialName: this.socialName},
@ -50,9 +61,13 @@ module.exports = Self => {
]
}
};
const client = await Self.app.models.Client.findOne(filter);
if (client)
const client = await Self.app.models.Country.findById(this.countryFk, {fields: ['isSocialNameUnique']});
const existingClient = await Self.findOne(filter);
if (existingClient && (existingClient.country().isSocialNameUnique || client.isSocialNameUnique))
err();
done();
}

View File

@ -181,6 +181,11 @@
"model": "Country",
"foreignKey": "countryFk"
},
"isSocialNameUnique": {
"type": "belongsTo",
"model": "Country",
"foreignKey": "countryFk"
},
"contactChannel": {
"type": "belongsTo",
"model": "ContactChannel",

View File

@ -33,7 +33,7 @@
"country": {
"type": "belongsTo",
"model": "Country",
"foreignKey": "country"
"foreignKey": "countryFk"
},
"payMethod": {
"type": "belongsTo",
@ -41,4 +41,4 @@
"foreignKey": "payMethod"
}
}
}
}

View File

@ -60,7 +60,7 @@
<th field="salesPersonFk">
<span translate>Comercial</span>
</th>
<th field="country">
<th field="countryFk">
<span translate>Country</span>
</th>
<th field="payMethod"
@ -132,7 +132,7 @@
</td>
<td>
{{::defaulter.payMethod}}
</td>
</td>
<td>{{::defaulter.amount | currency: 'EUR': 2}}</td>
<td>
<span

View File

@ -31,10 +31,11 @@ export default class Controller extends Section {
valueField: 'id',
}
}, {
field: 'country',
field: 'countryFk',
autocomplete: {
url: 'Countries',
showField: 'country',
valueField: 'country'
valueField: 'id'
}
}, {
field: 'payMethodFk',
@ -167,7 +168,7 @@ export default class Controller extends Section {
case 'amount':
case 'clientFk':
case 'workerFk':
case 'country':
case 'countryFk':
case 'payMethod':
case 'salesPersonFk':
return {[`d.${param}`]: value};

View File

@ -48,4 +48,5 @@ import './notification';
import './unpaid';
import './extended-list';
import './credit-management';
import './sms';

View File

@ -23,6 +23,7 @@
{"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"},
{"state": "client.card.sms", "icon": "sms"},
{
"description": "Credit management",
"icon": "monetization_on",
@ -373,6 +374,12 @@
"component": "vn-client-log",
"description": "Log"
},
{
"url" : "/sms",
"state": "client.card.sms",
"component": "vn-client-sms",
"description": "Sms"
},
{
"url": "/dms",
"state": "client.card.dms",

View File

@ -0,0 +1,40 @@
<vn-crud-model
vn-id="model"
url="ClientSms"
link="{clientFk: $ctrl.$params.id}"
filter="::$ctrl.filter"
data="clientSmsList"
limit="20"
auto-load="true">
</vn-crud-model>
<vn-data-viewer model="model">
<vn-card class="vn-w-md">
<vn-table model="model" auto-load="false">
<vn-thead>
<vn-tr>
<vn-th field="senderFk">Sender</vn-th>
<vn-th field="destination" number>Destination</vn-th>
<vn-th field="message">Message</vn-th>
<vn-th field="status">Status</vn-th>
<vn-th field="created" expand>Created</vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<vn-tr ng-repeat="clientSms in clientSmsList">
<vn-td>
<span class="link" ng-click="workerDescriptor.show($event, clientSms.sms.senderFk)">
{{::clientSms.sms.sender.name}}
</span>
</vn-td>
<vn-td number expand>{{::clientSms.sms.destination}}</vn-td>
<vn-td>{{::clientSms.sms.message}}</vn-td>
<vn-td>{{::clientSms.sms.status}}</vn-td>
<vn-td shrink-datetime>{{::clientSms.sms.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<vn-worker-descriptor-popover
vn-id="worker-descriptor">
</vn-worker-descriptor-popover>

View File

@ -0,0 +1,39 @@
import ngModule from '../module';
import Section from 'salix/components/section';
export default class Controller extends Section {
constructor($element, $) {
super($element, $);
this.filter = {
fields: ['id', 'smsFk'],
include: {
relation: 'sms',
scope: {
fields: [
'senderFk',
'sender',
'destination',
'message',
'statusCode',
'status',
'created'],
include: {
relation: 'sender',
scope: {
fields: ['name']
}
}
}
}
};
}
}
ngModule.vnComponent('vnClientSms', {
template: require('./index.html'),
controller: Controller,
bindings: {
client: '<'
}
});

View File

@ -0,0 +1,2 @@
Sender: Remitente
Number sender: Número remitente

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
require('../methods/invoice-in/filter')(Self);
require('../methods/invoice-in/summary')(Self);
@ -7,4 +9,9 @@ module.exports = Self => {
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
require('../methods/invoice-in/getSerial')(Self);
Self.rewriteDbError(function(err) {
if (err.code === 'ER_ROW_IS_REFERENCED_2' && err.sqlMessage.includes('vehicleInvoiceIn'))
return new UserError(`This invoice has a linked vehicle.`);
return err;
});
};

View File

@ -0,0 +1,37 @@
module.exports = Self => {
Self.remoteMethod('getInventory', {
description: 'Get list of itemShelving to review between two parking code',
accessType: 'WRITE',
accepts: [{
arg: 'parkingFrom',
type: 'string',
required: true,
description: 'Parking code from'
},
{
arg: 'parkingTo',
type: 'string',
required: true,
description: 'Parking code to'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/getInventory`,
verb: 'POST'
}
});
Self.getInventory = async(parkingFrom, parkingTo, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const [result] = await Self.rawSql(`CALL vn.itemShelving_inventory(?, ?)`, [parkingFrom, parkingTo], myOptions);
return result;
};
};

View File

@ -0,0 +1,24 @@
const models = require('vn-loopback/server/server').models;
describe('itemShelving getInventory()', () => {
it('should return a list of itemShelvings', async() => {
const tx = await models.ItemShelving.beginTransaction({});
let response;
try {
const options = {transaction: tx};
await models.ItemShelving.rawSql(`
UPDATE vn.config
SET mainWarehouseFk=1
WHERE id=1
`, null, options);
response = await models.ItemShelving.getInventory('100-01', 'LR-02-3', options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(response.length).toEqual(2);
});
});

View File

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

View File

@ -23,6 +23,12 @@
},
"isChecked": {
"type": "boolean"
},
"visible": {
"type": "number"
},
"userFk": {
"type": "number"
}
},
"relations": {

View File

@ -85,7 +85,7 @@
show-field="id"
value-field="id"
search-function="$ctrl.itemSearchFunc($search)"
on-change="$ctrl.upsertPrice(price, true)"
ng-change="$ctrl.upsertPrice(price, true)"
order="id DESC"
tabindex="1">
<tpl-item>

View File

@ -0,0 +1,27 @@
module.exports = Self => {
Self.remoteMethod('sorted', {
description: 'Sort the vehicles by warehouse',
accessType: 'WRITE',
accepts: [{
arg: 'warehouseFk',
type: 'number'
}],
returns: {
type: ['object'],
root: true
},
http: {
path: `/sorted`,
verb: `POST`
}
});
Self.sorted = async warehouseFk => {
return Self.rawSql(`
SELECT v.id, v.warehouseFk, v.numberPlate, w.name
FROM vehicle v
JOIN warehouse w ON w.id = v.warehouseFk
ORDER BY v.warehouseFk = ? DESC, w.id, v.numberPlate ASC;
`, [warehouseFk]);
};
};

View File

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

View File

@ -23,19 +23,21 @@
</tpl-item>
</vn-autocomplete>
<vn-autocomplete
label="Vehicle"
ng-model="$ctrl.route.vehicleFk"
url="Vehicles"
data="$ctrl.vehicles"
show-field="numberPlate"
value-field="id"
label="Vehicle"
order="false"
vn-name="vehicle">
<tpl-item>{{::numberPlate}} - {{::name}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-date-picker
label="Created"
ng-model="$ctrl.route.created"
vn-name="created">
vn-name="created">
</vn-date-picker>
<vn-autocomplete
ng-model="$ctrl.route.agencyModeFk"

View File

@ -2,6 +2,13 @@ import ngModule from '../module';
import Section from 'salix/components/section';
class Controller extends Section {
$onInit() {
this.$http.post(`Vehicles/sorted`, {warehouseFk: this.vnConfig.warehouseFk})
.then(res => {
this.vehicles = res.data;
});
}
onSubmit() {
this.$.watcher.submit().then(() =>
this.card.reload()

View File

@ -84,14 +84,21 @@
tabindex="-1">
</vn-icon-button>
</vn-td>
<vn-td>
<vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority"
rule="Ticket"
class="dense">
</vn-input-number>
</vn-td>
<vn-td-editable number>
<text>
<strong>{{ticket.priority}}</strong>
</text>
<field>
<vn-input-number
rule="Ticket"
ng-model="ticket.priority"
on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
step="1"
class="dense"
vn-focus>
</vn-input-number>
</field>
</vn-td-editable>
<vn-td expand title="{{::ticket.street}}">{{::ticket.street}}</vn-td>
<vn-td
expand

View File

@ -68,9 +68,9 @@ module.exports = Self => {
};
const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null;
const countryCode = this.nif.toLowerCase().substring(0, 2);
const countryCode = this.nif?.toLowerCase().substring(0, 2);
if (!this.nif || !validateTin(this.nif, code) || (this.isVies && countryCode == code))
if (!validateTin(this.nif, code) || (this.isVies && countryCode == code))
err();
done();
}
@ -122,7 +122,7 @@ module.exports = Self => {
});
async function hasSupplierSameName(err, done) {
if (!this.name || !this.countryFk) done();
if (!this.name || !this.countryFk) return done();
const supplier = await Self.app.models.Supplier.findOne(
{
where: {

View File

@ -1,4 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
@ -47,9 +46,7 @@ module.exports = Self => {
}
try {
const isEditable = await models.Ticket.isEditable(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
await models.Ticket.isEditableOrThrow(ctx, id, myOptions);
const item = await models.Item.findById(itemId, null, myOptions);
const ticket = await models.Ticket.findById(id, {

View File

@ -1,4 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
const loggable = require('vn-loopback/util/log');
module.exports = Self => {
@ -116,10 +115,7 @@ module.exports = Self => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const $t = ctx.req.__; // $translate
const isEditable = await models.Ticket.isEditable(ctx, args.id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
await models.Ticket.isEditableOrThrow(ctx, args.id, myOptions);
const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE');
if (!editZone) {

View File

@ -0,0 +1,55 @@
module.exports = Self => {
Self.remoteMethod('docuwareDownload', {
description: 'Download a ticket delivery note document',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: `/:id/docuwareDownload`,
verb: 'GET'
}
});
Self.docuwareDownload = async id => {
const filter = {
condition: [
{
DBName: docuwareInfo.findById,
Value: [id]
},
{
DBName: 'ESTADO',
Value: ['Firmado']
}
],
sortOrder: [
{
Field: 'FILENAME',
Direction: 'Desc'
}
]
};
return Self.app.models.Docuware.download(id, 'deliveryNote', filter);
};
};

View File

@ -20,41 +20,12 @@ module.exports = Self => {
});
Self.isEditable = async(ctx, id, options) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const state = await models.TicketState.findOne({
where: {ticketFk: id}
}, myOptions);
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
const alertLevel = state ? state.alertLevel : null;
const ticket = await models.Ticket.findById(id, {
fields: ['clientFk'],
include: [{
relation: 'client',
scope: {
include: {
relation: 'type'
}
}
}]
}, myOptions);
const isLocked = await models.Ticket.isLocked(id, myOptions);
const isWeekly = await models.TicketWeekly.findOne({where: {ticketFk: id}}, myOptions);
const alertLevelGreaterThanZero = (alertLevel && alertLevel > 0);
const isNormalClient = ticket && ticket.client().type().code == 'normal';
const isEditable = !(alertLevelGreaterThanZero && isNormalClient);
if (ticket && (isEditable || isRoleAdvanced) && !isLocked && !isWeekly)
return true;
return false;
try {
await Self.isEditableOrThrow(ctx, id, options);
} catch (e) {
if (e.name === 'ForbiddenError') return false;
throw e;
}
return true;
};
};

View File

@ -0,0 +1,49 @@
const ForbiddenError = require('vn-loopback/util/forbiddenError');
module.exports = Self => {
Self.isEditableOrThrow = async(ctx, id, options) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const state = await models.TicketState.findOne({
where: {ticketFk: id}
}, myOptions);
const isRoleAdvanced = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'isRoleAdvanced', '*');
const canEditWeeklyTicket = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'canEditWeekly', 'WRITE');
const alertLevel = state ? state.alertLevel : null;
const ticket = await models.Ticket.findById(id, {
fields: ['clientFk'],
include: [{
relation: 'client',
scope: {
include: {
relation: 'type'
}
}
}]
}, myOptions);
const isLocked = await models.Ticket.isLocked(id, myOptions);
const isWeekly = await models.TicketWeekly.findOne({where: {ticketFk: id}}, myOptions);
const alertLevelGreaterThanZero = (alertLevel && alertLevel > 0);
const isNormalClient = ticket && ticket.client().type().code == 'normal';
const isEditable = !(alertLevelGreaterThanZero && isNormalClient);
if (!ticket)
throw new ForbiddenError(`The ticket doesn't exist.`);
if (!isEditable && !isRoleAdvanced)
throw new ForbiddenError(`This ticket is not editable.`);
if (isLocked)
throw new ForbiddenError(`This ticket is locked.`);
if (isWeekly && !canEditWeeklyTicket)
throw new ForbiddenError(`You don't have enough privileges.`);
};
};

View File

@ -14,7 +14,7 @@ module.exports = function(Self) {
{
arg: 'companyFk',
description: 'The company id',
type: 'string',
type: 'number',
required: true
},
{
@ -67,7 +67,7 @@ module.exports = function(Self) {
const [firstTicket] = tickets;
const clientId = firstTicket.clientFk;
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions);
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, companyFk, myOptions);
if (!clientCanBeInvoiced)
throw new UserError(`This client can't be invoiced`);

View File

@ -1,5 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('priceDifference', {
description: 'Returns sales with price difference if the ticket is editable',
@ -72,10 +70,7 @@ module.exports = Self => {
}
try {
const isEditable = await Self.isEditable(ctx, args.id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
await Self.isEditableOrThrow(ctx, args.id, myOptions);
const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE');
if (!editZone) {

View File

@ -1,4 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('recalculateComponents', {
description: 'Calculates the price of a sale and its components',
@ -33,10 +32,7 @@ module.exports = Self => {
}
try {
const isEditable = await Self.isEditable(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The current ticket can't be modified`);
await Self.isEditableOrThrow(ctx, id, myOptions);
const recalculation = await Self.rawSql('CALL vn.ticket_recalcComponents(?, NULL)', [id], myOptions);

View File

@ -39,10 +39,7 @@ module.exports = Self => {
const ticketToDelete = await models.Ticket.findById(id, {fields: ['isDeleted']}, myOptions);
if (ticketToDelete.isDeleted) return false;
const isEditable = await Self.isEditable(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
await Self.isEditableOrThrow(ctx, id, myOptions);
// Check if ticket has refunds
const ticketRefunds = await models.TicketRefund.find({

View File

@ -97,6 +97,6 @@ describe('ticket addSale()', () => {
error = e;
}
expect(error.message).toEqual(`The sales of this ticket can't be modified`);
expect(error.message).toEqual(`This ticket is locked.`);
});
});

View File

@ -141,6 +141,7 @@ describe('ticket filter()', () => {
});
it('should return the tickets that are not pending', async() => {
pending('#6010 test intermitente');
const tx = await models.Ticket.beginTransaction({});
try {

View File

@ -1,156 +1,37 @@
const models = require('vn-loopback/server/server').models;
describe('ticket isEditable()', () => {
it('should return false if the given ticket does not exist', async() => {
describe('isEditable()', () => {
it('should return false if It is able to edit', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 9}}
};
result = await models.Ticket.isEditable(ctx, 9999, options);
const ctx = {req: {accessToken: {userId: 35}}};
result = await models.Ticket.isEditable(ctx, 5, options);
await tx.rollback();
} catch (e) {
} catch (error) {
await tx.rollback();
throw e;
}
expect(result).toEqual(false);
expect(result).toBeFalse();
});
it(`should return false if the given ticket isn't invoiced but isDeleted`, async() => {
it('should return true if It is able to edit', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const deletedTicket = await models.Ticket.findOne({
where: {
invoiceOut: null,
isDeleted: true
},
fields: ['id']
});
const ctx = {
req: {accessToken: {userId: 9}}
};
result = await models.Ticket.isEditable(ctx, deletedTicket.id, options);
const ctx = {req: {accessToken: {userId: 35}}};
result = await models.Ticket.isEditable(ctx, 15, options);
await tx.rollback();
} catch (e) {
} catch (error) {
await tx.rollback();
throw e;
}
expect(result).toEqual(false);
});
it('should return true if the given ticket is editable', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 9}}
};
result = await models.Ticket.isEditable(ctx, 16, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toEqual(true);
});
it('should not be able to edit a deleted or invoiced ticket even for salesAssistant', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 21}}
};
result = await models.Ticket.isEditable(ctx, 19, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toEqual(false);
});
it('should not be able to edit a deleted or invoiced ticket even for productionBoss', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 50}}
};
result = await models.Ticket.isEditable(ctx, 19, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toEqual(false);
});
it('should not be able to edit a deleted or invoiced ticket even for salesPerson', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 18}}
};
result = await models.Ticket.isEditable(ctx, 19, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toEqual(false);
});
it('should not be able to edit if is a ticket weekly', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}};
const result = await models.Ticket.isEditable(ctx, 15, options);
expect(result).toEqual(false);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toBeTrue();
});
});

View File

@ -0,0 +1,96 @@
const models = require('vn-loopback/server/server').models;
describe('ticket isEditableOrThrow()', () => {
it('should throw an error as the ticket does not exist', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 9}}
};
await models.Ticket.isEditableOrThrow(ctx, 9999, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`The ticket doesn't exist.`);
});
it('should throw an error as this ticket is not editable', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 1}}
};
await models.Ticket.isEditableOrThrow(ctx, 8, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`This ticket is not editable.`);
});
it('should throw an error as this ticket is locked.', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {
req: {accessToken: {userId: 18}}
};
await models.Ticket.isEditableOrThrow(ctx, 19, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`This ticket is locked.`);
});
it('should throw an error as you do not have enough privileges.', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 1}}};
await models.Ticket.isEditableOrThrow(ctx, 15, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`You don't have enough privileges.`);
});
it('should return undefined if It can be edited', async() => {
const tx = await models.Ticket.beginTransaction({});
let result;
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 35}}};
result = await models.Ticket.isEditableOrThrow(ctx, 15, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(result).toBeUndefined();
});
});

View File

@ -1,5 +1,6 @@
const models = require('vn-loopback/server/server').models;
const UserError = require('vn-loopback/util/user-error');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('sale priceDifference()', () => {
it('should return ticket price differences', async() => {
@ -59,7 +60,7 @@ describe('sale priceDifference()', () => {
await tx.rollback();
}
expect(error).toEqual(new UserError(`The sales of this ticket can't be modified`));
expect(error).toEqual(new ForbiddenError(`This ticket is not editable.`));
});
it('should return ticket movable', async() => {

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models;
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('ticket recalculateComponents()', () => {
const ticketId = 11;
@ -38,6 +39,6 @@ describe('ticket recalculateComponents()', () => {
error = e;
}
expect(error).toEqual(new Error(`The current ticket can't be modified`));
expect(error).toEqual(new ForbiddenError(`This ticket is locked.`));
});
});

View File

@ -0,0 +1,49 @@
const models = require('vn-loopback/server/server').models;
describe('Ticket transferClient()', () => {
const userId = 9;
const activeCtx = {
accessToken: {userId: userId},
};
const ctx = {req: activeCtx};
it('should throw an error as the ticket is not editable', async() => {
const tx = await models.Ticket.beginTransaction({});
let error;
try {
const options = {transaction: tx};
const ticketId = 4;
const clientId = 1;
await models.Ticket.transferClient(ctx, ticketId, clientId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual(`This ticket is locked.`);
});
it('should be assigned a different clientFk', async() => {
const tx = await models.Ticket.beginTransaction({});
let updatedTicket;
const ticketId = 10;
const clientId = 1;
try {
const options = {transaction: tx};
await models.Ticket.transferClient(ctx, ticketId, clientId, options);
updatedTicket = await models.Ticket.findById(ticketId, {fields: ['clientFk']}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
expect(updatedTicket.clientFk).toEqual(clientId);
});
});

View File

@ -33,7 +33,7 @@ describe('sale transferSales()', () => {
error = e;
}
expect(error.message).toEqual(`The sales of this ticket can't be modified`);
expect(error.message).toEqual(`This ticket is not editable.`);
});
it('should throw an error if the receiving ticket is not editable', async() => {

View File

@ -0,0 +1,45 @@
module.exports = Self => {
Self.remoteMethodCtx('transferClient', {
description: 'Transfering ticket to another client',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'the ticket id',
http: {source: 'path'}
},
{
arg: 'clientFk',
type: 'number',
required: true,
},
],
http: {
path: `/:id/transferClient`,
verb: 'PATCH'
}
});
Self.transferClient = async(ctx, id, clientFk, options) => {
const models = Self.app.models;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
await Self.isEditableOrThrow(ctx, id, myOptions);
const ticket = await models.Ticket.findById(
id,
{fields: ['id', 'shipped', 'clientFk', 'addressFk']},
myOptions
);
const client = await models.Client.findById(clientFk, {fields: ['id', 'defaultAddressFk']}, myOptions);
await ticket.updateAttributes({
clientFk,
addressFk: client.defaultAddressFk,
});
};
};

View File

@ -1,4 +1,4 @@
let UserError = require('vn-loopback/util/user-error');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('transferSales', {
@ -48,9 +48,7 @@ module.exports = Self => {
}
try {
const isEditable = await models.Ticket.isEditable(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
await models.Ticket.isEditableOrThrow(ctx, id, myOptions);
if (ticketId) {
const isReceiverEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions);

View File

@ -6,6 +6,7 @@ module.exports = function(Self) {
require('../methods/ticket/componentUpdate')(Self);
require('../methods/ticket/new')(Self);
require('../methods/ticket/isEditable')(Self);
require('../methods/ticket/isEditableOrThrow')(Self);
require('../methods/ticket/setDeleted')(Self);
require('../methods/ticket/restore')(Self);
require('../methods/ticket/getSales')(Self);
@ -19,6 +20,7 @@ module.exports = function(Self) {
require('../methods/ticket/uploadFile')(Self);
require('../methods/ticket/addSale')(Self);
require('../methods/ticket/transferSales')(Self);
require('../methods/ticket/transferClient')(Self);
require('../methods/ticket/recalculateComponents')(Self);
require('../methods/ticket/sendSms')(Self);
require('../methods/ticket/isLocked')(Self);

View File

@ -36,13 +36,12 @@
translate>
as PDF without prices
</vn-item>
<a class="vn-item"
<vn-item
ng-if="$ctrl.hasDocuwareFile"
href='api/Docuwares/{{$ctrl.ticket.id}}/download/deliveryNote?access_token={{$ctrl.vnToken.token}}'
target="_blank"
ng-click="$ctrl.docuwareDownload()"
translate>
as PDF signed
</a>
</vn-item>
<vn-item
ng-click="$ctrl.showCsvDeliveryNote()"
translate>

View File

@ -96,20 +96,14 @@ class Controller extends Section {
}
transferClient() {
this.$http.get(`Clients/${this.ticket.client.id}`).then(client => {
const ticket = this.ticket;
const ticket = this.ticket;
const clientFk = ticket.client.id;
const params =
{
clientFk: client.data.id,
addressFk: client.data.defaultAddressFk,
};
this.$http.patch(`Tickets/${ticket.id}`, params).then(() => {
this.$http.patch(`Tickets/${ticket.id}/transferClient`, {clientFk})
.then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.reload();
});
});
}
isTicketEditable() {
@ -327,6 +321,10 @@ class Controller extends Section {
});
}
docuwareDownload() {
this.vnFile.download(`api/Ticket/${this.ticket.id}/docuwareDownload`);
}
setTicketWeight(weight) {
return this.$http.patch(`Tickets/${this.ticket.id}`, {weight})
.then(() => {

View File

@ -326,17 +326,4 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('transferClient()', () => {
it(`should perform two queries, a get to obtain the clientData and a patch to update the ticket`, () => {
const client =
{
clientFk: 1101,
addressFk: 1,
};
$httpBackend.expect('GET', `Clients/${ticket.client.id}`).respond(client);
$httpBackend.expect('PATCH', `Tickets/${ticket.id}`).respond();
controller.transferClient();
});
});
});

View File

@ -38,10 +38,12 @@ class Controller extends SearchPanel {
applyFilters(param) {
if (typeof this.filter.scopeDays === 'number') {
const shippedFrom = Date.vnNew();
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(shippedFrom.getTime());
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});

View File

@ -0,0 +1,45 @@
module.exports = Self => {
Self.remoteMethod('docuwareDownload', {
description: 'Download a worker document',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The document id',
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: `/:id/docuwareDownload`,
verb: 'GET'
}
});
Self.docuwareDownload = async id => {
const filter = {
condition: [
{
DBName: 'FILENAME',
Value: [id]
}
]
};
return Self.app.models.Docuware.download(id, 'hr', filter);
};
};

View File

@ -5,6 +5,12 @@ module.exports = Self => {
description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The worker id',
http: {source: 'path'}
},
{
arg: 'filter',
type: 'Object',
@ -17,16 +23,17 @@ module.exports = Self => {
root: true
},
http: {
path: `/filter`,
path: `/:id/filter`,
verb: 'GET'
}
});
Self.filter = async(ctx, filter) => {
Self.filter = async(ctx, id, filter) => {
const conn = Self.dataSource.connector;
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const account = await Self.app.models.VnUser.findById(userId);
const account = await models.VnUser.findById(userId);
const stmt = new ParameterizedSQL(
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created, d.hardCopyNumber, d.hasFile
FROM workerDocument wd
@ -47,7 +54,38 @@ module.exports = Self => {
}]
}, oldWhere]};
stmt.merge(conn.makeSuffix(filter));
const workerDms = await conn.executeStmt(stmt);
return await conn.executeStmt(stmt);
// Get docuware info
const docuware = await models.Docuware.findOne({
fields: ['dmsTypeFk'],
where: {code: 'hr', action: 'find'}
});
const docuwareDmsType = docuware.dmsTypeFk;
let workerDocuware = [];
if (!docuwareDmsType || (docuwareDmsType && await models.DmsType.hasReadRole(ctx, docuwareDmsType))) {
const worker = await models.Worker.findById(id, {fields: ['fi', 'firstName', 'lastName']});
const docuwareParse = {
'Filename': 'dmsFk',
'Tipo Documento': 'description',
'Stored on': 'created',
'Document ID': 'id'
};
workerDocuware =
await models.Docuware.getById('hr', worker.lastName + worker.firstName, docuwareParse) ?? [];
for (document of workerDocuware) {
const defaultData = {
file: 'dw' + document.id + '.png',
isDocuware: true,
hardCopyNumber: null,
hasFile: false,
reference: worker.fi,
dmsFk: 'DW' + document.id
};
document = Object.assign(document, defaultData);
}
}
return workerDms.concat(workerDocuware);
};
};

View File

@ -2,6 +2,7 @@ module.exports = Self => {
require('../methods/worker-dms/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/filter')(Self);
require('../methods/worker-dms/docuwareDownload')(Self);
Self.isMine = async function(ctx, dmsId) {
const myUserId = ctx.req.accessToken.userId;

View File

@ -3,7 +3,7 @@
data="absenceTypes"
auto-load="true">
</vn-crud-model>
<div ng-if="$ctrl.worker.hasWorkCenter">
<div ng-if="$ctrl.card.hasWorkCenter">
<div class="vn-w-lg">
<vn-card class="vn-pa-sm calendars">
<vn-icon ng-if="::$ctrl.isSubordinate" icon="info" color-marginal
@ -23,7 +23,7 @@
</div>
</div>
<div
ng-if="!$ctrl.worker.hasWorkCenter"
ng-if="!$ctrl.card.hasWorkCenter"
class="bg-title"
translate>
Autonomous worker
@ -73,41 +73,39 @@
value-field="businessFk"
order="businessFk DESC"
limit="5">
<tpl-item>
<div>#{{businessFk}}</div>
<div class="text-caption text-secondary">
{{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}}
</div>
</tpl-item>
</vn-autocomplete>
</div>
<div name="absenceTypes" class="input vn-py-md" style="overflow: hidden;">
<vn-chip ng-repeat="absenceType in absenceTypes" ng-class="::{'selectable': $ctrl.isSubordinate}"
ng-click="$ctrl.pick(absenceType)">
<vn-avatar
ng-style="{backgroundColor: absenceType.rgb}">
<vn-icon icon="check" ng-if="absenceType.id == $ctrl.absenceType.id"></vn-icon>
</vn-avatar>
{{absenceType.name}}
</vn-chip>
</div>
<div class="vn-py-md">
<vn-chip>
<vn-avatar class="festive">
</vn-avatar>
<span translate>Festive</span>
</vn-chip>
<vn-chip>
<vn-avatar class="today">
</vn-avatar>
<span translate>Current day</span>
</vn-chip>
</div>
<tpl-item>
<div>#{{businessFk}}</div>
<div class="text-caption text-secondary">
{{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}}
</div>
</tpl-item>
</vn-autocomplete>
</div>
<div name="absenceTypes" class="input vn-py-md" style="overflow: hidden;">
<vn-chip ng-repeat="absenceType in absenceTypes" ng-class="::{'selectable': $ctrl.isSubordinate}" ng-click="$ctrl.pick(absenceType)">
<vn-avatar ng-style="{backgroundColor: absenceType.rgb}">
<vn-icon icon="check" ng-if="absenceType.id == $ctrl.absenceType.id"></vn-icon>
</vn-avatar>
{{absenceType.name}}
</vn-chip>
</div>
<div class="vn-py-md">
<vn-chip>
<vn-avatar class="festive">
</vn-avatar>
<span translate>Festive</span>
</vn-chip>
<vn-chip>
<vn-avatar class="today">
</vn-avatar>
<span translate>Current day</span>
</vn-chip>
</div>
</div>
</vn-side-menu>
<vn-confirm
vn-id="confirm"
message="This item will be deleted"
question="Are you sure you want to continue?">
</vn-confirm>

View File

@ -31,6 +31,8 @@ class Controller extends Section {
}
set businessId(value) {
if (!this.card.hasWorkCenter) return;
this._businessId = value;
if (value) {
this.refresh()
@ -64,7 +66,7 @@ class Controller extends Section {
set worker(value) {
this._worker = value;
if (value && value.hasWorkCenter) {
if (value) {
this.getIsSubordinate();
this.getActiveContract();
}
@ -293,5 +295,8 @@ ngModule.vnComponent('vnWorkerCalendar', {
controller: Controller,
bindings: {
worker: '<'
},
require: {
card: '^vnWorkerCard'
}
});

View File

@ -20,6 +20,9 @@ describe('Worker', () => {
controller.absenceType = {id: 1, name: 'Holiday', code: 'holiday', rgb: 'red'};
controller.$params.id = 1106;
controller._worker = {id: 1106};
controller.card = {
hasWorkCenter: true
};
}));
describe('year() getter', () => {
@ -74,7 +77,7 @@ describe('Worker', () => {
let yesterday = new Date(today.getTime());
yesterday.setDate(yesterday.getDate() - 1);
controller.worker = {id: 1107, hasWorkCenter: true};
controller.worker = {id: 1107};
expect(controller.getIsSubordinate).toHaveBeenCalledWith();
expect(controller.getActiveContract).toHaveBeenCalledWith();

View File

@ -3,7 +3,7 @@ import ModuleCard from 'salix/components/module-card';
class Controller extends ModuleCard {
reload() {
let filter = {
const filter = {
include: [
{
relation: 'user',
@ -32,13 +32,12 @@ class Controller extends ModuleCard {
]
};
this.$http.get(`Workers/${this.$params.id}`, {filter})
.then(res => this.worker = res.data)
.then(() =>
this.$http.get(`Workers/${this.$params.id}/activeContract`)
.then(res => {
if (res.data) this.worker.hasWorkCenter = res.data.workCenterFk;
}));
return Promise.all([
this.$http.get(`Workers/${this.$params.id}`, {filter})
.then(res => this.worker = res.data),
this.$http.get(`Workers/${this.$params.id}/activeContract`)
.then(res => this.hasWorkCenter = res.data.workCenterFk)
]);
}
}

View File

@ -5,8 +5,8 @@
<slot-before>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('user', '520x520', $ctrl.worker.id)}}"
zoom-image="{{$root.imagePath('user', '1600x1600', $ctrl.worker.id)}}"
ng-src="{{$root.imagePath('user', '520x520', $ctrl.worker.id)}}"
zoom-image="{{$root.imagePath('user', '1600x1600', $ctrl.worker.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('user', $ctrl.worker.id)"
icon="edit"
@ -15,39 +15,32 @@
</div>
</slot-before>
<slot-menu>
<vn-item
ng-click="$ctrl.handleExcluded()"
translate
ng-if="!$ctrl.excluded">
Click to exclude the user from getting disabled
</vn-item>
<vn-item
ng-click="$ctrl.handleExcluded()"
translate
ng-if="$ctrl.excluded">
Click to allow the user to be disabled
</vn-item>
<vn-item ng-click="$ctrl.handleExcluded()" translate>
{{$ctrl.workerExcluded
? 'Click to allow the user to be disabled'
: 'Click to exclude the user from getting disabled'}}
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
<vn-label-value
label="User"
label="User"
value="{{$ctrl.worker.user.name}}">
</vn-label-value>
<vn-label-value
label="Email"
label="Email"
value="{{$ctrl.worker.user.emailUser.email}}">
</vn-label-value>
<vn-label-value
label="Department"
label="Department"
value="{{$ctrl.worker.department.department.name}}">
</vn-label-value>
<vn-label-value
label="Phone"
label="Phone"
value="{{$ctrl.worker.phone}}">
</vn-label-value>
<vn-label-value
label="Extension"
label="Extension"
value="{{$ctrl.worker.sip.extension}}">
</vn-label-value>
</div>
@ -84,7 +77,7 @@
</vn-popup>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>
</vn-upload-photo>

View File

@ -18,28 +18,19 @@ class Controller extends Descriptor {
this.getIsExcluded();
}
get excluded() {
return this.entity.excluded;
}
set excluded(value) {
this.entity.excluded = value;
}
getIsExcluded() {
this.$http.get(`workerDisableExcludeds/${this.entity.id}/exists`).then(data => {
this.excluded = data.data.exists;
this.$http.get(`WorkerDisableExcludeds/${this.entity.id}/exists`).then(data => {
this.workerExcluded = data.data.exists;
});
}
handleExcluded() {
if (this.excluded) {
this.$http.delete(`workerDisableExcludeds/${this.entity.id}`);
this.excluded = false;
} else {
this.$http.post(`workerDisableExcludeds`, {workerFk: this.entity.id, dated: new Date});
this.excluded = true;
}
if (this.workerExcluded)
this.$http.delete(`WorkerDisableExcludeds/${this.entity.id}`);
else
this.$http.post(`WorkerDisableExcludeds`, {workerFk: this.entity.id, dated: new Date});
this.workerExcluded = !this.workerExcluded;
}
loadData() {

View File

@ -1,6 +1,6 @@
<vn-crud-model
vn-id="model"
url="WorkerDms/filter"
url="WorkerDms/{{$ctrl.$params.id}}/filter"
link="{worker: $ctrl.$params.id}"
limit="20"
data="$ctrl.workerDms"
@ -46,14 +46,14 @@
</span>
</vn-td>
<vn-td shrink>
<vn-check
<vn-check
ng-model="document.hasFile"
disabled="true">
</vn-check>
</vn-td>
<vn-td shrink>
<span title="{{'Download file' | translate}}" class="link"
ng-click="$ctrl.downloadFile(document.dmsFk)">
ng-click="$ctrl.downloadFile(document.dmsFk, document.isDocuware)">
{{::document.file}}
</span>
</vn-td>
@ -63,16 +63,14 @@
<vn-td shrink>
<vn-icon-button title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile(document.dmsFk)">
ng-click="$ctrl.downloadFile(document.dmsFk, document.isDocuware)">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-td expand ng-if="::!document.isDocuware">
<vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})"
icon="edit"
title="{{'Edit file' | translate}}">
</vn-icon-button>
</vn-td>
<vn-td shrink>
<vn-icon-button
icon="delete"
ng-click="confirm.show($index)"
@ -80,6 +78,14 @@
tabindex="-1">
</vn-icon-button>
</vn-td>
<vn-td expand ng-if="::document.isDocuware">
<vn-icon-button
icon="open_in_new"
ng-click="$ctrl.openDocuware()"
title="{{'Open in docuware' | translate}}"
tabindex="-1">
</vn-icon-button>
</vn-td>
</vn-tr>
</vn-tbody>
</vn-table>
@ -91,9 +97,9 @@
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>
<vn-confirm
<vn-confirm
vn-id="confirm"
message="This file will be deleted"
question="Are you sure you want to continue?"
on-accept="$ctrl.deleteDms($data)">
</vn-confirm>
</vn-confirm>

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