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 ### 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 ## [2328.01] - 2023-07-13
### Added ### Added

View File

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

View File

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

View File

@ -1,57 +1,15 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const axios = require('axios');
describe('docuware download()', () => { describe('docuware download()', () => {
const ticketId = 1; const ticketId = 1;
const userId = 9;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const docuwareModel = models.Docuware; const docuwareModel = models.Docuware;
const fileCabinetName = 'deliveryNote'; 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() => { it('should return false if there are no documents', async() => {
const response = { spyOn(docuwareModel, 'get').and.returnValue((new Promise(resolve => resolve({Items: []}))));
data: {
Items: []
}
};
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);
});
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);
expect(result).toEqual(false); expect(result).toEqual(false);
}); });
@ -59,23 +17,21 @@ describe('docuware download()', () => {
it('should return the document data', async() => { it('should return the document data', async() => {
const docuwareId = 1; const docuwareId = 1;
const response = { const response = {
data: { Items: [
Items: [ {
{ Id: docuwareId,
Id: docuwareId, Fields: [
Fields: [ {
{ FieldName: 'ESTADO',
FieldName: 'ESTADO', Item: 'Firmado'
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); 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(docuwareModel, 'checkFile').and.returnValue({});
spyOn(axios, 'get').and.returnValue(new stream.PassThrough({objectMode: true})); 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[1]).toEqual('application/pdf');
expect(result[2]).toEqual(`filename="${ticketId}.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": { "expired": {
"type": "date" "type": "date"
},
"supplierAccountFk": {
"type": "number"
} }
}, },
"scope": { "scope": {

View File

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

View File

@ -28,5 +28,12 @@
"findById": { "findById": {
"type": "string" "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/privileges')(Self);
require('../methods/vn-user/validate-auth')(Self); require('../methods/vn-user/validate-auth')(Self);
require('../methods/vn-user/renew-token')(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'); 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`.`roadmap` MODIFY COLUMN etd datetime NOT NULL;
ALTER TABLE `vn`.`expeditionTruck` COMMENT='Distintas paradas que hacen los trocales'; 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`) INSERT INTO `salix`.`AccessToken` (`id`, `ttl`, `created`, `userId`)
VALUES VALUES
('DEFAULT_TOKEN', '1209600', util.VN_CURDATE(), 66); ('DEFAULT_TOKEN', '1209600', CURDATE(), 66);
INSERT INTO `salix`.`printConfig` (`id`, `itRecipient`, `incidencesEmail`) INSERT INTO `salix`.`printConfig` (`id`, `itRecipient`, `incidencesEmail`)
VALUES VALUES
@ -2953,3 +2953,8 @@ INSERT INTO `vn`.`invoiceInSerial` (`code`, `description`, `cplusTerIdNifFk`, `t
('E', 'Midgard', 1, 'CEE'), ('E', 'Midgard', 1, 'CEE'),
('R', 'Jotunheim', 1, 'NATIONAL'), ('R', 'Jotunheim', 1, 'NATIONAL'),
('W', 'Vanaheim', 1, 'WORLD'); ('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; LEAVE cur1Loop;
END IF; END IF;
CALL zone_getLeaves2(vZoneFk, NULL, NULL); CALL zone_getLeaves(vZoneFk, NULL, NULL, TRUE);
myLoop: LOOP myLoop: LOOP
SET vGeoFk = NULL; SET vGeoFk = NULL;
@ -77844,7 +77844,7 @@ BEGIN
LEAVE myLoop; LEAVE myLoop;
END IF; END IF;
CALL zone_getLeaves2(vZoneFk, vGeoFk, NULL); CALL zone_getLeaves(vZoneFk, vGeoFk, NULL, TRUE);
UPDATE tmp.zoneNodes UPDATE tmp.zoneNodes
SET isChecked = TRUE SET isChecked = TRUE
WHERE geoFk = vGeoFk; WHERE geoFk = vGeoFk;
@ -78130,55 +78130,58 @@ DELIMITER ;
/*!50003 SET @saved_sql_mode = @@sql_mode */ ; /*!50003 SET @saved_sql_mode = @@sql_mode */ ;
/*!50003 SET sql_mode = 'IGNORE_SPACE,NO_ENGINE_SUBSTITUTION' */ ; /*!50003 SET sql_mode = 'IGNORE_SPACE,NO_ENGINE_SUBSTITUTION' */ ;
DELIMITER ;; DELIMITER ;;
CREATE DEFINER=`root`@`localhost` PROCEDURE `zone_getLeaves`(vSelf INT, vParentFk INT, vSearch VARCHAR(255)) CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`zone_getLeaves`(
BEGIN vSelf INT,
vParentFk INT,
vSearch VARCHAR(255),
vHasInsert BOOL
)
BEGIN
/** /**
* Devuelve las ubicaciones incluidas en la ruta y que sean hijos de parentFk. * Devuelve las ubicaciones incluidas en la ruta y que sean hijos de parentFk.
* @param vSelf Id de la zona * @param vSelf Id de la zona
* @param vParentFk Id del geo a calcular * @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 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 OR REPLACE TEMPORARY TABLE tNodes
CREATE TEMPORARY TABLE tNodes
(UNIQUE (id)) (UNIQUE (id))
ENGINE = MEMORY ENGINE = MEMORY
SELECT id SELECT id
FROM zoneGeo FROM zoneGeo
LIMIT 0; LIMIT 0;
IF vIsSearch THEN IF vIsSearch THEN
SET vIsNumber = vSearch REGEXP '^[0-9]+$'; SET vIsNumber = vSearch REGEXP '^[0-9]+$';
INSERT INTO tNodes INSERT INTO tNodes
SELECT id SELECT id
FROM zoneGeo FROM zoneGeo
WHERE (vIsNumber AND `name` = vSearch) WHERE (vIsNumber AND `name` = vSearch)
OR (!vIsNumber AND `name` LIKE CONCAT('%', vSearch, '%')) OR (!vIsNumber AND `name` LIKE CONCAT('%', vSearch, '%'))
LIMIT 1000; LIMIT 1000;
ELSEIF vParentFk IS NULL THEN ELSEIF vParentFk IS NULL THEN
INSERT INTO tNodes INSERT INTO tNodes
SELECT geoFk SELECT geoFk
FROM zoneIncluded FROM zoneIncluded
WHERE zoneFk = vSelf; WHERE zoneFk = vSelf;
END IF; END IF;
IF vParentFk IS NULL THEN IF vParentFk IS NULL THEN
DROP TEMPORARY TABLE IF EXISTS tChilds; CREATE OR REPLACE TEMPORARY TABLE tChilds
CREATE TEMPORARY TABLE tChilds (INDEX(id))
ENGINE = MEMORY ENGINE = MEMORY
SELECT id SELECT id FROM tNodes;
FROM tNodes;
DROP TEMPORARY TABLE IF EXISTS tParents; CREATE OR REPLACE TEMPORARY TABLE tParents
CREATE TEMPORARY TABLE tParents (INDEX(id))
ENGINE = MEMORY ENGINE = MEMORY
SELECT id SELECT id FROM zoneGeo LIMIT 0;
FROM zoneGeo
LIMIT 0;
myLoop: LOOP myLoop: LOOP
DELETE FROM tParents; DELETE FROM tParents;
@ -78186,43 +78189,67 @@ BEGIN
SELECT parentFk id SELECT parentFk id
FROM zoneGeo g FROM zoneGeo g
JOIN tChilds c ON c.id = g.id JOIN tChilds c ON c.id = g.id
WHERE g.parentFk IS NOT NULL; WHERE g.parentFk IS NOT NULL;
INSERT IGNORE INTO tNodes INSERT IGNORE INTO tNodes
SELECT id SELECT id FROM tParents;
FROM tParents;
IF NOT ROW_COUNT() THEN
IF ROW_COUNT() = 0 THEN
LEAVE myLoop; LEAVE myLoop;
END IF; END IF;
DELETE FROM tChilds; DELETE FROM tChilds;
INSERT INTO tChilds INSERT INTO tChilds
SELECT id SELECT id FROM tParents;
FROM tParents;
END LOOP; END LOOP;
DROP TEMPORARY TABLE tChilds, tParents; DROP TEMPORARY TABLE tChilds, tParents;
END IF; END IF;
IF !vIsSearch THEN IF NOT vIsSearch THEN
INSERT IGNORE INTO tNodes INSERT IGNORE INTO tNodes
SELECT id SELECT id
FROM zoneGeo FROM zoneGeo
WHERE parentFk <=> vParentFk; WHERE parentFk <=> vParentFk;
END IF; END IF;
SELECT g.id, CREATE OR REPLACE TEMPORARY TABLE tZones
g.name, SELECT g.id,
g.parentFk, g.name,
g.sons, g.parentFk,
isIncluded selected g.sons,
FROM zoneGeo g NOT g.sons OR `type` = 'country' isChecked,
JOIN tNodes n ON n.id = g.id i.isIncluded selected,
LEFT JOIN zoneIncluded i ON i.geoFk = g.id AND i.zoneFk = vSelf g.`depth`,
ORDER BY `depth`, selected DESC, name; 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 ;; END ;;
DELIMITER ; DELIMITER ;
/*!50003 SET sql_mode = @saved_sql_mode */ ; /*!50003 SET sql_mode = @saved_sql_mode */ ;
@ -78540,7 +78567,7 @@ BEGIN
INDEX(geoFk)) INDEX(geoFk))
ENGINE = MEMORY; ENGINE = MEMORY;
CALL zone_getLeaves2(vSelf, NULL , NULL); CALL zone_getLeaves(vSelf, NULL , NULL, TRUE);
UPDATE tmp.zoneNodes zn UPDATE tmp.zoneNodes zn
SET isChecked = 0 SET isChecked = 0
@ -78553,7 +78580,7 @@ BEGIN
WHERE NOT isChecked WHERE NOT isChecked
LIMIT 1; LIMIT 1;
CALL zone_getLeaves2(vSelf, vGeoFk, NULL); CALL zone_getLeaves(vSelf, vGeoFk, NULL, TRUE);
UPDATE tmp.zoneNodes UPDATE tmp.zoneNodes
SET isChecked = TRUE SET isChecked = TRUE
WHERE geoFk = vGeoFk; WHERE geoFk = vGeoFk;

View File

@ -875,7 +875,7 @@ export default {
}, },
routeTickets: { 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', firstTicketCheckbox: 'vn-route-tickets vn-tr:nth-child(1) vn-check',
buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]', buscamanButton: 'vn-route-tickets vn-button[icon="icon-buscaman"]',
firstTicketDeleteButton: 'vn-route-tickets vn-tr:nth-child(1) vn-icon[icon="delete"]', 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() => { it('should modify the first ticket priority', async() => {
await page.clearInput(selectors.routeTickets.firstTicketPriority); await page.writeOnEditableTD(selectors.routeTickets.firstTicketPriority, '9');
await page.type(selectors.routeTickets.firstTicketPriority, '9');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();

View File

@ -66,7 +66,11 @@ export default class App {
return this.logger.$http.get('Urls/findOne', {filter}) return this.logger.$http.get('Urls/findOne', {filter})
.then(res => { .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 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 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 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", "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", "You can not use the same password": "You can not use the same password",
"Valid priorities": "Valid priorities: %d", "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", "The renew period has not been exceeded": "El periodo de renovación no ha sido superado",
"Valid priorities": "Prioridades válidas: %d", "Valid priorities": "Prioridades válidas: %d",
"Negative basis of tickets": "Base negativa para los tickets: {{ticketsIds}}", "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() { onAddClick() {
this.addData = {account: this.$params.id};
this.$.dialog.show(); this.$.dialog.show();
} }
onAddSave() { onAddSave() {
return this.$http.post(`VnUsers/${this.$params.id}/addAlias`, this.addData) return this.$http.post(`MailAliasAccounts`, this.addData)
.then(() => this.refresh()) .then(() => this.refresh())
.then(() => this.vnApp.showSuccess( .then(() => this.vnApp.showSuccess(
this.$t('Subscribed to alias!')) this.$t('Subscribed to alias!'))
@ -33,12 +34,11 @@ export default class Controller extends Section {
} }
onRemove(row) { onRemove(row) {
const params = { return this.$http.delete(`MailAliasAccounts/${row.id}`)
mailAlias: row.mailAlias .then(() => {
}; this.$.data.splice(this.$.data.indexOf(row), 1);
return this.$http.post(`VnUsers/${this.$params.id}/removeAlias`, params) this.vnApp.showSuccess(this.$t('Unsubscribed from alias!'));
.then(() => this.refresh()) });
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')));
} }
} }

View File

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

View File

@ -1,3 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) { module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', { Self.remoteMethodCtx('canBeInvoiced', {
description: 'Change property isEqualizated in all client addresses', description: 'Change property isEqualizated in all client addresses',
@ -9,6 +11,12 @@ module.exports = function(Self) {
required: true, required: true,
description: 'Client id', description: 'Client id',
http: {source: 'path'} http: {source: 'path'}
},
{
arg: 'companyFk',
description: 'The company id',
type: 'number',
required: true
} }
], ],
returns: { 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 models = Self.app.models;
const myOptions = {}; const myOptions = {};
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const client = await models.Client.findById(id, { const client = await models.Client.findById(id, {
fields: ['id', 'isTaxDataChecked', 'hasToInvoice'] fields: ['id', 'isTaxDataChecked', 'hasToInvoice', 'payMethodFk'],
include:
{
relation: 'payMethod',
scope: {
fields: ['code']
}
}
}, myOptions); }, 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) if (client.isTaxDataChecked && client.hasToInvoice)
return true; return true;

View File

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

View File

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

View File

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

View File

@ -95,6 +95,9 @@
"ClientSample": { "ClientSample": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ClientSms": {
"dataSource": "vn"
},
"Sms": { "Sms": {
"dataSource": "vn" "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) { async function socialNameIsUnique(err, done) {
if (!this.countryFk)
return done();
const filter = { const filter = {
include: {
relation: 'country',
scope: {
fields: {
isSocialNameUnique: true,
},
},
},
where: { where: {
and: [ and: [
{socialName: this.socialName}, {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(); err();
done(); done();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
{"state": "client.card.recovery.index", "icon": "icon-recovery"}, {"state": "client.card.recovery.index", "icon": "icon-recovery"},
{"state": "client.card.webAccess", "icon": "cloud"}, {"state": "client.card.webAccess", "icon": "cloud"},
{"state": "client.card.log", "icon": "history"}, {"state": "client.card.log", "icon": "history"},
{"state": "client.card.sms", "icon": "sms"},
{ {
"description": "Credit management", "description": "Credit management",
"icon": "monetization_on", "icon": "monetization_on",
@ -373,6 +374,12 @@
"component": "vn-client-log", "component": "vn-client-log",
"description": "Log" "description": "Log"
}, },
{
"url" : "/sms",
"state": "client.card.sms",
"component": "vn-client-sms",
"description": "Sms"
},
{ {
"url": "/dms", "url": "/dms",
"state": "client.card.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 => { module.exports = Self => {
require('../methods/invoice-in/filter')(Self); require('../methods/invoice-in/filter')(Self);
require('../methods/invoice-in/summary')(Self); require('../methods/invoice-in/summary')(Self);
@ -7,4 +9,9 @@ module.exports = Self => {
require('../methods/invoice-in/invoiceInPdf')(Self); require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self); require('../methods/invoice-in/invoiceInEmail')(Self);
require('../methods/invoice-in/getSerial')(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 => { module.exports = Self => {
require('../methods/item-shelving/deleteItemShelvings')(Self); require('../methods/item-shelving/deleteItemShelvings')(Self);
require('../methods/item-shelving/getInventory')(Self);
}; };

View File

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

View File

@ -85,7 +85,7 @@
show-field="id" show-field="id"
value-field="id" value-field="id"
search-function="$ctrl.itemSearchFunc($search)" search-function="$ctrl.itemSearchFunc($search)"
on-change="$ctrl.upsertPrice(price, true)" ng-change="$ctrl.upsertPrice(price, true)"
order="id DESC" order="id DESC"
tabindex="1"> tabindex="1">
<tpl-item> <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> </tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete
label="Vehicle"
ng-model="$ctrl.route.vehicleFk" ng-model="$ctrl.route.vehicleFk"
url="Vehicles" data="$ctrl.vehicles"
show-field="numberPlate" show-field="numberPlate"
value-field="id" value-field="id"
label="Vehicle" order="false"
vn-name="vehicle"> vn-name="vehicle">
<tpl-item>{{::numberPlate}} - {{::name}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-date-picker <vn-date-picker
label="Created" label="Created"
ng-model="$ctrl.route.created" ng-model="$ctrl.route.created"
vn-name="created"> vn-name="created">
</vn-date-picker> </vn-date-picker>
<vn-autocomplete <vn-autocomplete
ng-model="$ctrl.route.agencyModeFk" ng-model="$ctrl.route.agencyModeFk"

View File

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

View File

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

View File

@ -68,9 +68,9 @@ module.exports = Self => {
}; };
const country = await Self.app.models.Country.findOne(filter); const country = await Self.app.models.Country.findOne(filter);
const code = country ? country.code.toLowerCase() : null; 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(); err();
done(); done();
} }
@ -122,7 +122,7 @@ module.exports = Self => {
}); });
async function hasSupplierSameName(err, done) { 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( const supplier = await Self.app.models.Supplier.findOne(
{ {
where: { where: {

View File

@ -1,4 +1,3 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
@ -47,9 +46,7 @@ module.exports = Self => {
} }
try { try {
const isEditable = await models.Ticket.isEditable(ctx, id, myOptions); await models.Ticket.isEditableOrThrow(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const item = await models.Item.findById(itemId, null, myOptions); const item = await models.Item.findById(itemId, null, myOptions);
const ticket = await models.Ticket.findById(id, { 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'); const loggable = require('vn-loopback/util/log');
module.exports = Self => { module.exports = Self => {
@ -116,10 +115,7 @@ module.exports = Self => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
const models = Self.app.models; const models = Self.app.models;
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
const isEditable = await models.Ticket.isEditable(ctx, args.id, myOptions); await models.Ticket.isEditableOrThrow(ctx, args.id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE'); const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE');
if (!editZone) { 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) => { Self.isEditable = async(ctx, id, options) => {
const models = Self.app.models; try {
const myOptions = {}; await Self.isEditableOrThrow(ctx, id, options);
} catch (e) {
if (typeof options == 'object') if (e.name === 'ForbiddenError') return false;
Object.assign(myOptions, options); throw e;
}
const state = await models.TicketState.findOne({ return true;
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;
}; };
}; };

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', arg: 'companyFk',
description: 'The company id', description: 'The company id',
type: 'string', type: 'number',
required: true required: true
}, },
{ {
@ -67,7 +67,7 @@ module.exports = function(Self) {
const [firstTicket] = tickets; const [firstTicket] = tickets;
const clientId = firstTicket.clientFk; const clientId = firstTicket.clientFk;
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, myOptions); const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, companyFk, myOptions);
if (!clientCanBeInvoiced) if (!clientCanBeInvoiced)
throw new UserError(`This client can't be invoiced`); 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 => { module.exports = Self => {
Self.remoteMethodCtx('priceDifference', { Self.remoteMethodCtx('priceDifference', {
description: 'Returns sales with price difference if the ticket is editable', description: 'Returns sales with price difference if the ticket is editable',
@ -72,10 +70,7 @@ module.exports = Self => {
} }
try { try {
const isEditable = await Self.isEditable(ctx, args.id, myOptions); await Self.isEditableOrThrow(ctx, args.id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE'); const editZone = await models.ACL.checkAccessAcl(ctx, 'Ticket', 'editZone', 'WRITE');
if (!editZone) { if (!editZone) {

View File

@ -1,4 +1,3 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('recalculateComponents', { Self.remoteMethodCtx('recalculateComponents', {
description: 'Calculates the price of a sale and its components', description: 'Calculates the price of a sale and its components',
@ -33,10 +32,7 @@ module.exports = Self => {
} }
try { try {
const isEditable = await Self.isEditable(ctx, id, myOptions); await Self.isEditableOrThrow(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The current ticket can't be modified`);
const recalculation = await Self.rawSql('CALL vn.ticket_recalcComponents(?, NULL)', [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); const ticketToDelete = await models.Ticket.findById(id, {fields: ['isDeleted']}, myOptions);
if (ticketToDelete.isDeleted) return false; if (ticketToDelete.isDeleted) return false;
const isEditable = await Self.isEditable(ctx, id, myOptions); await Self.isEditableOrThrow(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
// Check if ticket has refunds // Check if ticket has refunds
const ticketRefunds = await models.TicketRefund.find({ const ticketRefunds = await models.TicketRefund.find({

View File

@ -97,6 +97,6 @@ describe('ticket addSale()', () => {
error = e; 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() => { it('should return the tickets that are not pending', async() => {
pending('#6010 test intermitente');
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
try { try {

View File

@ -1,156 +1,37 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('ticket isEditable()', () => { describe('isEditable()', () => {
it('should return false if the given ticket does not exist', async() => { it('should return false if It is able to edit', async() => {
const tx = await models.Ticket.beginTransaction({}); const tx = await models.Ticket.beginTransaction({});
let result; let result;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = { const ctx = {req: {accessToken: {userId: 35}}};
req: {accessToken: {userId: 9}} result = await models.Ticket.isEditable(ctx, 5, options);
};
result = await models.Ticket.isEditable(ctx, 9999, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (error) {
await tx.rollback(); await tx.rollback();
throw e; 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({}); const tx = await models.Ticket.beginTransaction({});
let result; let result;
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const deletedTicket = await models.Ticket.findOne({ const ctx = {req: {accessToken: {userId: 35}}};
where: { result = await models.Ticket.isEditable(ctx, 15, options);
invoiceOut: null,
isDeleted: true
},
fields: ['id']
});
const ctx = {
req: {accessToken: {userId: 9}}
};
result = await models.Ticket.isEditable(ctx, deletedTicket.id, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (error) {
await tx.rollback(); await tx.rollback();
throw e; throw e;
} }
expect(result).toEqual(false); expect(result).toBeTrue();
});
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;
}
}); });
}); });

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 models = require('vn-loopback/server/server').models;
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('sale priceDifference()', () => { describe('sale priceDifference()', () => {
it('should return ticket price differences', async() => { it('should return ticket price differences', async() => {
@ -59,7 +60,7 @@ describe('sale priceDifference()', () => {
await tx.rollback(); 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() => { it('should return ticket movable', async() => {

View File

@ -1,4 +1,5 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const ForbiddenError = require('vn-loopback/util/forbiddenError');
describe('ticket recalculateComponents()', () => { describe('ticket recalculateComponents()', () => {
const ticketId = 11; const ticketId = 11;
@ -38,6 +39,6 @@ describe('ticket recalculateComponents()', () => {
error = e; 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; 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() => { 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 => { module.exports = Self => {
Self.remoteMethodCtx('transferSales', { Self.remoteMethodCtx('transferSales', {
@ -48,9 +48,7 @@ module.exports = Self => {
} }
try { try {
const isEditable = await models.Ticket.isEditable(ctx, id, myOptions); await models.Ticket.isEditableOrThrow(ctx, id, myOptions);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
if (ticketId) { if (ticketId) {
const isReceiverEditable = await models.Ticket.isEditable(ctx, ticketId, myOptions); 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/componentUpdate')(Self);
require('../methods/ticket/new')(Self); require('../methods/ticket/new')(Self);
require('../methods/ticket/isEditable')(Self); require('../methods/ticket/isEditable')(Self);
require('../methods/ticket/isEditableOrThrow')(Self);
require('../methods/ticket/setDeleted')(Self); require('../methods/ticket/setDeleted')(Self);
require('../methods/ticket/restore')(Self); require('../methods/ticket/restore')(Self);
require('../methods/ticket/getSales')(Self); require('../methods/ticket/getSales')(Self);
@ -19,6 +20,7 @@ module.exports = function(Self) {
require('../methods/ticket/uploadFile')(Self); require('../methods/ticket/uploadFile')(Self);
require('../methods/ticket/addSale')(Self); require('../methods/ticket/addSale')(Self);
require('../methods/ticket/transferSales')(Self); require('../methods/ticket/transferSales')(Self);
require('../methods/ticket/transferClient')(Self);
require('../methods/ticket/recalculateComponents')(Self); require('../methods/ticket/recalculateComponents')(Self);
require('../methods/ticket/sendSms')(Self); require('../methods/ticket/sendSms')(Self);
require('../methods/ticket/isLocked')(Self); require('../methods/ticket/isLocked')(Self);

View File

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

View File

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

View File

@ -326,17 +326,4 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
expect(controller.vnApp.showSuccess).toHaveBeenCalled(); 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) { applyFilters(param) {
if (typeof this.filter.scopeDays === 'number') { 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); 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.setDate(shippedTo.getDate() + this.filter.scopeDays);
shippedTo.setHours(23, 59, 59, 999); shippedTo.setHours(23, 59, 59, 999);
Object.assign(this.filter, {shippedFrom, shippedTo}); 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.', description: 'Find all instances of the model matched by filter from the data source.',
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
{
arg: 'id',
type: 'Number',
description: 'The worker id',
http: {source: 'path'}
},
{ {
arg: 'filter', arg: 'filter',
type: 'Object', type: 'Object',
@ -17,16 +23,17 @@ module.exports = Self => {
root: true root: true
}, },
http: { http: {
path: `/filter`, path: `/:id/filter`,
verb: 'GET' verb: 'GET'
} }
}); });
Self.filter = async(ctx, filter) => { Self.filter = async(ctx, id, filter) => {
const conn = Self.dataSource.connector; const conn = Self.dataSource.connector;
const userId = ctx.req.accessToken.userId; 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( const stmt = new ParameterizedSQL(
`SELECT d.id dmsFk, d.reference, d.description, d.file, d.created, d.hardCopyNumber, d.hasFile `SELECT d.id dmsFk, d.reference, d.description, d.file, d.created, d.hardCopyNumber, d.hasFile
FROM workerDocument wd FROM workerDocument wd
@ -47,7 +54,38 @@ module.exports = Self => {
}] }]
}, oldWhere]}; }, oldWhere]};
stmt.merge(conn.makeSuffix(filter)); 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/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self); require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/filter')(Self); require('../methods/worker-dms/filter')(Self);
require('../methods/worker-dms/docuwareDownload')(Self);
Self.isMine = async function(ctx, dmsId) { Self.isMine = async function(ctx, dmsId) {
const myUserId = ctx.req.accessToken.userId; const myUserId = ctx.req.accessToken.userId;

View File

@ -3,7 +3,7 @@
data="absenceTypes" data="absenceTypes"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<div ng-if="$ctrl.worker.hasWorkCenter"> <div ng-if="$ctrl.card.hasWorkCenter">
<div class="vn-w-lg"> <div class="vn-w-lg">
<vn-card class="vn-pa-sm calendars"> <vn-card class="vn-pa-sm calendars">
<vn-icon ng-if="::$ctrl.isSubordinate" icon="info" color-marginal <vn-icon ng-if="::$ctrl.isSubordinate" icon="info" color-marginal
@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<div <div
ng-if="!$ctrl.worker.hasWorkCenter" ng-if="!$ctrl.card.hasWorkCenter"
class="bg-title" class="bg-title"
translate> translate>
Autonomous worker Autonomous worker
@ -73,41 +73,39 @@
value-field="businessFk" value-field="businessFk"
order="businessFk DESC" order="businessFk DESC"
limit="5"> limit="5">
<tpl-item> <tpl-item>
<div>#{{businessFk}}</div> <div>#{{businessFk}}</div>
<div class="text-caption text-secondary"> <div class="text-caption text-secondary">
{{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}} {{started | date: 'dd/MM/yyyy'}} - {{ended ? (ended | date: 'dd/MM/yyyy') : 'Indef.'}}
</div> </div>
</tpl-item> </tpl-item>
</vn-autocomplete> </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> </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-side-menu>
<vn-confirm <vn-confirm
vn-id="confirm" vn-id="confirm"
message="This item will be deleted" message="This item will be deleted"
question="Are you sure you want to continue?"> question="Are you sure you want to continue?">
</vn-confirm> </vn-confirm>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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