Merge pull request 'feat: refs #8644 workerTimeControl add mandatory break times and update procedures' (!3511) from 8644-Fichador-restriccion-por-horas into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #3511
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Carlos Andrés 2025-03-02 10:29:54 +00:00
commit 23085a57b6
5 changed files with 188 additions and 77 deletions

View File

@ -18,14 +18,13 @@ BEGIN
* Solo retorna el primer problema, en caso de no ocurrir ningún error se añadirá
* fichada a la tabla vn.workerTimeControl
*/
DECLARE vLastIn DATETIME;
DECLARE vLastOut DATETIME;
DECLARE vNextIn DATETIME;
DECLARE vNextOut DATETIME;
DECLARE vNextDirection ENUM('in', 'out');
DECLARE vLastDirection ENUM('in', 'out');
DECLARE vDayMaxTime INTEGER;
DECLARE vDayMaxTime INTEGER;
DECLARE vDayBreak INT;
DECLARE vShortWeekBreak INT;
DECLARE vLongWeekBreak INT;
@ -40,6 +39,7 @@ BEGIN
DECLARE vIsManual BOOLEAN DEFAULT TRUE;
DECLARE vMaxWorkShortCycle INT;
DECLARE vMaxWorkLongCycle INT;
DECLARE vReentryWaitTime TIME DEFAULT NULL;
DECLARE EXIT HANDLER FOR SQLSTATE '45000'
BEGIN
@ -52,19 +52,19 @@ BEGIN
WHERE w.id = vWorkerFk;
SELECT `description` INTO vErrorMessage
FROM workerTimeControlError
FROM workerTimeControlError
WHERE `code` = vErrorCode;
IF vErrorMessage IS NULL THEN
SET vErrorMessage = 'Error sin definir';
END IF;
SELECT vErrorMessage `error`;
SELECT CONCAT(vErrorMessage, IFNULL(DATE_FORMAT(vReentryWaitTime, '%i:%s'), '')) `error`;
SELECT CONCAT(vUserName,
' no ha podido fichar por el siguiente problema: ',
vErrorMessage)
INTO vErrorMessage;
CALL mail_insert( vMailTo, vMailTo, 'Error al fichar', vErrorMessage);
END;
@ -97,19 +97,19 @@ BEGIN
JOIN workerTimeControlConfig wc
WHERE b.workerFk = vWorkerFk
AND vDated BETWEEN b.started AND IFNULL(b.ended, vDated);
-- CONTRATO EN VIGOR
IF vDayBreak IS NULL THEN
SET vErrorCode = 'INACTIVE_BUSINESS';
CALL util.throw(vErrorCode);
END IF;
-- FICHADAS A FUTURO
IF vTimed > util.VN_NOW() + INTERVAL 1 MINUTE THEN
SET vErrorCode = 'IS_NOT_ALLOWED_FUTURE';
CALL util.throw(vErrorCode);
END IF;
-- VERIFICAR SI ESTÁ PERMITIDO TRABAJAR
CALL timeBusiness_calculateByWorker(vWorkerFk, vDated, vDated);
SELECT isAllowedToWork INTO vIsAllowedToWork
@ -124,6 +124,16 @@ BEGIN
-- DIRECCION CORRECTA
CALL workerTimeControl_direction(vWorkerFk, vTimed);
SELECT reentryWaitTime INTO vReentryWaitTime
FROM tmp.workerTimeControlDirection
LIMIT 1;
IF vReentryWaitTime IS NOT NULL THEN
SET vErrorCode = 'WAIT_TIME';
CALL util.throw(vErrorCode);
END IF;
IF (SELECT
IF((IF(option1 IN ('inMiddle', 'outMiddle'),
'middle',
@ -138,13 +148,13 @@ BEGIN
) THEN
SET vIsError = TRUE;
END IF;
IF vIsError THEN
SET vErrorCode = 'WRONG_DIRECTION';
IF(SELECT option1 IS NULL AND option2 IS NULL
FROM tmp.workerTimeControlDirection) THEN
SET vErrorCode = 'DAY_MAX_TIME';
END IF;
CALL util.throw(vErrorCode);
@ -158,7 +168,7 @@ BEGIN
AND timed < vTimed
ORDER BY timed DESC
LIMIT 1;
IF (SELECT IF(vDirection = 'in',
MOD(COUNT(*), 2) ,
IF (vDirection = 'out', NOT MOD(COUNT(*), 2), FALSE))
@ -169,7 +179,7 @@ BEGIN
SET vErrorCode = 'ODD_WORKERTIMECONTROL';
CALL util.throw(vErrorCode);
END IF;
-- DESCANSO DIARIO
SELECT timed INTO vLastOut
FROM workerTimeControl
@ -178,7 +188,7 @@ BEGIN
AND timed < vTimed
ORDER BY timed DESC
LIMIT 1;
SELECT timed INTO vNextIn
FROM workerTimeControl
WHERE userFk = vWorkerFk
@ -186,7 +196,7 @@ BEGIN
AND timed > vTimed
ORDER BY timed ASC
LIMIT 1;
CASE vDirection
WHEN 'in' THEN
IF UNIX_TIMESTAMP(vTimed) - UNIX_TIMESTAMP(vLastOut) <= vDayBreak THEN
@ -204,11 +214,9 @@ BEGIN
CALL util.throw(vErrorCode);
END IF;
IF (vDirection IN('in', 'out')) THEN
-- VERIFICA MAXIMO TIEMPO DESDE ENTRADA HASTA LA SALIDA
SELECT timed INTO vNextOut
FROM workerTimeControl
WHERE userFk = vWorkerFk
@ -216,7 +224,7 @@ BEGIN
AND timed > vTimed
ORDER BY timed ASC
LIMIT 1;
SELECT direction INTO vNextDirection
FROM workerTimeControl
WHERE userFk = vWorkerFk
@ -224,7 +232,7 @@ BEGIN
AND timed > vTimed
ORDER BY timed ASC
LIMIT 1;
SELECT direction INTO vLastDirection
FROM workerTimeControl
WHERE userFk = vWorkerFk
@ -232,34 +240,34 @@ BEGIN
AND timed < vTimed
ORDER BY timed ASC
LIMIT 1;
IF (vDirection ='in'
AND vNextDirection = 'out'
IF (vDirection ='in'
AND vNextDirection = 'out'
AND UNIX_TIMESTAMP(vNextOut) - UNIX_TIMESTAMP(vTimed) > vDayMaxTime) OR
(vDirection ='out'
(vDirection ='out'
AND vLastDirection = 'in'
AND UNIX_TIMESTAMP(vTimed) -UNIX_TIMESTAMP(vLastIn) > vDayMaxTime) THEN
AND UNIX_TIMESTAMP(vTimed) - UNIX_TIMESTAMP(vLastIn) > vDayMaxTime) THEN
SET vErrorCode = 'DAY_MAX_TIME';
CALL util.throw(vErrorCode);
END IF;
-- VERIFICA DESCANSO SEMANAL
WITH wtc AS(
(SELECT timed
FROM vn.workerTimeControl
(SELECT timed
FROM vn.workerTimeControl
WHERE userFk = vWorkerFk
AND direction IN ('in', 'out')
AND timed BETWEEN vTimed - INTERVAL (vWeekScope * 2) SECOND
AND timed BETWEEN vTimed - INTERVAL (vWeekScope * 2) SECOND
AND vTimed + INTERVAL (vWeekScope * 2) SECOND )
UNION
UNION
(SELECT vTimed)
), wtcGap AS(
SELECT timed,
TIMESTAMPDIFF(SECOND, LAG(timed) OVER (ORDER BY timed), timed) gap
TIMESTAMPDIFF(SECOND, LAG(timed) OVER (ORDER BY timed), timed) gap
FROM wtc
ORDER BY timed
), wtcBreak AS(
), wtcBreak AS(
SELECT timed,
IF(IFNULL(gap, 0) > vShortWeekBreak, TRUE, FALSE) hasShortBreak,
IF(IFNULL(gap, 0) > vLongWeekBreak, TRUE, FALSE) hasLongBreak
@ -270,8 +278,8 @@ BEGIN
SUM(hasShortBreak) OVER (ORDER BY timed) breakCounter ,
LEAD(hasLongBreak) OVER (ORDER BY timed) nextHasLongBreak
FROM wtcBreak
)SELECT TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed)) > vMaxWorkLongCycle OR
(TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed))> vMaxWorkShortCycle
)SELECT TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed)) > vMaxWorkLongCycle OR
(TIMESTAMPDIFF(SECOND, MIN(timed), MAX(timed))> vMaxWorkShortCycle
AND NOT SUM(IFNULL(nextHasLongBreak, 1)))
hasError INTO vIsError
FROM wtcBreakCounter
@ -291,5 +299,6 @@ BEGIN
SELECT LAST_INSERT_ID() id;
END$$
END
$$
DELIMITER ;

View File

@ -1,39 +1,45 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`workerTimeControl_direction`(vWorkerFk VARCHAR(10), vTimed DATETIME)
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`workerTimeControl_direction`(
vWorkerFk VARCHAR(10),
vTimed DATETIME
)
BEGIN
/**
* Devuelve que direcciones de fichadas son lógicas a partir de la anterior fichada
* Devuelve que direcciones de fichadas son lógicas a partir de la anterior fichada,
* @param vWorkerFk Identificador del trabajador
* @return (option1, option2)
* Los valores posibles de retorno son ('in', 'inMiddle', 'outMiddle', 'out')
* @return tmp.workerTimeControlDirection(option1, option2, reentryWaitTime)
* Valores posibles options('in', 'inMiddle', 'outMiddle', 'out')
*/
DECLARE vLastIn DATETIME ;
DECLARE vIsMiddleOdd BOOLEAN ;
DECLARE vLastIn DATETIME;
DECLARE vLastMiddleIn DATETIME;
DECLARE vIsMiddleOdd BOOLEAN;
DECLARE vMailTo VARCHAR(50) DEFAULT NULL;
DECLARE vUserName VARCHAR(50) DEFAULT NULL;
DECLARE vReentryWaitTime TIME;
IF (vTimed IS NULL) THEN
IF (vTimed IS NULL) THEN
SET vTimed = util.VN_NOW();
END IF;
SELECT timed INTO vLastIn
FROM workerTimeControl
FROM workerTimeControl
WHERE userFk = vWorkerFk
AND direction = 'in'
AND direction = 'in'
AND timed < vTimed
ORDER BY timed DESC
LIMIT 1;
SELECT (COUNT(*)mod 2 = 1) INTO vIsMiddleOdd
FROM workerTimeControl
FROM workerTimeControl
WHERE userFk = vWorkerFk
AND direction = 'middle'
AND direction = 'middle'
AND timed BETWEEN vLastIn AND vTimed;
DROP TEMPORARY TABLE IF EXISTS tmp.workerTimeControlDirection;
CREATE TEMPORARY TABLE tmp.workerTimeControlDirection
SELECT IF(isCorrect, option1, NULL) option1,
IF(isCorrect, option2, NULL) option2
IF(isCorrect, option2, NULL) option2,
CAST(NULL AS TIME) reentryWaitTime
FROM( SELECT IF(w.direction <> 'out' AND (UNIX_TIMESTAMP(vTimed) - UNIX_TIMESTAMP(w.timed) > wc.dayBreak), FALSE, TRUE) isCorrect,
CASE WHEN w.direction ='in' THEN 'inMiddle'
WHEN w.direction = 'out' THEN 'in'
@ -59,6 +65,26 @@ BEGIN
VALUES('in', NULL);
END IF;
IF (SELECT option1 ='outMiddle' AND option2 IS NULL FROM tmp.workerTimeControlDirection) THEN
SELECT timed INTO vLastMiddleIn
FROM workerTimeControl
WHERE userFk = vWorkerFk
AND timed < vTimed
ORDER BY timed DESC
LIMIT 1;
SELECT TIMEDIFF(vLastMiddleIn + INTERVAL wtc.maxTimeToBreak SECOND, vTimed) INTO vReentryWaitTime
FROM tmp.workerTimeControlDirection wt
JOIN workerTimeControlConfig wtc
WHERE TIME(vLastMiddleIn) BETWEEN wtc.mandatoryBreakFrom AND wtc.mandatoryBreakTo
AND TIMESTAMPDIFF(SECOND, vLastMiddleIn, vTimed) < wtc.maxTimeToBreak;
IF vReentryWaitTime THEN
UPDATE tmp.workerTimeControlDirection
SET reentryWaitTime = vReentryWaitTime;
END IF;
END IF;
IF (SELECT option1 IS NULL AND option2 IS NULL FROM tmp.workerTimeControlDirection) THEN
SELECT CONCAT(u.name, '@verdnatura.es'), CONCAT(w.firstName, ' ', w.lastName)
INTO vMailTo, vUserName
@ -66,10 +92,10 @@ BEGIN
JOIN worker w ON w.bossFk = u.id
WHERE w.id = vWorkerFk;
CALL mail_insert(
vMailTo,
vMailTo,
'Error al fichar',
CALL mail_insert(
vMailTo,
vMailTo,
'Error al fichar',
CONCAT(vUserName, ' tiene problemas para fichar'));
END IF;
END$$

View File

@ -0,0 +1,8 @@
ALTER TABLE vn.workerTimeControlConfig
ADD COLUMN `mandatoryBreakFrom` time DEFAULT '12:00:00'
COMMENT 'Tiempo desde el que se obligará a realizar un descanso para jornada partida';
ALTER TABLE vn.workerTimeControlConfig
ADD COLUMN `mandatoryBreakTo` time DEFAULT '15:00:00';
INSERT INTO vn.workerTimeControlError (`code`, `description`)
VALUES ('WAIT_TIME', 'El descanso terminará en ');

View File

@ -45,7 +45,7 @@ describe('workerTimeControl clockIn()', () => {
throw e;
}
});
it('should throw an error trying to change a middle hour to out not resting 12h', async() => {
activeCtx.accessToken.userId = HHRRId;
const workerId = teamBossId;
@ -61,7 +61,7 @@ describe('workerTimeControl clockIn()', () => {
const middleTime ="2000-12-26T11:00:00.000Z";
ctx.args = {timed: middleTime, direction: 'middle'};
const middleEntryTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
const direction = 'out';
await models.WorkerTimeControl.updateTimeEntry(ctx, middleEntryTime.id, direction, options);
await tx.rollback();
@ -87,7 +87,7 @@ describe('workerTimeControl clockIn()', () => {
ctx.args = {timed: middleTime, direction: 'middle'};
const middleEntryTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
middleEntryTime.updateAttribute('manual', false);
const direction = 'out';
const outTimeEntryId = await models.WorkerTimeControl.updateTimeEntry(ctx, middleEntryTime.id, direction, options);
@ -111,10 +111,10 @@ describe('workerTimeControl clockIn()', () => {
ctx.args = {timed: "2000-12-25T22:00:00.000Z", direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
ctx.args = {timed: "2000-12-25T22:30:00.000Z", direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
ctx.args = {timed: "2000-12-26T01:00:00.000Z", direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
@ -196,7 +196,7 @@ describe('workerTimeControl clockIn()', () => {
todayAtTwo.setHours(2, 0, 0, 0);
ctx.args = {timed: todayAtTwo, direction: 'middle'};
const middleTime = await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
const direction = 'out';
const outTimeEntryId = await models.WorkerTimeControl.updateTimeEntry(
ctx, middleTime.id, direction, options
@ -204,7 +204,7 @@ describe('workerTimeControl clockIn()', () => {
const {direction: updatedDirection} = await models.WorkerTimeControl.findById(outTimeEntryId,{fields:['direction']},options);
expect(updatedDirection).toEqual('out');
await tx.rollback();
} catch (e) {
await tx.rollback();
@ -544,7 +544,7 @@ describe('workerTimeControl clockIn()', () => {
});
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);
@ -598,7 +598,7 @@ describe('workerTimeControl clockIn()', () => {
expect(error.message).toBe(`Descanso semanal`);
});
it('should throw an error when the 72h weekly rest is fulfilled', async() => {
let date = Date.vnNew();
@ -626,6 +626,74 @@ describe('workerTimeControl clockIn()', () => {
expect(error).not.toBeDefined;
});
it('should enforce a 1-hour break if the break starts between 12:00 and 15:00', 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};
try {
date.setHours(11, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(13, 30, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(14, 0, 0);
ctx.args = {timed: date, direction: 'middle'};
let error;
try {
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(error.statusCode).toBe(400);
expect(error.message).toMatch(/El descanso terminará en \d{2}:\d{2}/);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should allow resuming work after a full 1-hour break', 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};
try {
date.setHours(11, 0, 0);
ctx.args = {timed: date, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(14, 00, 0);
ctx.args = {timed: date, direction: 'middle'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
date.setHours(15, 00, 0);
ctx.args = {timed: date, direction: 'middle'};
let error;
try {
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
} catch (e) {
error = e;
}
expect(error).toBeUndefined();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});
});

View File

@ -95,9 +95,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(14, 59, 0);
dated.setHours(04, 59, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(15, 14, 0);
dated.setHours(05, 14, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -124,9 +124,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(15, 0, 0);
dated.setHours(05, 0, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(15, 15, 0);
dated.setHours(05, 15, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -153,9 +153,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(14, 59, 0);
dated.setHours(04, 59, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(15, 24, 0);
dated.setHours(05, 24, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -182,9 +182,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(15, 0, 0);
dated.setHours(05, 0, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(15, 25, 0);
dated.setHours(05, 25, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -211,9 +211,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(14, 59, 0);
dated.setHours(04, 59, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(15, 59, 0);
dated.setHours(05, 59, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -240,9 +240,9 @@ describe('workerTimeControl add/delete timeEntry()', () => {
try {
await populateWeek(dated, monday, monday, ctx, hankPymId, options);
dated.setHours(15, 0, 0);
dated.setHours(05, 0, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
dated.setHours(16, 0, 0);
dated.setHours(06, 0, 0);
await addTimeEntry(ctx, dated, 'middle', hankPymId, options);
const start = new Date(dated - 1);
@ -286,10 +286,10 @@ async function populateWeek(date, dayStart, dayEnd, ctx, workerId, options) {
dateEnd.setDate(dateStart.getDate() + dayEnd);
for (let i = dayStart; i <= dayEnd; i++) {
dateStart.setHours(10, 0, 0);
dateStart.setHours(00, 0, 0);
ctx.args = {timed: dateStart, direction: 'in'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setHours(18, 0, 0);
dateStart.setHours(08, 0, 0);
ctx.args = {timed: dateStart, direction: 'out'};
await models.WorkerTimeControl.addTimeEntry(ctx, workerId, options);
dateStart.setDate(dateStart.getDate() + 1);