refactor: fix conficts refs #6276

This commit is contained in:
Jorge Penadés 2024-01-04 08:51:18 +01:00
commit 6d0e1cc47c
61 changed files with 1371 additions and 886 deletions

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

@ -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

@ -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

@ -95,27 +95,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,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,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,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),
@ -3011,6 +3012,12 @@ INSERT INTO `vn`.`invoiceCorrectionType` (`id`, `description`)
(2, 'Error in sales details'), (2, 'Error in sales details'),
(3, 'Error in customer data'); (3, 'Error in customer data');
INSERT INTO `account`.`mailAliasAcl` (`mailAliasFk`, `roleFk`)
VALUES
(1, 1),
(2, 9),
(3, 15);
INSERT INTO `vn`.`docuwareTablet` (`tablet`,`description`) INSERT INTO `vn`.`docuwareTablet` (`tablet`,`description`)
VALUES VALUES
('Tablet1','Jarvis tablet'), ('Tablet1','Jarvis tablet'),

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

@ -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

@ -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",
@ -201,5 +203,6 @@
"keepPrice": "keepPrice", "keepPrice": "keepPrice",
"Cannot past travels with entries": "Cannot past travels with entries", "Cannot past travels with entries": "Cannot past travels with entries",
"The notification subscription of this worker cant be modified": "The notification subscription of this worker cant be modified", "The notification subscription of this worker cant be modified": "The notification subscription of this worker cant be modified",
"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",
@ -331,5 +333,9 @@
"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}}",
"The line could not be marked": "The line could not be marked", "The line could not be marked": "The line could not be marked",
"This user does not have an assigned tablet": "Este usuario no tiene tablet asignada" "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

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

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

@ -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

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

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>

View File

@ -129,15 +129,15 @@ class Controller extends Section {
transferInvoice() { transferInvoice() {
const params = { const params = {
id: this.invoiceOut.id, id: this.invoiceOut.id,
ref: this.invoiceOut.ref, refFk: this.invoiceOut.ref,
newClientFk: this.invoiceOut.client.id, newClientFk: this.clientId,
cplusRectificationId: this.cplusRectificationType, cplusRectificationTypeFk: this.cplusRectificationType,
siiTypeInvoiceOutId: this.siiTypeInvoiceOut, siiTypeInvoiceOutFk: this.siiTypeInvoiceOut,
invoiceCorrectionTypeId: this.invoiceCorrectionType invoiceCorrectionTypeFk: this.invoiceCorrectionType
}; };
this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => { this.$http.post(`InvoiceOuts/transferInvoice`, params).then(res => {
const invoiceId = res.data; const invoiceId = res.data;
this.vnApp.showSuccess(this.$t('Invoice trasfered!')); this.vnApp.showSuccess(this.$t('Transferred invoice'));
this.$state.go('invoiceOut.card.summary', {id: invoiceId}); this.$state.go('invoiceOut.card.summary', {id: invoiceId});
}); });
} }

View File

@ -22,4 +22,5 @@ The email can't be empty: El correo no puede estar vacío
The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}" The following refund tickets have been created: "Se han creado los siguientes tickets de abono: {{ticketIds}}"
Refund...: Abono... Refund...: Abono...
Transfer invoice to...: Transferir factura a... Transfer invoice to...: Transferir factura a...
Cplus Type: Cplus Tipo Rectificative type: Tipo rectificativa
Transferred invoice: Factura transferida

View File

@ -6,6 +6,7 @@
auto-load="true" auto-load="true"
url="InvoiceOutSerials" url="InvoiceOutSerials"
data="invoiceOutSerials" data="invoiceOutSerials"
where="{code: {neq: 'R'}}"
order="code"> order="code">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model

View File

@ -20,18 +20,10 @@
"type": "number", "type": "number",
"required": true "required": true
}, },
"isPreviousPreparedByPacking": {
"type": "boolean",
"required": true
},
"code": { "code": {
"type": "string", "type": "string",
"required": false "required": false
}, },
"isPreviousPrepared": {
"type": "boolean",
"required": true
},
"isPackagingArea": { "isPackagingArea": {
"type": "boolean", "type": "boolean",
"required": true "required": true

View File

@ -19,7 +19,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: ['number'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -54,7 +54,7 @@ module.exports = Self => {
if (tx) await tx.commit(); if (tx) await tx.commit();
return refundsTicket[0]; return refundsTicket;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
throw e; throw e;

View File

@ -23,9 +23,9 @@ describe('Sale refund()', () => {
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const refundedTicket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options); const refundedTickets = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
expect(refundedTicket).toBeDefined(); expect(refundedTickets).toBeDefined();
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -42,11 +42,11 @@ describe('Sale refund()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
const ticketsBefore = await models.Ticket.find({}, options); const ticketsBefore = await models.Ticket.find({}, options);
const ticket = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options); const tickets = await models.Sale.refund(ctx, salesIds, servicesIds, withWarehouse, options);
const refundedTicket = await models.Ticket.findOne({ const refundedTicket = await models.Ticket.findOne({
where: { where: {
id: ticket.id id: tickets[0].id
}, },
include: [ include: [
{ {

View File

@ -10,20 +10,20 @@ module.exports = function(Self) {
description: 'The tickets id', description: 'The tickets id',
type: ['number'], type: ['number'],
required: true required: true
},
{
arg: 'isRectificative',
description: 'If it is rectificative',
type: 'boolean'
} }
], ],
returns: {
arg: 'data',
type: 'boolean',
root: true
},
http: { http: {
path: `/canBeInvoiced`, path: `/canBeInvoiced`,
verb: 'get' verb: 'get'
} }
}); });
Self.canBeInvoiced = async(ctx, ticketsIds, options) => { Self.canBeInvoiced = async(ctx, ticketsIds, isRectificative, options) => {
const myOptions = {}; const myOptions = {};
const $t = ctx.req.__; // $translate const $t = ctx.req.__; // $translate
@ -34,26 +34,14 @@ module.exports = function(Self) {
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, },
fields: ['id', 'refFk', 'shipped', 'totalWithVat', 'companyFk'] fields: ['id', 'refFk', 'shipped', 'totalWithVat']
}, myOptions); }, myOptions);
const [firstTicket] = tickets;
const companyFk = firstTicket.companyFk;
const query =
`SELECT COUNT(*) isSpanishCompany
FROM supplier s
JOIN country c ON c.id = s.countryFk
AND c.code = 'ES'
WHERE s.id = ?`;
const [supplierCompany] = await Self.rawSql(query, [companyFk], options);
const isSpanishCompany = supplierCompany?.isSpanishCompany;
const [result] = await Self.rawSql('SELECT hasAnyNegativeBase() AS base', null, options);
const hasAnyNegativeBase = result?.base && isSpanishCompany;
if (hasAnyNegativeBase)
throw new UserError($t('Negative basis of tickets', {ticketsIds: ticketsIds}));
const taxBaseFunction = isRectificative ? 'hasAnyPositiveBase' : 'hasAnyNegativeBase';
const [hasAnyIncorrectBase] =
await Self.rawSql(`SELECT ${taxBaseFunction}() AS hasBasesProblem`, null, options);
if (hasAnyIncorrectBase?.hasBasesProblem)
throw new UserError($t(taxBaseFunction, {ticketsIds: ticketsIds}));
const today = Date.vnNew(); const today = Date.vnNew();
tickets.some(ticket => { tickets.some(ticket => {
@ -70,7 +58,5 @@ module.exports = function(Self) {
if (ticketsIds.length == 1 && priceZero) if (ticketsIds.length == 1 && priceZero)
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`);
}); });
return true;
}; };
}; };

View File

@ -10,7 +10,13 @@ module.exports = function(Self) {
description: 'The tickets id', description: 'The tickets id',
type: ['number'], type: ['number'],
required: true required: true
},
{
arg: 'invoiceCorrection',
description: 'The invoice correction',
type: 'object',
} }
], ],
returns: { returns: {
type: ['object'], type: ['object'],
@ -22,7 +28,7 @@ module.exports = function(Self) {
} }
}); });
Self.invoiceTickets = async(ctx, ticketsIds, options) => { Self.invoiceTickets = async(ctx, ticketsIds, invoiceCorrection, options) => {
const models = Self.app.models; const models = Self.app.models;
const date = Date.vnNew(); const date = Date.vnNew();
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
@ -41,10 +47,10 @@ module.exports = function(Self) {
let invoicesIds = []; let invoicesIds = [];
try { try {
const tickets = await models.Ticket.find({ const tickets = await models.Ticket.find({
fields: ['id', 'clientFk', 'companyFk', 'addressFk'],
where: { where: {
id: {inq: ticketsIds} id: {inq: ticketsIds}
}, }
fields: ['id', 'clientFk', 'companyFk']
}, myOptions); }, myOptions);
const [firstTicket] = tickets; const [firstTicket] = tickets;
@ -55,22 +61,20 @@ module.exports = function(Self) {
if (!isSameClient) if (!isSameClient)
throw new UserError(`You can't invoice tickets from multiple clients`); throw new UserError(`You can't invoice tickets from multiple clients`);
const client = await models.Client.findById(clientId, { const {hasToInvoiceByAddress} = await models.Client.findById(clientId, {
fields: ['id', 'hasToInvoiceByAddress'] fields: ['hasToInvoiceByAddress']
}, myOptions); }, myOptions);
if (client.hasToInvoiceByAddress) { let ticketsByAddress = hasToInvoiceByAddress
const query = ` ? Object.values(tickets.reduce((group, {id, addressFk}) => {
SELECT DISTINCT addressFk group[addressFk] = group[addressFk] ?? [];
FROM ticket t group[addressFk].push(id);
WHERE id IN (?)`; return group;
const result = await Self.rawSql(query, [ticketsIds], myOptions); }, {}))
: [ticketsIds];
const addressIds = result.map(address => address.addressFk); for (const ticketIds of ticketsByAddress)
for (const address of addressIds) invoicesIds.push(await createInvoice(ctx, companyId, ticketIds, invoiceCorrection, myOptions));
await createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions);
} else
await createInvoice(ctx, companyId, ticketsIds, null, invoicesIds, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
} catch (e) { } catch (e) {
@ -85,9 +89,8 @@ module.exports = function(Self) {
return invoicesIds; return invoicesIds;
}; };
async function createInvoice(ctx, companyId, ticketsIds, address, invoicesIds, myOptions) { async function createInvoice(ctx, companyId, ticketsIds, invoiceCorrection, myOptions) {
const models = Self.app.models; const models = Self.app.models;
await models.Ticket.rawSql(` await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id)) (PRIMARY KEY (id))
@ -95,11 +98,8 @@ module.exports = function(Self) {
SELECT id SELECT id
FROM vn.ticket FROM vn.ticket
WHERE id IN (?) WHERE id IN (?)
${address ? `AND addressFk = ${address}` : ''}
`, [ticketsIds], myOptions); `, [ticketsIds], myOptions);
return models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), invoiceCorrection, myOptions);
const invoiceId = await models.Ticket.makeInvoice(ctx, 'R', companyId, Date.vnNew(), myOptions);
invoicesIds.push(invoiceId);
} }
}; };

View File

@ -22,6 +22,11 @@ module.exports = function(Self) {
description: 'The invoice date', description: 'The invoice date',
type: 'date', type: 'date',
required: true required: true
},
{
arg: 'invoiceCorrection',
description: 'The invoice correction',
type: 'object',
} }
], ],
returns: { returns: {
@ -34,7 +39,7 @@ module.exports = function(Self) {
} }
}); });
Self.makeInvoice = async(ctx, invoiceType, companyFk, invoiceDate, options) => { Self.makeInvoice = async(ctx, invoiceType, companyFk, invoiceDate, invoiceCorrection, options) => {
const models = Self.app.models; const models = Self.app.models;
invoiceDate.setHours(0, 0, 0, 0); invoiceDate.setHours(0, 0, 0, 0);
@ -62,20 +67,24 @@ module.exports = function(Self) {
fields: ['id', 'clientFk', 'addressFk'] fields: ['id', 'clientFk', 'addressFk']
}, myOptions); }, myOptions);
await models.Ticket.canBeInvoiced(ctx, ticketsIds, myOptions); await models.Ticket.canBeInvoiced(ctx, ticketsIds, !!invoiceCorrection, myOptions);
const [firstTicket] = tickets; const [firstTicket] = tickets;
const clientId = firstTicket.clientFk; const clientId = firstTicket.clientFk;
const clientCanBeInvoiced = await models.Client.canBeInvoiced(clientId, companyFk, 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`);
const query = `SELECT vn.invoiceSerial(?, ?, ?) AS serial`; const [{serial}] = invoiceCorrection ? [{serial: 'R'}] : await Self.rawSql(
const [{serial}] = await Self.rawSql(query, [ `SELECT vn.invoiceSerial(?, ?, ?) AS serial`,
clientId, [
companyFk, clientId,
invoiceType, companyFk,
], myOptions); invoiceType
],
myOptions);
const invoiceOutSerial = await models.InvoiceOutSerial.findById(serial); const invoiceOutSerial = await models.InvoiceOutSerial.findById(serial);
if (invoiceOutSerial?.taxAreaFk == 'WORLD') { if (invoiceOutSerial?.taxAreaFk == 'WORLD') {
@ -87,11 +96,17 @@ module.exports = function(Self) {
await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions); await Self.rawSql('CALL invoiceOut_new(?, ?, null, @invoiceId)', [serial, invoiceDate], myOptions);
const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions); const [resultInvoice] = await Self.rawSql('SELECT @invoiceId id', [], myOptions);
if (!resultInvoice) if (!resultInvoice?.id)
throw new UserError('No tickets to invoice', 'notInvoiced'); throw new UserError('No tickets to invoice', 'notInvoiced');
if (serial != 'R' && resultInvoice.id) if (invoiceCorrection) {
await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions); await models.InvoiceCorrection.create(
Object.assign(invoiceCorrection, {correctingFk: resultInvoice.id}),
myOptions
);
}
await Self.rawSql('CALL invoiceOutBooking(?)', [resultInvoice.id], myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -15,7 +15,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: ['number'], type: ['object'],
root: true root: true
}, },
http: { http: {

View File

@ -27,10 +27,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketId], options); `, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options); await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
@ -59,10 +56,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketId], options); `, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options); await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
@ -95,10 +89,7 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketId], options); `, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options); await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
expect(canBeInvoiced).toEqual(false);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;
@ -123,14 +114,36 @@ describe('ticket canBeInvoiced()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketId], options); `, [ticketId], options);
const canBeInvoiced = await models.Ticket.canBeInvoiced(ctx, [ticketId], options); await models.Ticket.canBeInvoiced(ctx, [ticketId], false, options);
expect(canBeInvoiced).toEqual(true);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
await tx.rollback(); await tx.rollback();
throw e; throw e;
} }
}); });
it('should return falsy for a ticket has positiveBase', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
await models.Ticket.rawSql(`
CREATE OR REPLACE TEMPORARY TABLE tmp.ticketToInvoice
(PRIMARY KEY (id))
ENGINE = MEMORY
SELECT id
FROM vn.ticket
WHERE id IN (?)
`, [ticketId], options);
await models.Ticket.canBeInvoiced(ctx, [ticketId], true, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error.message).toEqual(`hasAnyPositiveBase`);
});
}); });

View File

@ -31,7 +31,7 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
const ticketsIds = [11, 16]; const ticketsIds = [11, 16];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options); await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -57,7 +57,7 @@ describe('ticket invoiceTickets()', () => {
await client.updateAttribute('isTaxDataChecked', false, options); await client.updateAttribute('isTaxDataChecked', false, options);
const ticketsIds = [11]; const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options); await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -80,8 +80,8 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
const ticketsIds = [11]; const ticketsIds = [11];
await models.Ticket.invoiceTickets(ctx, ticketsIds, options); await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await models.Ticket.invoiceTickets(ctx, ticketsIds, options); await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -102,7 +102,7 @@ describe('ticket invoiceTickets()', () => {
const options = {transaction: tx}; const options = {transaction: tx};
const ticketsIds = [11]; const ticketsIds = [11];
const invoicesIds = await models.Ticket.invoiceTickets(ctx, ticketsIds, options); const invoicesIds = await models.Ticket.invoiceTickets(ctx, ticketsIds, null, options);
expect(invoicesIds.length).toBeGreaterThan(0); expect(invoicesIds.length).toBeGreaterThan(0);

View File

@ -42,7 +42,7 @@ describe('ticket makeInvoice()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketsIds], options); `, [ticketsIds], options);
const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options); const invoiceId = await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, null, options);
expect(invoiceId).toBeDefined(); expect(invoiceId).toBeDefined();
@ -70,7 +70,7 @@ describe('ticket makeInvoice()', () => {
WHERE id IN (?) WHERE id IN (?)
`, [ticketsId], options); `, [ticketsId], options);
await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, options); await models.Ticket.makeInvoice(ctx, invoiceType, companyFk, invoiceDate, null, options);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
error = e; error = e;

View File

@ -20,9 +20,6 @@
}, },
"stateFk": { "stateFk": {
"type": "number" "type": "number"
},
"userFk": {
"type": "number"
} }
}, },
"relations": { "relations": {

View File

@ -292,7 +292,7 @@ class Controller extends Section {
const query = 'Tickets/refund'; const query = 'Tickets/refund';
return this.$http.post(query, params) return this.$http.post(query, params)
.then(res => { .then(res => {
const refundTicket = res.data; const [refundTicket] = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id ticketId: refundTicket.id
})); }));

View File

@ -262,11 +262,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
const params = { const params = {
ticketsIds: [16] ticketsIds: [16]
}; };
$httpBackend.expectPOST('Tickets/refund', params).respond({id: 99}); const response = {id: 99};
$httpBackend.expectPOST('Tickets/refund', params).respond([response]);
controller.refund(); controller.refund();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', {id: 99}); expect(controller.$state.go).toHaveBeenCalledWith('ticket.card.sale', response);
}); });
}); });

View File

@ -526,7 +526,7 @@ class Controller extends Section {
const params = {salesIds: salesIds, withWarehouse: withWarehouse}; const params = {salesIds: salesIds, withWarehouse: withWarehouse};
const query = 'Sales/refund'; const query = 'Sales/refund';
this.$http.post(query, params).then(res => { this.$http.post(query, params).then(res => {
const refundTicket = res.data; const [refundTicket] = res.data;
this.vnApp.showSuccess(this.$t('The following refund ticket have been created', { this.vnApp.showSuccess(this.$t('The following refund ticket have been created', {
ticketId: refundTicket.id ticketId: refundTicket.id
})); }));

View File

@ -729,7 +729,7 @@ describe('Ticket', () => {
salesIds: [1, 4], salesIds: [1, 4],
}; };
const refundTicket = {id: 99}; const refundTicket = {id: 99};
$httpBackend.expect('POST', 'Sales/refund', params).respond(200, refundTicket); $httpBackend.expect('POST', 'Sales/refund', params).respond(200, [refundTicket]);
controller.createRefund(); controller.createRefund();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -23,9 +23,9 @@
<vn-td>{{::tracking.state.name}}</vn-td> <vn-td>{{::tracking.state.name}}</vn-td>
<vn-td expand> <vn-td expand>
<span <span
ng-class="{'link': tracking.worker.id}" ng-class="{'link': tracking.user.id}"
ng-click="workerDescriptor.show($event, tracking.worker.user.id)"> ng-click="workerDescriptor.show($event, tracking.user.id)">
{{::tracking.worker.user.name || 'System' | translate}} {{::tracking.user.name || 'System' | translate}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink-datetime>{{::tracking.created | date:'dd/MM/yyyy HH:mm'}}</vn-td> <vn-td shrink-datetime>{{::tracking.created | date:'dd/MM/yyyy HH:mm'}}</vn-td>

View File

@ -39,6 +39,7 @@ module.exports = Self => {
started.setFullYear(year); started.setFullYear(year);
started.setMonth(0); started.setMonth(0);
started.setDate(1); started.setDate(1);
started.setHours(0, 0, 0, 0);
const ended = Date.vnNew(); const ended = Date.vnNew();
ended.setFullYear(year); ended.setFullYear(year);

View File

@ -43,16 +43,9 @@ module.exports = Self => {
const isTeamBoss = await models.ACL.checkAccessAcl(ctx, 'Worker', 'isTeamBoss', 'WRITE'); const isTeamBoss = await models.ACL.checkAccessAcl(ctx, 'Worker', 'isTeamBoss', 'WRITE');
const isHimself = userId == workerId; const isHimself = userId == workerId;
if (!isSubordinate || (isSubordinate && isHimself && !isTeamBoss)) if (!isSubordinate || (isHimself && !isTeamBoss))
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
query = `CALL vn.workerTimeControl_clockIn(?,?,?)`; return Self.clockIn(workerId, args.timed, args.direction, myOptions);
const [response] = await Self.rawSql(query, [workerId, args.timed, args.direction], myOptions);
if (response[0] && response[0].error)
throw new UserError(response[0].error);
await models.WorkerTimeControl.resendWeeklyHourEmail(ctx, workerId, args.timed, myOptions);
return response;
}; };
}; };

View File

@ -0,0 +1,45 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('clockIn', {
description: 'Check if the employee can clock in',
accessType: 'WRITE',
accepts: [
{
arg: 'workerFk',
type: 'number',
required: true,
},
{
arg: 'timed',
type: 'date'
},
{
arg: 'direction',
type: 'string'
},
],
http: {
path: `/clockIn`,
verb: 'POST'
},
returns: {
type: 'Object',
root: true
}
});
Self.clockIn = async(workerFk, timed, direction, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = 'CALL vn.workerTimeControl_clockIn(?, ?, ?)';
const [[response]] = await Self.rawSql(query, [workerFk, timed, direction], myOptions);
if (response && response.error)
throw new UserError(response.error);
return response;
};
};

View File

@ -0,0 +1,32 @@
module.exports = Self => {
Self.remoteMethod('getClockIn', {
description: 'Shows the clockings for each day, in columns per day',
accessType: 'READ',
accepts: [
{
arg: 'workerFk',
type: 'int',
required: true,
},
],
http: {
path: `/getClockIn`,
verb: 'GET'
},
returns: {
type: ['Object'],
root: true
},
});
Self.getClockIn = async(workerFk, options) => {
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = `CALL vn.workerTimeControl_getClockIn(?, ?)`;
const [result] = await Self.rawSql(query, [workerFk, Date.vnNew()], myOptions);
return result;
};
};

View File

@ -0,0 +1,35 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('login', {
description: 'Consult the user\'s information and the buttons that must be activated after logging in',
accessType: 'READ',
accepts: [
{
arg: 'pin',
type: 'string',
required: true
},
],
returns: {
type: 'Object',
root: true
},
http: {
path: `/login`,
verb: 'POST'
}
});
Self.login = async(ctx, pin, options) => {
const myOptions = {};
const $t = ctx.req.__;
if (typeof options == 'object')
Object.assign(myOptions, options);
const query = `CALL vn.workerTimeControl_login(?)`;
const [[user]] = await Self.rawSql(query, [pin], myOptions);
if (!user) throw new UserError($t('Incorrect pin'));
return user;
};
};

View File

@ -1,6 +1,6 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('resendWeeklyHourEmail', { Self.remoteMethodCtx('resendWeeklyHourEmail', {
description: 'Adds a new hour registry', description: 'Send the records for the week of the date provided',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'id', arg: 'id',

View File

@ -0,0 +1,581 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('workerTimeControl clockIn()', () => {
const workerId = 9;
const salesBossId = 19;
const hankPymId = 1107;
const jessicaJonesId = 1110;
const HHRRId = 37;
const teamBossId = 13;
const monday = 1;
const tuesday = 2;
const thursday = 4;
const friday = 5;
const sunday = 7;
const inTime = '2001-01-01T00:00:00.000Z';
const activeCtx = {
accessToken: {userId: 50},
};
const ctx = {req: activeCtx};
beforeAll(() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
});
it('should correctly clock in', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
await models.WorkerTimeControl.clockIn(workerId, inTime, 'in', options);
const isClockIn = await models.WorkerTimeControl.findOne({
where: {
userFk: workerId
}
}, options);
expect(isClockIn).toBeDefined();
expect(isClockIn.direction).toBe('in');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
describe('as Role errors', () => {
it('should add if the current user is team boss and the target user is himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should delete the created time entry for the team boss as himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should edit the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const createdTimeEntry = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
ctx.args = {direction: 'out'};
const updatedTimeEntry = await models.WorkerTimeControl.updateTimeEntry(
ctx, createdTimeEntry.id, options
);
expect(updatedTimeEntry.direction).toEqual('out');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
describe('as saleBoss editor', () => {
let workerId;
beforeEach(() => {
activeCtx.accessToken.userId = salesBossId;
workerId = hankPymId;
});
it('should fail to add a time entry if the target user has an absence that day', async() => {
const date = Date.vnNew();
date.setHours(8, 0, 0);
date.setDate(date.getDate() - 16);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No está permitido trabajar`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
const date = Date.vnNew();
date.setFullYear(date.getFullYear() - 2);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No hay un contrato en vigor`);
});
it('should fail to add a time entry for a worker without an existing contract and exceeding time', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(0, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(20, 0, 1);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`);
});
describe('direction errors', () => {
let date = Date.vnNew();
date.setDate(date.getDate() - 1);
let error;
it('should throw an error when trying "in" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('Should throw an error when trying "out" before closing a "middle" couple', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "middle" after "out"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "out" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
});
describe('12h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = hankPymId;
it('should throw an error when the 12h rest is not fulfilled yet', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail as the 12h rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 3500kg drivers with enforced 9h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = jessicaJonesId;
it('should throw an error when the 9h enforced rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail when the 9h enforced rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 72h weekly rest', () => {
it('should throw an error when work 11 consecutive days', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, friday);
date.setHours(10, 0, 1);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(17, 59, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(18, 00, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
});
});
function weekDay(date, dayToSet) {
const currentDay = date.getDay();
const distance = dayToSet - currentDay;
date.setDate(date.getDate() + distance);
return date;
}
function nextWeek(date) {
const sunday = 7;
const currentDay = date.getDay();
let newDate = date;
if (currentDay != 0)
newDate = weekDay(date, sunday);
newDate.setDate(newDate.getDate() + 1);
return newDate;
}
async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
const dateStart = new Date(weekDay(date, dayStart));
const dateEnd = new Date(dateStart);
dateEnd.setDate(dateStart.getDate() + dayEnd);
for (let i = dayStart; i <= dayEnd; i++) {
dateStart.setHours(10, 0, 0);
ctx.args = {timed: dateStart, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setHours(18, 0, 0);
ctx.args = {timed: dateStart, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setDate(dateStart.getDate() + 1);
}
}

View File

@ -0,0 +1,16 @@
const models = require('vn-loopback/server/server').models;
describe('workerTimeControl getClockIn()', () => {
it('should correctly get the timetable of a worker', async() => {
const response = await models.WorkerTimeControl.getClockIn(1106, {});
expect(response.length).toEqual(4);
const [inHrs, middleOutHrs, middleInHrs, outHrs] = response;
expect(inHrs['0daysAgo']).toEqual('07:00');
expect(middleOutHrs['0daysAgo']).toEqual('10:00');
expect(middleInHrs['0daysAgo']).toEqual('10:20');
expect(outHrs['0daysAgo']).toEqual('14:50');
});
});

View File

@ -0,0 +1,34 @@
const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
const UserError = require('vn-loopback/util/user-error');
describe('workerTimeControl login()', () => {
let ctx;
beforeAll(async() => {
ctx = {
accessToken: {userId: 9},
req: {
headers: {origin: 'http://localhost'},
__: key => key
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: ctx
});
});
it('should correctly login', async() => {
const response = await models.WorkerTimeControl.login(ctx, 9);
expect(response.name).toBe('developer');
});
it('should throw UserError if pin is not provided', async() => {
try {
await models.WorkerTimeControl.login(ctx);
} catch (error) {
expect(error).toBeInstanceOf(UserError);
expect(error.message).toBe('Incorrect pin');
}
});
});

View File

@ -3,18 +3,10 @@ const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('workerTimeControl add/delete timeEntry()', () => { describe('workerTimeControl add/delete timeEntry()', () => {
const HHRRId = 37;
const teamBossId = 13;
const employeeId = 1; const employeeId = 1;
const salesPersonId = 1106;
const salesBossId = 19; const salesBossId = 19;
const hankPymId = 1107; const hankPymId = 1107;
const jessicaJonesId = 1110;
const monday = 1; const monday = 1;
const tuesday = 2;
const thursday = 4;
const friday = 5;
const sunday = 7;
const activeCtx = { const activeCtx = {
accessToken: {userId: 50}, accessToken: {userId: 50},
}; };
@ -61,560 +53,11 @@ describe('workerTimeControl add/delete timeEntry()', () => {
expect(error.statusCode).toBe(400); expect(error.statusCode).toBe(400);
expect(error.message).toBe(`You don't have enough privileges`); expect(error.message).toBe(`You don't have enough privileges`);
}); });
it('should add if the current user is team boss and the target user is himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should try but fail to delete his own time entry', async() => {
activeCtx.accessToken.userId = salesBossId;
const workerId = salesBossId;
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
activeCtx.accessToken.userId = salesPersonId;
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
await tx.rollback();
} catch (e) {
error = e;
await tx.rollback();
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toBe(`You don't have enough privileges`);
});
it('should delete the created time entry for the team boss as himself', async() => {
activeCtx.accessToken.userId = teamBossId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should delete the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
await models.WorkerTimeControl.deleteTimeEntry(ctx, createdTimeEntry.id, options);
const deletedTimeEntry = await models.WorkerTimeControl.findById(createdTimeEntry.id, null, options);
expect(deletedTimeEntry).toBeNull();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should edit the created time entry for the team boss as HHRR', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
const tx = await models.WorkerTimeControl.beginTransaction({});
try {
const options = {transaction: tx};
const todayAtOne = Date.vnNew();
todayAtOne.setHours(1, 0, 0, 0);
ctx.args = {timed: todayAtOne, direction: 'in'};
const [createdTimeEntry] = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
expect(createdTimeEntry.id).toBeDefined();
ctx.args = {direction: 'out'};
const updatedTimeEntry = await models.WorkerTimeControl.updateTimeEntry(ctx, createdTimeEntry.id, options);
expect(updatedTimeEntry.direction).toEqual('out');
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });
describe('WorkerTimeControl_clockIn calls', () => { describe('WorkerTimeControl_clockIn calls', () => {
let workerId; beforeEach(() => activeCtx.accessToken.userId = salesBossId);
beforeEach(() => {
activeCtx.accessToken.userId = salesBossId;
workerId = hankPymId;
});
it('should fail to add a time entry if the target user has an absence that day', async() => {
const date = Date.vnNew();
date.setHours(8, 0, 0);
date.setDate(date.getDate() - 16);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No está permitido trabajar`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
const date = Date.vnNew();
date.setFullYear(date.getFullYear() - 2);
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
try {
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`No hay un contrato en vigor`);
});
it('should fail to add a time entry for a worker without an existing contract', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(0, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(20,0, 1);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Superado el tiempo máximo entre entrada y salida`);
});
describe('direction errors', () => {
let date = Date.vnNew();
date.setDate(date.getDate() - 1);
let error;
it('should throw an error when trying "in" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "in" direction after insert "in" and "middle"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('Should throw an error when trying "out" before closing a "middle" couple', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "middle" after "out"', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
it('should throw an error when trying "out" direction twice', async() => {
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(9, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date.setHours(10, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Dirección incorrecta`);
});
});
describe('12h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = hankPymId;
it('should throw an error when the 12h rest is not fulfilled yet', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail as the 12h rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(4, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 3500kg drivers with enforced 9h rest', () => {
activeCtx.accessToken.userId = salesBossId;
const workerId = jessicaJonesId;
it('should throw an error when the 9h enforced rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso diario`);
});
it('should not fail when the 9h enforced rest is fulfilled', async() => {
let date = Date.vnNew();
date.setDate(date.getDate() - 2);
date = weekDay(date, monday);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
date.setHours(8, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(16, 0, 0);
ctx.args = {timed: date, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
try {
date = weekDay(date, tuesday);
date.setHours(1, 1, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('for 72h weekly rest', () => {
it('should throw an error when work 11 consecutive days', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, friday);
date.setHours(10, 0, 1);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is not fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(17, 59, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is fulfilled', async() => {
let date = Date.vnNew();
date.setMonth(date.getMonth() - 1);
date.setDate(1);
let error;
const tx = await models.WorkerTimeControl.beginTransaction({});
const options = {transaction: tx};
await populateWeek(date, monday, sunday, ctx, workerId, options);
date = nextWeek(date);
await populateWeek(date, monday, thursday, ctx, workerId, options);
try {
date = weekDay(date, sunday);
date.setHours(18, 00, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
await tx.rollback();
} catch (e) {
await tx.rollback();
error = e;
}
expect(error).not.toBeDefined;
});
});
describe('WorkerTimeControl_calculate calls', () => { describe('WorkerTimeControl_calculate calls', () => {
let dated = Date.vnNew(); let dated = Date.vnNew();
dated.setDate(dated.getDate() - 7); dated.setDate(dated.getDate() - 7);
@ -836,25 +279,6 @@ function weekDay(date, dayToSet) {
return date; return date;
} }
function nextWeek(date) {
const sunday = 7;
const currentDay = date.getDay();
let newDate = date;
if (currentDay != 0)
newDate = weekDay(date, sunday);
newDate.setDate(newDate.getDate() + 1);
return newDate;
}
function lastWeek(date) {
const monday = 1;
newDate = weekDay(date, monday);
newDate.setDate(newDate.getDate() - 1);
return newDate;
}
async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) { async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
const dateStart = new Date(weekDay(date, dayStart)); const dateStart = new Date(weekDay(date, dayStart));
const dateEnd = new Date(dateStart); const dateEnd = new Date(dateStart);

View File

@ -10,6 +10,9 @@ module.exports = Self => {
require('../methods/worker-time-control/weeklyHourRecordEmail')(Self); require('../methods/worker-time-control/weeklyHourRecordEmail')(Self);
require('../methods/worker-time-control/getMailStates')(Self); require('../methods/worker-time-control/getMailStates')(Self);
require('../methods/worker-time-control/resendWeeklyHourEmail')(Self); require('../methods/worker-time-control/resendWeeklyHourEmail')(Self);
require('../methods/worker-time-control/login')(Self);
require('../methods/worker-time-control/getClockIn')(Self);
require('../methods/worker-time-control/clockIn')(Self);
Self.rewriteDbError(function(err) { Self.rewriteDbError(function(err) {
if (err.code === 'ER_DUP_ENTRY') if (err.code === 'ER_DUP_ENTRY')

4
package-lock.json generated
View File

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

View File

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