feat: refs #8644 workerTimeControl add mandatory break times and update procedures
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Carlos Andrés 2025-03-02 09:41:53 +01:00
parent 5a44b5d332
commit 2ca8de1d2e
5 changed files with 188 additions and 77 deletions

View File

@ -18,7 +18,6 @@ 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;
@ -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
@ -59,7 +59,7 @@ BEGIN
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)
@ -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',
@ -204,8 +214,6 @@ BEGIN
CALL util.throw(vErrorCode);
END IF;
IF (vDirection IN('in', 'out')) THEN
-- VERIFICA MAXIMO TIEMPO DESDE ENTRADA HASTA LA SALIDA
@ -238,7 +246,7 @@ BEGIN
AND UNIX_TIMESTAMP(vNextOut) - UNIX_TIMESTAMP(vTimed) > vDayMaxTime) OR
(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;
@ -291,5 +299,6 @@ BEGIN
SELECT LAST_INSERT_ID() id;
END$$
END
$$
DELIMITER ;

View File

@ -1,16 +1,21 @@
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
SET vTimed = util.VN_NOW();
@ -33,7 +38,8 @@ BEGIN
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

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

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