Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4774-traductions

This commit is contained in:
Carlos Satorres 2024-10-17 09:37:48 +02:00
commit b3472ca5db
49 changed files with 566 additions and 258 deletions

View File

@ -19,8 +19,15 @@ module.exports = Self => {
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
const [info, info2, [{'@vCollectionFk': collectionFk}]] = await Self.rawSql(
'CALL vn.collection_getAssigned(?, @vCollectionFk);SELECT @vCollectionFk', [userId], myOptions); const randStr = Math.random().toString(36).substring(3);
const result = await Self.rawSql(`
CALL vn.collection_getAssigned(?, @vCollectionFk);
SELECT @vCollectionFk ?
`, [userId, randStr], myOptions);
const collectionFk = result.find(item => item[0]?.[randStr] !== undefined)?.[0]?.[randStr];
if (!collectionFk) throw new UserError('There are not picking tickets'); if (!collectionFk) throw new UserError('There are not picking tickets');
await Self.rawSql('CALL vn.collection_printSticker(?, NULL)', [collectionFk], myOptions); await Self.rawSql('CALL vn.collection_printSticker(?, NULL)', [collectionFk], myOptions);

View File

@ -72,9 +72,9 @@ describe('Renew Token', () => {
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
const query = 'SELECT * FROM util.debug';
const debugLog = await models.Application.rawSql(query, null); const query = 'SELECT * FROM util.debug WHERE variable = "renewToken"';
const debugLog = await models.Application.rawSql(query);
expect(debugLog.length).toEqual(1); expect(debugLog.length).toEqual(1);
}); });

View File

@ -175,6 +175,9 @@
"PrintConfig": { "PrintConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },
"QueueMember": {
"dataSource": "vn"
},
"ViaexpressConfig": { "ViaexpressConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -0,0 +1,38 @@
{
"name": "QueueMember",
"base": "VnModel",
"options": {
"mysql": {
"table": "pbx.queueMember"
}
},
"properties": {
"id": {
"type": "number",
"id": true
},
"queue": {
"type": "string"
},
"extension": {
"type": "string"
}
},
"relations": {
"queueRelation": {
"type": "belongsTo",
"model": "Queue",
"foreignKey": "queue",
"primaryKey": "name"
}
},
"acls": [
{
"property": "*",
"accessType": "READ",
"principalType": "ROLE",
"principalId": "employee",
"permission": "ALLOW"
}
]
}

View File

@ -546,7 +546,8 @@ INSERT INTO `vn`.`observationType`(`id`,`description`, `code`)
(6, 'Weight', 'weight'), (6, 'Weight', 'weight'),
(7, 'InvoiceOut', 'invoiceOut'), (7, 'InvoiceOut', 'invoiceOut'),
(8, 'DropOff', 'dropOff'), (8, 'DropOff', 'dropOff'),
(9, 'Sustitución', 'substitution'); (9, 'Sustitución', 'substitution'),
(10, 'Finance', 'finance');
INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`) INSERT INTO `vn`.`addressObservation`(`id`,`addressFk`,`observationTypeFk`,`description`)
VALUES VALUES
@ -3988,3 +3989,25 @@ VALUES
INSERT IGNORE INTO ormConfig INSERT IGNORE INTO ormConfig
SET id =1, SET id =1,
selectLimit = 1000; selectLimit = 1000;
INSERT INTO pbx.queueMultiConfig
SET id = 'ring',
strategy = 20,
timeout = 2,
retry = 0,
weight = 0,
maxLen = 0,
ringInUse = 0;
INSERT INTO pbx.queue (description, name, config)
VALUES ('X-men', '1000', 1),
('Avengers', '2000', 1);
INSERT IGNORE INTO pbx.queueMember
SET queue = '1000',
extension = '1010';
UPDATE vn.department SET pbxQueue = '1000' WHERE name = "CAMARA";
UPDATE vn.department SET pbxQueue = '2000' WHERE name = "VENTAS";

View File

@ -22,7 +22,6 @@ BEGIN
DECLARE vItemFk INT; DECLARE vItemFk INT;
DECLARE vConcept VARCHAR(30); DECLARE vConcept VARCHAR(30);
DECLARE vAmount INT; DECLARE vAmount INT;
DECLARE vAvailable INT;
DECLARE vPrice DECIMAL(10,2); DECLARE vPrice DECIMAL(10,2);
DECLARE vSaleFk INT; DECLARE vSaleFk INT;
DECLARE vRowFk INT; DECLARE vRowFk INT;
@ -32,7 +31,6 @@ BEGIN
DECLARE vClientFk INT; DECLARE vClientFk INT;
DECLARE vCompanyFk INT; DECLARE vCompanyFk INT;
DECLARE vAgencyModeFk INT; DECLARE vAgencyModeFk INT;
DECLARE vCalcFk INT;
DECLARE vIsTaxDataChecked BOOL; DECLARE vIsTaxDataChecked BOOL;
DECLARE vDates CURSOR FOR DECLARE vDates CURSOR FOR
@ -109,7 +107,7 @@ BEGIN
) INTO vHas0Amount; ) INTO vHas0Amount;
IF vHas0Amount THEN IF vHas0Amount THEN
CALL util.throw('Remove lines with quantity = 0 before confirming'); CALL util.throw('Hay líneas vacías. Por favor, elimínelas');
END IF; END IF;
START TRANSACTION; START TRANSACTION;

View File

@ -4,7 +4,7 @@ CREATE OR REPLACE DEFINER=`root`@`localhost` TRIGGER `hedera`.`orderRow_afterIns
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
UPDATE `order` UPDATE `order`
SET rowUpdated = NOW() SET rowUpdated = util.VN_NOW()
WHERE id = NEW.orderFk; WHERE id = NEW.orderFk;
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -5,100 +5,139 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`collection_getAssigne
) )
BEGIN BEGIN
/** /**
* Comprueba si existen colecciones libres que se ajustan al perfil del usuario * Comprueba si existen colecciones libres que se ajustan
* y le asigna la más antigua. * al perfil del usuario y le asigna la más antigua.
* Añade un registro al semillero de colecciones y hace la reserva para la colección * Añade un registro al semillero de colecciones.
* *
* @param vUserFk Id de usuario * @param vUserFk Id de usuario
* @param vCollectionFk Id de colección * @param vCollectionFk Id de colección
*/ */
DECLARE vHasTooMuchCollections BOOL; DECLARE vHasTooMuchCollections BOOL;
DECLARE vItemPackingTypeFk VARCHAR(1); DECLARE vDone BOOL DEFAULT FALSE;
DECLARE vWarehouseFk INT; DECLARE vCollectionWorker INT;
DECLARE vLockName VARCHAR(215); DECLARE vMaxNotAssignedCollectionLifeTime TIME;
DECLARE vLockTime INT DEFAULT 30;
DECLARE vCollections CURSOR FOR
WITH collections AS (
SELECT tc.collectionFk,
SUM(sv.volume) volume,
c.saleTotalCount,
c.itemPackingTypeFk,
c.trainFk,
c.warehouseFk,
c.wagons
FROM vn.ticketCollection tc
JOIN vn.collection c ON c.id = tc.collectionFk
JOIN vn.saleVolume sv ON sv.ticketFk = tc.ticketFk
WHERE c.workerFk IS NULL
AND sv.shipped >= util.VN_CURDATE()
GROUP BY tc.collectionFk
) SELECT c.collectionFk
FROM collections c
JOIN vn.operator o
WHERE o.workerFk = vUserFk
AND (c.saleTotalCount <= o.linesLimit OR o.linesLimit IS NULL)
AND (c.itemPackingTypeFk = o.itemPackingTypeFk OR o.itemPackingTypeFk IS NULL)
AND o.numberOfWagons = c.wagons
AND o.trainFk = c.trainFk
AND o.warehouseFk = c.warehouseFk;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
-- Si hay colecciones sin terminar, sale del proceso
DECLARE EXIT HANDLER FOR SQLEXCEPTION DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN BEGIN
IF vLockName IS NOT NULL THEN ROLLBACK;
DO RELEASE_LOCK(vLockName);
END IF;
RESIGNAL; RESIGNAL;
END; END;
-- Si hay colecciones sin terminar, sale del proceso
CALL collection_get(vUserFk); CALL collection_get(vUserFk);
SELECT (pc.maxNotReadyCollections - COUNT(*)) <= 0, SELECT (pc.maxNotReadyCollections - COUNT(*)) <= 0, pc.maxNotAssignedCollectionLifeTime
pc.collection_assign_lockname INTO vHasTooMuchCollections, vMaxNotAssignedCollectionLifeTime
INTO vHasTooMuchCollections, FROM productionConfig pc
vLockName LEFT JOIN tmp.collection ON TRUE;
FROM tmp.collection c
JOIN productionConfig pc;
DROP TEMPORARY TABLE tmp.collection; DROP TEMPORARY TABLE tmp.collection;
IF vHasTooMuchCollections THEN IF vHasTooMuchCollections THEN
CALL util.throw('There are pending collections'); CALL util.throw('Hay colecciones pendientes');
END IF;
SELECT warehouseFk, itemPackingTypeFk
INTO vWarehouseFk, vItemPackingTypeFk
FROM operator
WHERE workerFk = vUserFk;
SET vLockName = CONCAT_WS('/',
vLockName,
vWarehouseFk,
vItemPackingTypeFk
);
IF NOT GET_LOCK(vLockName, vLockTime) THEN
CALL util.throw(CONCAT('Cannot get lock: ', vLockName));
END IF; END IF;
-- Se eliminan las colecciones sin asignar que estan obsoletas -- Se eliminan las colecciones sin asignar que estan obsoletas
INSERT INTO ticketTracking(stateFk, ticketFk)
SELECT s.id, tc.ticketFk
FROM collection c
JOIN ticketCollection tc ON tc.collectionFk = c.id
JOIN state s ON s.code = 'PRINTED_AUTO'
JOIN productionConfig pc
WHERE c.workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), c.created) > pc.maxNotAssignedCollectionLifeTime;
DELETE c INSERT INTO ticketTracking(stateFk, ticketFk)
FROM collection c SELECT s.id, tc.ticketFk
JOIN productionConfig pc FROM `collection` c
WHERE c.workerFk IS NULL JOIN ticketCollection tc ON tc.collectionFk = c.id
AND TIMEDIFF(util.VN_NOW(), c.created) > pc.maxNotAssignedCollectionLifeTime; JOIN `state` s ON s.code = 'PRINTED_AUTO'
WHERE c.workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), c.created) > vMaxNotAssignedCollectionLifeTime;
DELETE FROM `collection`
WHERE workerFk IS NULL
AND TIMEDIFF(util.VN_NOW(), created) > vMaxNotAssignedCollectionLifeTime;
-- Se añade registro al semillero -- Se añade registro al semillero
INSERT INTO collectionHotbed
SET userFk = vUserFk; INSERT INTO collectionHotbed(userFk) VALUES(vUserFk);
-- Comprueba si hay colecciones disponibles que se ajustan a su configuracion -- Comprueba si hay colecciones disponibles que se ajustan a su configuracion
SELECT MIN(c.id) INTO vCollectionFk
FROM collection c OPEN vCollections;
JOIN operator o ON (o.itemPackingTypeFk = c.itemPackingTypeFk l: LOOP
OR c.itemPackingTypeFk IS NULL) SET vDone = FALSE;
AND o.numberOfWagons = c.wagons FETCH vCollections INTO vCollectionFk;
AND o.trainFk = c.trainFk
AND o.warehouseFk = c.warehouseFk IF vDone THEN
AND c.workerFk IS NULL LEAVE l;
WHERE o.workerFk = vUserFk; END IF;
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET vCollectionFk = NULL;
RESIGNAL;
END;
START TRANSACTION;
SELECT workerFk INTO vCollectionWorker
FROM `collection`
WHERE id = vCollectionFk FOR UPDATE;
IF vCollectionWorker IS NULL THEN
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
COMMIT;
LEAVE l;
END IF;
ROLLBACK;
END;
END LOOP;
CLOSE vCollections;
IF vCollectionFk IS NULL THEN IF vCollectionFk IS NULL THEN
CALL collection_new(vUserFk, vCollectionFk); CALL collection_new(vUserFk, vCollectionFk);
START TRANSACTION;
SELECT workerFk INTO vCollectionWorker
FROM `collection`
WHERE id = vCollectionFk FOR UPDATE;
IF vCollectionWorker IS NULL THEN
UPDATE `collection`
SET workerFk = vUserFk
WHERE id = vCollectionFk;
END IF;
COMMIT;
END IF; END IF;
UPDATE collection
SET workerFk = vUserFk
WHERE id = vCollectionFk;
CALL itemShelvingSale_addByCollection(vCollectionFk); CALL itemShelvingSale_addByCollection(vCollectionFk);
DO RELEASE_LOCK(vLockName);
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -45,7 +45,7 @@ BEGIN
LEFT JOIN observation ob ON ob.ticketFk = t.id LEFT JOIN observation ob ON ob.ticketFk = t.id
WHERE t.id = vParamFk WHERE t.id = vParamFk
AND t.shipped >= vYesterday AND t.shipped >= vYesterday
UNION ALL UNION
SELECT t.id ticketFk, SELECT t.id ticketFk,
IF(NOT(vItemPackingTypeFk <=> 'V'), cc.code, CONCAT(SUBSTRING('ABCDEFGH', tc.wagon, 1), '-', tc.`level`)) `level`, IF(NOT(vItemPackingTypeFk <=> 'V'), cc.code, CONCAT(SUBSTRING('ABCDEFGH', tc.wagon, 1), '-', tc.`level`)) `level`,
am.name agencyName, am.name agencyName,
@ -66,7 +66,7 @@ BEGIN
LEFT JOIN vn.worker w ON w.id = c.salesPersonFk LEFT JOIN vn.worker w ON w.id = c.salesPersonFk
LEFT JOIN observation ob ON ob.ticketFk = t.id LEFT JOIN observation ob ON ob.ticketFk = t.id
WHERE tc.collectionFk = vParamFk WHERE tc.collectionFk = vParamFk
UNION ALL UNION
SELECT sg.ticketFk, SELECT sg.ticketFk,
NULL `level`, NULL `level`,
am.name agencyName, am.name agencyName,
@ -83,6 +83,7 @@ BEGIN
LEFT JOIN observation ob ON ob.ticketFk = t.id LEFT JOIN observation ob ON ob.ticketFk = t.id
LEFT JOIN vn.client c ON c.id = t.clientFk LEFT JOIN vn.client c ON c.id = t.clientFk
WHERE sc.id = vParamFk WHERE sc.id = vParamFk
AND t.shipped >= vYesterday; AND t.shipped >= vYesterday
GROUP BY ticketFk;
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -0,0 +1,49 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`itemShelvingSale_deleteAdded`(
vSelf INT(11)
)
proc: BEGIN
/**
* Borra una reservea devolviendo la cantidad al itemShelving
*
* @param vSelf Identificador del itemShelvingSale
*/
DECLARE vSaleFk INT;
DECLARE vHasSalesPicked BOOL;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SELECT iss.saleFk INTO vSaleFk
FROM itemShelvingSale iss
JOIN sale s ON s.id = iss.saleFk
WHERE iss.id = vSelf AND s.isAdded
FOR UPDATE;
IF vSaleFk IS NULL THEN
CALL util.throw('The sale can not be deleted');
END IF;
SELECT COUNT(*) INTO vHasSalesPicked
FROM itemShelvingSale
WHERE saleFk = vSaleFk AND isPicked;
IF vHasSalesPicked THEN
CALL util.throw('A sale with picked sales cannot be deleted');
END IF;
UPDATE itemShelvingSale iss
JOIN itemShelving ish ON ish.id = iss.itemShelvingFk
SET ish.available = ish.available + iss.quantity
WHERE iss.saleFk = vSaleFk;
DELETE FROM sale WHERE id = vSaleFk;
COMMIT;
END$$
DELIMITER ;

View File

@ -0,0 +1,29 @@
DELIMITER $$
CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`queueMember_updateQueue`(
vBusinessFk INT
)
BEGIN
/**
* Elimina la entrada de la cola anterior y luego inserta la nueva para un trabajador.
*
* @param vBusinessFk ID del negocio
*/
DECLARE vNewQueue VARCHAR(10);
DECLARE vExtension VARCHAR(10);
DECLARE exit handler FOR SQLEXCEPTION
SELECT d.pbxQueue, s.extension
INTO vNewQueue, vExtension
FROM business b
JOIN department d ON d.id = b.departmentFk
JOIN pbx.sip s ON s.user_id = b.workerFk
WHERE b.id = vBusinessFk;
DELETE FROM pbx.queueMember
WHERE extension = vExtension COLLATE utf8_general_ci;
INSERT IGNORE INTO pbx.queueMember (queue, extension)
VALUES (vNewQueue, vExtension);
END$$
DELIMITER ;

View File

@ -21,6 +21,8 @@ BEGIN
SET businessFk = vNewBusinessFk SET businessFk = vNewBusinessFk
WHERE id = vSelf; WHERE id = vSelf;
CALL queueMember_updateQueue(vNewBusinessFk);
IF vOldBusinessFk IS NULL THEN IF vOldBusinessFk IS NULL THEN
CALL account.account_enable(vSelf); CALL account.account_enable(vSelf);

View File

@ -3,10 +3,20 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` TRIGGER `vn`.`business_afterUpdate`
AFTER UPDATE ON `business` AFTER UPDATE ON `business`
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
DECLARE vIsActive BOOL;
DECLARE vExtension VARCHAR(10);
CALL worker_updateBusiness(NEW.workerFk); CALL worker_updateBusiness(NEW.workerFk);
IF NOT (OLD.workerFk <=> NEW.workerFk) THEN IF NOT (OLD.workerFk <=> NEW.workerFk) THEN
CALL worker_updateBusiness(OLD.workerFk); CALL worker_updateBusiness(OLD.workerFk);
END IF; END IF;
IF NOT (OLD.departmentFk <=> NEW.departmentFk) THEN
SELECT COUNT(*) INTO vIsActive FROM worker WHERE businessFk = NEW.id;
IF vIsActive THEN
CALL queueMember_updateQueue(NEW.id);
END IF;
END IF;
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -9,5 +9,8 @@ BEGIN
SET NEW.userFk = account.myUser_getId(); SET NEW.userFk = account.myUser_getId();
END IF; END IF;
IF NEW.shelvingFk <> OLD.shelvingFk THEN
SET NEW.movingState = NULL;
END IF;
END$$ END$$
DELIMITER ; DELIMITER ;

View File

@ -0,0 +1,3 @@
ALTER TABLE vn.clientObservation DROP COLUMN IF EXISTS observationTypeFk;
ALTER TABLE vn.clientObservation ADD COLUMN IF NOT EXISTS observationTypeFk TINYINT(3) UNSIGNED NULL;
ALTER TABLE vn.clientObservation ADD CONSTRAINT clientObservationTypeFk FOREIGN KEY IF NOT EXISTS (observationTypeFk) REFERENCES vn.observationType(id);

View File

@ -0,0 +1,3 @@
INSERT IGNORE INTO vn.observationType
SET description = 'Finance',
code = 'finance';

View File

@ -0,0 +1,3 @@
DELETE FROM salix.ACL
WHERE model = 'WorkerLog'
AND property = '*';

View File

@ -0,0 +1,3 @@
UPDATE vn.sale
SET originalQuantity = quantity
WHERE originalQuantity IS NULL

View File

@ -0,0 +1 @@
ALTER TABLE vn.sale MODIFY COLUMN originalQuantity decimal(10,2) DEFAULT 0.00 NOT NULL COMMENT 'Se utiliza para notificar a través de rocket los cambios de quantity';

View File

@ -0,0 +1,2 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS isMoving BOOL DEFAULT FALSE NOT NULL COMMENT 'Indica que se ha marcado este registro para transferirlo a otro sector';

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE hedera.`order` ADD IF NOT EXISTS rowUpdated DATETIME NULL
COMMENT 'Timestamp for last updated record in orderRow table';

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE hedera.`order` ADD IF NOT EXISTS rowUpdated DATETIME NULL
COMMENT 'Timestamp for last updated record in orderRow table';

View File

@ -0,0 +1,2 @@
ALTER TABLE vn.sector ADD isOnReservationMode tinyint(1) DEFAULT FALSE NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
-- Place your SQL code here
ALTER TABLE vn.itemShelving DROP COLUMN IF EXISTS isMoving;
ALTER TABLE vn.itemShelving ADD IF NOT EXISTS movingState ENUM('selected','printed') NULL;

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`ticketConfig`
ADD COLUMN `closureDaysAgo` int(11) NOT NULL DEFAULT 2 COMMENT 'Number of days to look back for ticket closure',
ADD CONSTRAINT `closureDaysAgo_check` CHECK (`closureDaysAgo` > 0);

View File

@ -1,42 +0,0 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Client Add notes path', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
await page.accessToSearchResult('Bruce Banner');
await page.accessToSection('client.card.note.index');
});
afterAll(async() => {
await browser.close();
});
it(`should reach the notes index`, async() => {
await page.waitForState('client.card.note.index');
});
it(`should click on the add note button`, async() => {
await page.waitToClick(selectors.clientNotes.addNoteFloatButton);
await page.waitForState('client.card.note.create');
});
it(`should create a note`, async() => {
await page.waitForSelector(selectors.clientNotes.note);
await page.type(`${selectors.clientNotes.note} textarea`, 'Meeting with Black Widow 21st 9am');
await page.waitToClick(selectors.clientNotes.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it('should confirm the note was created', async() => {
const result = await page.waitToGetProperty(selectors.clientNotes.firstNoteText, 'innerText');
expect(result).toEqual('Meeting with Black Widow 21st 9am');
});
});

View File

@ -28,12 +28,12 @@ describe('Client defaulter path', () => {
const salesPersonName = const salesPersonName =
await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText'); await page.waitToGetProperty(selectors.clientDefaulter.firstSalesPersonName, 'innerText');
expect(clientName).toEqual('Bruce Banner'); expect(clientName).toEqual('Ororo Munroe');
expect(salesPersonName).toEqual('developer'); expect(salesPersonName).toEqual('salesperson');
}); });
it('should first observation not changed', async() => { it('should first observation not changed', async() => {
const expectedObservation = 'Meeting with Black Widow 21st 9am'; const expectedObservation = 'Madness, as you know, is like gravity, all it takes is a little push';
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value'); const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(result).toContain(expectedObservation); expect(result).toContain(expectedObservation);
@ -62,13 +62,4 @@ describe('Client defaulter path', () => {
await page.write(selectors.clientDefaulter.observation, 'My new observation'); await page.write(selectors.clientDefaulter.observation, 'My new observation');
await page.waitToClick(selectors.clientDefaulter.saveButton); await page.waitToClick(selectors.clientDefaulter.saveButton);
}); });
it('should first observation changed', async() => {
const message = await page.waitForSnackbar();
await page.waitForSelector(selectors.clientDefaulter.firstObservation);
const result = await page.waitToGetProperty(selectors.clientDefaulter.firstObservation, 'value');
expect(message.text).toContain('Observation saved!');
expect(result).toContain('My new observation');
});
}); });

View File

@ -381,5 +381,6 @@
"The entry does not have stickers": "La entrada no tiene etiquetas", "The entry does not have stickers": "La entrada no tiene etiquetas",
"This buyer has already made a reservation for this date": "Este comprador ya ha hecho una reserva para esta fecha", "This buyer has already made a reservation for this date": "Este comprador ya ha hecho una reserva para esta fecha",
"No valid travel thermograph found": "No se encontró un termógrafo válido", "No valid travel thermograph found": "No se encontró un termógrafo válido",
"The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea" "The quantity claimed cannot be greater than the quantity of the line": "La cantidad reclamada no puede ser mayor que la cantidad de la línea",
"type cannot be blank": "Se debe rellenar el tipo"
} }

View File

@ -25,7 +25,12 @@
"type": "belongsTo", "type": "belongsTo",
"model": "VnUser", "model": "VnUser",
"foreignKey": "user_id" "foreignKey": "user_id"
},
"queueMember": {
"type": "belongsTo",
"model": "QueueMember",
"foreignKey": "extension",
"primaryKey": "extension"
} }
} }
} }

View File

@ -1,8 +1,11 @@
module.exports = function(Self) { module.exports = function(Self) {
Self.validate('text', isEnabled, {message: 'Description cannot be blank'}); Self.validate('text', function(err) {
function isEnabled(err) {
if (!this.text) err(); if (!this.text) err();
} }, {message: 'Description cannot be blank'});
Self.validate('observationTypeFk', function(err) {
if (!this.observationTypeFk) err();
}, {message: 'type cannot be blank'});
Self.observe('before save', function(ctx, next) { Self.observe('before save', function(ctx, next) {
ctx.instance.created = Date(); ctx.instance.created = Date();

View File

@ -1,10 +1,10 @@
{ {
"name": "ClientObservation", "name": "ClientObservation",
"description": "Client notes", "description": "Client notes",
"base": "VnModel", "base": "VnModel",
"mixins": { "mixins": {
"Loggable": true "Loggable": true
}, },
"options": { "options": {
"mysql": { "mysql": {
"table": "clientObservation" "table": "clientObservation"
@ -26,6 +26,10 @@
"created": { "created": {
"type": "date", "type": "date",
"description": "Creation date and time" "description": "Creation date and time"
},
"observationTypeFk": {
"type": "number",
"description": "Type of observation"
} }
}, },
"relations": { "relations": {
@ -44,11 +48,15 @@
"include": { "include": {
"relation": "worker", "relation": "worker",
"scope": { "scope": {
"fields": ["id"], "fields": [
"id"
],
"include": { "include": {
"relation": "user", "relation": "user",
"scope": { "scope": {
"fields": ["nickname"] "fields": [
"nickname"
]
} }
} }
} }

View File

@ -1,31 +0,0 @@
<vn-crud-model
vn-id="model"
url="clientObservations"
filter="$ctrl.filter"
link="{clientFk: $ctrl.$params.id}"
data="notes"
auto-load="true">
</vn-crud-model>
<vn-data-viewer
model="model"
class="vn-w-md">
<vn-card class="vn-pa-md">
<div
ng-repeat="note in notes"
class="note vn-pa-sm border-solid border-radius vn-mb-md">
<vn-horizontal class="vn-mb-sm" style="color: #666">
<vn-one>{{::note.worker.user.nickname}}</vn-one>
<vn-auto>{{::note.created | date:'dd/MM/yyyy HH:mm'}}</vn-auto>
</vn-horizontal>
<vn-horizontal class="text">
{{::note.text}}
</vn-horizontal>
</div>
</vn-card>
</vn-data-viewer>
<a vn-tooltip="New note"
ui-sref="client.card.note.create({id: $ctrl.$params.id})"
vn-bind="+"
fixed-bottom-right>
<vn-float-button icon="add"></vn-float-button>
</a>

View File

@ -5,9 +5,10 @@ import './style.scss';
export default class Controller extends Section { export default class Controller extends Section {
constructor($element, $) { constructor($element, $) {
super($element, $); super($element, $);
this.filter = { }
order: 'created DESC', async $onInit() {
}; this.$state.go('home');
window.location.href = await this.vnApp.getUrl(`customer/${this.$params.id}/notes`);
} }
} }

View File

@ -0,0 +1,16 @@
Tickets monitor: Monitor de tickets
Clients on website: Clientes activos en la web
Recent order actions: Acciones recientes en pedidos
Search tickets: Buscar tickets
Delete selected elements: Eliminar los elementos seleccionados
All the selected elements will be deleted. Are you sure you want to continue?: Todos los elementos seleccionados serán eliminados. ¿Seguro que quieres continuar?
Component lack: Faltan componentes
Ticket too little: Ticket demasiado pequeño
Minimize/Maximize: Minimizar/Maximizar
Problems: Problemas
Theoretical: Teórica
Practical: Práctica
Preparation: Preparación
Auto-refresh: Auto-refresco
Toggle auto-refresh every 2 minutes: Conmuta el refresco automático cada 2 minutos
Is fragile: Es frágil

View File

@ -59,6 +59,9 @@
"isReserve": { "isReserve": {
"type": "boolean", "type": "boolean",
"required": true "required": true
},
"isOnReservationMode": {
"type": "boolean"
} }
} }
} }

View File

@ -0,0 +1,45 @@
module.exports = Self => {
Self.remoteMethod('getWithPackaging', {
description: 'Returns the list of suppliers with an entry of type packaging',
accessType: 'READ',
returns: {
type: 'object',
root: true
},
http: {
path: `/getWithPackaging`,
verb: 'GET'
},
nolimit: true
});
Self.getWithPackaging = async options => {
const models = Self.app.models;
const myOptions = {};
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
if (typeof options == 'object')
Object.assign(myOptions, options);
const entries = await models.Entry.find({
where: {
typeFk: 'packaging',
created: {gte: oneYearAgo}
},
include: {
relation: 'supplier',
scope: {
fields: ['id', 'name']
}
},
fields: {supplierFk: true}
}, myOptions);
const result = entries.map(item => ({
id: item.supplier().id,
name: item.supplier().name
}));
return Array.from(new Map(result.map(entry => [entry.id, entry])).values());
};
};

View File

@ -0,0 +1,33 @@
const {models} = require('vn-loopback/server/server');
describe('Supplier getWithPackaging()', () => {
it('should return a list of suppliers with an entry of type packaging', async() => {
const typeFk = 'packaging';
const tx = await models.Supplier.beginTransaction({});
const myOptions = {transaction: tx};
try {
const entry = await models.Entry.findOne(
{
where: {
id: 1
},
myOptions
});
await entry.updateAttributes({
typeFk: typeFk,
created: new Date()
});
const result = await models.Supplier.getWithPackaging(myOptions);
expect(result.length).toEqual(1);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

@ -12,6 +12,7 @@ module.exports = Self => {
require('../methods/supplier/campaignMetricsEmail')(Self); require('../methods/supplier/campaignMetricsEmail')(Self);
require('../methods/supplier/newSupplier')(Self); require('../methods/supplier/newSupplier')(Self);
require('../methods/supplier/getItemsPackaging')(Self); require('../methods/supplier/getItemsPackaging')(Self);
require('../methods/supplier/getWithPackaging')(Self);
Self.validatesPresenceOf('name', { Self.validatesPresenceOf('name', {
message: 'The social name cannot be empty' message: 'The social name cannot be empty'

View File

@ -53,8 +53,9 @@ module.exports = Self => {
JOIN province p ON p.id = c.provinceFk JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
JOIN ticketConfig tc ON TRUE
WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered')) WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered'))
AND DATE(t.shipped) BETWEEN ? - INTERVAL 2 DAY AND util.dayEnd(?) AND t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?)
AND t.refFk IS NULL AND t.refFk IS NULL
GROUP BY t.id GROUP BY t.id
`, [toDate, toDate]); `, [toDate, toDate]);
@ -108,6 +109,7 @@ module.exports = Self => {
JOIN alertLevel al ON al.id = ts.alertLevel JOIN alertLevel al ON al.id = ts.alertLevel
JOIN client c ON c.id = t.clientFk JOIN client c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk JOIN province p ON p.id = c.provinceFk
JOIN ticketConfig tc ON TRUE
LEFT JOIN autonomy a ON a.id = p.autonomyFk LEFT JOIN autonomy a ON a.id = p.autonomyFk
JOIN country co ON co.id = p.countryFk JOIN country co ON co.id = p.countryFk
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
@ -116,7 +118,7 @@ module.exports = Self => {
LEFT JOIN vn.invoiceOutSerial ios ON ios.taxAreaFk = 'WORLD' LEFT JOIN vn.invoiceOutSerial ios ON ios.taxAreaFk = 'WORLD'
AND ios.code = invoiceSerial(t.clientFk, t.companyFk, 'multiple') AND ios.code = invoiceSerial(t.clientFk, t.companyFk, 'multiple')
WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered')) WHERE (al.code = 'PACKED' OR (am.code = 'refund' AND al.code <> 'delivered'))
AND DATE(t.shipped) BETWEEN ? - INTERVAL 2 DAY AND util.dayEnd(?) AND t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?)
AND t.refFk IS NULL AND t.refFk IS NULL
AND IFNULL(a.hasDailyInvoice, co.hasDailyInvoice) AND IFNULL(a.hasDailyInvoice, co.hasDailyInvoice)
GROUP BY ticketFk GROUP BY ticketFk
@ -141,9 +143,10 @@ module.exports = Self => {
JOIN alertLevel al ON al.id = ts.alertLevel JOIN alertLevel al ON al.id = ts.alertLevel
JOIN agencyMode am ON am.id = t.agencyModeFk JOIN agencyMode am ON am.id = t.agencyModeFk
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN ticketConfig tc ON TRUE
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id
SET t.routeFk = NULL SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN ? - INTERVAL 2 DAY AND util.dayEnd(?) WHERE t.shipped BETWEEN ? - INTERVAL tc.closureDaysAgo DAY AND util.dayEnd(?)
AND al.code NOT IN ('DELIVERED', 'PACKED') AND al.code NOT IN ('DELIVERED', 'PACKED')
AND NOT t.packages AND NOT t.packages
AND tob.id IS NULL AND tob.id IS NULL

View File

@ -12,16 +12,15 @@ module.exports = async function(ctx, Self, tickets, options) {
Object.assign(myOptions, options); Object.assign(myOptions, options);
let tx; let tx;
if (!myOptions.transaction) { // IMPORTANT: Due to its high cost in production, wrapping this process in a transaction may cause timeouts.
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
if (tickets.length == 0) return; if (tickets.length == 0) return;
const failedtickets = []; const failedtickets = [];
for (const ticket of tickets) { for (const ticket of tickets) {
try { try {
await Self.rawSql(`CALL util.debugAdd('invoicingTicket', ?)`, [ticket.id], {userId});
await Self.app.models.InvoiceOut.getSerial(ticket.clientFk, ticket.companyFk, ticket.addressFk, 'quick'); await Self.app.models.InvoiceOut.getSerial(ticket.clientFk, ticket.companyFk, ticket.addressFk, 'quick');
await Self.rawSql( await Self.rawSql(
`CALL vn.ticket_closeByTicket(?)`, `CALL vn.ticket_closeByTicket(?)`,
@ -149,6 +148,11 @@ module.exports = async function(ctx, Self, tickets, options) {
myOptions); myOptions);
} }
} catch (error) { } catch (error) {
await Self.rawSql(`
INSERT INTO util.debug (variable, value)
VALUES ('invoicingTicketError', ?)
`, [ticket.id + ' - ' + error]);
if (error.responseCode == 450) { if (error.responseCode == 450) {
await invalidEmail(ticket); await invalidEmail(ticket);
continue; continue;

View File

@ -50,9 +50,9 @@ describe('Ticket closure functionality', () => {
expect(ticketStateBefore.code).not.toBe(ticketStateAfter.code); expect(ticketStateBefore.code).not.toBe(ticketStateAfter.code);
const ticketAfter = await models.TicketState.findById(ticketId, null, options); const ticketAfter = await models.Ticket.findById(ticketId, null, options);
expect(ticketAfter.refFk).toBeUndefined(); expect(ticketAfter.refFk).toBeNull();
}); });
it('should send Incoterms authorization email on first order', async() => { it('should send Incoterms authorization email on first order', async() => {

View File

@ -87,7 +87,7 @@ module.exports = Self => {
{ {
relation: 'sector', relation: 'sector',
scope: { scope: {
fields: ['warehouseFk', 'description'], fields: ['warehouseFk', 'description', 'isOnReservationMode'],
} }
}, { }, {
relation: 'printer', relation: 'printer',

View File

@ -67,6 +67,12 @@ module.exports = Self => {
type: 'String', type: 'String',
description: 'The worker user name', description: 'The worker user name',
http: {source: 'query'} http: {source: 'query'}
},
{
arg: 'email',
type: 'String',
description: 'The user email',
http: {source: 'query'}
} }
], ],
returns: { returns: {
@ -99,6 +105,8 @@ module.exports = Self => {
return {'w.firstName': {like: `%${value}%`}}; return {'w.firstName': {like: `%${value}%`}};
case 'lastName': case 'lastName':
return {'w.lastName': {like: `%${value}%`}}; return {'w.lastName': {like: `%${value}%`}};
case 'nickname':
return {'u.nickname': {like: `%${value}%`}};
case 'extension': case 'extension':
return {'p.extension': value}; return {'p.extension': value};
case 'fi': case 'fi':
@ -107,6 +115,8 @@ module.exports = Self => {
return {'d.id': value}; return {'d.id': value};
case 'userName': case 'userName':
return {'u.name': {like: `%${value}%`}}; return {'u.name': {like: `%${value}%`}};
case 'email':
return {'eu.email': {like: `%${value}%`}};
} }
}); });
@ -116,15 +126,23 @@ module.exports = Self => {
let stmt; let stmt;
stmt = new ParameterizedSQL( stmt = new ParameterizedSQL(
`SELECT w.id, u.email, p.extension, u.name as userName, `SELECT w.id,
d.name AS department, w.lastName, u.nickname, mu.email w.lastName,
w.firstName,
u.email,
u.nickname,
p.extension,
u.name as userName,
d.name AS department,
eu.email,
c.fi
FROM worker w FROM worker w
LEFT JOIN workerDepartment wd ON wd.workerFk = w.id LEFT JOIN workerDepartment wd ON wd.workerFk = w.id
LEFT JOIN department d ON d.id = wd.departmentFk LEFT JOIN department d ON d.id = wd.departmentFk
LEFT JOIN client c ON c.id = w.id LEFT JOIN client c ON c.id = w.id
LEFT JOIN account.user u ON u.id = w.id LEFT JOIN account.user u ON u.id = w.id
LEFT JOIN pbx.sip p ON p.user_id = u.id LEFT JOIN pbx.sip p ON p.user_id = u.id
LEFT JOIN account.emailUser mu ON mu.userFk = u.id` LEFT JOIN account.emailUser eu ON eu.userFk = u.id`
); );
stmt.merge(conn.makeSuffix(filter)); stmt.merge(conn.makeSuffix(filter));

View File

@ -217,6 +217,7 @@ module.exports = Self => {
const code = e.code; const code = e.code;
const message = e.sqlMessage; const message = e.sqlMessage;
if (e.message && e.message.includes('Invalid email')) throw new UserError('Invalid email');
if (e.message && e.message.includes(`Email already exists`)) throw new UserError(`This personal mail already exists`); if (e.message && e.message.includes(`Email already exists`)) throw new UserError(`This personal mail already exists`);
if (code === 'ER_DUP_ENTRY' && message.includes(`CodigoTrabajador_UNIQUE`)) throw new UserError(`This worker code already exists`); if (code === 'ER_DUP_ENTRY' && message.includes(`CodigoTrabajador_UNIQUE`)) throw new UserError(`This worker code already exists`);
if (code === 'ER_DUP_ENTRY' && message.includes(`PRIMARY`)) throw new UserError(`This worker already exists`); if (code === 'ER_DUP_ENTRY' && message.includes(`PRIMARY`)) throw new UserError(`This worker already exists`);

View File

@ -253,7 +253,18 @@
"relation": "client" "relation": "client"
}, },
{ {
"relation": "sip" "relation": "sip",
"scope": {
"include": {
"relation": "queueMember",
"scope": {
"fields": [
"queue",
"extension"
]
}
}
}
} }
] ]
}, },

View File

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