refs #6291 validateTin #1836

Merged
carlossa merged 12 commits from 6291-comprobarDNI into dev 2024-01-09 14:03:21 +00:00
172 changed files with 1925 additions and 1209 deletions
Showing only changes of commit 782772bffc - Show all commits

View File

@ -3,7 +3,7 @@
// Carácter predeterminado de final de línea. // Carácter predeterminado de final de línea.
"files.eol": "\n", "files.eol": "\n",
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"search.useIgnoreFiles": false, "search.useIgnoreFiles": false,
"editor.defaultFormatter": "dbaeumer.vscode-eslint", "editor.defaultFormatter": "dbaeumer.vscode-eslint",
@ -16,6 +16,7 @@
}, },
"cSpell.words": [ "cSpell.words": [
"salix", "salix",
"fdescribe" "fdescribe",
"Loggable"
] ]
} }

View File

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2404.01] - 2024-01-25
### Added
### Changed
### Fixed
## [2402.01] - 2024-01-11 ## [2402.01] - 2024-01-11
### Added ### Added

View File

@ -24,15 +24,40 @@ describe('docuware upload()', () => {
}); });
it('should try upload file', async() => { it('should try upload file', async() => {
const tx = await models.Docuware.beginTransaction({});
spyOn(ticketModel, 'deliveryNotePdf').and.returnValue(new Promise(resolve => resolve({}))); spyOn(ticketModel, 'deliveryNotePdf').and.returnValue(new Promise(resolve => resolve({})));
let error; let error;
try { try {
await models.Docuware.upload(ctx, ticketIds, fileCabinetName); const options = {transaction: tx};
const user = await models.UserConfig.findById(userId, null, options);
await user.updateAttribute('tabletFk', 'Tablet1', options);
await models.Docuware.upload(ctx, ticketIds, fileCabinetName, options);
await tx.rollback();
} catch (e) { } catch (e) {
error = e.message; error = e;
await tx.rollback();
} }
expect(error).toEqual('Action not allowed on the test environment'); expect(error.message).toEqual('Action not allowed on the test environment');
});
it('should throw error when not have tablet assigned', async() => {
const tx = await models.Docuware.beginTransaction({});
spyOn(ticketModel, 'deliveryNotePdf').and.returnValue(new Promise(resolve => resolve({})));
let error;
try {
const options = {transaction: tx};
await models.Docuware.upload(ctx, ticketIds, fileCabinetName, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual('This user does not have an assigned tablet');
}); });
}); });

View File

@ -29,12 +29,24 @@ module.exports = Self => {
} }
}); });
Self.upload = async function(ctx, ticketIds, fileCabinet) { Self.upload = async function(ctx, ticketIds, fileCabinet, options) {
delete ctx.args.ticketIds; delete ctx.args.ticketIds;
const models = Self.app.models; const models = Self.app.models;
const action = 'store'; const action = 'store';
const options = await Self.getOptions(); const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const userConfig = await models.UserConfig.findById(ctx.req.accessToken.userId, {
fields: ['tabletFk']
}, myOptions);
if (!userConfig?.tabletFk)
throw new UserError('This user does not have an assigned tablet');
const docuwareOptions = await Self.getOptions();
const fileCabinetId = await Self.getFileCabinet(fileCabinet); const fileCabinetId = await Self.getFileCabinet(fileCabinet);
const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId); const dialogId = await Self.getDialog(fileCabinet, action, fileCabinetId);
@ -45,7 +57,7 @@ module.exports = Self => {
const deliveryNote = await models.Ticket.deliveryNotePdf(ctx, { const deliveryNote = await models.Ticket.deliveryNotePdf(ctx, {
id, id,
type: 'deliveryNote' type: 'deliveryNote'
}); }, myOptions);
// get ticket data // get ticket data
const ticket = await models.Ticket.findById(id, { const ticket = await models.Ticket.findById(id, {
include: [{ include: [{
@ -54,7 +66,7 @@ module.exports = Self => {
fields: ['id', 'name', 'fi'] fields: ['id', 'name', 'fi']
} }
}] }]
}); }, myOptions);
// upload file // upload file
const templateJson = { const templateJson = {
@ -102,7 +114,7 @@ module.exports = Self => {
{ {
'FieldName': 'FILTRO_TABLET', 'FieldName': 'FILTRO_TABLET',
'ItemElementName': 'string', 'ItemElementName': 'string',
'Item': 'Tablet1', 'Item': userConfig.tabletFk,
} }
] ]
}; };
@ -116,11 +128,11 @@ module.exports = Self => {
const deleteJson = { const deleteJson = {
'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}] 'Field': [{'FieldName': 'ESTADO', 'Item': 'Pendiente eliminar', 'ItemElementName': 'String'}]
}; };
const deleteUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`; const deleteUri = `${docuwareOptions.url}/FileCabinets/${fileCabinetId}/Documents/${docuwareFile.id}/Fields`;
await axios.put(deleteUri, deleteJson, options.headers); await axios.put(deleteUri, deleteJson, docuwareOptions.headers);
} }
const uploadUri = `${options.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`; const uploadUri = `${docuwareOptions.url}/FileCabinets/${fileCabinetId}/Documents?StoreDialogId=${dialogId}`;
const FormData = require('form-data'); const FormData = require('form-data');
const data = new FormData(); const data = new FormData();
@ -130,7 +142,7 @@ module.exports = Self => {
headers: { headers: {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
'X-File-ModifiedDate': Date.vnNew(), 'X-File-ModifiedDate': Date.vnNew(),
'Cookie': options.headers.headers.Cookie, 'Cookie': docuwareOptions.headers.headers.Cookie,
...data.getHeaders() ...data.getHeaders()
}, },
}; };
@ -141,11 +153,11 @@ module.exports = Self => {
const $t = ctx.req.__; const $t = ctx.req.__;
const message = $t('Failed to upload delivery note', {id}); const message = $t('Failed to upload delivery note', {id});
if (uploaded.length) if (uploaded.length)
await models.TicketTracking.setDelivered(ctx, uploaded); await models.TicketTracking.setDelivered(ctx, uploaded, myOptions);
throw new UserError(message); throw new UserError(message);
} }
uploaded.push(id); uploaded.push(id);
} }
return models.TicketTracking.setDelivered(ctx, ticketIds); return models.TicketTracking.setDelivered(ctx, ticketIds, myOptions);
}; };
}; };

View File

@ -68,7 +68,7 @@ module.exports = Self => {
userToUpdate.hasGrant = hasGrant; userToUpdate.hasGrant = hasGrant;
if (roleFk) { if (roleFk) {
const role = await models.Role.findById(roleFk, {fields: ['name']}, myOptions); const role = await models.VnRole.findById(roleFk, {fields: ['name']}, myOptions);
const hasRole = await Self.hasRole(userId, role.name, myOptions); const hasRole = await Self.hasRole(userId, role.name, myOptions);
if (!hasRole) if (!hasRole)

View File

@ -1,14 +1,5 @@
const UserError = require('vn-loopback/util/user-error');
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
const handlePromiseLogout = (Self, {id}, courtesyTime) => {
new Promise(res => {
setTimeout(() => {
res(Self.logout(id));
}
, courtesyTime * 1000);
});
};
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('renewToken', { Self.remoteMethodCtx('renewToken', {
description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it', description: 'Checks if the token has more than renewPeriod seconds to live and if so, renews it',
@ -28,14 +19,26 @@ module.exports = Self => {
const {accessToken: token} = ctx.req; const {accessToken: token} = ctx.req;
// Check if current token is valid // Check if current token is valid
const isValid = await validateToken(token);
if (isValid) const {renewPeriod, courtesyTime} = await models.AccessTokenConfig.findOne({
fields: ['renewPeriod', 'courtesyTime']
});
const now = Date.now();
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const isNotExceeded = differenceSeconds < renewPeriod - courtesyTime;
if (isNotExceeded)
return token; return token;
const {courtesyTime} = await models.AccessTokenConfig.findOne({fields: ['courtesyTime']});
// Schedule to remove current token // Schedule to remove current token
handlePromiseLogout(Self, token, courtesyTime); setTimeout(async() => {
try {
await Self.logout(token.id);
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
}
}, courtesyTime * 1000);
// Create new accessToken // Create new accessToken
const user = await Self.findById(token.userId); const user = await Self.findById(token.userId);
@ -43,14 +46,4 @@ module.exports = Self => {
return {id: accessToken.id, ttl: accessToken.ttl}; return {id: accessToken.id, ttl: accessToken.ttl};
}; };
async function validateToken(token) {
const accessTokenConfig = await models.AccessTokenConfig.findOne({fields: ['renewPeriod', 'courtesyTime']});
const now = Date.now();
const differenceMilliseconds = now - token.created;
const differenceSeconds = Math.floor(differenceMilliseconds / 1000);
const isValid = differenceSeconds < accessTokenConfig.renewPeriod - accessTokenConfig.courtesyTime;
return isValid;
}
}; };

View File

@ -70,7 +70,7 @@ describe('VnUser privileges()', () => {
const tx = await models.VnUser.beginTransaction({}); const tx = await models.VnUser.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};
const agency = await models.Role.findOne({ const agency = await models.VnRole.findOne({
where: { where: {
name: 'agency' name: 'agency'
} }

View File

@ -27,8 +27,9 @@ describe('Renew Token', () => {
jasmine.clock().uninstall(); jasmine.clock().uninstall();
}); });
it('should renew process', async() => { it('should renew token', async() => {
jasmine.clock().mockDate(new Date(startingTime + 21600000)); const mockDate = new Date(startingTime + 26600000);
jasmine.clock().mockDate(mockDate);
const {id} = await models.VnUser.renewToken(ctx); const {id} = await models.VnUser.renewToken(ctx);
expect(id).not.toEqual(ctx.req.accessToken.id); expect(id).not.toEqual(ctx.req.accessToken.id);

View File

@ -20,10 +20,7 @@ describe('VnUser Sign-in()', () => {
let ctx = {req: {accessToken: accessToken}}; let ctx = {req: {accessToken: accessToken}};
let signInLog = await SignInLog.find({where: {token: accessToken.id}}); let signInLog = await SignInLog.find({where: {token: accessToken.id}});
expect(signInLog.length).toEqual(1); expect(signInLog.length).toEqual(0);
expect(signInLog[0].userFk).toEqual(accessToken.userId);
expect(signInLog[0].owner).toEqual(true);
expect(login.token).toBeDefined();
await VnUser.logout(ctx.req.accessToken.id); await VnUser.logout(ctx.req.accessToken.id);
}); });

View File

@ -139,9 +139,6 @@
"Warehouse": { "Warehouse": {
"dataSource": "vn" "dataSource": "vn"
}, },
"VnUser": {
"dataSource": "vn"
},
"OsTicket": { "OsTicket": {
"dataSource": "osticket" "dataSource": "osticket"
}, },
@ -156,6 +153,12 @@
}, },
"ViaexpressConfig": { "ViaexpressConfig": {
"dataSource": "vn" "dataSource": "vn"
},
"VnUser": {
"dataSource": "vn"
},
"VnRole": {
"dataSource": "vn"
} }
} }

View File

@ -29,12 +29,12 @@
"relations": { "relations": {
"readRole": { "readRole": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "readRoleFk" "foreignKey": "readRoleFk"
}, },
"writeRole": { "writeRole": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "writeRoleFk" "foreignKey": "writeRoleFk"
} }
}, },

View File

@ -0,0 +1,17 @@
{
"name": "docuwareTablet",
"base": "VnModel",
"options": {
"mysql": {
"table": "docuwareTablet"
}
},
"properties": {
"tablet": {
"type": "string"
},
"description": {
"type": "string"
}
}
}

View File

@ -46,12 +46,12 @@
}, },
"readRole": { "readRole": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "readRoleFk" "foreignKey": "readRoleFk"
}, },
"writeRole": { "writeRole": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "writeRoleFk" "foreignKey": "writeRoleFk"
} }
}, },
@ -64,4 +64,3 @@
} }
] ]
} }

View File

@ -24,7 +24,7 @@
}, },
"role": { "role": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "roleFk" "foreignKey": "roleFk"
} }
} }

View File

@ -0,0 +1,73 @@
const models = require('vn-loopback/server/server').models;
describe('loopback model MailAliasAccount', () => {
it('should fail to add a mail Alias if the worker doesnt have ACLs', async() => {
const tx = await models.MailAliasAccount.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 57}};
await models.MailAliasAccount.create({mailAlias: 2, account: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toEqual('The alias cant be modified');
});
it('should add a mail Alias', async() => {
const tx = await models.MailAliasAccount.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.MailAliasAccount.create({mailAlias: 2, account: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should add a mail Alias of an inherit role', async() => {
const tx = await models.MailAliasAccount.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 9}};
await models.MailAliasAccount.create({mailAlias: 3, account: 5}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
it('should delete a mail Alias', async() => {
const tx = await models.MailAliasAccount.beginTransaction({});
let error;
try {
const options = {transaction: tx, accessToken: {userId: 1}};
const mailAclId = 2;
await models.MailAliasAccount.destroyAll({id: mailAclId}, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).toBeUndefined();
});
});

View File

@ -26,6 +26,9 @@
}, },
"darkMode": { "darkMode": {
"type": "boolean" "type": "boolean"
},
"tabletFk": {
"type": "string"
} }
}, },
"relations": { "relations": {
@ -43,6 +46,11 @@
"type": "belongsTo", "type": "belongsTo",
"model": "VnUser", "model": "VnUser",
"foreignKey": "userFk" "foreignKey": "userFk"
} },
"Tablet": {
"type": "belongsTo",
"model": "docuwareTablet",
"foreignKey": "tabletFk"
}
} }
} }

13
back/models/vn-role.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "VnRole",
"base": "Role",
"validateUpsert": true,
"options": {
"mysql": {
"table": "account.role"
}
},
"mixins": {
"Loggable": true
}
}

View File

@ -134,15 +134,16 @@ module.exports = function(Self) {
Self.signInValidate = async(user, userToken, token, ctx) => { Self.signInValidate = async(user, userToken, token, ctx) => {
const [[key, value]] = Object.entries(Self.userUses(user)); const [[key, value]] = Object.entries(Self.userUses(user));
const isOwner = Self.rawSql(`SELECT ? = ? `, [userToken[key], value]); const isOwner = Self.rawSql(`SELECT ? = ? `, [userToken[key], value]);
await Self.app.models.SignInLog.create({ if (!isOwner) {
userName: user, await Self.app.models.SignInLog.create({
token: token.id, userName: user,
userFk: userToken.id, token: token.id,
ip: ctx.req.ip, userFk: userToken.id,
owner: isOwner ip: ctx.req.ip,
}); owner: isOwner
if (!isOwner) });
throw new UserError('Try again'); throw new UserError('Try again');
}
}; };
/** /**

View File

@ -7,6 +7,9 @@
"table": "account.user" "table": "account.user"
} }
}, },
"mixins": {
"Loggable": true
},
"resetPasswordTokenTTL": "604800", "resetPasswordTokenTTL": "604800",
"properties": { "properties": {
"id": { "id": {
@ -63,7 +66,7 @@
"relations": { "relations": {
"role": { "role": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "roleFk" "foreignKey": "roleFk"
}, },
"roles": { "roles": {
@ -95,27 +98,30 @@
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{ "property": "recoverPassword",
"property": "recoverPassword",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "validateAuth",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
}, }, {
{ "property": "validateAuth",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}, {
"property": "privileges", "property": "privileges",
"accessType": "*", "accessType": "*",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, {
"property": "renewToken",
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW"
} }
], ],
"scopes": { "scopes": {

View File

@ -0,0 +1,6 @@
INSERT INTO salix.ACL (model,property,accessType,permission,principalType,principalId) VALUES
('VnRole','*','READ','ALLOW','ROLE','employee'),
('VnRole','*','WRITE','ALLOW','ROLE','it');
DELETE FROM`salix`.`ACL` WHERE model='Role';

View File

@ -0,0 +1,8 @@
-- Definición de la tabla mailAliasACL
CREATE OR REPLACE TABLE `account`.`mailAliasAcl` (
`mailAliasFk` int(10) unsigned NOT NULL,
`roleFk` int(10) unsigned NOT NULL,
FOREIGN KEY (`mailAliasFk`) REFERENCES `account`.`mailAlias` (`id`),
FOREIGN KEY (`roleFk`) REFERENCES `account`.`role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;

View File

@ -0,0 +1,7 @@
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `cplusInvoiceTyoeFk`;
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `invoiceCorrectionType_Fk33`;
ALTER TABLE `vn`.`invoiceCorrection` DROP FOREIGN KEY `invoiceCorrection_ibfk_1`;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `siiTypeInvoiceOut_FK` FOREIGN KEY (`siiTypeInvoiceOutFk`) REFERENCES `vn`.`siiTypeInvoiceOut`(id) ON UPDATE CASCADE;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `invoiceCorrectionType_FK` FOREIGN KEY (`invoiceCorrectionTypeFk`) REFERENCES `vn`.`invoiceCorrectionType`(id) ON UPDATE CASCADE;
ALTER TABLE `vn`.`invoiceCorrection` ADD CONSTRAINT `cplusRectificationType_FK` FOREIGN KEY (`cplusRectificationTypeFk`) REFERENCES `vn`.`cplusRectificationType`(id) ON UPDATE CASCADE;

View File

@ -0,0 +1,33 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` PROCEDURE `vn`.`getTaxBases`()
BEGIN
/**
* Calcula y devuelve en número de bases imponibles postivas y negativas
* Requiere la tabla temporal tmp.ticketToInvoice(id)
*
* returns tmp.taxBases
*/
CREATE OR REPLACE TEMPORARY TABLE tmp.ticket
(KEY (ticketFk))
ENGINE = MEMORY
SELECT id ticketFk
FROM tmp.ticketToInvoice;
CALL ticket_getTax(NULL);
DROP TEMPORARY TABLE IF EXISTS tmp.taxBases;
CREATE TEMPORARY TABLE tmp.taxBases
ENGINE = MEMORY
SELECT
SUM(taxableBase > 0) as positive,
SUM(taxableBase < 0) as negative
FROM(
SELECT SUM(taxableBase) taxableBase
FROM tmp.ticketTax
GROUP BY pgcFk
) t;
END$$
DELIMITER ;

View File

@ -0,0 +1 @@
DELETE FROM `account`.`signInLog` where owner <> FALSE

View File

@ -0,0 +1,30 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`hasAnyPositiveBase`() RETURNS tinyint(1)
DETERMINISTIC
BEGIN
/**
* Calcula si existe alguna base imponible positiva
* Requiere la tabla temporal tmp.ticketToInvoice(id) para getTaxBases()
*
* returns BOOLEAN
*/
DECLARE hasAnyPositiveBase BOOLEAN;
CALL getTaxBases();
SELECT positive INTO hasAnyPositiveBase
FROM tmp.taxBases
LIMIT 1;
DROP TEMPORARY TABLE
tmp.ticketTax,
tmp.ticket,
tmp.taxBases;
RETURN hasAnyPositiveBase;
END$$
DELIMITER ;

View File

@ -0,0 +1,32 @@
DELIMITER $$
$$
CREATE OR REPLACE DEFINER=`root`@`localhost` FUNCTION `vn`.`hasAnyNegativeBase`() RETURNS tinyint(1)
DETERMINISTIC
BEGIN
/**
* Calcula si existe alguna base imponible negativa
* Requiere la tabla temporal tmp.ticketToInvoice(id) para getTaxBases()
*
* returns BOOLEAN
*/
DECLARE hasAnyNegativeBase BOOLEAN;
CALL getTaxBases();
SELECT negative INTO hasAnyNegativeBase
FROM tmp.taxBases
LIMIT 1;
DROP TEMPORARY TABLE
tmp.ticketTax,
tmp.ticket,
tmp.taxBases;
RETURN hasAnyNegativeBase;
END$$
DELIMITER ;

View File

@ -0,0 +1,10 @@
-- vn.docuwareTablet definition
CREATE TABLE `vn`.`docuwareTablet` (
`tablet` varchar(100) NOT NULL PRIMARY KEY,
`description` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;
ALTER TABLE `vn`.`userConfig`
ADD COLUMN tabletFk varchar(100) DEFAULT NULL,
ADD FOREIGN KEY (tabletFk) REFERENCES `vn`.`docuwareTablet`(tablet);

View File

@ -0,0 +1,9 @@
ALTER TABLE `vn`.`clientSms` ADD `ticketFk` int(11) NULL;
ALTER TABLE `vn`.`clientSms` ADD CONSTRAINT `clientSms_FK_2` FOREIGN KEY (`ticketFk`) REFERENCES `vn`.`ticket`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
INSERT INTO`vn`.`clientSms` (`clientFk`, `smsFk`, `ticketFk`)
SELECT `t`.`clientFk`, `s`.`smsFk`, `s`.`ticketFk`
FROM `vn`.`clientSms` `s`
JOIN `vn`.`ticket` `t` ON `t`.`id` = `s`.`ticketFk`;
RENAME TABLE `vn`.`ticketSms` TO `vn`.`ticketSms__`;

View File

@ -0,0 +1,17 @@
DELETE FROM `salix`.`ACL`
WHERE model = 'VnUser'
AND property = 'renewToken';
INSERT INTO `account`.`role` (name, description)
VALUES ('timeControl','Tablet para fichar');
INSERT INTO `account`.`roleInherit` (role, inheritsFrom)
VALUES (127, 11);
INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId)
VALUES
('WorkerTimeControl', 'login', 'READ', 'ALLOW', 'ROLE', 'timeControl'),
('WorkerTimeControl', 'getClockIn', 'READ', 'ALLOW', 'ROLE', 'timeControl'),
('WorkerTimeControl', 'clockIn', 'WRITE', 'ALLOW', 'ROLE', 'timeControl');
CALL `account`.`role_sync`();

View File

View File

@ -602,18 +602,19 @@ INSERT INTO `vn`.`taxArea` (`code`, `claveOperacionFactura`, `CodigoTransaccion`
INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaFk`, `isCEE`, `type`) INSERT INTO `vn`.`invoiceOutSerial` (`code`, `description`, `isTaxed`, `taxAreaFk`, `isCEE`, `type`)
VALUES VALUES
('A', 'Global nacional', 1, 'NATIONAL', 0, 'global'), ('A', 'Global nacional', 1, 'NATIONAL', 0, 'global'),
('T', 'Española rapida', 1, 'NATIONAL', 0, 'quick'), ('T', 'Española rapida', 1, 'NATIONAL', 0, 'quick'),
('V', 'Intracomunitaria global', 0, 'CEE', 1, 'global'), ('V', 'Intracomunitaria global', 0, 'CEE', 1, 'global'),
('M', 'Múltiple nacional', 1, 'NATIONAL', 0, 'quick'), ('M', 'Múltiple nacional', 1, 'NATIONAL', 0, 'quick'),
('E', 'Exportación rápida', 0, 'WORLD', 0, 'quick'); ('R', 'Rectificativa', 1, 'NATIONAL', 0, NULL),
('E', 'Exportación rápida', 0, 'WORLD', 0, 'quick');
INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`) INSERT INTO `vn`.`invoiceOut`(`id`, `serial`, `amount`, `issued`,`clientFk`, `created`, `companyFk`, `dued`, `booked`, `bankFk`, `hasPdf`)
VALUES VALUES
(1, 'T', 1026.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (1, 'T', 1026.24, util.VN_CURDATE(), 1101, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(2, 'T', 121.36, util.VN_CURDATE(), 1102, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (2, 'T', 121.36, util.VN_CURDATE(), 1102, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(3, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (3, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(4, 'T', 8.88, util.VN_CURDATE(), 1103, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0), (4, 'T', 8.88, util.VN_CURDATE(), 1104, util.VN_CURDATE(), 442, util.VN_CURDATE(), util.VN_CURDATE(), 1, 0),
(5, 'A', 8.88, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 442, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 0); (5, 'A', 8.88, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 442, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 0);
UPDATE `vn`.`invoiceOut` SET ref = 'T1111111' WHERE id = 1; UPDATE `vn`.`invoiceOut` SET ref = 'T1111111' WHERE id = 1;
@ -719,7 +720,7 @@ INSERT INTO `vn`.`route`(`id`, `time`, `workerFk`, `created`, `vehicleFk`, `agen
INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`) INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeFk`, `shipped`, `landed`, `clientFk`,`nickname`, `addressFk`, `refFk`, `isDeleted`, `zoneFk`, `zonePrice`, `zoneBonus`, `created`, `weight`)
VALUES VALUES
(1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1), (1 , 3, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 121, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1),
(2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2), (2 , 1, 1, 1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -1 MONTH), INTERVAL +1 DAY), 1101, 'Bat cave', 1, NULL, 0, 1, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2),
(3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL), (3 , 1, 7, 1, 6, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -2 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 3, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -2 MONTH), NULL),
(4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL), (4 , 3, 2, 1, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -3 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 9, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -3 MONTH), NULL),
(5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL), (5 , 3, 3, 3, 3, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), DATE_ADD(DATE_ADD(util.VN_CURDATE(),INTERVAL -4 MONTH), INTERVAL +1 DAY), 1104, 'Stark tower', 124, NULL, 0, 10, 5, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -4 MONTH), NULL),
@ -3019,3 +3020,27 @@ UPDATE `vn`.`worker`
WHERE id=1106; WHERE id=1106;
INSERT INTO `account`.`mailAliasAcl` (`mailAliasFk`, `roleFk`)
VALUES
(1, 1),
(2, 9),
(3, 15);
INSERT INTO `vn`.`docuwareTablet` (`tablet`,`description`)
VALUES
('Tablet1','Jarvis tablet'),
('Tablet2','Avengers tablet');
INSERT INTO `vn`.`sms` (`id`, `senderFk`, `sender`, `destination`, `message`, `statusCode`, `status`, `created`)
VALUES (1, 66, '111111111', '0001111111111', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'OK', util.VN_CURDATE()),
(2, 66, '222222222', '0002222222222', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'PENDING', util.VN_CURDATE()),
(3, 66, '333333333', '0003333333333', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'ERROR', util.VN_CURDATE()),
(4, 66, '444444444', '0004444444444', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', 0, 'OK', util.VN_CURDATE());
INSERT INTO `vn`.`clientSms` (`id`, `clientFk`, `smsFk`, `ticketFk`)
VALUES(1, 1103, 1, NULL),
(2, 1103, 2, NULL),
(3, 1103, 3, 32),
(4, 1103, 4, 32),
(13, 1101, 1, NULL),
(14, 1101, 4, 27);

View File

@ -35,7 +35,7 @@ describe('Ticket index payout path', () => {
await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton); await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton);
await page.write(selectors.ticketsIndex.advancedSearchClient, '1101'); await page.write(selectors.ticketsIndex.advancedSearchClient, '1101');
await page.keyboard.press('Enter'); await page.keyboard.press('Enter');
await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 9); await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 10);
await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox); await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox);
await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox); await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox);

View File

@ -28,7 +28,6 @@ describe('InvoiceOut summary path', () => {
it('should contain the tax breakdown', async() => { it('should contain the tax breakdown', async() => {
const firstTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxOne, 'innerText'); const firstTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxOne, 'innerText');
const secondTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxTwo, 'innerText'); const secondTax = await page.waitToGetProperty(selectors.invoiceOutSummary.taxTwo, 'innerText');
expect(firstTax).toContain('10%'); expect(firstTax).toContain('10%');
@ -37,10 +36,9 @@ describe('InvoiceOut summary path', () => {
it('should contain the tickets info', async() => { it('should contain the tickets info', async() => {
const firstTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketOne, 'innerText'); const firstTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketOne, 'innerText');
const secondTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketTwo, 'innerText'); const secondTicket = await page.waitToGetProperty(selectors.invoiceOutSummary.ticketTwo, 'innerText');
expect(firstTicket).toContain('Bat cave'); expect(firstTicket).toContain('Bat cave');
expect(secondTicket).toContain('Stark tower'); expect(secondTicket).toContain('Bat cave');
}); });
}); });

View File

@ -23,7 +23,6 @@ export function directive($translate, $window) {
let rule = $attrs.rule.split('.'); let rule = $attrs.rule.split('.');
let modelName = rule.shift(); let modelName = rule.shift();
let fieldName = rule.shift(); let fieldName = rule.shift();
let split = $attrs.ngModel.split('.'); let split = $attrs.ngModel.split('.');
if (!fieldName) fieldName = split.pop() || null; if (!fieldName) fieldName = split.pop() || null;
if (!modelName) modelName = firstUpper(split.pop() || ''); if (!modelName) modelName = firstUpper(split.pop() || '');

View File

@ -68,3 +68,4 @@ Load more results: Cargar más resultados
Send cau: Enviar cau Send cau: Enviar cau
By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc By sending this ticket, all the data related to the error, the section, the user, etc., are already sent.: Al enviar este cau ya se envían todos los datos relacionados con el error, la sección, el usuario, etc
ExplainReason: Explique el motivo por el que no deberia aparecer este fallo ExplainReason: Explique el motivo por el que no deberia aparecer este fallo
You already have the mailAlias: Ya tienes este alias de correo

View File

@ -0,0 +1,12 @@
const LoopBackContext = require('loopback-context');
async function handleObserve(ctx) {
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
}
module.exports = function(Self) {
let Mixin = {
'before save': handleObserve,
'before delete': handleObserve,
};
for (const [listener, handler] of Object.entries(Mixin))
Self.observe(listener, handler);
};

View File

@ -1,15 +0,0 @@
const LoopBackContext = require('loopback-context');
module.exports = function(Self) {
Self.setup = function() {
Self.super_.setup.call(this);
};
Self.observe('before save', async function(ctx) {
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
});
Self.observe('before delete', async function(ctx) {
ctx.options.httpCtx = LoopBackContext.getCurrentContext();
});
};

View File

@ -1,5 +0,0 @@
{
"name": "Loggable",
"base": "VnModel",
"validateUpsert": true
}

View File

@ -45,6 +45,7 @@
"Extension format is invalid": "Extension format is invalid", "Extension format is invalid": "Extension format is invalid",
"NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS", "NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS",
"This client can't be invoiced": "This client can't be invoiced", "This client can't be invoiced": "This client can't be invoiced",
"You must provide the correction information to generate a corrective invoice": "You must provide the correction information to generate a corrective invoice",
"The introduced hour already exists": "The introduced hour already exists", "The introduced hour already exists": "The introduced hour already exists",
"Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket", "Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket",
"Concept cannot be blank": "Concept cannot be blank", "Concept cannot be blank": "Concept cannot be blank",
@ -178,7 +179,8 @@
"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}}", "hasAnyNegativeBase": "Negative basis of tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Positive basis of tickets: {{ticketsIds}}",
"This ticket cannot be left empty.": "This ticket cannot be left empty. %s", "This ticket cannot be left empty.": "This ticket cannot be left empty. %s",
"Social name should be uppercase": "Social name should be uppercase", "Social name should be uppercase": "Social name should be uppercase",
"Street should be uppercase": "Street should be uppercase", "Street should be uppercase": "Street should be uppercase",
@ -200,5 +202,6 @@
"Try again": "Try again", "Try again": "Try again",
"keepPrice": "keepPrice", "keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries", "Cannot past travels with entries": "Cannot past travels with entries",
"It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}" "It was not able to remove the next expeditions:": "It was not able to remove the next expeditions: {{expeditions}}",
"Incorrect pin": "Incorrect pin."
} }

View File

@ -72,6 +72,7 @@
"The secret can't be blank": "La contraseña no puede estar en blanco", "The secret can't be blank": "La contraseña no puede estar en blanco",
"We weren't able to send this SMS": "No hemos podido enviar el SMS", "We weren't able to send this SMS": "No hemos podido enviar el SMS",
"This client can't be invoiced": "Este cliente no puede ser facturado", "This client can't be invoiced": "Este cliente no puede ser facturado",
"You must provide the correction information to generate a corrective invoice": "Debes informar la información de corrección para generar una factura rectificativa",
"This ticket can't be invoiced": "Este ticket no puede ser facturado", "This ticket can't be invoiced": "Este ticket no puede ser facturado",
"You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado", "You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado",
"This ticket can not be modified": "Este ticket no puede ser modificado", "This ticket can not be modified": "Este ticket no puede ser modificado",
@ -305,7 +306,8 @@
"Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico", "Mail not sent": "Se ha producido un fallo al enviar la factura al cliente [{{clientId}}]({{{clientUrl}}}), por favor revisa la dirección de correo electrónico",
"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}}", "hasAnyNegativeBase": "Base negativa para los tickets: {{ticketsIds}}",
"hasAnyPositiveBase": "Base positivas para los tickets: {{ticketsIds}}",
"You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado", "You cannot assign an alias that you are not assigned to": "No puede asignar un alias que no tenga asignado",
"This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s", "This ticket cannot be left empty.": "Este ticket no se puede dejar vacío. %s",
"The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias", "The company has not informed the supplier account for bank transfers": "La empresa no tiene informado la cuenta de proveedor para transferencias bancarias",
@ -329,5 +331,10 @@
"The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mínima", "The amount cannot be less than the minimum": "La cantidad no puede ser menor que la cantidad mínima",
"quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima", "quantityLessThanMin": "La cantidad no puede ser menor que la cantidad mínima",
"Cannot past travels with entries": "No se pueden pasar envíos con entradas", "Cannot past travels with entries": "No se pueden pasar envíos con entradas",
"It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}" "It was not able to remove the next expeditions:": "No se pudo eliminar las siguientes expediciones: {{expeditions}}",
"This user does not have an assigned tablet": "Este usuario no tiene tablet asignada",
"Incorrect pin": "Pin incorrecto.",
"You already have the mailAlias": "Ya tienes este alias de correo",
"The alias cant be modified": "Este alias de correo no puede ser modificado",
"No tickets to invoice": "No hay tickets para facturar"
} }

View File

@ -25,20 +25,19 @@
"FieldAcl": { "FieldAcl": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Role": {
"dataSource": "vn",
"options": {
"mysql": {
"table": "salix.Role"
}
}
},
"RoleMapping": { "RoleMapping": {
"dataSource": "vn", "dataSource": "vn",
"options": { "options": {
"mysql": { "mysql": {
"table": "salix.RoleMapping" "table": "salix.RoleMapping"
} }
},
"relations": {
"role": {
"type": "belongsTo",
"model": "VnRole",
"foreignKey": "roleId"
}
} }
}, },
"Schema": { "Schema": {

View File

@ -14,6 +14,9 @@
"MailAliasAccount": { "MailAliasAccount": {
"dataSource": "vn" "dataSource": "vn"
}, },
"MailAliasAcl": {
"dataSource": "vn"
},
"MailConfig": { "MailConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -1,49 +1,49 @@
{ {
"name": "Account", "name": "Account",
"base": "VnModel", "base": "VnModel",
"options": { "options": {
"mysql": { "mysql": {
"table": "account.account" "table": "account.account"
} }
}, },
"properties": { "properties": {
"id": { "id": {
"id": true "id": true
} }
}, },
"relations": { "relations": {
"user": { "user": {
"type": "belongsTo", "type": "belongsTo",
"model": "VnUser", "model": "VnUser",
"foreignKey": "id" "foreignKey": "id"
},
"aliases": {
"type": "hasMany",
"model": "MailAliasAccount",
"foreignKey": "account"
}
},
"acls": [
{
"property": "login",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}, },
{ "aliases": {
"type": "hasMany",
"model": "MailAliasAccount",
"foreignKey": "account"
}
},
"acls": [
{
"property": "login",
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"property": "logout", "property": "logout",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$authenticated", "principalId": "$authenticated",
"permission": "ALLOW" "permission": "ALLOW"
}, },
{ {
"property": "changePassword", "property": "changePassword",
"accessType": "EXECUTE", "accessType": "EXECUTE",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "$everyone",
"permission": "ALLOW" "permission": "ALLOW"
} }
] ]
} }

View File

@ -239,7 +239,7 @@ module.exports = Self => {
// Prepare data // Prepare data
let roles = await $.Role.find({ let roles = await $.VnRole.find({
fields: ['id', 'name', 'description'] fields: ['id', 'name', 'description']
}); });
let roleRoles = await $.RoleRole.find({ let roleRoles = await $.RoleRole.find({

View File

@ -2,54 +2,44 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY')
return new UserError(`You already have the mailAlias`);
return err;
});
Self.observe('before save', async ctx => { Self.observe('before save', async ctx => {
const changes = ctx.currentInstance || ctx.instance; const changes = ctx.currentInstance || ctx.instance;
await Self.hasGrant(ctx, changes.mailAlias); await checkModifyPermission(ctx, changes.mailAlias);
}); });
Self.observe('before delete', async ctx => { Self.observe('before delete', async ctx => {
const mailAliasAccount = await Self.findById(ctx.where.id); const mailAliasAccount = await Self.findById(ctx.where.id);
await Self.hasGrant(ctx, mailAliasAccount.mailAlias); await checkModifyPermission(ctx, mailAliasAccount.mailAlias);
}); });
/** async function checkModifyPermission(ctx, mailAliasFk) {
* Checks if current user has const userId = ctx.options.accessToken.userId;
* 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 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'); const roles = await models.RoleMapping.find({
if (canEditAlias) return true; fields: ['roleId'],
where: {principalId: userId}
});
const user = await models.VnUser.findById(userId, {fields: ['hasGrant']}); const availableMailAlias = await models.MailAliasAcl.findOne({
if (!user.hasGrant) fields: ['mailAliasFk'],
throw new UserError(`You don't have grant privilege`); include: {relation: 'mailAlias'},
where: {
const account = await models.Account.findById(userId, { roleFk: {
fields: ['id'], inq: roles.map(role => role.roleId),
include: { },
relation: 'aliases', mailAliasFk
scope: {
fields: ['mailAlias']
}
} }
}); });
const aliases = account.aliases().map(alias => alias.mailAlias); if (!availableMailAlias) throw new UserError('The alias cant be modified');
}
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

@ -0,0 +1,31 @@
{
"name": "MailAliasAcl",
"base": "VnModel",
"options": {
"mysql": {
"table": "account.mailAliasAcl"
}
},
"properties": {
"mailAliasFk": {
"id": true,
"type": "number"
},
"roleFk": {
"id": true,
"type": "number"
}
},
"relations": {
"mailAlias": {
"type": "belongsTo",
"model": "MailAlias",
"foreignKey": "mailAliasFk"
},
"role": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "roleFk"
}
}
}

View File

@ -15,12 +15,12 @@
"relations": { "relations": {
"owner": { "owner": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "role" "foreignKey": "role"
}, },
"inherits": { "inherits": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "inheritsFrom" "foreignKey": "inheritsFrom"
} }
} }

View File

@ -14,12 +14,12 @@
"relations": { "relations": {
"owner": { "owner": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "role" "foreignKey": "role"
}, },
"inherits": { "inherits": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "inheritsFrom" "foreignKey": "inheritsFrom"
} }
} }

View File

@ -15,7 +15,7 @@
<vn-autocomplete <vn-autocomplete
label="Role" label="Role"
ng-model="$ctrl.acl.principalId" ng-model="$ctrl.acl.principalId"
url="Roles" url="VnRoles"
id-field="name" id-field="name"
value-field="name" value-field="name"
vn-focus> vn-focus>

View File

@ -4,7 +4,7 @@
<vn-autocomplete <vn-autocomplete
label="Role" label="Role"
ng-model="filter.principalId" ng-model="filter.principalId"
url="Roles" url="VnRoles"
value-field="name"> value-field="name">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete

View File

@ -30,7 +30,7 @@
<vn-autocomplete <vn-autocomplete
label="Role" label="Role"
ng-model="$ctrl.user.roleFk" ng-model="$ctrl.user.roleFk"
url="Roles" url="VnRoles"
rule="VnUser"> rule="VnUser">
</vn-autocomplete> </vn-autocomplete>
<vn-textfield <vn-textfield

View File

@ -22,7 +22,7 @@
<vn-autocomplete <vn-autocomplete
label="Role" label="Role"
ng-model="$ctrl.user.roleFk" ng-model="$ctrl.user.roleFk"
url="Roles"> url="VnRoles">
</vn-autocomplete> </vn-autocomplete>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

@ -1,25 +1,27 @@
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="Roles" url="VnRoles"
data="$ctrl.role" data="$ctrl.role"
form="form"> form="form">
</vn-watcher> </vn-watcher>
<form <form
name="form" name="form"
ng-submit="watcher.submit()" ng-submit="watcher.submit()"
class="vn-w-md"> class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-vertical> <vn-vertical>
<vn-textfield <vn-textfield
label="Name" label="Name"
ng-model="$ctrl.role.name" ng-model="$ctrl.role.name"
rule rule="VnRole.name"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Description" label="Description"
ng-model="$ctrl.role.description" ng-model="$ctrl.role.description"
rule> rule="VnRole.description"
>
</vn-textfield> </vn-textfield>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

@ -3,7 +3,7 @@ import ModuleCard from 'salix/components/module-card';
class Controller extends ModuleCard { class Controller extends ModuleCard {
reload() { reload() {
this.$http.get(`Roles/${this.$params.id}`) this.$http.get(`VnRoles/${this.$params.id}`)
.then(res => this.role = res.data); .then(res => this.role = res.data);
} }
} }

View File

@ -1,6 +1,6 @@
import './index'; import './index';
describe('component vnRoleCard', () => { fdescribe('component vnRoleCard', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
@ -15,7 +15,7 @@ describe('component vnRoleCard', () => {
it('should reload the controller data', () => { it('should reload the controller data', () => {
controller.$params.id = 1; controller.$params.id = 1;
$httpBackend.expectGET('Roles/1').respond('foo'); $httpBackend.expectGET('VnRoles/1').respond('foo');
controller.reload(); controller.reload();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -1,6 +1,6 @@
<vn-watcher <vn-watcher
vn-id="watcher" vn-id="watcher"
url="Roles" url="VnRoles"
data="$ctrl.role" data="$ctrl.role"
insert-mode="true" insert-mode="true"
form="form"> form="form">
@ -14,13 +14,13 @@
<vn-textfield <vn-textfield
label="Name" label="Name"
ng-model="$ctrl.role.name" ng-model="$ctrl.role.name"
rule rule="VnRole.name"
vn-focus> vn-focus>
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield
label="Description" label="Description"
ng-model="$ctrl.role.description" ng-model="$ctrl.role.description"
rule> rule="VnRole.description">
</vn-textfield> </vn-textfield>
</vn-vertical> </vn-vertical>
</vn-card> </vn-card>

View File

@ -11,7 +11,7 @@ class Controller extends Descriptor {
} }
onDelete() { onDelete() {
return this.$http.delete(`Roles/${this.id}`) return this.$http.delete(`VnRoles/${this.id}`)
.then(() => this.$state.go('account.role')) .then(() => this.$state.go('account.role'))
.then(() => this.vnApp.showSuccess(this.$t('Role removed'))); .then(() => this.vnApp.showSuccess(this.$t('Role removed')));
} }

View File

@ -1,6 +1,6 @@
import './index'; import './index';
describe('component vnRoleDescriptor', () => { fdescribe('component vnRoleDescriptor', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
@ -18,7 +18,7 @@ describe('component vnRoleDescriptor', () => {
controller.$state.go = jest.fn(); controller.$state.go = jest.fn();
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.expectDELETE('Roles/1').respond(); $httpBackend.expectDELETE('VnRoles/1').respond();
controller.onDelete(); controller.onDelete();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -1,6 +1,6 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="Roles" url="VnRoles"
filter="::$ctrl.filter" filter="::$ctrl.filter"
limit="20"> limit="20">
</vn-crud-model> </vn-crud-model>

View File

@ -40,7 +40,7 @@
<vn-autocomplete <vn-autocomplete
label="Role" label="Role"
ng-model="$ctrl.addData.inheritsFrom" ng-model="$ctrl.addData.inheritsFrom"
url="Roles" url="VnRoles"
vn-focus> vn-focus>
</vn-autocomplete> </vn-autocomplete>
</tpl-body> </tpl-body>

View File

@ -6,7 +6,7 @@ class Controller extends Component {
this._role = value; this._role = value;
this.$.summary = null; this.$.summary = null;
if (!value) return; if (!value) return;
this.$http.get(`Roles/${value.id}`) this.$http.get(`VnRoles/${value.id}`)
.then(res => this.$.summary = res.data); .then(res => this.$.summary = res.data);
} }

View File

@ -19,7 +19,7 @@
vn-one vn-one
label="Role" label="Role"
ng-model="filter.roleFk" ng-model="filter.roleFk"
url="Roles" url="VnRoles"
value-field="id" value-field="id"
show-field="name"> show-field="name">
</vn-autocomplete> </vn-autocomplete>

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimBeginning", "name": "ClaimBeginning",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimBeginning" "table": "claimBeginning"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimDevelopment", "name": "ClaimDevelopment",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimDevelopment" "table": "claimDevelopment"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimDms", "name": "ClaimDms",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimDms" "table": "claimDms"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimEnd", "name": "ClaimEnd",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimEnd" "table": "claimEnd"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimObservation", "name": "ClaimObservation",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimObservation" "table": "claimObservation"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClaimState", "name": "ClaimState",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claimState" "table": "claimState"
@ -32,7 +35,7 @@
"relations": { "relations": {
"writeRole": { "writeRole": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "roleFk" "foreignKey": "roleFk"
} }
}, },

View File

@ -1,6 +1,9 @@
{ {
"name": "Claim", "name": "Claim",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "claim" "table": "claim"

View File

@ -1,7 +1,7 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = function(Self) { module.exports = function(Self) {
Self.remoteMethodCtx('canBeInvoiced', { Self.remoteMethod('canBeInvoiced', {
description: 'Change property isEqualizated in all client addresses', description: 'Change property isEqualizated in all client addresses',
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [

View File

@ -16,7 +16,7 @@ describe('client consumption() filter', () => {
}; };
const result = await models.Client.consumption(ctx, filter, options); const result = await models.Client.consumption(ctx, filter, options);
expect(result.length).toEqual(10); expect(result.length).toEqual(11);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -49,7 +49,7 @@ describe('client consumption() filter', () => {
const thirdRow = result[2]; const thirdRow = result[2];
expect(result.length).toEqual(3); expect(result.length).toEqual(3);
expect(firstRow.quantity).toEqual(10); expect(firstRow.quantity).toEqual(11);
expect(secondRow.quantity).toEqual(15); expect(secondRow.quantity).toEqual(15);
expect(thirdRow.quantity).toEqual(20); expect(thirdRow.quantity).toEqual(20);

View File

@ -113,9 +113,6 @@
"SageTransactionType": { "SageTransactionType": {
"dataSource": "vn" "dataSource": "vn"
}, },
"TicketSms": {
"dataSource": "vn"
},
"TpvError": { "TpvError": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -1,7 +1,10 @@
{ {
"name": "Address", "name": "Address",
"description": "Client addresses", "description": "Client addresses",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "address" "table": "address"

View File

@ -1,7 +1,10 @@
{ {
"name": "ClientContact", "name": "ClientContact",
"description": "Client phone contacts", "description": "Client phone contacts",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "clientContact" "table": "clientContact"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClientDms", "name": "ClientDms",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "clientDms" "table": "clientDms"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClientInforma", "name": "ClientInforma",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"log": { "log": {
"model":"ClientLog", "model":"ClientLog",
"relation": "client", "relation": "client",

View File

@ -1,7 +1,10 @@
{ {
"name": "ClientObservation", "name": "ClientObservation",
"description": "Client notes", "description": "Client notes",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "clientObservation" "table": "clientObservation"

View File

@ -1,6 +1,9 @@
{ {
"name": "ClientSample", "name": "ClientSample",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "clientSample" "table": "clientSample"

View File

@ -21,6 +21,11 @@
"type": "belongsTo", "type": "belongsTo",
"model": "Sms", "model": "Sms",
"foreignKey": "smsFk" "foreignKey": "smsFk"
},
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk"
} }
} }
} }

View File

@ -1,6 +1,9 @@
{ {
"name": "Client", "name": "Client",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "client" "table": "client"

View File

@ -1,6 +1,9 @@
{ {
"name": "Greuge", "name": "Greuge",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "greuge" "table": "greuge"

View File

@ -1,6 +1,9 @@
{ {
"name": "Recovery", "name": "Recovery",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "recovery" "table": "recovery"

View File

@ -19,7 +19,7 @@
"relations": { "relations": {
"role": { "role": {
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "VnRole",
"foreignKey": "roleFk" "foreignKey": "roleFk"
} }
} }

View File

@ -1,28 +0,0 @@
{
"name": "TicketSms",
"base": "VnModel",
"options": {
"mysql": {
"table": "ticketSms"
}
},
"properties": {
"smsFk": {
"type": "number",
"id": true,
"description": "Identifier"
}
},
"relations": {
"ticket": {
"type": "belongsTo",
"model": "Ticket",
"foreignKey": "ticketFk"
},
"sms": {
"type": "belongsTo",
"model": "Sms",
"foreignKey": "smsFk"
}
}
}

View File

@ -1,40 +1,2 @@
<vn-crud-model <vn-card>
vn-id="model" </vn-card>
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-lg">
<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 expand vn-tooltip="{{::clientSms.sms.message}}">{{::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

@ -1,32 +1,14 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
export default class Controller extends Section { class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
}
this.filter = { async $onInit() {
fields: ['id', 'smsFk'], this.$state.go('client.card.summary', {id: this.$params.id});
include: { window.location.href = await this.vnApp.getUrl(`Customer/${this.$params.id}/sms`);
relation: 'sms',
scope: {
fields: [
'senderFk',
'sender',
'destination',
'message',
'statusCode',
'status',
'created'],
include: {
relation: 'sender',
scope: {
fields: ['name']
}
}
}
}
};
} }
} }

View File

@ -1,6 +1,9 @@
{ {
"name": "Buy", "name": "Buy",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "buy" "table": "buy"

View File

@ -1,6 +1,9 @@
{ {
"name": "EntryObservation", "name": "EntryObservation",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "entryObservation" "table": "entryObservation"

View File

@ -1,6 +1,9 @@
{ {
"name": "Entry", "name": "Entry",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "entry" "table": "entry"

View File

@ -1,6 +1,9 @@
{ {
"name": "InvoiceInTax", "name": "InvoiceInTax",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "invoiceInTax" "table": "invoiceInTax"

View File

@ -1,6 +1,9 @@
{ {
"name": "InvoiceIn", "name": "InvoiceIn",
"base": "Loggable", "base": "VnModel",
"mixins": {
"Loggable": true
},
"options": { "options": {
"mysql": { "mysql": {
"table": "invoiceIn" "table": "invoiceIn"

View File

@ -85,7 +85,7 @@ module.exports = Self => {
throw new UserError(`A ticket with an amount of zero can't be invoiced`); throw new UserError(`A ticket with an amount of zero can't be invoiced`);
// Validates ticket nagative base // Validates ticket nagative base
const hasNegativeBase = await getNegativeBase(ticketId, myOptions); const hasNegativeBase = await getNegativeBase(maxShipped, clientId, companyId, myOptions);
if (hasNegativeBase && company.code == 'VNL') if (hasNegativeBase && company.code == 'VNL')
throw new UserError(`A ticket with a negative base can't be invoiced`); throw new UserError(`A ticket with a negative base can't be invoiced`);
} else { } else {
@ -162,10 +162,13 @@ module.exports = Self => {
return result.invoiceable; return result.invoiceable;
} }
async function getNegativeBase(ticketId, options) { async function getNegativeBase(maxShipped, clientId, companyId, options) {
const models = Self.app.models; const models = Self.app.models;
const query = 'SELECT vn.hasSomeNegativeBase(?) AS base'; await models.InvoiceOut.rawSql('CALL invoiceOut_exportationFromClient(?,?,?)',
const [result] = await models.InvoiceOut.rawSql(query, [ticketId], options); [maxShipped, clientId, companyId], options
);
const query = 'SELECT vn.hasAnyNegativeBase() AS base';
const [result] = await models.InvoiceOut.rawSql(query, [], options);
return result.base; return result.base;
} }

View File

@ -80,6 +80,7 @@ module.exports = Self => {
invoiceType, invoiceType,
args.companyFk, args.companyFk,
args.invoiceDate, args.invoiceDate,
null,
options options
); );

View File

@ -90,7 +90,7 @@ module.exports = Self => {
AND t.refFk IS NULL AND t.refFk IS NULL
AND c.typeFk IN ('normal','trust') AND c.typeFk IN ('normal','trust')
GROUP BY t.clientFk, negativeBase.taxableBase GROUP BY t.clientFk, negativeBase.taxableBase
HAVING amount <> 0`, [args.from, args.to])); HAVING amount < 0`, [args.from, args.to]));
stmt = new ParameterizedSQL(` stmt = new ParameterizedSQL(`
SELECT f.* SELECT f.*

View File

@ -10,13 +10,17 @@ module.exports = Self => {
type: 'date', type: 'date',
description: 'From date', description: 'From date',
required: true required: true
}, }, {
{
arg: 'to', arg: 'to',
type: 'date', type: 'date',
description: 'To date', description: 'To date',
required: true required: true
}], }, {
arg: 'filter',
type: 'object',
description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string'
},
],
returns: [ returns: [
{ {
arg: 'body', arg: 'body',

View File

@ -2,7 +2,7 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('InvoiceOut tranferInvoice()', () => { describe('InvoiceOut transferInvoice()', () => {
const activeCtx = { const activeCtx = {
accessToken: {userId: 5}, accessToken: {userId: 5},
http: { http: {
@ -23,20 +23,29 @@ describe('InvoiceOut tranferInvoice()', () => {
const tx = await models.InvoiceOut.beginTransaction({}); const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx}; const options = {transaction: tx};
const args = { const args = {
id: '1', id: '4',
ref: 'T4444444', refFk: 'T4444444',
newClientFk: 1, newClientFk: 1,
cplusRectificationId: 1, cplusRectificationTypeFk: 1,
siiTypeInvoiceOutId: 1, siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeId: 1 invoiceCorrectionTypeFk: 1
}; };
ctx.args = args; ctx.args = args;
try { try {
const {clientFk: oldClient} = await models.InvoiceOut.findById(args.id, {fields: ['clientFk']});
const invoicesBefore = await models.InvoiceOut.find({}, options);
const result = await models.InvoiceOut.transferInvoice( const result = await models.InvoiceOut.transferInvoice(
ctx, ctx,
options); options);
const invoicesAfter = await models.InvoiceOut.find({}, options);
const rectificativeInvoice = invoicesAfter[invoicesAfter.length - 2];
const newInvoice = invoicesAfter[invoicesAfter.length - 1];
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(invoicesAfter.length - invoicesBefore.length).toEqual(2);
expect(rectificativeInvoice.clientFk).toEqual(oldClient);
expect(newInvoice.clientFk).toEqual(args.newClientFk);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
@ -49,20 +58,44 @@ describe('InvoiceOut tranferInvoice()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
const args = { const args = {
id: '1', id: '1',
ref: 'T1111111', refFk: 'T1111111',
newClientFk: 1101, newClientFk: 1101,
cplusRectificationId: 1, cplusRectificationTypeFk: 1,
siiTypeInvoiceOutId: 1, siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeId: 1 invoiceCorrectionTypeFk: 1
}; };
ctx.args = args; ctx.args = args;
try { try {
await models.InvoiceOut.transferInvoice( await models.InvoiceOut.transferInvoice(
ctx, ctx,
options); options);
await tx.rollback();
} catch (e) { } catch (e) {
expect(e.message).toBe(`Select a different client`); expect(e.message).toBe(`Select a different client`);
await tx.rollback(); await tx.rollback();
} }
}); });
it('should throw an UserError when it is refund', async() => {
const tx = await models.InvoiceOut.beginTransaction({});
const options = {transaction: tx};
const args = {
id: '1',
refFk: 'T1111111',
newClientFk: 1102,
cplusRectificationTypeFk: 1,
siiTypeInvoiceOutFk: 1,
invoiceCorrectionTypeFk: 1
};
ctx.args = args;
try {
await models.InvoiceOut.transferInvoice(
ctx,
options);
await tx.rollback();
} catch (e) {
expect(e.message).toContain(`This ticket is already a refund`);
await tx.rollback();
}
});
}); });

View File

@ -12,7 +12,7 @@ module.exports = Self => {
description: 'Issued invoice id' description: 'Issued invoice id'
}, },
{ {
arg: 'ref', arg: 'refFk',
type: 'string', type: 'string',
required: true required: true
}, },
@ -22,17 +22,17 @@ module.exports = Self => {
required: true required: true
}, },
{ {
arg: 'cplusRectificationId', arg: 'cplusRectificationTypeFk',
type: 'number', type: 'number',
required: true required: true
}, },
{ {
arg: 'siiTypeInvoiceOutId', arg: 'siiTypeInvoiceOutFk',
type: 'number', type: 'number',
required: true required: true
}, },
{ {
arg: 'invoiceCorrectionTypeId', arg: 'invoiceCorrectionTypeFk',
type: 'number', type: 'number',
required: true required: true
}, },
@ -50,14 +50,14 @@ module.exports = Self => {
Self.transferInvoice = async(ctx, options) => { Self.transferInvoice = async(ctx, options) => {
const models = Self.app.models; const models = Self.app.models;
const myOptions = {userId: ctx.req.accessToken.userId}; const myOptions = {userId: ctx.req.accessToken.userId};
const args = ctx.args; const {id, refFk, newClientFk, cplusRectificationTypeFk, siiTypeInvoiceOutFk, invoiceCorrectionTypeFk} = ctx.args;
let tx; let tx;
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const {clientFk} = await models.InvoiceOut.findById(args.id); const {clientFk} = await models.InvoiceOut.findById(id);
if (clientFk == args.newClientFk) if (clientFk == newClientFk)
throw new UserError(`Select a different client`); throw new UserError(`Select a different client`);
if (!myOptions.transaction) { if (!myOptions.transaction) {
@ -65,10 +65,10 @@ module.exports = Self => {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
try { try {
const filterRef = {where: {refFk: args.ref}}; const filterRef = {where: {refFk: refFk}};
const tickets = await models.Ticket.find(filterRef, myOptions); const tickets = await models.Ticket.find(filterRef, myOptions);
const ticketsIds = tickets.map(ticket => ticket.id); const ticketsIds = tickets.map(ticket => ticket.id);
await models.Ticket.refund(ctx, ticketsIds, null, myOptions); const refundTickets = await models.Ticket.refund(ctx, ticketsIds, null, myOptions);
const filterTicket = {where: {ticketFk: {inq: ticketsIds}}}; const filterTicket = {where: {ticketFk: {inq: ticketsIds}}};
@ -82,20 +82,16 @@ module.exports = Self => {
const clonedTicketIds = []; const clonedTicketIds = [];
for (const clonedTicket of clonedTickets) { for (const clonedTicket of clonedTickets) {
await clonedTicket.updateAttribute('clientFk', args.newClientFk, myOptions); await clonedTicket.updateAttribute('clientFk', newClientFk, myOptions);
clonedTicketIds.push(clonedTicket.id); clonedTicketIds.push(clonedTicket.id);
} }
const invoiceIds = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, myOptions); const invoiceCorrection =
const [invoiceId] = invoiceIds; {correctedFk: id, cplusRectificationTypeFk, siiTypeInvoiceOutFk, invoiceCorrectionTypeFk};
const refundTicketIds = refundTickets.map(ticket => ticket.id);
await models.InvoiceCorrection.create({ await models.Ticket.invoiceTickets(ctx, refundTicketIds, invoiceCorrection, myOptions);
correctingFk: invoiceId, const [invoiceId] = await models.Ticket.invoiceTickets(ctx, clonedTicketIds, null, myOptions);
correctedFk: args.id,
cplusRectificationTypeFk: args.cplusRectificationId,
siiTypeInvoiceOutFk: args.siiTypeInvoiceOutId,
invoiceCorrectionTypeFk: args.invoiceCorrectionTypeId
}, myOptions);
if (tx) { if (tx) {
await tx.commit(); await tx.commit();

View File

@ -16,13 +16,43 @@
"type": "number" "type": "number"
}, },
"cplusRectificationTypeFk": { "cplusRectificationTypeFk": {
"type": "number" "type": "number",
"required": true
}, },
"siiTypeInvoiceOutFk": { "siiTypeInvoiceOutFk": {
"type": "number" "type": "number",
"required": true
}, },
"invoiceCorrectionTypeFk": { "invoiceCorrectionTypeFk": {
"type": "number" "type": "number",
"required": true
},
"relations": {
"correcting": {
"type": "belongsTo",
"model": "InvoiceOut",
"foreignKey": "correctingFk"
},
"corrected": {
"type": "belongsTo",
"model": "InvoiceOut",
"foreignKey": "correctedFk"
},
"cplusRectificationType": {
"type": "belongsTo",
"model": "cplusRectificationType",
"foreignKey": "cplusRectificationTypeFk"
},
"siiTypeInvoiceOut": {
"type": "belongsTo",
"model": "siiTypeInvoiceOut",
"foreignKey": "siiTypeInvoiceOutFk"
},
"invoiceCorrectionType": {
"type": "belongsTo",
"model": "invoiceCorrectionType",
"foreignKey": "invoiceCorrectionTypeFk"
}
} }
} }
} }

View File

@ -12,6 +12,9 @@
"type": "number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"code": {
"type": "string"
},
"description": { "description": {
"type": "string" "type": "string"
} }

View File

@ -7,7 +7,8 @@
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"
url="SiiTypeInvoiceOuts" url="SiiTypeInvoiceOuts"
data="siiTypeInvoiceOut"> data="siiTypeInvoiceOuts"
where="{code: {like: 'R%'}}">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model
auto-load="true" auto-load="true"
@ -185,64 +186,69 @@
vn-id="transferInvoice" vn-id="transferInvoice"
title="transferInvoice" title="transferInvoice"
on-accept="$ctrl.transferInvoice()"> on-accept="$ctrl.transferInvoice()">
<tpl-title translate>
transferInvoice
</tpl-title>
<tpl-body> <tpl-body>
<section class="transferInvoice"> <section class="transferInvoice">
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-one vn-one
vn-id="client" vn-id="client"
required="true" required="true"
url="Clients" url="Clients"
label="Client" label="Client"
show-field="name" show-field="name"
value-field="id" value-field="id"
search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}" search-function="{or: [{id: $search}, {name: {like: '%'+ $search +'%'}}]}"
ng-model="$ctrl.invoiceOut.client.id" ng-model="$ctrl.clientId"
initial-data="$ctrl.invoiceOut.client.id" order="id">
order="id"> <tpl-item>
<tpl-item> #{{id}} - {{::name}}
#{{id}} - {{::name}} </tpl-item>
</tpl-item> </vn-autocomplete>
</vn-autocomplete> <vn-autocomplete
<vn-autocomplete vn-one
vn-one vn-id="cplusRectificationType"
vn-id="cplusRectificationType" required="true"
required="true" data="cplusRectificationTypes"
data="cplusRectificationTypes" show-field="description"
show-field="description" value-field="id"
value-field="id" ng-model="$ctrl.cplusRectificationType"
ng-model="$ctrl.cplusRectificationType" search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}" label="Rectificative type">
label="Cplus Type"> <tpl-item>
<tpl-item> {{::description}}
{{::description}} </tpl-item>
</tpl-item> </vn-autocomplete>
</vn-autocomplete> </vn-horizontal>
</vn-horizontal> <vn-horizontal>
<vn-horizontal> <vn-autocomplete
<vn-autocomplete vn-one
vn-one vn-id="siiTypeInvoiceOut"
vn-id="cplusInvoiceType" data="siiTypeInvoiceOuts"
data="siiTypeInvoiceOut" show-field="description"
show-field="description" value-field="id"
value-field="id" fields="['id','code','description']"
required="true" required="true"
ng-model="$ctrl.siiTypeInvoiceOut" ng-model="$ctrl.siiTypeInvoiceOut"
search-function="{or: [{id: $search}, {description: {like: '%'+ $search +'%'}}]}" label="Class">
label="Class"> <tpl-item>
</vn-autocomplete> {{::code}} - {{::description}}
<vn-autocomplete </tpl-item>
vn-one </vn-autocomplete>
vn-id="invoiceCorrectionType" <vn-autocomplete
data="invoiceCorrectionTypes" vn-one
ng-model="$ctrl.invoiceCorrectionType" vn-id="invoiceCorrectionType"
show-field="description" data="invoiceCorrectionTypes"
value-field="id" ng-model="$ctrl.invoiceCorrectionType"
required="true" show-field="description"
label="Type"> value-field="id"
</vn-autocomplete> required="true"
</vn-horizontal> label="Type">
</section> </vn-autocomplete>
</vn-horizontal>
</section>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>
<button response="accept" translate>Transfer client</button> <button response="accept" translate>Transfer client</button>

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