diff --git a/.eslintrc.yml b/.eslintrc.yml index edbc47195..0d74348f2 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -36,3 +36,7 @@ rules: jasmine/no-focused-tests: 0 jasmine/prefer-toHaveBeenCalledWith: 0 arrow-spacing: ["error", { "before": true, "after": true }] + no-restricted-syntax: + - "error" + - selector: "NewExpression[callee.name='Date']" + message: "Use Date.vnNew() instead of new Date()." diff --git a/back/methods/collection/spec/assign.spec.js b/back/methods/collection/spec/assign.spec.js index 745343819..b00631d22 100644 --- a/back/methods/collection/spec/assign.spec.js +++ b/back/methods/collection/spec/assign.spec.js @@ -28,9 +28,10 @@ describe('ticket assign()', () => { await tx.rollback(); }); - it('should throw an error when there is not picking tickets', async() => { + it('should throw an error when there are no picking tickets', async() => { try { await models.Collection.assign(ctx, options); + fail('Expected an error to be thrown, but none was thrown.'); } catch (e) { expect(e.message).toEqual('There are not picking tickets'); } diff --git a/back/methods/workerActivity/add.js b/back/methods/workerActivity/add.js index 4592a0797..89131491d 100644 --- a/back/methods/workerActivity/add.js +++ b/back/methods/workerActivity/add.js @@ -31,7 +31,7 @@ module.exports = Self => { return await Self.rawSql(` INSERT INTO workerActivity (workerFk, workerActivityTypeFk, model) SELECT ?, ?, ? - FROM workerTimeControlParams wtcp + FROM workerTimeControlConfig wtcc LEFT JOIN ( SELECT wa.workerFk, wa.created, @@ -44,7 +44,7 @@ module.exports = Self => { ) sub ON TRUE WHERE sub.workerFk IS NULL OR sub.code <> ? - OR TIMESTAMPDIFF(SECOND, sub.created, util.VN_NOW()) > wtcp.dayBreak;` + OR TIMESTAMPDIFF(SECOND, sub.created, util.VN_NOW()) > wtcc.dayBreak;` , [userId, code, model, userId, code], myOptions); }; }; diff --git a/back/models/expedition_PrintOut.json b/back/models/expedition_PrintOut.json index dd49b0234..23a2fdbc4 100644 --- a/back/models/expedition_PrintOut.json +++ b/back/models/expedition_PrintOut.json @@ -14,9 +14,6 @@ }, "itemFk": { "type": "number" - }, - "isChecked": { - "type": "boolean" } } } \ No newline at end of file diff --git a/db/dump/fixtures.before.sql b/db/dump/fixtures.before.sql index 12b885437..e5d5f53dc 100644 --- a/db/dump/fixtures.before.sql +++ b/db/dump/fixtures.before.sql @@ -1518,19 +1518,19 @@ INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseO (11, util.VN_CURDATE() - INTERVAL 1 DAY , util.VN_CURDATE(), 6, 3, 0, 50.00, 500, 'eleventh travel', 1, 2, 4, FALSE, NULL), (12, util.VN_CURDATE() , util.VN_CURDATE() + INTERVAL 1 DAY, 6, 3, 0, 50.00, 500, 'eleventh travel', 1, 2, 4, FALSE, NULL); -INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `isConfirmed`, `companyFk`, `invoiceNumber`, `reference`, `isExcludedFromAvailable`, `evaNotes`) - VALUES - (1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 1, 442, 'IN2001', 'Movement 1', 0, ''), - (2, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 442, 'IN2002', 'Movement 2', 0, 'observation two'), - (3, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 3, 0, 442, 'IN2003', 'Movement 3', 0, 'observation three'), - (4, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 69, 'IN2004', 'Movement 4', 0, 'observation four'), - (5, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 0, 442, 'IN2005', 'Movement 5', 0, 'observation five'), - (6, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 6, 0, 442, 'IN2006', 'Movement 6', 0, 'observation six'), - (7, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2007', 'Movement 7', 0, 'observation seven'), - (8, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2008', 'Movement 8', 1,''), - (9, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL +2 DAY), 10, 0, 442, 'IN2009', 'Movement 9', 1, ''), - (10, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL +2 DAY), 10, 0, 442, 'IN2009', 'Movement 10', 1, ''), - (99, 69, '2000-12-01 00:00:00.000', 11, 0, 442, 'IN2009', 'Movement 99', 0, ''); +INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `isConfirmed`, `companyFk`, `invoiceNumber`, `reference`, `isExcludedFromAvailable`, `evaNotes`, `typeFk`) + VALUES + (1, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 1, 1, 442, 'IN2001', 'Movement 1', 0, '', 'packaging'), + (2, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 442, 'IN2002', 'Movement 2', 0, 'observation two', 'product'), + (3, 1, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 3, 0, 442, 'IN2003', 'Movement 3', 0, 'observation three', 'product'), + (4, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 2, 0, 69, 'IN2004', 'Movement 4', 0, 'observation four', 'product'), + (5, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 5, 0, 442, 'IN2005', 'Movement 5', 0, 'observation five', 'product'), + (6, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 6, 0, 442, 'IN2006', 'Movement 6', 0, 'observation six', 'product'), + (7, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2007', 'Movement 7', 0, 'observation seven', 'product'), + (8, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 7, 0, 442, 'IN2008', 'Movement 8', 1, '', 'product'), + (9, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL +2 DAY), 10, 0, 442, 'IN2009', 'Movement 9', 1, '', 'product'), + (10, 2, DATE_ADD(util.VN_CURDATE(), INTERVAL +2 DAY), 10, 0, 442, 'IN2009', 'Movement 10', 1, '', 'product'), + (99, 69, util.VN_CURDATE() - INTERVAL 1 MONTH, 11, 0, 442, 'IN2009', 'Movement 99', 0, '', 'product'); INSERT INTO `vn`.`entryConfig` (`defaultEntry`, `inventorySupplierFk`, `defaultSupplierFk`) VALUES (2, 4, 1); @@ -2521,10 +2521,6 @@ INSERT INTO `vn`.`queuePriority`(`id`, `priority`, `code`) (2, 'Normal', 'normal'), (3, 'Baja', 'low'); -INSERT INTO `vn`.`workerTimeControlParams` (`id`, `dayBreak`, `weekBreak`, `weekScope`, `dayWorkMax`, `dayStayMax`, `weekMaxBreak`, `weekMaxScope`, `askInOut`) - VALUES - (1, 43200, 129600, 734400, 43200, 50400, 259200, 1296000, 36000); - INSERT IGNORE INTO `vn`.`greugeConfig` (`id`, `freightPickUpPrice`) VALUES ('1', '11'); INSERT INTO `vn`.`thermograph`(`id`, `model`) @@ -4028,12 +4024,12 @@ INSERT INTO srt.buffer (id, x, y, `size`, `length`, stateFk, typeFk, isActive, c (9, 1400, 1500, 500, 13000, 1, 1, 1, '04B', 4, 0, NULL, NULL, NULL, NULL, 0, 1, 1, NULL), (10, 0, 2000, 500, 13000, 1, 1, 1, '05A', 5, 0, NULL, NULL, NULL, NULL, 0, 1, 1, NULL); - INSERT IGNORE INTO vn.saySimpleCountry (countryFk, channel) VALUES (19, '1169'), (8, '1183'); INSERT IGNORE INTO vn.saySimpleConfig (url, defaultChannel) - VALUES ('saysimle-url-mock', 1320);INSERT INTO vn.workerIrpf (workerFk,spouseNif,geographicMobilityDate) - VALUES - (1106,'26493101E','2019-09-20'); + VALUES ('saysimle-url-mock', 1320); + +INSERT INTO vn.workerIrpf (workerFk,spouseNif, geographicMobilityDate) + VALUES (1106,'26493101E','2019-09-20'); diff --git a/db/routines/vn/functions/timeWorkerControl_getDirection.sql b/db/routines/vn/functions/timeWorkerControl_getDirection.sql deleted file mode 100644 index c0f1e67ea..000000000 --- a/db/routines/vn/functions/timeWorkerControl_getDirection.sql +++ /dev/null @@ -1,65 +0,0 @@ -DELIMITER $$ -CREATE OR REPLACE DEFINER=`vn`@`localhost` FUNCTION `vn`.`timeWorkerControl_getDirection`(vUserFk INT, vTimed DATETIME) - RETURNS varchar(6) CHARSET utf8mb3 COLLATE utf8mb3_unicode_ci - NOT DETERMINISTIC - READS SQL DATA -BEGIN -/** - * Verifica la dirección de la fichada - * @param vUserFk Identificador del trabajador - * @param vTimed Hora de la fichada - * @return Retorna sentido de la fichada 'in, out, middle' - */ - - DECLARE vPrevious DATETIME ; - DECLARE vNext DATETIME ; - DECLARE vPreviousDirection VARCHAR(3) ; - DECLARE vNextDirection VARCHAR(3) ; - DECLARE vDayStayMax INT; - DECLARE vTimedSeconds INT; - DECLARE vLastTimeIn INT; - - SELECT UNIX_TIMESTAMP(vTimed) INTO vTimedSeconds; - - SELECT dayStayMax INTO vDayStayMax - FROM vn.workerTimeControlParams; - - SELECT timed, direction INTO vNext,vNextDirection - FROM vn.workerTimeControl - WHERE userFk = vUserFk - AND direction IN ('in','out') - AND timed > vTimed - ORDER BY timed ASC - LIMIT 1; - - SELECT timed, direction INTO vPrevious, vPreviousDirection - FROM vn.workerTimeControl - WHERE userFk = vUserFk - AND direction IN ('in','out') - AND timed < vTimed - ORDER BY timed DESC - LIMIT 1; - - IF (vTimedSeconds - UNIX_TIMESTAMP(vPrevious) + UNIX_TIMESTAMP(vNext) - vTimedSeconds)<= vDayStayMax AND vPreviousDirection = 'in' AND vNextDirection = 'out' THEN - RETURN 'middle'; - END IF; - - - IF (vTimedSeconds> UNIX_TIMESTAMP(vPrevious)) THEN - IF vPreviousDirection = 'in' THEN - RETURN 'out'; - ELSE - SELECT UNIX_TIMESTAMP(MAX(timed)) INTO vLastTimeIn - FROM vn.workerTimeControl - WHERE userFk = vUserFk - AND direction ='in' - AND timed < vPrevious; - IF vTimedSeconds - vLastTimeIn <= vDayStayMax THEN - RETURN 'out'; - END IF; - END IF; - END IF; - - RETURN 'in'; -END$$ -DELIMITER ; diff --git a/db/routines/vn/procedures/entry_isEditable.sql b/db/routines/vn/procedures/entry_isEditable.sql index 12b6d0ef6..c417f6789 100644 --- a/db/routines/vn/procedures/entry_isEditable.sql +++ b/db/routines/vn/procedures/entry_isEditable.sql @@ -9,16 +9,18 @@ BEGIN * * @param vSelf Id de entrada */ - DECLARE vIsEditable BOOL; + DECLARE vIsNotEditable BOOL DEFAULT FALSE; - SELECT e.isBooked INTO vIsEditable + SELECT TRUE INTO vIsNotEditable FROM `entry` e - JOIN entryType et ON et.code = e.typeFk - WHERE NOT et.isInformal - AND e.id = vSelf; + LEFT JOIN entryType et ON et.code = e.typeFk + WHERE e.id = vSelf + AND e.isBooked + AND (e.typeFk IS NULL OR NOT et.isInformal); - IF vIsEditable AND NOT IFNULL(@isModeInventory, FALSE) THEN + IF vIsNotEditable AND NOT IFNULL(@isModeInventory, FALSE) THEN CALL util.throw(CONCAT('Entry ', vSelf, ' is not editable')); END IF; + END$$ DELIMITER ; diff --git a/db/routines/vn/procedures/expeditionScan_Put.sql b/db/routines/vn/procedures/expeditionScan_Put.sql index fc7d4da23..999b9af10 100644 --- a/db/routines/vn/procedures/expeditionScan_Put.sql +++ b/db/routines/vn/procedures/expeditionScan_Put.sql @@ -4,12 +4,12 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`expeditionScan_Put`( vExpeditionFk INT ) BEGIN - IF NOT (SELECT TRUE FROM expedition WHERE id = vExpeditionFk LIMIT 1) THEN - CALL util.throw('Expedition not exists'); + IF NOT EXISTS (SELECT id FROM expeditionPallet WHERE id = vPalletFk) THEN + CALL util.throw('Pallet not exists'); END IF; - IF NOT (SELECT TRUE FROM expeditionPallet WHERE id = vPalletFk LIMIT 1) THEN - CALL util.throw('Pallet not exists'); + IF NOT EXISTS (SELECT id FROM expedition WHERE id = vExpeditionFk) THEN + CALL util.throw('Expedition not exists'); END IF; REPLACE expeditionScan(expeditionFk, palletFk) diff --git a/db/routines/vn/procedures/itemShelving_get.sql b/db/routines/vn/procedures/itemShelving_get.sql index 7e59a5cfc..0038257c2 100644 --- a/db/routines/vn/procedures/itemShelving_get.sql +++ b/db/routines/vn/procedures/itemShelving_get.sql @@ -22,7 +22,7 @@ BEGIN ic.url, ish.available, ish.buyFk, - sh.shelvingFk + ish.shelvingFk FROM itemShelving ish JOIN item i ON i.id = ish.itemFk JOIN shelving s ON s.id = ish.shelvingFk diff --git a/db/routines/vn/procedures/ticket_splitItemPackingType.sql b/db/routines/vn/procedures/ticket_splitItemPackingType.sql index 0ee865af5..31e0c24e7 100644 --- a/db/routines/vn/procedures/ticket_splitItemPackingType.sql +++ b/db/routines/vn/procedures/ticket_splitItemPackingType.sql @@ -5,122 +5,122 @@ CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`ticket_splitItemPacki ) BEGIN /** - * Clona y reparte las ventas de un ticket en funcion del tipo de empaquetado. - * Respeta el id inicial para el tipo propuesto. + * Separa en diferentes tickets según el tipo de empaquetado + * El ticket original conserva las líneas del tipo de empaquetado especificado + * Las líneas sin tipo de empaquetado se asignan al ticket del tipo por defecto. * - * @param vSelf Id ticket - * @param vOriginalItemPackingTypeFk Tipo para el que se reserva el número de ticket original + * @param vSelf Id del ticket original + * @param vOriginalItemPackingTypeFk Tipo de empaquetado a mantener en el ticket original * @return table tmp.ticketIPT(ticketFk, itemPackingTypeFk) */ - DECLARE vItemPackingTypeFk VARCHAR(1) DEFAULT 'H'; + DECLARE vIsDone BOOLEAN DEFAULT FALSE; + DECLARE vCurrentPackingType VARCHAR(1); + DECLARE vDefaultPackingType VARCHAR(1); + DECLARE vHasOriginalPackingType BOOLEAN; DECLARE vNewTicketFk INT; - DECLARE vPackingTypesToSplit INT; - DECLARE vDone INT DEFAULT FALSE; + DECLARE vTicketFk INT; - DECLARE vSaleGroup CURSOR FOR - SELECT itemPackingTypeFk - FROM tSaleGroup - WHERE itemPackingTypeFk IS NOT NULL - ORDER BY (itemPackingTypeFk = vOriginalItemPackingTypeFk) DESC; + DECLARE vItemPackingTypes CURSOR FOR + SELECT DISTINCT itemPackingTypeFk FROM tSalesToMove; - DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vIsDone = TRUE; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + CREATE OR REPLACE TEMPORARY TABLE tSalesToMove ( + ticketFk INT, + saleFk INT, + itemPackingTypeFk VARCHAR(1) + ) ENGINE=MEMORY; + + SELECT COALESCE(MAX(ic.defaultPackingTypeFk), MAX(i.itemPackingTypeFk)) INTO vDefaultPackingType + FROM vn.sale s + JOIN item i ON i.id = s.itemFk + LEFT JOIN itemConfig ic ON ic.defaultPackingTypeFk = i.itemPackingTypeFk + WHERE s.ticketFk = vSelf + GROUP BY s.ticketFk; + + SELECT EXISTS ( + SELECT TRUE + FROM sale s + JOIN item i ON i.id = s.itemFk + WHERE s.ticketFk = vSelf + AND i.itemPackingTypeFk = vOriginalItemPackingTypeFk + ) INTO vHasOriginalPackingType; + + IF vOriginalItemPackingTypeFk IS NULL OR NOT vHasOriginalPackingType THEN + SET vOriginalItemPackingTypeFk = vDefaultPackingType; + END IF; START TRANSACTION; - SELECT id - FROM sale - WHERE ticketFk = vSelf - AND NOT quantity + SELECT t.id INTO vTicketFk + FROM ticket t + JOIN sale s ON s.id = t.id + WHERE t.id = vSelf FOR UPDATE; - DELETE FROM sale - WHERE NOT quantity - AND ticketFk = vSelf; - - CREATE OR REPLACE TEMPORARY TABLE tSale - (PRIMARY KEY (id)) - ENGINE = MEMORY - SELECT s.id, i.itemPackingTypeFk, IFNULL(sv.litros, 0) litros + INSERT INTO tSalesToMove (saleFk, itemPackingTypeFk) + SELECT s.id, i.itemPackingTypeFk FROM sale s JOIN item i ON i.id = s.itemFk - LEFT JOIN saleVolume sv ON sv.saleFk = s.id - WHERE s.ticketFk = vSelf; + WHERE s.ticketFk = vSelf + AND i.itemPackingTypeFk <> vOriginalItemPackingTypeFk; - CREATE OR REPLACE TEMPORARY TABLE tSaleGroup - ENGINE = MEMORY - SELECT itemPackingTypeFk, SUM(litros) totalLitros - FROM tSale - GROUP BY itemPackingTypeFk; + OPEN vItemPackingTypes; + l: LOOP + SET vIsDone = FALSE; + FETCH vItemPackingTypes INTO vCurrentPackingType; - SELECT COUNT(*) INTO vPackingTypesToSplit - FROM tSaleGroup - WHERE itemPackingTypeFk IS NOT NULL; + IF vIsDone THEN + LEAVE l; + END IF; - CREATE OR REPLACE TEMPORARY TABLE tmp.ticketIPT( - ticketFk INT, - itemPackingTypeFk VARCHAR(1) - ) ENGINE = MEMORY; + CALL ticket_Clone(vSelf, vNewTicketFk); - CASE vPackingTypesToSplit - WHEN 0 THEN - INSERT INTO tmp.ticketIPT(ticketFk, itemPackingTypeFk) - VALUES(vSelf, vItemPackingTypeFk); - WHEN 1 THEN - INSERT INTO tmp.ticketIPT(ticketFk, itemPackingTypeFk) - SELECT vSelf, itemPackingTypeFk - FROM tSaleGroup - WHERE itemPackingTypeFk IS NOT NULL; - ELSE - OPEN vSaleGroup; - FETCH vSaleGroup INTO vItemPackingTypeFk; + SELECT id INTO vTicketFk + FROM ticket t + WHERE t.id = vNewTicketFk + FOR UPDATE; - INSERT INTO tmp.ticketIPT(ticketFk, itemPackingTypeFk) - VALUES(vSelf, vItemPackingTypeFk); + UPDATE tSalesToMove + SET ticketFk = vNewTicketFk + WHERE itemPackingTypeFk = vCurrentPackingType; - l: LOOP - SET vDone = FALSE; - FETCH vSaleGroup INTO vItemPackingTypeFk; + IF vCurrentPackingType = vDefaultPackingType THEN + INSERT INTO tSalesToMove (ticketFk, saleFk, itemPackingTypeFk) + SELECT vNewTicketFk, s.id, i.itemPackingTypeFk + FROM sale s + JOIN item i ON i.id = s.itemFk + WHERE s.ticketFk = vSelf + AND i.itemPackingTypeFk IS NULL; + END IF; - IF vDone THEN - LEAVE l; - END IF; + END LOOP; + CLOSE vItemPackingTypes; - CALL ticket_Clone(vSelf, vNewTicketFk); + UPDATE sale s + JOIN tSalesToMove t ON t.saleFk = s.id + SET s.ticketFk = t.ticketFk; - INSERT INTO tmp.ticketIPT(ticketFk, itemPackingTypeFk) - VALUES(vNewTicketFk, vItemPackingTypeFk); - END LOOP; - - CLOSE vSaleGroup; - - SELECT s.id - FROM sale s - JOIN tSale ts ON ts.id = s.id - JOIN tmp.ticketIPT t ON t.itemPackingTypeFk = ts.itemPackingTypeFk - FOR UPDATE; - - UPDATE sale s - JOIN tSale ts ON ts.id = s.id - JOIN tmp.ticketIPT t ON t.itemPackingTypeFk = ts.itemPackingTypeFk - SET s.ticketFk = t.ticketFk; - - SELECT itemPackingTypeFk INTO vItemPackingTypeFk - FROM tSaleGroup sg - WHERE sg.itemPackingTypeFk IS NOT NULL - ORDER BY sg.itemPackingTypeFk - LIMIT 1; - - UPDATE sale s - JOIN tSale ts ON ts.id = s.id - JOIN tmp.ticketIPT t ON t.itemPackingTypeFk = vItemPackingTypeFk - SET s.ticketFk = t.ticketFk - WHERE ts.itemPackingTypeFk IS NULL; - END CASE; + CREATE OR REPLACE TEMPORARY TABLE tmp.ticketIPT + ENGINE=MEMORY + SELECT s.ticketFk, MAX(i.itemPackingTypeFk) itemPackingTypeFk + FROM sale s + JOIN item i ON i.id = s.itemFk + WHERE s.ticketFk = vSelf + GROUP BY s.ticketFk + UNION + SELECT ticketFk, MAX(itemPackingTypeFk) + FROM tSalesToMove + GROUP BY ticketFk; COMMIT; - DROP TEMPORARY TABLE - tSale, - tSaleGroup; + DROP TEMPORARY TABLE tSalesToMove; END$$ DELIMITER ; diff --git a/db/routines/vn/procedures/workerTimeControl_check.sql b/db/routines/vn/procedures/workerTimeControl_check.sql deleted file mode 100644 index 30cf5c639..000000000 --- a/db/routines/vn/procedures/workerTimeControl_check.sql +++ /dev/null @@ -1,168 +0,0 @@ -DELIMITER $$ -CREATE OR REPLACE DEFINER=`vn`@`localhost` PROCEDURE `vn`.`workerTimeControl_check`(vUserFk INT, vDated DATE,vTabletFk VARCHAR(100)) -proc: BEGIN -/** - * Verifica si el empleado puede fichar en el momento actual, si puede fichar llama a workerTimeControlAdd - * - * @param vUserFk Identificador del trabajador - * @return Retorna si encuentra un problema 'odd','maxTimeWork','breakDay','breakWeek' ; - * En caso de tener algun problema retorna el primero que encuentra - */ - DECLARE vLastIn DATETIME ; - DECLARE vLastOut DATETIME ; - DECLARE vDayWorkMax INT; - DECLARE vDayBreak INT; - DECLARE vWeekBreak INT ; - DECLARE vWeekScope INT; - DECLARE vDayStayMax INT; - DECLARE vProblem VARCHAR(20) DEFAULT NULL; - DECLARE vTimedWorked INT; - DECLARE vCalendarStateType VARCHAR(20) DEFAULT NULL; - DECLARE vDepartmentFk INT; - DECLARE vTo VARCHAR(50) DEFAULT NULL; - DECLARE vUserName VARCHAR(50) DEFAULT NULL; - DECLARE vBody VARCHAR(255) DEFAULT NULL; - - SELECT dayBreak, weekBreak, weekScope, dayWorkMax, dayStayMax - INTO vDayBreak, vWeekBreak, vWeekScope, vDayWorkMax, vDayStayMax - FROM workerTimeControlParams; - - SELECT MAX(timed) INTO vLastIn - FROM workerTimeControl - WHERE userFk = vUserFk - AND direction = 'in'; - - SELECT MAX(timed) INTO vLastOut - FROM workerTimeControl - WHERE userFk = vUserFk - AND direction = 'out'; - - SELECT CONCAT(u.name,'@verdnatura.es') INTO vTo - FROM account.user u - WHERE u.id = (SELECT bossFk FROM worker WHERE id = vUserFk); - - SELECT CONCAT(firstName,' ',lastName) INTO vUserName - FROM worker w - WHERE w.id = vUserFk; - - - IF UNIX_TIMESTAMP(util.VN_NOW()) - UNIX_TIMESTAMP(vLastIn) > vDayStayMax THEN -- NUEVA JORNADA - - -- VERIFICAR DESCANSO DIARIO - IF UNIX_TIMESTAMP(util.VN_NOW()) - UNIX_TIMESTAMP(vLastOut) < vDayBreak THEN - SELECT "Descansos 12 h" AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"Descansos 12 h") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - END IF; - - -- VERIFICAR FICHADAS IMPARES DEL ÚLTIMO DÍA QUE SE FICHÓ - IF (SELECT MOD(COUNT(*),2) -- <>0 - FROM workerTimeControl - WHERE userFk = vUserFk - AND timed >= vLastIn - ) THEN - SELECT "Dias con fichadas impares" AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"Dias con fichadas impares") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - END IF; - - -- VERIFICAR VACACIONES - SELECT at2.name INTO vCalendarStateType - FROM calendar c - JOIN business b ON b.id = c.businessFk - JOIN absenceType at2 ON at2.id = c.dayOffTypeFk - WHERE c.dated = util.VN_CURDATE() - AND at2.isAllowedToWork = FALSE - AND b.workerFk = vUserFk - LIMIT 1; - - IF(LENGTH(vCalendarStateType)) THEN - SELECT vCalendarStateType AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"Vacaciones") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - - END IF; - - -- VERIFICAR CONTRATO EN VIGOR - IF (SELECT COUNT(*) - FROM business b - WHERE b.workerFk = vUserFk - AND b.started <= vDated - AND IFNULL(b.ended, vDated) >= vDated - ) = 0 THEN - SELECT "No hay un contrato en vigor" AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"No hay un contrato en vigor") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - - END IF; - - -- VERIFICAR DESCANSO SEMANAL - SET @vHasBreakWeek:= FALSE; - SET @vLastTimed:= UNIX_TIMESTAMP((util.VN_NOW() - INTERVAL vWeekScope SECOND)); - - DROP TEMPORARY TABLE IF EXISTS tmp.trash; - CREATE TEMPORARY TABLE tmp.trash - SELECT IF(vWeekBreak-(UNIX_TIMESTAMP(timed)-@vLastTimed) <= 0, @vHasBreakWeek:=TRUE, TRUE) alias, - @vLastTimed:= UNIX_TIMESTAMP(timed) - FROM workerTimeControl - WHERE timed>= (util.VN_NOW() - INTERVAL vWeekScope SECOND) - AND userFk= vUserFk - AND direction IN ('in','out') - ORDER BY timed ASC; - - IF UNIX_TIMESTAMP(util.VN_NOW()) - UNIX_TIMESTAMP(vLastOut) < vWeekBreak AND @vHasBreakWeek = FALSE THEN -- REVISA SI EL DESCANSO SE HA REALIZADO DESPUÉS DE LA ÚLTIMA FICHADA - SELECT "Descansos 36 h" AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"Descansos 36 h") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - END IF; - - DROP TEMPORARY TABLE tmp.trash; - - ELSE -- DIA ACTUAL - - -- VERIFICA QUE EL TIEMPO EFECTIVO NO SUPERE EL MÁXIMO - SELECT IFNULL(SUM(if( mod(wtc.order,2)=1, -UNIX_TIMESTAMP(timed), UNIX_TIMESTAMP(timed))),0) - IF( MOD(COUNT(*),2), UNIX_TIMESTAMP(util.VN_NOW()), 0) INTO vTimedWorked - FROM workerTimeControl wtc - WHERE userFk = vUserFk - AND timed >= vLastIn - ORDER BY timed; - - IF vTimedWorked > vDayWorkMax THEN - SELECT "Jornadas" AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No ha podido fichar por el siguiente problema: ',"Jornadas") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - END IF; - - END IF; - - -- VERIFICAR DEPARTAMENTO - /* IF vTabletFk IS NOT NULL THEN - SELECT wtcu.departmentFk INTO vDepartmentFk - FROM workerTimeControlUserInfo wtcu - WHERE wtcu.userFk = vUserFk; - IF (SELECT COUNT(td.tabletFk) - FROM tabletDepartment td - WHERE td.tabletFk = vTabletFk AND td.departmentFk = vDepartmentFk - ) = 0 THEN - SELECT "No perteneces a este departamento." AS problem; - -- ENVIAMOS CORREO AL BOSSFK - SELECT CONCAT(vUserName,' No a podido fichar por el siguiente problema: ',"No perteneces a este departamento.") INTO vBody; - CALL mail_insert(vTo,vTo,'error al fichar',vBody); - LEAVE proc; - END IF; - END IF;*/ - -END$$ -DELIMITER ; diff --git a/db/routines/vn/triggers/entry_beforeUpdate.sql b/db/routines/vn/triggers/entry_beforeUpdate.sql index 1d0c26fc1..4f2c5ed4a 100644 --- a/db/routines/vn/triggers/entry_beforeUpdate.sql +++ b/db/routines/vn/triggers/entry_beforeUpdate.sql @@ -38,10 +38,10 @@ BEGIN CALL travel_throwAwb(NEW.travelFk); END IF; - SELECT isRaid INTO vIsRaid + SELECT t.isRaid INTO vIsRaid FROM travel t JOIN entry e ON e.travelFk = t.id - WHERE entryFk = NEW.id; + WHERE e.id = NEW.id; SELECT NOT (o.warehouseInFk <=> n.warehouseInFk) OR NOT (o.warehouseOutFk <=> n.warehouseOutFk) diff --git a/db/versions/11312-navyAralia/00-firstScript.sql b/db/versions/11312-navyAralia/00-firstScript.sql index 466b18f6b..452b1feed 100644 --- a/db/versions/11312-navyAralia/00-firstScript.sql +++ b/db/versions/11312-navyAralia/00-firstScript.sql @@ -6,10 +6,18 @@ INSERT INTO salix.ACL (model,property,accessType,permission,principalType,princi ('WorkerRelative','updateAttributes','*','ALLOW','ROLE','hr'), ('WorkerRelative','crud','WRITE','ALLOW','ROLE','hr'), ('WorkerRelative','findById','*','ALLOW','ROLE','hr'), + ('WorkerRelative','find','*','ALLOW','ROLE','hr'), + ('WorkerRelative','upsert','*','ALLOW','ROLE','hr'), + ('WorkerRelative','filter','*','ALLOW','ROLE','hr'), ('WorkerIrpf','updateAttributes','*','ALLOW','ROLE','hr'), ('WorkerIrpf','crud','*','ALLOW','ROLE','hr'), - ('WorkerRelative','findById','*','ALLOW','ROLE','hr'), + ('WorkerIrpf','findById','*','ALLOW','ROLE','hr'), + ('WorkerIrpf','find','*','ALLOW','ROLE','hr'), + ('WorkerIrpf','upsert','*','ALLOW','ROLE','hr'), + ('WorkerIrpf','filter','*','ALLOW','ROLE','hr'), ('DisabilityGrade','updateAttributes','*','ALLOW','ROLE','hr'), ('DisabilityGrade','crud','*','ALLOW','ROLE','hr'), - ('DisabilityGrade','findById','*','ALLOW','ROLE','hr'); + ('DisabilityGrade','findById','*','ALLOW','ROLE','hr'), + ('DisabilityGrade','find','*','ALLOW','ROLE','hr'), + ('DisabilityGrade','upsert','*','ALLOW','ROLE','hr'); diff --git a/db/versions/11322-azureAspidistra/00-entryAcl.sql b/db/versions/11322-azureAspidistra/00-entryAcl.sql new file mode 100644 index 000000000..836737d4b --- /dev/null +++ b/db/versions/11322-azureAspidistra/00-entryAcl.sql @@ -0,0 +1,40 @@ +-- Eliminar registros existentes donde property = '*' +DELETE FROM `salix`.ACL WHERE model = 'entry' AND property = '*'; + +-- Insertar permisos para los métodos solicitados en el modelo Entry +INSERT INTO `salix`.ACL (model, property, accessType, permission, principalType, principalId) +VALUES + -- Permisos para administrative + ('Entry', 'upsert', 'WRITE', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'updateAttributes', 'WRITE', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'isBooked', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'findById', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'find', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'filter', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'count', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'getEntry', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'getBuys', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'findOne', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'deleteBuys', 'WRITE', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'editLatestBuys', 'WRITE', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'importBuys', 'WRITE', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'importBuysPreview', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'lastItemBuys', 'READ', 'ALLOW', 'ROLE', 'administrative'), + ('Entry', 'latestBuysFilter', 'READ', 'ALLOW', 'ROLE', 'administrative'), + + -- Permisos para buyer (excluyendo isBooked) + ('Entry', 'upsert', 'WRITE', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'updateAttributes', 'WRITE', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'findById', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'find', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'filter', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'count', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'getEntry', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'getBuys', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'findOne', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'deleteBuys', 'WRITE', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'editLatestBuys', 'WRITE', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'importBuys', 'WRITE', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'importBuysPreview', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'lastItemBuys', 'READ', 'ALLOW', 'ROLE', 'buyer'), + ('Entry', 'latestBuysFilter', 'READ', 'ALLOW', 'ROLE', 'buyer'); diff --git a/db/versions/11351-bronzeMedeola/00-firstScript.sql b/db/versions/11351-bronzeMedeola/00-firstScript.sql new file mode 100644 index 000000000..c94447a89 --- /dev/null +++ b/db/versions/11351-bronzeMedeola/00-firstScript.sql @@ -0,0 +1,2 @@ + +ALTER TABLE vn.itemConfig ADD defaultPackingTypeFk VARCHAR(1) DEFAULT 'H' NULL; diff --git a/db/versions/11354-aquaMastic/00-firstScript.sql b/db/versions/11354-aquaMastic/00-firstScript.sql new file mode 100644 index 000000000..dda3f4752 --- /dev/null +++ b/db/versions/11354-aquaMastic/00-firstScript.sql @@ -0,0 +1,2 @@ +RENAME TABLE vn.workerTimeControlParams TO vn.workerTimeControlParams__; +ALTER TABLE vn.workerTimeControlParams__ COMMENT='@deprecated 2024-11-19'; \ No newline at end of file diff --git a/db/versions/11357-whiteGerbera/00-firstScript.sql b/db/versions/11357-whiteGerbera/00-firstScript.sql new file mode 100644 index 000000000..c10ec4d26 --- /dev/null +++ b/db/versions/11357-whiteGerbera/00-firstScript.sql @@ -0,0 +1,4 @@ +ALTER TABLE vn.travel ADD IF NOT EXISTS isRaid tinyint(1) DEFAULT 0 NOT NULL COMMENT 'Redada'; + +ALTER TABLE vn.travel MODIFY COLUMN daysInForward int(10) unsigned DEFAULT NULL + COMMENT 'Cuando es una redada, indica el número de días que se añadirán a la fecha de hoy para establecer el landed. NULL si no es una redada'; diff --git a/e2e/paths/04-item/01_summary.spec.js b/e2e/paths/04-item/01_summary.spec.js new file mode 100644 index 000000000..51195be48 --- /dev/null +++ b/e2e/paths/04-item/01_summary.spec.js @@ -0,0 +1,133 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item summary path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'item'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should search for an item', async() => { + await page.doSearch('Ranged weapon'); + const resultsCount = await page.countElement(selectors.itemsIndex.searchResult); + + await page.waitForTextInElement(selectors.itemsIndex.firstSearchResult, 'Ranged weapon'); + await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); + const isVisible = await page.isVisible(selectors.itemSummary.basicData); + + expect(resultsCount).toBe(4); + expect(isVisible).toBeTruthy(); + }); + + it(`should check the item summary preview shows fields from basic data`, async() => { + await page.waitForTextInElement(selectors.itemSummary.basicData, 'Ranged weapon longbow 200cm'); + const result = await page.waitToGetProperty(selectors.itemSummary.basicData, 'innerText'); + + expect(result).toContain('Ranged weapon longbow 200cm'); + }); + + it(`should check the item summary preview shows fields from tags`, async() => { + await page.waitForTextInElement(selectors.itemSummary.tags, 'Brown'); + const result = await page.waitToGetProperty(selectors.itemSummary.tags, 'innerText'); + + expect(result).toContain('Brown'); + }); + + it(`should check the item summary preview shows fields from botanical`, async() => { + await page.waitForTextInElement(selectors.itemSummary.botanical, 'Abelia'); + const result = await page.waitToGetProperty(selectors.itemSummary.botanical, 'innerText'); + + expect(result).toContain('Abelia'); + }); + + it(`should check the item summary preview shows fields from barcode`, async() => { + await page.waitForTextInElement(selectors.itemSummary.barcode, '1'); + const result = await page.waitToGetProperty(selectors.itemSummary.barcode, 'innerText'); + + expect(result).toContain('1'); + }); + + it(`should close the summary popup`, async() => { + await page.closePopup(); + await page.waitForSelector(selectors.itemSummary.basicData, {hidden: true}); + }); + + it('should search for other item', async() => { + await page.doSearch('Melee Reinforced'); + const resultsCount = await page.countElement(selectors.itemsIndex.searchResult); + + await page.waitToClick(selectors.itemsIndex.firstResultPreviewButton); + await page.waitForSelector(selectors.itemSummary.basicData, {visible: true}); + + expect(resultsCount).toBe(3); + }); + + it(`should now check the item summary preview shows fields from basic data`, async() => { + await page.waitForTextInElement(selectors.itemSummary.basicData, 'Melee Reinforced weapon combat fist 15cm'); + const result = await page.waitToGetProperty(selectors.itemSummary.basicData, 'innerText'); + + expect(result).toContain('Melee Reinforced weapon combat fist 15cm'); + }); + + it(`should now check the item summary preview shows fields from tags`, async() => { + await page.waitForTextInElement(selectors.itemSummary.tags, 'Silver'); + const result = await page.waitToGetProperty(selectors.itemSummary.tags, 'innerText'); + + expect(result).toContain('Silver'); + }); + + it(`should now check the item summary preview shows fields from botanical`, async() => { + await page.waitForTextInElement(selectors.itemSummary.botanical, '-'); + const result = await page.waitToGetProperty(selectors.itemSummary.botanical, 'innerText'); + + expect(result).toContain('-'); + }); + + it(`should now close the summary popup`, async() => { + await page.closePopup(); + await page.waitForSelector(selectors.itemSummary.basicData, {hidden: true}); + }); + + it(`should navigate to one of the items detailed section`, async() => { + await page.accessToSearchResult('Melee weapon combat fist 15cm'); + await page.waitForState('item.card.summary'); + }); + + it(`should check the descritor edit button is not visible for employee`, async() => { + const visibleButton = await page.isVisible(selectors.itemDescriptor.editButton); + + expect(visibleButton).toBeFalsy(); + }); + + it(`should check the item summary shows fields from basic data section`, async() => { + await page.waitForTextInElement(selectors.itemSummary.basicData, 'Melee weapon combat fist 15cm'); + const result = await page.waitToGetProperty(selectors.itemSummary.basicData, 'innerText'); + + expect(result).toContain('Melee weapon combat fist 15cm'); + }); + + it(`should check the item summary shows fields from tags section`, async() => { + const result = await page.waitToGetProperty(selectors.itemSummary.tags, 'innerText'); + + expect(result).toContain('Silver'); + }); + + it(`should check the item summary shows fields from botanical section`, async() => { + const result = await page.waitToGetProperty(selectors.itemSummary.botanical, 'innerText'); + + expect(result).toContain('procera'); + }); + + it(`should check the item summary shows fields from barcodes section`, async() => { + const result = await page.waitToGetProperty(selectors.itemSummary.barcode, 'innerText'); + + expect(result).toContain('4'); + }); +}); diff --git a/e2e/paths/04-item/02_basic_data.spec.js b/e2e/paths/04-item/02_basic_data.spec.js new file mode 100644 index 000000000..3bad18303 --- /dev/null +++ b/e2e/paths/04-item/02_basic_data.spec.js @@ -0,0 +1,64 @@ +import getBrowser from '../../helpers/puppeteer'; + +const $ = { + form: 'vn-item-basic-data form', + intrastatForm: '.vn-dialog.shown form', + newIntrastatButton: 'vn-item-basic-data vn-icon-button[vn-tooltip="New intrastat"] > button' +}; + +describe('Item Edit basic data path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('Melee weapon combat fist 15cm'); + }); + + beforeEach(async() => { + await page.accessToSection('item.card.basicData'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should edit the item basic data and confirm the item data was edited`, async() => { + const values = { + type: 'Anthurium', + intrastat: 'Coral y materiales similares', + relevancy: 1, + generic: 'Pallet', + isActive: false, + priceInKg: true, + isFragile: true, + packingOut: 5 + }; + + const message = await page.sendForm($.form, values); + await page.reloadSection('item.card.basicData'); + const formValues = await page.fetchForm($.form, Object.keys(values)); + + expect(message.isSuccess).toBeTrue(); + expect(formValues).toEqual(values); + }); + + it(`should create a new intrastat and save it`, async() => { + await page.click($.newIntrastatButton); + await page.waitForSelector($.intrastatForm); + await page.fillForm($.intrastatForm, { + id: '588420239', + description: 'Tropical Flowers' + }); + await page.respondToDialog('accept'); + + const message = await page.sendForm($.form); + await page.reloadSection('item.card.basicData'); + const formValues = await page.fetchForm($.form, ['intrastat']); + + expect(message.isSuccess).toBeTrue(); + expect(formValues).toEqual({intrastat: 'Tropical Flowers'}); + }); +}); diff --git a/e2e/paths/04-item/03_tax.spec.js b/e2e/paths/04-item/03_tax.spec.js new file mode 100644 index 000000000..6013094e9 --- /dev/null +++ b/e2e/paths/04-item/03_tax.spec.js @@ -0,0 +1,48 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item edit tax path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('Ranged weapon longbow 200cm'); + await page.accessToSection('item.card.tax'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should add the item tax to all countries`, async() => { + await page.autocompleteSearch(selectors.itemTax.firstClass, 'General VAT'); + await page.autocompleteSearch(selectors.itemTax.secondClass, 'General VAT'); + await page.waitToClick(selectors.itemTax.submitTaxButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the first item tax class was edited`, async() => { + await page.reloadSection('item.card.tax'); + const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value'); + + expect(firstVatType).toEqual('General VAT'); + }); + + it(`should confirm the second item tax class was edited`, async() => { + const secondVatType = await page + .waitToGetProperty(selectors.itemTax.secondClass, 'value'); + + expect(secondVatType).toEqual('General VAT'); + }); + + it(`should edit the first class without saving the form`, async() => { + await page.autocompleteSearch(selectors.itemTax.firstClass, 'Reduced VAT'); + const firstVatType = await page.waitToGetProperty(selectors.itemTax.firstClass, 'value'); + + expect(firstVatType).toEqual('Reduced VAT'); + }); +}); diff --git a/e2e/paths/04-item/04_tags.spec.js b/e2e/paths/04-item/04_tags.spec.js new file mode 100644 index 000000000..f13cf9aa4 --- /dev/null +++ b/e2e/paths/04-item/04_tags.spec.js @@ -0,0 +1,79 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item create tags path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('Ranged weapon longbow 200cm'); + await page.accessToSection('item.card.tags'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should create a new tag and delete a former one', async() => { + await page.waitToClick(selectors.itemTags.fourthRemoveTagButton); + await page.waitToClick(selectors.itemTags.addItemTagButton); + await page.autocompleteSearch(selectors.itemTags.seventhTag, 'Ancho de la base'); + await page.write(selectors.itemTags.seventhValue, '50'); + await page.clearInput(selectors.itemTags.seventhRelevancy); + await page.write(selectors.itemTags.seventhRelevancy, '4'); + await page.waitToClick(selectors.itemTags.submitItemTagsButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the fourth row data is the expected one', async() => { + await page.reloadSection('item.card.tags'); + await page.waitForSelector('vn-item-tags'); + let result = await page.waitToGetProperty(selectors.itemTags.fourthTag, 'value'); + + expect(result).toEqual('Ancho de la base'); + + result = await page + .waitToGetProperty(selectors.itemTags.fourthValue, 'value'); + + expect(result).toEqual('50'); + + result = await page + .waitToGetProperty(selectors.itemTags.fourthRelevancy, 'value'); + + expect(result).toEqual('4'); + }); + + it('should confirm the fifth row data is the expected one', async() => { + let tag = await page + .waitToGetProperty(selectors.itemTags.fifthTag, 'value'); + + let value = await page + .waitToGetProperty(selectors.itemTags.fifthValue, 'value'); + + let relevancy = await page + .waitToGetProperty(selectors.itemTags.fifthRelevancy, 'value'); + + expect(tag).toEqual('Color'); + expect(value).toEqual('Brown'); + expect(relevancy).toEqual('5'); + }); + + it('should confirm the sixth row data is the expected one', async() => { + let tag = await page + .waitToGetProperty(selectors.itemTags.sixthTag, 'value'); + + let value = await page + .waitToGetProperty(selectors.itemTags.sixthValue, 'value'); + + let relevancy = await page + .waitToGetProperty(selectors.itemTags.sixthRelevancy, 'value'); + + expect(tag).toEqual('Categoria'); + expect(value).toEqual('+1 precission'); + expect(relevancy).toEqual('6'); + }); +}); diff --git a/e2e/paths/04-item/05_botanical.spec.js b/e2e/paths/04-item/05_botanical.spec.js new file mode 100644 index 000000000..1671cc5d2 --- /dev/null +++ b/e2e/paths/04-item/05_botanical.spec.js @@ -0,0 +1,66 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item Create botanical path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('Ranged weapon pistol 9mm'); + await page.accessToSection('item.card.botanical'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should create a new botanical for the item`, async() => { + await page.autocompleteSearch(selectors.itemBotanical.genus, 'Abelia'); + await page.autocompleteSearch(selectors.itemBotanical.species, 'dealbata'); + await page.waitToClick(selectors.itemBotanical.submitBotanicalButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the Genus for the item was created`, async() => { + await page.waitForTextInField(selectors.itemBotanical.genus, 'Abelia'); + const result = await page + .waitToGetProperty(selectors.itemBotanical.genus, 'value'); + + expect(result).toEqual('Abelia'); + }); + + it(`should confirm the Species for the item was created`, async() => { + const result = await page + .waitToGetProperty(selectors.itemBotanical.species, 'value'); + + expect(result).toEqual('dealbata'); + }); + + it(`should edit botanical for the item`, async() => { + await page.autocompleteSearch(selectors.itemBotanical.genus, 'Abies'); + await page.autocompleteSearch(selectors.itemBotanical.species, 'decurrens'); + await page.waitToClick(selectors.itemBotanical.submitBotanicalButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the Genus for the item was edited`, async() => { + await page.waitForTextInField(selectors.itemBotanical.genus, 'Abies'); + const result = await page + .waitToGetProperty(selectors.itemBotanical.genus, 'value'); + + expect(result).toEqual('Abies'); + }); + + it(`should confirm the Species for the item was edited`, async() => { + const result = await page + .waitToGetProperty(selectors.itemBotanical.species, 'value'); + + expect(result).toEqual('decurrens'); + }); +}); diff --git a/e2e/paths/04-item/06_barcode.spec.js b/e2e/paths/04-item/06_barcode.spec.js new file mode 100644 index 000000000..36c9c39ae --- /dev/null +++ b/e2e/paths/04-item/06_barcode.spec.js @@ -0,0 +1,37 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item Create barcodes path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('Ranged weapon longbow 200cm'); + await page.accessToSection('item.card.itemBarcode'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should click create a new code and delete a former one`, async() => { + await page.waitToClick(selectors.itemBarcodes.firstCodeRemoveButton); + await page.waitToClick(selectors.itemBarcodes.addBarcodeButton); + await page.write(selectors.itemBarcodes.thirdCode, '5'); + await page.waitToClick(selectors.itemBarcodes.submitBarcodesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the barcode 5 is created and it is now the third barcode as the first was deleted`, async() => { + await page.reloadSection('item.card.itemBarcode'); + await page.waitForTextInField(selectors.itemBarcodes.thirdCode, '5'); + const result = await page + .waitToGetProperty(selectors.itemBarcodes.thirdCode, 'value'); + + expect(result).toEqual('5'); + }); +}); diff --git a/e2e/paths/04-item/07_create.spec.js b/e2e/paths/04-item/07_create.spec.js new file mode 100644 index 000000000..c20be9ebc --- /dev/null +++ b/e2e/paths/04-item/07_create.spec.js @@ -0,0 +1,65 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +const $ = { + form: 'vn-item-create form' +}; + +describe('Item Create', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should access to the create item view by clicking the create floating button', async() => { + await page.waitToClick(selectors.itemsIndex.createItemButton); + await page.waitForState('item.create'); + }); + + it('should return to the item index by clickig the cancel button', async() => { + await page.waitToClick(selectors.itemCreateView.cancelButton); + await page.waitForState('item.index'); + }); + + it('should now access to the create item view by clicking the create floating button', async() => { + await page.waitToClick(selectors.itemsIndex.createItemButton); + await page.waitForState('item.create'); + }); + + it('should throw an error when insert an invalid priority', async() => { + const values = { + name: 'Infinity Gauntlet', + type: 'Crisantemo', + intrastat: 'Coral y materiales similares', + origin: 'Holand', + priority: null + }; + const message = await page.sendForm($.form, values); + + expect(message.text).toContain('Valid priorities'); + }); + + it('should create the Infinity Gauntlet item', async() => { + const values = { + name: 'Infinity Gauntlet', + type: 'Crisantemo', + intrastat: 'Coral y materiales similares', + origin: 'Holand', + priority: '2' + }; + + await page.fillForm($.form, values); + const formValues = await page.fetchForm($.form, Object.keys(values)); + const message = await page.sendForm($.form); + + expect(message.isSuccess).toBeTrue(); + expect(formValues).toEqual(values); + }); +}); diff --git a/e2e/paths/04-item/08_regularize.spec.js b/e2e/paths/04-item/08_regularize.spec.js new file mode 100644 index 000000000..9b3074776 --- /dev/null +++ b/e2e/paths/04-item/08_regularize.spec.js @@ -0,0 +1,141 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item regularize path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'item'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should edit the user local warehouse', async() => { + await page.waitForSpinnerLoad(); + await page.waitToClick(selectors.globalItems.userMenuButton); + await page.autocompleteSearch(selectors.globalItems.userLocalWarehouse, 'Warehouse Four'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should check the local settings were saved', async() => { + const userLocalWarehouse = await page + .waitToGetProperty(selectors.globalItems.userLocalWarehouse, 'value'); + + await page.closePopup(); + + expect(userLocalWarehouse).toContain('Warehouse Four'); + }); + + it('should search for a specific item', async() => { + await page.accessToSearchResult('Ranged weapon pistol 9mm'); + await page.waitForState('item.card.summary'); + }); + + it('should open the regularize dialog and check the warehouse matches the local user settings', async() => { + await page.waitToClick(selectors.itemDescriptor.moreMenu); + await page.waitToClick(selectors.itemDescriptor.moreMenuRegularizeButton); + const result = await page.waitToGetProperty(selectors.itemDescriptor.regularizeWarehouse, 'value'); + + expect(result).toEqual('Warehouse Four'); + }); + + it('should regularize the item', async() => { + await page.write(selectors.itemDescriptor.regularizeQuantity, '100'); + await page.autocompleteSearch(selectors.itemDescriptor.regularizeWarehouse, 'Warehouse One'); + await page.waitToClick(selectors.itemDescriptor.regularizeSaveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should click on the Tickets button of the top bar menu', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await Promise.all([ + page.waitForNavigation({waitUntil: ['load', 'networkidle0', 'domcontentloaded']}), + page.waitToClick(selectors.globalItems.ticketsButton) + ]); + await page.waitForState('ticket.index'); + }); + + it('should clear the user local settings now', async() => { + await page.waitToClick(selectors.globalItems.userMenuButton); + await page.waitForContentLoaded(); + await page.clearInput(selectors.globalItems.userConfigFirstAutocomplete); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should search for the ticket with alias missing', async() => { + await page.keyboard.press('Escape'); + await page.accessToSearchResult('missing'); + await page.waitForState('ticket.card.summary'); + }); + + it(`should check the ticket sale quantity is showing a negative value`, async() => { + await page.waitForTextInElement(selectors.ticketSummary.firstSaleQuantity, '-100'); + const result = await page + .waitToGetProperty(selectors.ticketSummary.firstSaleQuantity, 'innerText'); + + expect(result).toContain('-100'); + }); + + it(`should check the ticket sale discount is 100%`, async() => { + const result = await page + .waitToGetProperty(selectors.ticketSummary.firstSaleDiscount, 'innerText'); + + expect(result).toContain('100 %'); + }); + + it('should now click on the Items button of the top bar menu', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.itemsButton); + await page.waitForState('item.index'); + }); + + it('should search for the item once again', async() => { + await page.accessToSearchResult('Ranged weapon pistol 9mm'); + await page.waitForState('item.card.summary'); + }); + + it('should regularize the item once more', async() => { + await page.waitToClick(selectors.itemDescriptor.moreMenu); + await page.waitToClick(selectors.itemDescriptor.moreMenuRegularizeButton); + await page.write(selectors.itemDescriptor.regularizeQuantity, '100'); + await page.autocompleteSearch(selectors.itemDescriptor.regularizeWarehouse, 'Warehouse One'); + await page.waitToClick(selectors.itemDescriptor.regularizeSaveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should again click on the Tickets button of the top bar menu', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await Promise.all([ + page.waitForNavigation({waitUntil: ['load', 'networkidle0', 'domcontentloaded']}), + page.waitToClick(selectors.globalItems.ticketsButton) + ]); + await page.waitForState('ticket.index'); + }); + + it('should search for the ticket missing once again', async() => { + await page.accessToSearchResult('Missing'); + await page.waitForState('ticket.card.summary'); + }); + + it(`should check the ticket contains now two sales`, async() => { + await page.waitForTextInElement(selectors.ticketSummary.firstSaleQuantity, '-100'); + const result = await page.countElement(selectors.ticketSummary.sale); + + expect(result).toEqual(2); + }); +}); diff --git a/e2e/paths/04-item/09_index.spec.js b/e2e/paths/04-item/09_index.spec.js new file mode 100644 index 000000000..6e0a4bd5c --- /dev/null +++ b/e2e/paths/04-item/09_index.spec.js @@ -0,0 +1,84 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item index path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('salesPerson', 'item'); + await page.waitToClick(selectors.globalItems.searchButton); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should click on the fields to show button to open the list of columns to show', async() => { + await page.waitToClick(selectors.itemsIndex.shownColumns); + const visible = await page.isVisible(selectors.itemsIndex.shownColumnsList); + + expect(visible).toBeTruthy(); + }); + + it('should unmark all checkboxes except the first and the last ones', async() => { + await page.waitToClick(selectors.itemsIndex.idCheckbox); + await page.waitToClick(selectors.itemsIndex.stemsCheckbox); + await page.waitToClick(selectors.itemsIndex.sizeCheckbox); + await page.waitToClick(selectors.itemsIndex.typeCheckbox); + await page.waitToClick(selectors.itemsIndex.categoryCheckbox); + await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); + await page.waitToClick(selectors.itemsIndex.originCheckbox); + await page.waitToClick(selectors.itemsIndex.buyerCheckbox); + await page.waitToClick(selectors.itemsIndex.weightByPieceCheckbox); + await page.waitToClick(selectors.itemsIndex.saveFieldsButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should navigate forth and back to see the images column is still visible', async() => { + await page.closePopup(); + await page.waitToClick(selectors.itemsIndex.firstSearchResult); + await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); + await page.waitToClick(selectors.globalItems.searchButton); + await page.waitForSelector(selectors.itemsIndex.searchResult); + await page.waitImgLoad(selectors.itemsIndex.firstItemImage); + const imageVisible = await page.isVisible(selectors.itemsIndex.firstItemImageTd); + + expect(imageVisible).toBeTruthy(); + }); + + it('should check the ids column is not visible', async() => { + await page.waitForSelector(selectors.itemsIndex.firstItemId, {hidden: true}); + }); + + it('should mark all unchecked boxes to leave the index as it was', async() => { + await page.waitToClick(selectors.itemsIndex.shownColumns); + await page.waitToClick(selectors.itemsIndex.idCheckbox); + await page.waitToClick(selectors.itemsIndex.stemsCheckbox); + await page.waitToClick(selectors.itemsIndex.sizeCheckbox); + await page.waitToClick(selectors.itemsIndex.typeCheckbox); + await page.waitToClick(selectors.itemsIndex.categoryCheckbox); + await page.waitToClick(selectors.itemsIndex.intrastadCheckbox); + await page.waitToClick(selectors.itemsIndex.originCheckbox); + await page.waitToClick(selectors.itemsIndex.buyerCheckbox); + await page.waitToClick(selectors.itemsIndex.weightByPieceCheckbox); + await page.waitToClick(selectors.itemsIndex.saveFieldsButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should now navigate forth and back to see the ids column is now visible', async() => { + await page.closePopup(); + await page.waitToClick(selectors.itemsIndex.firstSearchResult); + await page.waitToClick(selectors.itemDescriptor.goBackToModuleIndexButton); + await page.waitToClick(selectors.globalItems.searchButton); + await page.waitForSelector(selectors.itemsIndex.searchResult); + const idVisible = await page.isVisible(selectors.itemsIndex.firstItemId); + + expect(idVisible).toBeTruthy(); + }); +}); diff --git a/e2e/paths/04-item/10_item_log.spec.js b/e2e/paths/04-item/10_item_log.spec.js new file mode 100644 index 000000000..c88fbd337 --- /dev/null +++ b/e2e/paths/04-item/10_item_log.spec.js @@ -0,0 +1,45 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item log path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('developer', 'item'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should search for the Knowledge artifact to confirm it isn't created yet`, async() => { + await page.doSearch('Knowledge artifact'); + const nResults = await page.countElement(selectors.itemsIndex.searchResult); + + expect(nResults).toEqual(1); + }); + + it('should access to the create item view by clicking the create floating button', async() => { + await page.waitToClick(selectors.itemsIndex.createItemButton); + await page.waitForState('item.create'); + }); + + it('should create the Knowledge artifact item', async() => { + await page.write(selectors.itemCreateView.temporalName, 'Knowledge artifact'); + await page.autocompleteSearch(selectors.itemCreateView.type, 'Crisantemo'); + await page.autocompleteSearch(selectors.itemCreateView.intrastat, 'Coral y materiales similares'); + await page.autocompleteSearch(selectors.itemCreateView.origin, 'Holand'); + await page.waitToClick(selectors.itemCreateView.createButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should return to the items index by clicking the return to items button', async() => { + await page.waitToClick(selectors.itemBasicData.goToItemIndexButton); + await page.waitForSelector(selectors.itemsIndex.createItemButton); + await page.waitForState('item.index'); + }); +}); diff --git a/e2e/paths/04-item/11_descriptor.spec.js b/e2e/paths/04-item/11_descriptor.spec.js new file mode 100644 index 000000000..eb9ed2573 --- /dev/null +++ b/e2e/paths/04-item/11_descriptor.spec.js @@ -0,0 +1,41 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item descriptor path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSearchResult('1'); + await page.accessToSection('item.card.basicData'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should set the item to inactive', async() => { + await page.waitToClick(selectors.itemBasicData.isActiveCheckbox); + await page.waitToClick(selectors.itemBasicData.submitBasicDataButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should reload the section and check the inactive icon is visible', async() => { + await page.reloadSection('item.card.basicData'); + const visibleIcon = await page.isVisible(selectors.itemDescriptor.inactiveIcon); + + expect(visibleIcon).toBeTruthy(); + }); + + it('should set the item back to active', async() => { + await page.waitToClick(selectors.itemBasicData.isActiveCheckbox); + await page.waitToClick(selectors.itemBasicData.submitBasicDataButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/e2e/paths/04-item/12_request.spec.js b/e2e/paths/04-item/12_request.spec.js new file mode 100644 index 000000000..e0f3a1b45 --- /dev/null +++ b/e2e/paths/04-item/12_request.spec.js @@ -0,0 +1,45 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Item request path', () => { + let browser; + let page; + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSection('item.request'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should reach the item request section', async() => { + await page.waitForState('item.request'); + }); + + it('should fill the id and quantity then check the concept was updated', async() => { + await page.writeOnEditableTD(selectors.itemRequest.firstRequestItemID, '4'); + await page.writeOnEditableTD(selectors.itemRequest.firstRequestQuantity, '10'); + await page.waitForTextInElement(selectors.itemRequest.firstRequestConcept, 'Melee weapon heavy shield 100cm'); + let filledConcept = await page.waitToGetProperty(selectors.itemRequest.firstRequestConcept, 'innerText'); + + expect(filledConcept).toContain('Melee weapon heavy shield 100cm'); + }); + + it('should check the status of the request should now be accepted', async() => { + let status = await page.waitToGetProperty(selectors.itemRequest.firstRequestStatus, 'innerText'); + + expect(status).toContain('Accepted'); + }); + + it('should now click on the second declain request icon then type the reason', async() => { + await page.waitToClick(selectors.itemRequest.secondRequestDecline); + await page.write(selectors.itemRequest.declineReason, 'Not quite as expected'); + await page.respondToDialog('accept'); + let status = await page.waitToGetProperty(selectors.itemRequest.secondRequestStatus, 'innerText'); + + expect(status).toContain('Denied'); + }); +}); diff --git a/e2e/paths/04-item/13_fixedPrice.spec.js b/e2e/paths/04-item/13_fixedPrice.spec.js new file mode 100644 index 000000000..f36138e18 --- /dev/null +++ b/e2e/paths/04-item/13_fixedPrice.spec.js @@ -0,0 +1,97 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +const $ = selectors.itemFixedPrice; + +describe('Item fixed prices path', () => { + let browser; + let page; + let httpRequest; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'item'); + await page.accessToSection('item.fixedPrice'); + page.on('request', req => { + if (req.url().includes(`FixedPrices/filter`)) + httpRequest = req.url(); + }); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should filter using all the fields', async() => { + await page.write($.generalSearchFilter, 'item'); + await page.keyboard.press('Enter'); + + expect(httpRequest).toContain('search=item'); + + await page.click($.chip); + await page.click($.reignFilter); + + expect(httpRequest).toContain('categoryFk'); + + await page.autocompleteSearch($.typeFilter, 'Alstroemeria'); + + expect(httpRequest).toContain('typeFk'); + + await page.click($.chip); + await page.autocompleteSearch($.buyerFilter, 'buyerNick'); + + expect(httpRequest).toContain('buyerFk'); + + await page.click($.chip); + await page.autocompleteSearch($.warehouseFilter, 'Algemesi'); + + expect(httpRequest).toContain('warehouseFk'); + + await page.click($.chip); + await page.click($.mineFilter); + + expect(httpRequest).toContain('mine=true'); + + await page.click($.chip); + await page.click($.hasMinPriceFilter); + + expect(httpRequest).toContain('hasMinPrice=true'); + + await page.click($.chip); + await page.click($.addTag); + await page.autocompleteSearch($.tagFilter, 'Color'); + await page.autocompleteSearch($.tagValueFilter, 'Brown'); + + expect(httpRequest).toContain('tags'); + + await page.click($.chip); + }); + + it('should click on the add new fixed price button', async() => { + await page.waitToClick($.add); + await page.waitForSelector($.fourthFixedPrice); + }); + + it('should fill the fixed price data', async() => { + const now = Date.vnNew(); + await page.autocompleteSearch($.fourthWarehouse, 'Warehouse one'); + await page.writeOnEditableTD($.fourthGroupingPrice, '1'); + await page.writeOnEditableTD($.fourthPackingPrice, '1'); + await page.write($.fourthMinPrice, '1'); + await page.pickDate($.fourthStarted, now); + await page.pickDate($.fourthEnded, now); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should reload the section and check the created price has the expected ID', async() => { + await page.goto(`http://localhost:5000/#!/item/fixed-price`); + await page.autocompleteSearch($.warehouseFilter, 'Warehouse one'); + await page.click($.chip); + const result = await page.waitToGetProperty($.fourthItemID, 'value'); + + expect(result).toContain('13'); + }); +}); diff --git a/e2e/paths/05-ticket/01-sale/01_list_sales.spec.js b/e2e/paths/05-ticket/01-sale/01_list_sales.spec.js new file mode 100644 index 000000000..ad0975889 --- /dev/null +++ b/e2e/paths/05-ticket/01-sale/01_list_sales.spec.js @@ -0,0 +1,99 @@ +import selectors from '../../../helpers/selectors.js'; +import getBrowser from '../../../helpers/puppeteer'; + +describe('Ticket List sale path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('13'); + await page.accessToSection('ticket.card.sale'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should confirm the first ticket sale contains the colour tag', async() => { + const value = await page + .waitToGetProperty(selectors.ticketSales.firstSaleColour, 'innerText'); + + expect(value).toContain('Black'); + }); + + it('should confirm the first sale contains the price', async() => { + const value = await page + .waitToGetProperty(selectors.ticketSales.firstSalePrice, 'innerText'); + + expect(value).toContain('1.72'); + }); + + it('should confirm the first sale contains the discount', async() => { + const value = await page + .waitToGetProperty(selectors.ticketSales.firstSaleDiscount, 'innerText'); + + expect(value).toContain('0.00%'); + }); + + it('should confirm the first sale contains the total import', async() => { + const value = await page + .waitToGetProperty(selectors.ticketSales.firstSaleImport, 'innerText'); + + expect(value).toContain('34.40'); + }); + + it('should add an empty item to the sale list', async() => { + await page.waitToClick(selectors.ticketSales.newItemButton); + const sales = await page + .countElement(selectors.ticketSales.saleLine); + + expect(sales).toEqual(2); + }); + + it('should select a valid item to be added as the second item in the sales list', async() => { + let searchValue = 'Melee weapon heavy shield 100cm'; + await page.autocompleteSearch(selectors.ticketSales.secondSaleIdAutocomplete, searchValue); + await page.waitToClick(selectors.ticketSales.secondSaleQuantityCell); + await page.type(selectors.ticketSales.secondSaleQuantity, '8'); + await page.keyboard.press('Enter'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should update the description of the new sale', async() => { + await page.click(selectors.ticketSales.secondSaleConceptCell); + await page.write(selectors.ticketSales.secondSaleConceptInput, 'Aegis of Valor'); + await page.keyboard.press('Enter'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should add a third empty item to the sale list', async() => { + await page.waitToClick(selectors.ticketSales.newItemButton); + await page.waitForNumberOfElements(selectors.ticketSales.saleLine, 3); + const sales = await page.countElement(selectors.ticketSales.saleLine); + + expect(sales).toEqual(3); + }); + + it('should select the 2nd and 3th item and delete both', async() => { + await page.waitToClick(selectors.ticketSales.secondSaleCheckbox); + await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); + await page.waitToClick(selectors.ticketSales.deleteSaleButton); + await page.waitToClick(selectors.globalItems.acceptButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should verify there's only 1 single line remaining`, async() => { + const sales = await page.countElement(selectors.ticketSales.saleLine); + + expect(sales).toEqual(1); + }); +}); diff --git a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js new file mode 100644 index 000000000..d9689e31a --- /dev/null +++ b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js @@ -0,0 +1,415 @@ +import selectors from '../../../helpers/selectors.js'; +import getBrowser from '../../../helpers/puppeteer'; + +describe('Ticket Edit sale path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('salesPerson', 'ticket'); + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should click on the first sale claim icon to navigate over there`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleClaimIcon); + await page.waitForNavigation(); + await page.goBack(); + await page.goBack(); + }); + + it('should navigate to the tickets index', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.ticketsButton); + await page.waitForState('ticket.index'); + }); + + it(`should search for a ticket and then navigate to it's sales`, async() => { + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + }); + + it(`should set the ticket as libre`, async() => { + const searchValue = 'libre'; + await page.waitToClick(selectors.ticketSales.stateMenuButton); + await page.write(selectors.ticketSales.moreMenuState, searchValue); + try { + await page.waitForFunction(searchValue => { + const element = document.querySelector('li.active'); + if (element) + return element.innerText.toLowerCase().includes(searchValue.toLowerCase()); + }, {}, searchValue); + } catch (error) { + const builtSelector = await page.selectorFormater(selectors.ticketSales.moreMenuState); + const inputValue = await page.evaluate(() => { + return document.querySelector('.vn-drop-down.shown vn-textfield input').value; + }); + throw new Error(`${builtSelector} value is ${inputValue}! ${error}`); + } + await page.waitForState('ticket.card.sale'); + await page.keyboard.press('Enter'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should check it's state is libre now`, async() => { + await page.waitForTextInElement(selectors.ticketDescriptor.stateLabelValue, 'Libre'); + const result = await page.waitToGetProperty(selectors.ticketDescriptor.stateLabelValue, 'innerText'); + + expect(result).toEqual('State Libre'); + }); + + it(`should set the ticket as OK`, async() => { + await page.waitToClick(selectors.ticketSales.setOk); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should check it's state is OK now`, async() => { + await page.waitForTextInElement(selectors.ticketDescriptor.stateLabelValue, 'OK'); + const result = await page.waitToGetProperty(selectors.ticketDescriptor.stateLabelValue, 'innerText'); + + expect(result).toEqual('State OK'); + }); + + it(`should check the zoomed image isn't present`, async() => { + const result = await page.countElement(selectors.ticketSales.firstSaleZoomedImage); + + expect(result).toEqual(0); + }); + + it(`should click on the thumbnail image of the 1st sale and see the zoomed image`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleThumbnailImage); + const result = await page.countElement(selectors.ticketSales.firstSaleZoomedImage); + + expect(result).toEqual(1); + }); + + it(`should click on the zoomed image to close it`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleZoomedImage); + const result = await page.countElement(selectors.ticketSales.firstSaleZoomedImage); + + expect(result).toEqual(0); + }); + + it(`should click on the first sale ID making now the item descriptor visible`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleId); + await page.waitImgLoad(selectors.ticketSales.firstSaleDescriptorImage); + const visible = await page.isVisible(selectors.ticketSales.saleDescriptorPopover); + + expect(visible).toBeTruthy(); + }); + + it(`should click on the descriptor image of the 1st sale and see the zoomed image`, async() => { + await page.waitToClick('vn-item-descriptor img'); + const result = await page.countElement(selectors.ticketSales.firstSaleZoomedImage); + + expect(result).toEqual(1); + }); + + it(`should now click on the zoomed image to close it`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleZoomedImage); + const result = await page.countElement(selectors.ticketSales.firstSaleZoomedImage); + + expect(result).toEqual(0); + }); + + it(`should click on the summary icon of the item-descriptor to access to the item summary`, async() => { + await page.waitToClick(selectors.ticketSales.saleDescriptorPopoverSummaryButton); + await page.waitForState('item.card.summary'); + }); + + it('should return to ticket sales section', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.ticketsButton); + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + }); + + it('should remove 1 from the first sale quantity', async() => { + await page.waitToClick(selectors.ticketSales.firstSaleQuantityCell); + await page.waitForSelector(selectors.ticketSales.firstSaleQuantity); + await page.type(selectors.ticketSales.firstSaleQuantity, '9\u000d'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should update the price', async() => { + await page.waitToClick(selectors.ticketSales.firstSalePrice); + await page.waitForSelector(selectors.ticketSales.firstSalePriceInput); + await page.type(selectors.ticketSales.firstSalePriceInput, '5\u000d'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the price have been updated', async() => { + const result = await page.waitToGetProperty(selectors.ticketSales.firstSalePrice, 'innerText'); + + expect(result).toContain('5.00'); + }); + + it('should confirm the total price for that item have been updated', async() => { + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleImport, 'innerText'); + + expect(result).toContain('45.00'); + }); + + it('should update the discount', async() => { + await page.waitToClick(selectors.ticketSales.firstSaleDiscount); + await page.waitForSelector(selectors.ticketSales.firstSaleDiscountInput); + await page.type(selectors.ticketSales.firstSaleDiscountInput, '50'); + await page.waitToClick(selectors.ticketSales.saveSaleDiscountButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the discount have been updated', async() => { + await page.waitForTextInElement(selectors.ticketSales.firstSaleDiscount, '50.00%'); + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleDiscount, 'innerText'); + + expect(result).toContain('50.00%'); + }); + + it('should confirm the total import for that item have been updated', async() => { + await page.waitForTextInElement(selectors.ticketSales.firstSaleImport, '22.50'); + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleImport, 'innerText'); + + expect(result).toContain('22.50'); + }); + + it('should recalculate price of sales', async() => { + await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); + await page.waitToClick(selectors.ticketSales.secondSaleCheckbox); + + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuRecalculatePrice); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should log in as salesAssistant and navigate to ticket sales', async() => { + await page.loginAndModule('salesAssistant', 'ticket'); + await page.accessToSearchResult('15'); + await page.accessToSection('ticket.card.sale'); + }); + + it('should select the first sale and create a refund with warehouse', async() => { + await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuRefund); + await page.waitToClick(selectors.ticketSales.refundWithWarehouse); + await page.waitForSnackbar(); + await page.waitForState('ticket.card.sale'); + }); + + it('should select the first sale and create a refund without warehouse', async() => { + await page.accessToSearchResult('18'); + await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuRefund); + await page.waitToClick(selectors.ticketSales.refundWithoutWarehouse); + await page.waitForSnackbar(); + await page.waitForState('ticket.card.sale'); + }); + + it('should show error trying to delete a ticket with a refund', async() => { + await page.loginAndModule('salesPerson', 'ticket'); + await page.accessToSearchResult('8'); + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); + await page.waitToClick(selectors.globalItems.acceptButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Tickets with associated refunds can\'t be deleted'); + await page.waitToClick(selectors.globalItems.cancelButton); + }); + + it('should select the third sale and create a claim of it', async() => { + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuCreateClaim); + await page.waitToClick(selectors.globalItems.acceptButton); + await page.waitForNavigation(); + }); + + it('should search for a ticket then access to the sales section', async() => { + await page.goBack(); + await page.goBack(); + await page.loginAndModule('salesPerson', 'ticket'); + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + }); + + it('should select the third sale and delete it', async() => { + await page.waitToClick(selectors.ticketSales.thirdSaleCheckbox); + await page.waitToClick(selectors.ticketSales.deleteSaleButton); + await page.waitToClick(selectors.globalItems.acceptButton); + await page.waitForSpinnerLoad(); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the third sale was deleted`, async() => { + const result = await page.countElement(selectors.ticketSales.saleLine); + + expect(result).toEqual(3); + }); + + it('should select the second sale and transfer it to a valid ticket', async() => { + const targetTicketId = '12'; + + await page.waitToClick(selectors.ticketSales.secondSaleCheckbox); + await page.waitToClick(selectors.ticketSales.transferSaleButton); + await page.waitToClick(selectors.ticketSales.transferQuantityCell); + await page.type(selectors.ticketSales.transferQuantityInput, '10\u000d'); + await page.type(selectors.ticketSales.moveToTicketInput, targetTicketId); + await page.waitToClick(selectors.ticketSales.moveToTicketButton); + await page.expectURL(`ticket/${targetTicketId}/sale`); + }); + + it('should confirm the transfered line is the correct one', async() => { + await page.waitForSelector(selectors.ticketSales.secondSaleText); + const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleText, 'innerText'); + + expect(result).toContain(`Melee weapon heavy shield`); + }); + + it('should confirm the transfered quantity is the correct one', async() => { + const result = await page.waitToGetProperty(selectors.ticketSales.firstSaleQuantityCell, 'innerText'); + + expect(result).toContain('20'); + }); + + it('should go back to the original ticket sales section', async() => { + await page.waitToClick(selectors.ticketDescriptor.goBackToModuleIndexButton); + await page.accessToSearchResult('16'); + await page.accessToSection('ticket.card.sale'); + }); + + it(`should confirm the original ticket has still three lines`, async() => { + await page.waitForSelector(selectors.ticketSales.saleLine); + const result = await page.countElement(selectors.ticketSales.saleLine); + + expect(result).toEqual(3); + }); + + it(`should confirm the second sale quantity is now half of it's original value after the transfer`, async() => { + const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText'); + + expect(result).toContain('10'); + }); + + it('should go back to the receiver ticket sales section', async() => { + await page.waitToClick(selectors.ticketDescriptor.goBackToModuleIndexButton); + await page.accessToSearchResult('12'); + await page.accessToSection('ticket.card.sale'); + }); + + it('should transfer the sale back to the original ticket', async() => { + const targetTicketId = '16'; + + await page.waitToClick(selectors.ticketSales.secondSaleCheckbox); + await page.waitToClick(selectors.ticketSales.transferSaleButton); + await page.type(selectors.ticketSales.moveToTicketInput, targetTicketId); + await page.waitToClick(selectors.ticketSales.moveToTicketButton); + await page.expectURL(`ticket/${targetTicketId}/sale`); + }); + + it('should confirm the original ticket received the line', async() => { + const expectedLines = 4; + await page.waitForNumberOfElements(selectors.ticketSales.saleLine, expectedLines); + const result = await page.countElement(selectors.ticketSales.saleLine); + + expect(result).toEqual(expectedLines); + }); + + it(`should throw an error when attempting to create a ticket for an inactive client`, async() => { + await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); + await page.waitToClick(selectors.ticketSales.transferSaleButton); + await page.waitToClick(selectors.ticketSales.moveToNewTicketButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You can't create a ticket for an inactive client`); + + await page.closePopup(); + }); + + it('should go now to the ticket sales section of an active, not frozen client', async() => { + await page.waitToClick(selectors.ticketDescriptor.goBackToModuleIndexButton); + await page.accessToSearchResult('13'); + await page.accessToSection('ticket.card.sale'); + }); + + it(`should select all sales, tranfer them to a new ticket and delete the sender ticket as it would've been left empty`, async() => { + const senderTicketId = '13'; + + await page.waitToClick(selectors.ticketSales.selectAllSalesCheckbox); + await page.waitToClick(selectors.ticketSales.transferSaleButton); + await page.waitToClick(selectors.ticketSales.moveToNewTicketButton); + await page.evaluate((selector, ticketId) => { + return document.querySelector(selector).innerText.toLowerCase().indexOf(`#${ticketId}`) == -1; + }, selectors.ticketDescriptor.id, senderTicketId); + await page.waitForState('ticket.card.sale'); + }); + + it('should confirm the new ticket received the line', async() => { + const expectedLines = 1; + const result = await page.countElement(selectors.ticketSales.saleLine); + + expect(result).toEqual(expectedLines); + }); + + it('should check the first sale reserved icon isnt visible', async() => { + const result = await page.isVisible(selectors.ticketSales.firstSaleReservedIcon); + + expect(result).toBeFalsy(); + }); + + it('should mark the first sale as reserved', async() => { + await page.waitToClick(selectors.ticketSales.firstSaleCheckbox); + + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuReserve); + await page.closePopup(); + await page.waitForClassNotPresent(selectors.ticketSales.firstSaleReservedIcon, 'ng-hide'); + const result = await page.isVisible(selectors.ticketSales.firstSaleReservedIcon); + + expect(result).toBeTruthy(); + }); + + it('should unmark the first sale as reserved', async() => { + await page.waitToClick(selectors.ticketSales.moreMenu); + await page.waitToClick(selectors.ticketSales.moreMenuUnmarkReseved); + await page.waitForClassPresent(selectors.ticketSales.firstSaleReservedIcon, 'ng-hide'); + const result = await page.isVisible(selectors.ticketSales.firstSaleReservedIcon); + + expect(result).toBeFalsy(); + }); + + it('should log in as Production role and go to a target ticket summary', async() => { + await page.loginAndModule('production', 'ticket'); + await page.accessToSearchResult('13'); + await page.waitForState('ticket.card.summary'); + }); + + it(`should check the ticket is deleted`, async() => { + await page.waitForSelector(selectors.ticketDescriptor.isDeletedIcon); + }); +}); diff --git a/e2e/paths/05-ticket/01_observations.spec.js b/e2e/paths/05-ticket/01_observations.spec.js new file mode 100644 index 000000000..cf37f9ff1 --- /dev/null +++ b/e2e/paths/05-ticket/01_observations.spec.js @@ -0,0 +1,50 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Create notes path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('5'); + await page.accessToSection('ticket.card.observation'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should create a new note', async() => { + await page.waitToClick(selectors.ticketNotes.addNoteButton); + await page.autocompleteSearch(selectors.ticketNotes.firstNoteType, 'ItemPicker'); + await page.write(selectors.ticketNotes.firstDescription, 'description'); + await page.waitToClick(selectors.ticketNotes.submitNotesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the note is the expected one', async() => { + await page.reloadSection('ticket.card.observation'); + const result = await page + .waitToGetProperty(selectors.ticketNotes.firstNoteType, 'value'); + + expect(result).toEqual('ItemPicker'); + + const firstDescription = await page + .waitToGetProperty(selectors.ticketNotes.firstDescription, 'value'); + + expect(firstDescription).toEqual('description'); + }); + + it('should delete the note', async() => { + await page.waitToClick(selectors.ticketNotes.firstNoteRemoveButton); + await page.waitToClick(selectors.ticketNotes.submitNotesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/e2e/paths/05-ticket/02_expeditions_and_log.spec.js b/e2e/paths/05-ticket/02_expeditions_and_log.spec.js new file mode 100644 index 000000000..4e8005043 --- /dev/null +++ b/e2e/paths/05-ticket/02_expeditions_and_log.spec.js @@ -0,0 +1,32 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket expeditions and log path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('production', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.expedition'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should delete a former expedition and confirm the remaining expedition are the expected ones`, async() => { + await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox); + await page.waitToClick(selectors.ticketExpedition.deleteExpeditionButton); + await page.waitToClick(selectors.globalItems.acceptButton); + await page.reloadSection('ticket.card.expedition'); + + await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {}); + const result = await page + .countElement(selectors.ticketExpedition.expeditionRow); + + expect(result).toEqual(6); + }); +}); diff --git a/e2e/paths/05-ticket/04_packages.spec.js b/e2e/paths/05-ticket/04_packages.spec.js new file mode 100644 index 000000000..1e6a0a173 --- /dev/null +++ b/e2e/paths/05-ticket/04_packages.spec.js @@ -0,0 +1,78 @@ +import getBrowser from '../../helpers/puppeteer'; + +const $ = { + firstPackage: 'vn-autocomplete[label="Package"]', + firstQuantity: 'vn-ticket-package vn-horizontal:nth-child(1) vn-input-number[ng-model="package.quantity"]', + firstRemovePackageButton: 'vn-icon-button[vn-tooltip="Remove package"]', + addPackageButton: 'vn-icon-button[vn-tooltip="Add package"]', + savePackagesButton: `button[type=submit]` +}; + +describe('Ticket Create packages path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.package'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should attempt create a new package but receive an error if package is blank`, async() => { + await page.waitToClick($.firstRemovePackageButton); + await page.waitToClick($.addPackageButton); + await page.write($.firstQuantity, '99'); + await page.waitToClick($.savePackagesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Package cannot be blank'); + }); + + it(`should delete the first package and receive and error to save a new one with blank quantity`, async() => { + await page.clearInput($.firstQuantity); + await page.autocompleteSearch($.firstPackage, 'Container medical box 100cm'); + await page.waitToClick($.savePackagesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Some fields are invalid'); + }); + + it(`should confirm the quantity input isn't invalid yet`, async() => { + const result = await page + .evaluate(selector => { + return document.querySelector(`${selector} input`).checkValidity(); + }, $.firstQuantity); + + expect(result).toBeTruthy(); + }); + + it(`should create a new package with correct data`, async() => { + await page.clearInput($.firstQuantity); + await page.write($.firstQuantity, '-99'); + await page.waitToClick($.savePackagesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the first select is the expected one`, async() => { + await page.reloadSection('ticket.card.package'); + await page.waitForTextInField($.firstPackage, 'Container medical box 100cm'); + const result = await page.waitToGetProperty($.firstPackage, 'value'); + + expect(result).toEqual('Container medical box 100cm'); + }); + + it(`should confirm quantity is just a number and the string part was ignored by the imput number`, async() => { + await page.waitForTextInField($.firstQuantity, '-99'); + const result = await page.waitToGetProperty($.firstQuantity, 'value'); + + expect(result).toEqual('-99'); + }); +}); diff --git a/e2e/paths/05-ticket/05_tracking_state.spec.js b/e2e/paths/05-ticket/05_tracking_state.spec.js new file mode 100644 index 000000000..5cfc1c9d4 --- /dev/null +++ b/e2e/paths/05-ticket/05_tracking_state.spec.js @@ -0,0 +1,72 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Create new tracking state path', () => { + let browser; + let page; + + afterAll(async() => { + await browser.close(); + }); + + describe('as production', () => { + it('should log into the ticket 1 tracking', async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('production', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.tracking.index'); + }); + + it('should access to the create state view by clicking the create floating button', async() => { + await page.waitToClick(selectors.ticketTracking.createStateButton); + await page.waitForSelector(selectors.createStateView.state, {visible: true}); + await page.waitForState('ticket.card.tracking.edit'); + }); + + it(`should create a new state`, async() => { + await page.autocompleteSearch(selectors.createStateView.state, 'OK'); + await page.waitToClick(selectors.createStateView.saveStateButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + }); + + describe('as salesPerson', () => { + it('should now log into the ticket 1 tracking', async() => { + await page.loginAndModule('salesPerson', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.tracking.index'); + }); + + it('should now access to the create state view by clicking the create floating button', async() => { + await page.waitForSelector('.vn-popup', {hidden: true}); + await page.waitToClick(selectors.ticketTracking.createStateButton); + await page.waitForState('ticket.card.tracking.edit'); + }); + + it(`should attemp to create an state for which salesPerson doesn't have permissions`, async() => { + await page.autocompleteSearch(selectors.createStateView.state, 'Encajado'); + await page.waitToClick(selectors.createStateView.saveStateButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`You don't have enough privileges`); + }); + + it(`should make sure the worker gets autocomplete uppon selecting the assigned state`, async() => { + await page.autocompleteSearch(selectors.createStateView.state, 'asignado'); + const result = await page + .waitToGetProperty(selectors.createStateView.worker, 'value'); + + expect(result).toEqual('salesperson'); + }); + + it(`should succesfully create a valid state`, async() => { + await page.waitToClick(selectors.createStateView.saveStateButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + }); +}); diff --git a/e2e/paths/05-ticket/06_basic_data_steps.spec.js b/e2e/paths/05-ticket/06_basic_data_steps.spec.js new file mode 100644 index 000000000..77f0e0459 --- /dev/null +++ b/e2e/paths/05-ticket/06_basic_data_steps.spec.js @@ -0,0 +1,143 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Edit basic data path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('11'); + await page.accessToSection('ticket.card.basicData.stepOne'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should confirm the zone autocomplete is disabled unless your role is productionBoss`, async() => { + await page.waitForSelector(selectors.ticketBasicData.zone, {}); + const disabled = await page.evaluate(selector => { + return document.querySelector(selector).disabled; + }, `${selectors.ticketBasicData.zone} input`); + + expect(disabled).toBeTruthy(); + }); + + it(`should now log as productionBoss to perform the rest of the tests`, async() => { + await page.loginAndModule('productionBoss', 'ticket'); + await page.accessToSearchResult('11'); + await page.accessToSection('ticket.card.basicData.stepOne'); + }); + + it(`should confirm the zone autocomplete is enabled for the role productionBoss`, async() => { + await page.waitForSpinnerLoad(); + await page.waitForSelector(selectors.ticketBasicData.zone); + const disabled = await page.evaluate(selector => { + return document.querySelector(selector).disabled; + }, `${selectors.ticketBasicData.zone} input`); + + expect(disabled).toBeFalsy(); + }); + + it(`should check the zone is for Gotham247`, async() => { + let zone = await page + .waitToGetProperty(selectors.ticketBasicData.zone, 'value'); + + expect(zone).toContain('Zone 247 A'); + }); + + it(`should edit the ticket agency then check there are no zones for it`, async() => { + await page.autocompleteSearch(selectors.ticketBasicData.agency, 'Super-Man delivery'); + let emptyZone = await page + .expectPropertyValue(selectors.ticketBasicData.zone, 'value', ''); + + expect(emptyZone).toBeTruthy(); + }); + + it(`should edit the ticket zone then check the agency is for the new zone`, async() => { + await page.clearInput(selectors.ticketBasicData.agency); + await page.autocompleteSearch(selectors.ticketBasicData.zone, 'Zone expensive A'); + let zone = await page + .waitToGetProperty(selectors.ticketBasicData.agency, 'value'); + + expect(zone).toContain('Gotham247Expensive'); + }); + + it(`should click next`, async() => { + await page.waitToClick(selectors.ticketBasicData.nextStepButton); + await page.waitForState('ticket.card.basicData.stepTwo'); + }); + + it(`should have a price diference`, async() => { + const result = await page + .waitToGetProperty(selectors.ticketBasicData.stepTwoTotalPriceDif, 'innerText'); + + expect(result).toContain('-€228.25'); + }); + + it(`should select a new reason for the changes made then click on finalize`, async() => { + await page.waitToClick(selectors.ticketBasicData.chargesReason); + await page.waitToClick(selectors.ticketBasicData.finalizeButton); + await page.waitForState('ticket.card.summary'); + }); + + it(`should not find ticket`, async() => { + await page.doSearch('29'); + const count = await page.countElement(selectors.ticketsIndex.searchResult); + + expect(count).toEqual(0); + }); + + it(`should split ticket without negatives`, async() => { + const newAgency = 'Gotham247'; + const newDate = Date.vnNew(); + newDate.setDate(newDate.getDate() - 1); + + await page.accessToSearchResult('14'); + await page.accessToSection('ticket.card.basicData.stepOne'); + + await page.autocompleteSearch(selectors.ticketBasicData.agency, newAgency); + await page.pickDate(selectors.ticketBasicData.shipped, newDate); + + await page.waitToClick(selectors.ticketBasicData.nextStepButton); + + await page.waitToClick(selectors.ticketBasicData.finalizeButton); + + await page.waitForState('ticket.card.summary'); + + const newTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const newTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(newAgency).toEqual(newTicketAgency); + expect(newTicketDate).toContain(newDate.getDate()); + }); + + it(`should new ticket have sale of old ticket`, async() => { + await page.accessToSection('ticket.card.sale'); + await page.waitForState('ticket.card.sale'); + + const item = await page.waitToGetProperty(selectors.ticketSales.firstSaleId, 'innerText'); + + expect(item).toEqual('4'); + }); + + it(`should old ticket have old date and agency`, async() => { + const oldDate = Date.vnNew(); + const oldAgency = 'Super-Man delivery'; + + await page.accessToSearchResult('14'); + + const oldTicketAgency = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryAgency, 'innerText'); + const oldTicketDate = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(oldTicketAgency).toEqual(oldAgency); + expect(oldTicketDate).toContain(oldDate.getDate()); + }); +}); diff --git a/e2e/paths/05-ticket/08_components.spec.js b/e2e/paths/05-ticket/08_components.spec.js new file mode 100644 index 000000000..ab2aa85b2 --- /dev/null +++ b/e2e/paths/05-ticket/08_components.spec.js @@ -0,0 +1,30 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket List components path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.components'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should confirm the total base is correct', async() => { + const name = 'Base €'; + const minLength = name.length; + + await page.waitPropertyLength(selectors.ticketComponents.base, 'innerText', minLength); + const base = await page.waitToGetProperty(selectors.ticketComponents.base, 'innerText'); + + expect(base).toContain('Base'); + expect(base.length).toBeGreaterThan(minLength); + }); +}); diff --git a/e2e/paths/05-ticket/09_weekly.spec.js b/e2e/paths/05-ticket/09_weekly.spec.js new file mode 100644 index 000000000..370d422e6 --- /dev/null +++ b/e2e/paths/05-ticket/09_weekly.spec.js @@ -0,0 +1,123 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket descriptor path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyerBoss', 'ticket'); + await page.accessToSection('ticket.weekly.index'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should count the amount of tickets in the turns section', async() => { + const result = await page.countElement(selectors.ticketsIndex.weeklyTicket); + + expect(result).toEqual(6); + }); + + it('should go back to the ticket index then search and access a ticket summary', async() => { + await page.accessToSection('ticket.index'); + await page.accessToSearchResult('33'); + }); + + it('should add the ticket to thursday turn using the descriptor more menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn); + await page.waitToClick(selectors.ticketDescriptor.thursdayButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Current ticket deleted and added to shift'); + }); + + it('should again click on the Tickets button of the top bar menu', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.ticketsButton); + await page.waitForState('ticket.index'); + }); + + it('should confirm the ticket 33 was added to thursday', async() => { + await page.accessToSection('ticket.weekly.index'); + const result = await page.waitToGetProperty(selectors.ticketsIndex.thirdWeeklyTicket, 'value'); + + expect(result).toEqual('Thursday'); + }); + + it('should click on the Tickets button of the top bar menu once more', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.ticketsButton); + await page.waitForState('ticket.index'); + }); + + it('should now search for the ticket 33', async() => { + await page.accessToSearchResult('33'); + await page.waitForState('ticket.card.summary'); + }); + + it('should add the ticket to saturday turn using the descriptor more menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuAddToTurn); + await page.waitToClick(selectors.ticketDescriptor.saturdayButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Current ticket deleted and added to shift'); + }); + + it('should click on the Tickets button of the top bar menu once again', async() => { + await page.waitToClick(selectors.globalItems.applicationsMenuButton); + await page.waitForSelector(selectors.globalItems.applicationsMenuVisible); + await page.waitToClick(selectors.globalItems.ticketsButton); + await page.waitForState('ticket.index'); + }); + + it('should confirm the ticket 33 was added on saturday', async() => { + await page.accessToSection('ticket.weekly.index'); + await page.waitForTimeout(5000); + + const result = await page.waitToGetProperty(selectors.ticketsIndex.thirdWeeklyTicket, 'value'); + + expect(result).toEqual('Saturday'); + }); + + it('should now search for the weekly ticket 33', async() => { + await page.doSearch('33'); + const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult); + + expect(nResults).toEqual(2); + }); + + it('should delete the weekly ticket 33', async() => { + await page.waitToClick(selectors.ticketsIndex.firstWeeklyTicketDeleteIcon); + await page.waitToClick(selectors.ticketsIndex.acceptDeleteTurn); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the sixth weekly ticket was deleted', async() => { + await page.doSearch(); + const nResults = await page.countElement(selectors.ticketsIndex.searchWeeklyResult); + + expect(nResults).toEqual(6); + }); + + it('should update the agency then remove it afterwards', async() => { + await page.autocompleteSearch(selectors.ticketsIndex.firstWeeklyTicketAgency, 'Gotham247'); + let message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + + await page.clearInput(selectors.ticketsIndex.firstWeeklyTicketAgency); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/e2e/paths/05-ticket/10_request.spec.js b/e2e/paths/05-ticket/10_request.spec.js new file mode 100644 index 000000000..1b580aec2 --- /dev/null +++ b/e2e/paths/05-ticket/10_request.spec.js @@ -0,0 +1,77 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket purchase request path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('salesPerson', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.request.index'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should add a new request', async() => { + await page.waitToClick(selectors.ticketRequests.addRequestButton); + await page.write(selectors.ticketRequests.descriptionInput, 'New stuff'); + await page.write(selectors.ticketRequests.quantity, '9'); + await page.autocompleteSearch(selectors.ticketRequests.atender, 'buyerNick'); + await page.write(selectors.ticketRequests.price, '999'); + await page.waitToClick(selectors.ticketRequests.saveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should have been redirected to the request index', async() => { + await page.waitForState('ticket.card.request.index'); + }); + + it(`should edit the third request quantity as it's state is still new`, async() => { + await page.write(selectors.ticketRequests.thirdRequestQuantity, '9'); + await page.keyboard.press('Enter'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should check the new request was added', async() => { + await page.reloadSection('ticket.card.request.index'); + const result = await page.waitToGetProperty(selectors.ticketRequests.thirdRequestQuantity, 'value'); + + expect(result).toEqual('99'); + }); + + it(`should check the first request can't be edited as its state is different to new`, async() => { + await page.waitForClassPresent(selectors.ticketRequests.firstRequestQuantity, 'disabled'); + const result = await page.isDisabled(selectors.ticketRequests.firstRequestQuantity); + + expect(result).toBe(true); + }); + + it(`should check the second request can't be edited as its state is different to new`, async() => { + await page.waitForClassPresent(selectors.ticketRequests.secondRequestQuantity, 'disabled'); + const result = await page.isDisabled(selectors.ticketRequests.secondRequestQuantity); + + expect(result).toBe(true); + }); + + it('should delete the added request', async() => { + await page.waitToClick(selectors.ticketRequests.thirdRemoveRequestButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should check the request was deleted', async() => { + await page.reloadSection('ticket.card.request.index'); + await page.waitForSelector(selectors.ticketRequests.addRequestButton); + await page.waitForSelector(selectors.ticketRequests.thirdDescription, {hidden: true}); + }); +}); diff --git a/e2e/paths/05-ticket/12_descriptor.spec.js b/e2e/paths/05-ticket/12_descriptor.spec.js new file mode 100644 index 000000000..95a114c45 --- /dev/null +++ b/e2e/paths/05-ticket/12_descriptor.spec.js @@ -0,0 +1,148 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket descriptor path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('salesperson', 'ticket'); + }); + + afterAll(async() => { + await browser.close(); + }); + + describe('Delete ticket', () => { + it('should search for an specific ticket', async() => { + await page.accessToSearchResult('18'); + await page.waitForState('ticket.card.summary'); + }); + + it(`should update the shipped hour using the descriptor menu`, async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuChangeShippedHour); + await page.pickTime(selectors.ticketDescriptor.changeShippedHour, '08:15'); + await page.waitToClick(selectors.ticketDescriptor.acceptChangeHourButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Shipped hour updated'); + }); + + it(`should confirm the ticket descriptor shows the correct shipping hour`, async() => { + await page.waitForState('ticket.card.summary'); + const result = await page + .waitToGetProperty(selectors.ticketDescriptor.descriptorDeliveryDate, 'innerText'); + + expect(result).toContain('08:15'); + }); + + it('should delete the ticket using the descriptor menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Ticket deleted. You can undo this action within the first hour'); + }); + + it('should have been relocated to the ticket index', async() => { + await page.waitForState('ticket.index'); + }); + + it(`should search for the deleted ticket and check the deletedTicket icon and it's date`, async() => { + await page.write(selectors.ticketsIndex.topbarSearch, '18'); + await page.waitToClick(selectors.globalItems.searchButton); + await page.waitForState('ticket.card.summary'); + await page.isVisible(selectors.ticketDescriptor.isDeletedIcon); + const result = await page.waitToGetProperty(selectors.ticketsIndex.searchResultDate, 'innerText'); + + expect(result).toContain(2000); + }); + }); + + describe('Restore ticket', () => { + it('should restore the ticket using the descriptor menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuRestoreTicket); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); + await page.waitForState('ticket.card.summary'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + }); + + describe('Make invoice', () => { + it('should login as administrative role then search for a ticket', async() => { + const invoiceableTicketId = '14'; + + await page.loginAndModule('administrative', 'ticket'); + await page.accessToSearchResult(invoiceableTicketId); + await page.waitForState('ticket.card.summary'); + }); + + it(`should make sure the ticket doesn't have an invoiceOutFk yet`, async() => { + const result = await page + .waitToGetProperty(selectors.ticketSummary.invoiceOutRef, 'innerText'); + + expect(result).toEqual('-'); + }); + + it('should invoice the ticket using the descriptor menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitForContentLoaded(); + await page.waitToClick(selectors.ticketDescriptor.moreMenuMakeInvoice); + await page.waitToClick(selectors.ticketDescriptor.acceptInvoiceOutButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Ticket invoiced'); + }); + + it(`should make sure the ticket summary have an invoiceOutFk`, async() => { + await page.waitForTextInElement(selectors.ticketSummary.invoiceOutRef, 'T4444445'); + const result = await page.waitToGetProperty(selectors.ticketSummary.invoiceOutRef, 'innerText'); + + expect(result).toEqual('T4444445'); + }); + + it(`should regenerate the invoice using the descriptor menu`, async() => { + const expectedMessage = 'The invoice PDF document has been regenerated'; + + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitForContentLoaded(); + await page.waitToClick(selectors.ticketDescriptor.moreMenuRegenerateInvoice); + await page.respondToDialog('accept'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(expectedMessage); + }); + }); + + describe('SMS', () => { + it('should send the payment SMS using the descriptor menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuSMSOptions); + await page.waitToClick(selectors.ticketDescriptor.moreMenuPaymentSMS); + await page.waitForSelector(selectors.ticketDescriptor.SMStext); + await page.waitPropertyLength(selectors.ticketDescriptor.SMStext, 'value', 128); + await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton); + const message = await page.waitForSnackbar(); + + expect(message).toBeDefined(); + }); + + it('should send the import SMS using the descriptor menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenuSMSOptions); + await page.waitToClick(selectors.ticketDescriptor.moreMenuSendImportSms); + await page.waitForSelector(selectors.ticketDescriptor.SMStext); + await page.waitPropertyLength(selectors.ticketDescriptor.SMStext, 'value', 144); + await page.waitToClick(selectors.ticketDescriptor.sendSMSbutton); + const message = await page.waitForSnackbar(); + + expect(message).toBeDefined(); + }); + }); +}); diff --git a/e2e/paths/05-ticket/13_services.spec.js b/e2e/paths/05-ticket/13_services.spec.js new file mode 100644 index 000000000..50df23582 --- /dev/null +++ b/e2e/paths/05-ticket/13_services.spec.js @@ -0,0 +1,127 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket services path', () => { + let browser; + let page; + const invoicedTicketId = '1'; + + afterAll(async() => { + await browser.close(); + }); + + describe('as employee', () => { + it('should log in as employee, search for an invoice and get to services', async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult(invoicedTicketId); + await page.accessToSection('ticket.card.service'); + }); + + it('should find the add descripton button disabled for this user role', async() => { + await page.waitForClassPresent(selectors.ticketService.firstAddServiceTypeButton, 'disabled'); + await page.waitToClick(selectors.ticketService.addServiceButton); + await page.waitForSelector(selectors.ticketService.firstAddServiceTypeButton); + const disabled = await page.isDisabled(selectors.ticketService.firstAddServiceTypeButton); + + expect(disabled).toBe(true); + }); + + it('should receive an error if you attempt to save a service without access rights', async() => { + await page.clearInput(selectors.ticketService.firstPrice); + await page.write(selectors.ticketService.firstPrice, '999'); + await page.waitToClick(selectors.ticketService.saveServiceButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`The current ticket can't be modified`); + }); + }); + + describe('as administrative', () => { + let editableTicketId = '16'; + it('should navigate to the services of a target ticket', async() => { + await page.loginAndModule('administrative', 'ticket'); + await page.accessToSearchResult(editableTicketId); + await page.accessToSection('ticket.card.service'); + }); + + it('should click on the add button to prepare the form to create a new service', async() => { + await page.waitToClick(selectors.ticketService.addServiceButton); + const result = await page + .isVisible(selectors.ticketService.firstServiceType); + + expect(result).toBeTruthy(); + }); + + it('should receive an error if you attempt to save it with empty fields', async() => { + await page.waitToClick(selectors.ticketService.saveServiceButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`can't be blank`); + }); + + it('should click on the add new service type to open the dialog', async() => { + await page.waitToClick(selectors.ticketService.firstAddServiceTypeButton); + await page.waitForSelector('.vn-dialog.shown'); + const result = await page.isVisible(selectors.ticketService.newServiceTypeName); + + expect(result).toBeTruthy(); + }); + + it('should receive an error if service type is empty on submit', async() => { + await page.waitToClick(selectors.ticketService.saveServiceTypeButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain(`Name can't be empty`); + }); + + it('should create a new service type then add price then create the service', async() => { + await page.write(selectors.ticketService.newServiceTypeName, 'Documentos'); + await page.waitToClick(selectors.ticketService.saveServiceTypeButton); + await page.write(selectors.ticketService.firstPrice, '999'); + await page.waitToClick(selectors.ticketService.saveServiceButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the service description was created correctly', async() => { + await page.reloadSection('ticket.card.service'); + const result = await page + .waitToGetProperty(selectors.ticketService.firstServiceType, 'value'); + + expect(result).toEqual('Documentos'); + }); + + it('should confirm the service quantity was created correctly', async() => { + const result = await page + .waitToGetProperty(selectors.ticketService.firstQuantity, 'value'); + + expect(result).toEqual('1'); + }); + + it('should confirm the service price was created correctly', async() => { + const result = await page + .waitToGetProperty(selectors.ticketService.firstPrice, 'value'); + + expect(result).toEqual('999'); + }); + + it('should delete the service', async() => { + await page.waitToClick(selectors.ticketService.fistDeleteServiceButton); + await page.waitForNumberOfElements(selectors.ticketService.serviceLine, 0); + await page.waitToClick(selectors.ticketService.saveServiceButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should confirm the service was removed`, async() => { + await page.reloadSection('ticket.card.service'); + const nResults = await page.countElement(selectors.ticketService.serviceLine); + + expect(nResults).toEqual(0); + }); + }); +}); diff --git a/e2e/paths/05-ticket/14_create_ticket.spec.js b/e2e/paths/05-ticket/14_create_ticket.spec.js new file mode 100644 index 000000000..1f9c0c40a --- /dev/null +++ b/e2e/paths/05-ticket/14_create_ticket.spec.js @@ -0,0 +1,69 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket create path', () => { + let browser; + let page; + let nextMonth = Date.vnNew(); + nextMonth.setMonth(nextMonth.getMonth() + 1); + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('salesPerson', 'ticket'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should open the new ticket form', async() => { + await page.waitToClick(selectors.ticketsIndex.newTicketButton); + await page.waitForState('ticket.create'); + }); + + it('should succeed to create a ticket', async() => { + await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent'); + await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth); + await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse Two'); + await page.autocompleteSearch(selectors.createTicketView.agency, 'Gotham247'); + await page.waitToClick(selectors.createTicketView.createButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should check the url is now the summary of the ticket', async() => { + await page.waitForState('ticket.card.summary'); + }); + + it('should again open the new ticket form', async() => { + await page.waitToClick(selectors.globalItems.returnToModuleIndexButton); + await page.waitToClick(selectors.ticketsIndex.newTicketButton); + await page.waitForState('ticket.create'); + }); + + it('should succeed to create another ticket for the same client', async() => { + await page.autocompleteSearch(selectors.createTicketView.client, 'Clark Kent'); + await page.pickDate(selectors.createTicketView.deliveryDate, nextMonth); + await page.autocompleteSearch(selectors.createTicketView.warehouse, 'Warehouse One'); + await page.autocompleteSearch(selectors.createTicketView.agency, 'Gotham247'); + await page.waitToClick(selectors.createTicketView.createButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should check the url is now the summary of the created ticket', async() => { + await page.waitForState('ticket.card.summary'); + }); + + it('should delete the current ticket', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Ticket deleted. You can undo this action within the first hour'); + }); +}); diff --git a/e2e/paths/05-ticket/15_create_ticket_from_client.spec.js b/e2e/paths/05-ticket/15_create_ticket_from_client.spec.js new file mode 100644 index 000000000..51ead6461 --- /dev/null +++ b/e2e/paths/05-ticket/15_create_ticket_from_client.spec.js @@ -0,0 +1,37 @@ +import getBrowser from '../../helpers/puppeteer'; + +const $ = { + form: 'vn-ticket-create-card', + moreMenu: 'vn-client-descriptor vn-icon-button[icon=more_vert]', + simpleTicketButton: '.vn-menu [name="simpleTicket"]' +}; + +describe('Ticket create from client path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'client'); + await page.accessToSearchResult('Petter Parker'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should create simple ticket and check if the client details are the expected ones', async() => { + await page.waitToClick($.moreMenu); + await page.waitToClick($.simpleTicketButton); + await page.waitForState('ticket.create'); + + const values = { + client: 'Petter Parker', + address: 'Petter Parker' + }; + const formValues = await page.fetchForm($.form, Object.keys(values)); + + expect(formValues).toEqual(values); + }); +}); diff --git a/e2e/paths/05-ticket/16_summary.spec.js b/e2e/paths/05-ticket/16_summary.spec.js new file mode 100644 index 000000000..a6017e454 --- /dev/null +++ b/e2e/paths/05-ticket/16_summary.spec.js @@ -0,0 +1,108 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Summary path', () => { + let browser; + let page; + const ticketId = '20'; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should navigate to the target ticket summary section', async() => { + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult(ticketId); + await page.waitForState('ticket.card.summary'); + }); + + it(`should display details from the ticket and it's client on the top of the header`, async() => { + await page.waitForTextInElement(selectors.ticketSummary.header, 'Bruce Banner'); + const result = await page.waitToGetProperty(selectors.ticketSummary.header, 'innerText'); + + expect(result).toContain(`Ticket #${ticketId}`); + expect(result).toContain('Bruce Banner (1109)'); + expect(result).toContain('Somewhere in Thailand'); + }); + + it('should display ticket details', async() => { + let result = await page + .waitToGetProperty(selectors.ticketSummary.state, 'innerText'); + + expect(result).toContain('Arreglar'); + }); + + it('should display delivery details', async() => { + let result = await page + .waitToGetProperty(selectors.ticketSummary.route, 'innerText'); + + expect(result).toContain('3'); + }); + + it('should display the ticket total', async() => { + let result = await page + .waitToGetProperty(selectors.ticketSummary.total, 'innerText'); + + expect(result).toContain('€155.54'); + }); + + it('should display the ticket line(s)', async() => { + let result = await page + .waitToGetProperty(selectors.ticketSummary.firstSaleItemId, 'innerText'); + + expect(result).toContain('2'); + }); + + it(`should click on the first sale ID to make the item descriptor visible`, async() => { + await page.waitToClick(selectors.ticketSummary.firstSaleItemId); + await page.waitImgLoad(selectors.ticketSummary.firstSaleDescriptorImage); + const visible = await page.isVisible(selectors.ticketSummary.itemDescriptorPopover); + + expect(visible).toBeTruthy(); + }); + + it(`should check the url for the item diary link of the descriptor is for the right item id`, async() => { + await page.waitForSelector(selectors.ticketSummary.itemDescriptorPopoverItemDiaryButton, {visible: true}); + }); + + it('should log in as production then navigate to the summary of the same ticket', async() => { + await page.loginAndModule('production', 'ticket'); + await page.accessToSearchResult(ticketId); + await page.waitForState('ticket.card.summary'); + }); + + it('should set the ticket state to OK using the top right button', async() => { + const searchValue = 'OK'; + await page.waitToClick(selectors.ticketSummary.stateButton); + await page.write(selectors.ticketSummary.stateAutocomplete, searchValue); + try { + await page.waitForFunction(text => { + const element = document.querySelector('li.active'); + if (element) + return element.innerText.toLowerCase().includes(text.toLowerCase()); + }, {}, searchValue); + } catch (error) { + const state = await page.evaluate(() => { + const stateSelector = 'vn-ticket-summary vn-label-value:nth-child(1) > section > span'; + return document.querySelector(stateSelector).value; + }); + throw new Error(`${stateSelector} innerText is ${state}! ${error}`); + } + await page.keyboard.press('Enter'); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it('should confirm the ticket state was updated', async() => { + await page.waitForSpinnerLoad(); + const result = await page.waitToGetProperty(selectors.ticketSummary.state, 'innerText'); + + expect(result).toContain('OK'); + }); +}); diff --git a/e2e/paths/05-ticket/17_log.spec.js b/e2e/paths/05-ticket/17_log.spec.js new file mode 100644 index 000000000..e1da2df44 --- /dev/null +++ b/e2e/paths/05-ticket/17_log.spec.js @@ -0,0 +1,34 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket log path', () => { + let browser; + let page; + const ticketId = '5'; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should navigate to the target ticket notes section', async() => { + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult(ticketId); + await page.accessToSection('ticket.card.observation'); + await page.waitForState('ticket.card.observation'); + }); + + it('should create a new note for the test', async() => { + await page.waitToClick(selectors.ticketNotes.addNoteButton); + await page.autocompleteSearch(selectors.ticketNotes.firstNoteType, 'ItemPicker'); + await page.write(selectors.ticketNotes.firstDescription, 'description'); + await page.waitToClick(selectors.ticketNotes.submitNotesButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); +}); diff --git a/e2e/paths/05-ticket/18_index_payout.spec.js b/e2e/paths/05-ticket/18_index_payout.spec.js new file mode 100644 index 000000000..9c5518424 --- /dev/null +++ b/e2e/paths/05-ticket/18_index_payout.spec.js @@ -0,0 +1,70 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; +const $ = { + newPayment: '.vn-dialog.shown', + anyBalanceLine: 'vn-client-balance-index vn-tbody > vn-tr', + firstLineReference: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable' +}; + +describe('Ticket index payout path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('administrative', 'ticket'); + await page.waitForState('ticket.index'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should check the second ticket from a client and 1 of another', async() => { + await page.waitToClick(selectors.globalItems.searchButton); + await page.waitToClick(selectors.ticketsIndex.thirdTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.fifthTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.payoutButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('You cannot make a payment on account from multiple clients'); + }); + + it('should search for tickets of the same client then open the payout form', async() => { + await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton); + await page.write(selectors.ticketsIndex.advancedSearchClient, '1101'); + await page.keyboard.press('Enter'); + await page.waitForNumberOfElements(selectors.ticketsIndex.anySearchResult, 10); + await page.waitToClick(selectors.ticketsIndex.firstTicketCheckbox); + await page.waitToClick(selectors.ticketsIndex.secondTicketCheckbox); + + await page.waitToClick(selectors.ticketsIndex.payoutButton); + + await page.waitForSelector(selectors.ticketsIndex.payoutCompany); + }); + + it('should fill the company and bank to perform a payout and check a new balance line was entered', async() => { + await page.fillForm($.newPayment, { + company: 'VNL', + bank: 'cash', + amountPaid: 100, + description: 'Payment', + viewReceipt: false + }); + await page.respondToDialog('accept'); + const message = await page.waitForSnackbar(); + + await page.waitToClick(selectors.globalItems.homeButton); + await page.selectModule('client'); + await page.accessToSearchResult('1101'); + await page.accessToSection('client.card.balance.index'); + await page.waitForSelector($.anyBalanceLine); + const count = await page.countElement($.anyBalanceLine); + const reference = await page.innerText($.firstLineReference); + + expect(message.isSuccess).toBeTrue(); + expect(count).toEqual(4); + expect(reference).toContain('Payment'); + }); +}); diff --git a/e2e/paths/05-ticket/19_dms.spec.js b/e2e/paths/05-ticket/19_dms.spec.js new file mode 100644 index 000000000..be2ac4338 --- /dev/null +++ b/e2e/paths/05-ticket/19_dms.spec.js @@ -0,0 +1,49 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket DMS path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.dms.index'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should import a document', async() => { + await page.waitToClick(selectors.ticketDms.import); + await page.autocompleteSearch(selectors.ticketDms.document, '1'); + await page.waitToClick(selectors.ticketDms.saveImport); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should check there's a listed document now`, async() => { + const result = await page.countElement(selectors.ticketDms.anyDocument); + + expect(result).toEqual(1); + }); + + it('should attempt to import an existing document on this ticket', async() => { + await page.waitToClick(selectors.ticketDms.import); + await page.autocompleteSearch(selectors.ticketDms.document, '1'); + await page.waitToClick(selectors.ticketDms.saveImport); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('This document already exists on this ticket'); + }); + + it(`should check there's still one document`, async() => { + const result = await page.countElement(selectors.ticketDms.anyDocument); + + expect(result).toEqual(1); + }); +}); diff --git a/e2e/paths/05-ticket/20_moveExpedition.spec.js b/e2e/paths/05-ticket/20_moveExpedition.spec.js new file mode 100644 index 000000000..ae23c9c99 --- /dev/null +++ b/e2e/paths/05-ticket/20_moveExpedition.spec.js @@ -0,0 +1,50 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket expeditions', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('production', 'ticket'); + await page.accessToSearchResult('1'); + await page.accessToSection('ticket.card.expedition'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it(`should move one expedition to new ticket withoute route`, async() => { + await page.waitToClick(selectors.ticketExpedition.thirdSaleCheckbox); + await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton); + await page.waitToClick(selectors.ticketExpedition.moreMenuWithoutRoute); + await page.waitToClick(selectors.ticketExpedition.saveButton); + await page.waitForState('ticket.card.summary'); + await page.accessToSection('ticket.card.expedition'); + + await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {}); + const result = await page + .countElement(selectors.ticketExpedition.expeditionRow); + + expect(result).toEqual(2); + }); + + it(`should move one expedition to new ticket with route`, async() => { + await page.waitToClick(selectors.ticketExpedition.firstSaleCheckbox); + await page.waitToClick(selectors.ticketExpedition.moveExpeditionButton); + await page.waitToClick(selectors.ticketExpedition.moreMenuWithRoute); + await page.write(selectors.ticketExpedition.newRouteId, '1'); + await page.waitToClick(selectors.ticketExpedition.saveButton); + await page.waitForState('ticket.card.summary'); + await page.accessToSection('ticket.card.expedition'); + + await page.waitForSelector(selectors.ticketExpedition.expeditionRow, {}); + const result = await page + .countElement(selectors.ticketExpedition.expeditionRow); + + expect(result).toEqual(2); + }); +}); diff --git a/e2e/paths/05-ticket/21_future.spec.js b/e2e/paths/05-ticket/21_future.spec.js new file mode 100644 index 000000000..60bb9c38d --- /dev/null +++ b/e2e/paths/05-ticket/21_future.spec.js @@ -0,0 +1,99 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Future path', () => { + let browser; + let page; + let httpRequest; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSection('ticket.future'); + page.on('request', req => { + if (req.url().includes(`Tickets/getTicketsFuture`)) + httpRequest = req.url(); + }); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should search with required data, check three last tickets and move to the future', async() => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.warehouseFk); + await page.waitToClick(selectors.ticketFuture.submit); + let message = await page.waitForSnackbar(); + + expect(message.text).toContain('warehouseFk is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.futureScopeDays); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('futureScopeDays is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.originScopeDays); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('originScopeDays is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + + expect(httpRequest).toBeDefined(); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.autocompleteSearch(selectors.ticketFuture.ipt, 'H'); + await page.waitToClick(selectors.ticketFuture.submit); + + expect(httpRequest).toContain('ipt=H'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.ipt); + + await page.autocompleteSearch(selectors.ticketFuture.futureIpt, 'H'); + await page.waitToClick(selectors.ticketFuture.submit); + + expect(httpRequest).toContain('futureIpt=H'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.futureIpt); + + await page.autocompleteSearch(selectors.ticketFuture.state, 'Free'); + await page.waitToClick(selectors.ticketFuture.submit); + + expect(httpRequest).toContain('state=0'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.state); + + await page.autocompleteSearch(selectors.ticketFuture.futureState, 'Free'); + await page.waitToClick(selectors.ticketFuture.submit); + + expect(httpRequest).toContain('futureState=0'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.futureState); + await page.waitToClick(selectors.ticketFuture.submit); + + await page.waitForNumberOfElements(selectors.ticketFuture.searchResult, 5); + await page.waitToClick(selectors.ticketFuture.multiCheck); + await page.waitToClick(selectors.ticketFuture.firstCheck); + await page.waitToClick(selectors.ticketFuture.moveButton); + await page.waitToClick(selectors.globalItems.acceptButton); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('Tickets moved successfully!'); + }); +}); diff --git a/e2e/paths/05-ticket/22_advance.spec.js b/e2e/paths/05-ticket/22_advance.spec.js new file mode 100644 index 000000000..0e5b5e0c3 --- /dev/null +++ b/e2e/paths/05-ticket/22_advance.spec.js @@ -0,0 +1,79 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Advance path', () => { + let browser; + let page; + let httpRequest; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSection('ticket.advance'); + page.on('request', req => { + if (req.url().includes(`Tickets/getTicketsAdvance`)) + httpRequest = req.url(); + }); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should search with the required data, check the first ticket and move to the present', async() => { + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.clearInput(selectors.ticketAdvance.warehouseFk); + + await page.waitToClick(selectors.ticketAdvance.submit); + let message = await page.waitForSnackbar(); + + expect(message.text).toContain('warehouseFk is a required argument'); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.clearInput(selectors.ticketAdvance.dateToAdvance); + await page.waitToClick(selectors.ticketAdvance.submit); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('dateToAdvance is a required argument'); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.clearInput(selectors.ticketAdvance.dateFuture); + await page.waitToClick(selectors.ticketAdvance.submit); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('dateFuture is a required argument'); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketAdvance.submit); + + expect(httpRequest).toBeDefined(); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.autocompleteSearch(selectors.ticketAdvance.futureIpt, 'H'); + await page.waitToClick(selectors.ticketAdvance.submit); + + expect(httpRequest).toContain('futureIpt=H'); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.clearInput(selectors.ticketAdvance.futureIpt); + await page.waitToClick(selectors.ticketAdvance.submit); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.autocompleteSearch(selectors.ticketAdvance.ipt, 'H'); + await page.waitToClick(selectors.ticketAdvance.submit); + + expect(httpRequest).toContain('ipt=H'); + + await page.waitToClick(selectors.ticketAdvance.openAdvancedSearchButton); + await page.clearInput(selectors.ticketAdvance.ipt); + await page.waitToClick(selectors.ticketAdvance.submit); + + await page.waitToClick(selectors.ticketAdvance.firstCheck); + await page.waitToClick(selectors.ticketAdvance.moveButton); + await page.waitToClick(selectors.ticketAdvance.acceptButton); + message = await page.waitForSnackbar(); + + expect(message.text).toContain('Tickets moved successfully!'); + }); +}); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index e8565d42e..6756db37d 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -240,11 +240,11 @@ "The height must be greater than 50cm": "The height must be greater than 50cm", "The maximum height of the wagon is 200cm": "The maximum height of the wagon is 200cm", "The quantity claimed cannot be greater than the quantity of the line": "The quantity claimed cannot be greater than the quantity of the line", + "There are tickets for this area, delete them first": "There are tickets for this area, delete them first", + "You do not have permission to modify the booked field": "You do not have permission to modify the booked field", "Invalid or expired verification code": "Invalid or expired verification code", - "There are tickets for this area, delete them first": "There are tickets for this area, delete them first", "ticketLostExpedition": "The ticket [{{ticketId}}]({{{ticketUrl}}}) has the following lost expedition:{{ expeditionId }}", - "Payment method is required": "Payment method is required", "The raid information is not correct": "The raid information is not correct", + "Payment method is required": "Payment method is required", "Sales already moved": "Sales already moved" - } diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 0ec3f6e2e..0b10d4702 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -385,9 +385,10 @@ "type cannot be blank": "Se debe rellenar el tipo", "There are tickets for this area, delete them first": "Hay tickets para esta sección, borralos primero", "There is no company associated with that warehouse": "No hay ninguna empresa asociada a ese almacén", + "You do not have permission to modify the booked field": "No tienes permisos para modificar el campo contabilizada", "ticketLostExpedition": "El ticket [{{ticketId}}]({{{ticketUrl}}}) tiene la siguiente expedición perdida:{{ expeditionId }}", "The web user's email already exists": "El correo del usuario web ya existe", - "Sales already moved": "Ya han sido transferidas", + "Sales already moved": "Ya han sido transferidas", "The raid information is not correct": "La información de la redada no es correcta" } diff --git a/loopback/locale/fr.json b/loopback/locale/fr.json index 0e876f89c..9941358be 100644 --- a/loopback/locale/fr.json +++ b/loopback/locale/fr.json @@ -362,8 +362,9 @@ "The invoices have been created but the PDFs could not be generated": "La facture a été émise mais le PDF n'a pas pu être généré", "It has been invoiced but the PDF of refund not be generated": "Il a été facturé mais le PDF de remboursement n'a pas été généré", "Cannot send mail": "Impossible d'envoyer le mail", - "Original invoice not found": "Facture originale introuvable", - "The quantity claimed cannot be greater than the quantity of the line": "Le montant réclamé ne peut pas être supérieur au montant de la ligne", + "Original invoice not found": "Facture originale introuvable", + "The quantity claimed cannot be greater than the quantity of the line": "Le montant réclamé ne peut pas être supérieur au montant de la ligne", + "You do not have permission to modify the booked field": "Vous n'avez pas la permission de modifier le champ comptabilisé", "ticketLostExpedition": "Le ticket [{{ticketId}}]({{{ticketUrl}}}) a l'expédition perdue suivante : {{expeditionId}}", "The web user's email already exists": "L'email de l'internaute existe déjà" -} \ No newline at end of file +} diff --git a/loopback/locale/pt.json b/loopback/locale/pt.json index e08336273..e84b30f3d 100644 --- a/loopback/locale/pt.json +++ b/loopback/locale/pt.json @@ -366,4 +366,4 @@ "The quantity claimed cannot be greater than the quantity of the line": "O valor reclamado não pode ser superior ao valor da linha", "ticketLostExpedition": "O ticket [{{ticketId}}]({{{ticketUrl}}}) tem a seguinte expedição perdida: {{expeditionId}}", "The web user's email already exists": "O e-mail do utilizador da web já existe." -} \ No newline at end of file +} diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js index 8ca79f531..593f3fdcc 100644 --- a/modules/entry/back/models/entry.js +++ b/modules/entry/back/models/entry.js @@ -1,3 +1,4 @@ +const UserError = require('vn-loopback/util/user-error'); const LoopBackContext = require('loopback-context'); module.exports = Self => { require('../methods/entry/filter')(Self); @@ -18,11 +19,20 @@ module.exports = Self => { const changes = ctx.data || ctx.instance; const orgData = ctx.currentInstance; + const loopBackContext = LoopBackContext.getCurrentContext(); + const accessToken = {req: loopBackContext.active}; + const hasChanges = orgData && changes; + + const isBookedChanged = changes.isBooked !== undefined && orgData.isBooked !== changes.isBooked; + + if (isBookedChanged) { + const canEditIsBooked = await Self.app.models.ACL.checkAccessAcl(accessToken, 'Entry', 'isBooked', 'READ'); + if (!canEditIsBooked) + throw new UserError('You do not have permission to modify the booked field'); + } const observation = changes.observation || orgData.observation; - const hasChanges = orgData && changes; - const observationChanged = hasChanges - && orgData.observation != observation; + const observationChanged = hasChanges && orgData.observation != observation; if (observationChanged) { let tx; @@ -37,8 +47,7 @@ module.exports = Self => { } try { - const loopbackContext = LoopBackContext.getCurrentContext(); - const userId = loopbackContext.active.accessToken.userId; + const userId = loopBackContext.active.accessToken.userId; const id = changes.id || orgData.id; const entry = await Self.app.models.Entry.findById(id, null, myOptions); await entry.updateAttribute('observationEditorFk', userId, myOptions); diff --git a/modules/entry/back/models/specs/entry.spec.js b/modules/entry/back/models/specs/entry.spec.js new file mode 100644 index 000000000..15a8202c4 --- /dev/null +++ b/modules/entry/back/models/specs/entry.spec.js @@ -0,0 +1,97 @@ +const {models} = require('vn-loopback/server/server'); +const LoopBackContext = require('loopback-context'); + +describe('entry_isEditable trigger', () => { + const activeCtx = { + accessToken: {userId: 5}, + http: { + req: { + headers: {origin: 'http://localhost'} + } + } + }; + const ctx = {req: activeCtx}; + const entryId = 1; + let tx; + let options; + let entry; + + beforeEach(async() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({active: ctx.req}); + tx = await models.Entry.beginTransaction({}); + options = {transaction: tx}; + + entry = await models.Entry.findById(entryId, null, options); + }); + + afterEach(async() => { + await tx.rollback(); + }); + + async function prepareEntry(isBooked, typeFk) { + let newCreated = Date.vnNew(); + await entry.updateAttributes({isBooked, typeFk}, options); + await entry.updateAttributes({dated: newCreated}, options); + } + + it('should throw an error when entry is booked and typeFk is null', async() => { + let error; + try { + await prepareEntry(true, null); + } catch (e) { + error = e; + } + + expect(error.message).toContain(`Entry ${entryId} is not editable`); + }); + + it('should throw an error when entry is booked and typeFk is not informal', async() => { + let error; + try { + const type = await models.EntryType.findOne({where: {isInformal: false}}, options); + await prepareEntry(true, type.code); + } catch (e) { + error = e; + } + + expect(error.message).toContain(`Entry ${entryId} is not editable`); + }); + + it('should not throw an error when entry is booked and typeFk is informal', async() => { + let error; + try { + const type = await models.EntryType.findOne({where: {isInformal: true}}, options); + await prepareEntry(true, type.code); + } catch (e) { + error = e; + } + + expect(error).toBeUndefined(); + }); + + it('should not throw an error when entry is not booked', async() => { + let error; + try { + const type = await models.EntryType.findOne({}, options); + await prepareEntry(false, type.code); + } catch (e) { + error = e; + } + + expect(error).toBeUndefined(); + }); + + it('should not throw an error when @isModeInventory is true', async() => { + let error; + try { + await models.Application.rawSql('SET @isModeInventory = TRUE;', null, options); + await prepareEntry(true, null); + } catch (e) { + error = e; + } finally { + await models.Application.rawSql('SET @isModeInventory = FALSE;', null, options); + } + + expect(error).toBeUndefined(); + }); +}); diff --git a/modules/item/front/barcode/index.html b/modules/item/front/barcode/index.html new file mode 100644 index 000000000..8d6cb3af8 --- /dev/null +++ b/modules/item/front/barcode/index.html @@ -0,0 +1,57 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/item/front/barcode/index.js b/modules/item/front/barcode/index.js new file mode 100644 index 000000000..4ceb87b9d --- /dev/null +++ b/modules/item/front/barcode/index.js @@ -0,0 +1,17 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + this.$.watcher.check(); + this.$.model.save().then(() => { + this.$.watcher.notifySaved(); + this.$.watcher.updateOriginalData(); + }); + } +} + +ngModule.vnComponent('vnItemBarcode', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/basic-data/index.html b/modules/item/front/basic-data/index.html new file mode 100644 index 000000000..af76d5966 --- /dev/null +++ b/modules/item/front/basic-data/index.html @@ -0,0 +1,318 @@ + + + + + + +
+ + + + +
{{::name}}
+
+ {{::category.name}} +
+
+
+ + + + +
+ + + + + + + +
{{::name}}
+
+ #{{::id}} +
+
+ + + + +
+
+ + + +
{{::description}}
+
+ #{{::id}} +
+
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ID + Item + Size + Producer + Color + + + + + + + {{::item.id}} + + + {{::item.name}} + {{::item.size}} + {{::item.producer.name}} + {{::item.ink.name}} + + + + + + + + diff --git a/modules/item/front/basic-data/index.js b/modules/item/front/basic-data/index.js new file mode 100644 index 000000000..5a303f15f --- /dev/null +++ b/modules/item/front/basic-data/index.js @@ -0,0 +1,87 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + showIntrastat(event) { + if (event.defaultPrevented) return; + event.preventDefault(); + + this.newIntrastat = { + taxClassFk: this.item.taxClassFk + }; + this.$.intrastat.show(); + } + + onIntrastatAccept() { + const query = `Items/${this.$params.id}/createIntrastat`; + return this.$http.patch(query, this.newIntrastat) + .then(res => this.item.intrastatFk = res.data.id); + } + + itemSearchFunc($search) { + return /^\d+$/.test($search) + ? {id: $search} + : {name: {like: '%' + $search + '%'}}; + } + + showFilterDialog(item) { + this.activeItem = item; + this.itemFilterParams = {}; + this.itemFilter = { + include: [ + { + relation: 'producer', + scope: { + fields: ['name'] + } + }, + { + relation: 'ink', + scope: { + fields: ['name'] + } + } + ] + }; + + this.$.filterDialog.show(); + } + + selectItem(id) { + this.activeItem['genericFk'] = id; + this.$.filterDialog.hide(); + } + + filter() { + const filter = this.itemFilter; + const params = this.itemFilterParams; + const where = {}; + for (let key in params) { + const value = params[key]; + if (!value) continue; + + switch (key) { + case 'name': + where[key] = {like: `%${value}%`}; + break; + case 'producerFk': + case 'typeFk': + case 'size': + case 'inkFk': + where[key] = value; + } + } + + filter.where = where; + + this.$.itemsModel.applyFilter(filter); + } +} + +ngModule.vnComponent('vnItemBasicData', { + template: require('./index.html'), + bindings: { + item: '<' + }, + controller: Controller +}); diff --git a/modules/item/front/basic-data/index.spec.js b/modules/item/front/basic-data/index.spec.js new file mode 100644 index 000000000..274453d30 --- /dev/null +++ b/modules/item/front/basic-data/index.spec.js @@ -0,0 +1,32 @@ +import './index.js'; + +describe('vnItemBasicData', () => { + describe('Component vnItemBasicData', () => { + let $httpBackend; + let $scope; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnItemBasicData', {$element, $scope}); + controller.$.watcher = {}; + controller.$params.id = 1; + controller.item = {id: 1, name: 'Rainbow Coral'}; + })); + + describe('onIntrastatAccept()', () => { + it('should pass the data to the watcher', () => { + const newIntrastatId = 20; + $httpBackend.expect('PATCH', 'Items/1/createIntrastat').respond({id: 20}); + controller.onIntrastatAccept(); + $httpBackend.flush(); + + expect(controller.item.intrastatFk).toEqual(newIntrastatId); + }); + }); + }); +}); diff --git a/modules/item/front/basic-data/locale/es.yml b/modules/item/front/basic-data/locale/es.yml new file mode 100644 index 000000000..fc490e448 --- /dev/null +++ b/modules/item/front/basic-data/locale/es.yml @@ -0,0 +1,19 @@ +Reference: Referencia +Full name calculates based on tags 1-3. Is not recommended to change it manually: >- + El nombre completo se calcula + basado en los tags 1-3. + No se recomienda cambiarlo manualmente +Is active: Activo +Expense: Gasto +Price in kg: Precio en kg +New intrastat: Nuevo intrastat +Identifier: Identificador +Fragile: Frágil +Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...) +Multiplier: Multiplicador +Generic: Genérico +This item does need a photo: Este artículo necesita una foto +Do photo: Hacer foto +Recycled Plastic: Plástico reciclado +Non recycled plastic: Plástico no reciclado +Minimum sales quantity: Cantidad mínima de venta diff --git a/modules/item/front/botanical/index.html b/modules/item/front/botanical/index.html new file mode 100644 index 000000000..2388f4e8f --- /dev/null +++ b/modules/item/front/botanical/index.html @@ -0,0 +1,102 @@ + + +
+ + + + + + + + + + +
{{name}}
+
+ {{genus.name}} +
+
+ + + + +
+
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/botanical/index.js b/modules/item/front/botanical/index.js new file mode 100644 index 000000000..8ade0fd9d --- /dev/null +++ b/modules/item/front/botanical/index.js @@ -0,0 +1,99 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +class Controller extends Section { + get item() { + return this._item; + } + + set item(value) { + this._item = value; + if (value) + this.getBotanicalData(); + } + + showGenus(event) { + if (event.defaultPrevented) return; + event.preventDefault(); + + this.$.genus.show(); + } + + showSpecies(event) { + if (event.defaultPrevented) return; + event.preventDefault(); + + this.$.species.show(); + } + + onGenusAccept() { + try { + if (!this.data.name) + throw new Error(`The name of the genus can't be empty`); + + this.$http.post(`genera`, this.data).then(res => { + this.vnApp.showMessage(this.$t('The genus has been created')); + this.emit('response', {$response: res.data}); + this.onGenusResponse(res.data); + }); + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + return true; + } + + onSpeciesAccept() { + try { + if (!this.data.name) + throw new Error(`The name of the species can't be empty`); + + this.$http.post(`species`, this.data).then(res => { + this.vnApp.showMessage(this.$t('The species has been created')); + this.emit('response', {$response: res.data}); + this.onSpeciesResponse(res.data); + }); + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + return true; + } + + getBotanicalData() { + const filter = { + where: {itemFk: this.item.id} + }; + const filterParams = encodeURIComponent(JSON.stringify(filter)); + this.$http.get(`ItemBotanicals?filter=${filterParams}`).then(res => { + if (res.data[0]) + this.botanical = res.data[0]; + + else + this.botanical = {itemFk: this.item.id}; + }); + } + + onSubmit() { + this.$.watcher.check(); + this.$http.patch(`ItemBotanicals`, this.botanical).then(() => { + this.$.watcher.notifySaved(); + this.$.watcher.updateOriginalData(); + }); + } + + onGenusResponse(response) { + this.botanical.genusFk = response.id; + } + + onSpeciesResponse(response) { + this.botanical.specieFk = response.id; + } +} + +ngModule.vnComponent('vnItemBotanical', { + template: require('./index.html'), + bindings: { + item: '<' + }, + controller: Controller +}); diff --git a/modules/item/front/botanical/index.spec.js b/modules/item/front/botanical/index.spec.js new file mode 100644 index 000000000..afec83cfd --- /dev/null +++ b/modules/item/front/botanical/index.spec.js @@ -0,0 +1,179 @@ +import './index.js'; + +describe('vnItemBotanical', () => { + describe('Component vnItemBotanical', () => { + let $httpBackend; + let $scope; + let controller; + let vnApp; + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _vnApp_) => { + $httpBackend = _$httpBackend_; + vnApp = _vnApp_; + jest.spyOn(vnApp, 'showError'); + $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnItemBotanical', {$element, $scope}); + controller.item = {id: 5}; + controller.$params = {itemFk: 5}; + controller.$ = { + watcher: { + check: () => {}, + notifySaved: () => {}, + updateOriginalData: () => {}}, + genus: { + show: () => {} + }, + species: { + show: () => {} + }}; + })); + + beforeEach(() => { + const response = {data: 'MyResult'}; + $httpBackend.whenRoute('GET', 'ItemBotanicals').respond(response); + }); + + describe('showGenus()', () => { + it('should do nothing in genus field if it default is prevented', () => { + const event = { + defaultPrevented: true, + preventDefault: () => {} + }; + jest.spyOn(event, 'preventDefault'); + jest.spyOn(controller.$.genus, 'show'); + + controller.showGenus(event); + + expect(event.preventDefault).not.toHaveBeenCalledWith(); + expect(controller.$.genus.show).not.toHaveBeenCalledWith(); + }); + + it('should call show function in genus field when the default is not prevented', () => { + const event = { + defaultPrevented: false, + preventDefault: () => {} + }; + + jest.spyOn(event, 'preventDefault'); + jest.spyOn(controller.$.genus, 'show'); + + controller.showGenus(event); + + expect(event.preventDefault).toHaveBeenCalledWith(); + expect(controller.$.genus.show).toHaveBeenCalledWith(); + }); + }); + + describe('showSpecies()', () => { + it('should do nothing in species field if it default is prevented', () => { + const event = { + defaultPrevented: true, + preventDefault: () => {} + }; + jest.spyOn(event, 'preventDefault'); + jest.spyOn(controller.$.species, 'show'); + + controller.showSpecies(event); + + expect(event.preventDefault).not.toHaveBeenCalledWith(); + expect(controller.$.species.show).not.toHaveBeenCalledWith(); + }); + + it('should call show function in species field when the default is not prevented', () => { + const event = { + defaultPrevented: false, + preventDefault: () => {} + }; + + jest.spyOn(event, 'preventDefault'); + jest.spyOn(controller.$.species, 'show'); + + controller.showSpecies(event); + + expect(event.preventDefault).toHaveBeenCalledWith(); + expect(controller.$.species.show).toHaveBeenCalledWith(); + }); + }); + + describe('onGenusAccept()', () => { + it('should throw an error if the item botanical has no genus name', () => { + jest.spyOn(controller.vnApp, 'showMessage'); + + controller.data = {}; + + controller.onGenusAccept(); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`The name of the genus can't be empty`); + }); + + it('should add the new genus', () => { + controller.data = { + id: 4, + name: 'Anilius' + }; + + $httpBackend.expectPOST('genera', controller.data).respond(200, controller.data); + + controller.onGenusAccept(); + $httpBackend.flush(); + + controller.onGenusResponse(controller.data); + + expect(controller.botanical.genusFk).toEqual(controller.data.id); + }); + }); + + describe('onSpeciesAccept()', () => { + it('should throw an error if the item botanical has no species name', () => { + jest.spyOn(controller.vnApp, 'showMessage'); + + controller.data = {}; + + controller.onSpeciesAccept(); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`The name of the species can't be empty`); + }); + + it('should add the new species', () => { + controller.data = { + id: 2, + name: 'Spasiva' + }; + + $httpBackend.expectPOST('species', controller.data).respond(200, controller.data); + + controller.onSpeciesAccept(); + $httpBackend.flush(); + controller.onSpeciesResponse(controller.data); + }); + }); + + describe('onSubmit()', () => { + it('should make HTTP POST request to save the genus and species data', () => { + jest.spyOn(controller.$.watcher, 'updateOriginalData'); + jest.spyOn(controller.$.watcher, 'check'); + jest.spyOn(controller.$.watcher, 'notifySaved'); + + $httpBackend.expectPATCH('ItemBotanicals').respond(); + controller.onSubmit(); + $httpBackend.flush(); + + expect(controller.$.watcher.updateOriginalData).toHaveBeenCalledWith(); + expect(controller.$.watcher.check).toHaveBeenCalledWith(); + expect(controller.$.watcher.notifySaved).toHaveBeenCalledWith(); + }); + }); + + describe('getBotanicalData()', () => { + it('should get the species and genus data references of the item', () => { + controller.getBotanicalData(); + $httpBackend.flush(); + + expect(controller.botanical).toEqual(controller.$params); + }); + }); + }); +}); + diff --git a/modules/item/front/botanical/locale/es.yml b/modules/item/front/botanical/locale/es.yml new file mode 100644 index 000000000..e1234bd71 --- /dev/null +++ b/modules/item/front/botanical/locale/es.yml @@ -0,0 +1,5 @@ +The genus has been created: El genus ha sido creado +The species has been created: La especie ha sido creada +Latin species name: Nombre de la especie en latín +Latin genus name: Nombre del genus en latín +Species: Especie \ No newline at end of file diff --git a/modules/item/front/card/index.html b/modules/item/front/card/index.html new file mode 100644 index 000000000..330d274c0 --- /dev/null +++ b/modules/item/front/card/index.html @@ -0,0 +1,8 @@ + + + + + diff --git a/modules/item/front/card/index.js b/modules/item/front/card/index.js new file mode 100644 index 000000000..2fe42fd04 --- /dev/null +++ b/modules/item/front/card/index.js @@ -0,0 +1,33 @@ +import ngModule from '../module'; +import ModuleCard from 'salix/components/module-card'; + +class Controller extends ModuleCard { + reload() { + this.$http.get(`Items/${this.$params.id}/getCard`) + .then(res => this.item = res.data); + + this.$http.get('ItemConfigs/findOne') + .then(res => { + if (this.$state.getCurrentPath()[4].state.name === 'item.card.diary') return; + this.warehouseFk = res.data.warehouseFk; + this.getWarehouseName(res.data.warehouseFk); + }); + } + + getWarehouseName(warehouseFk) { + const filter = { + where: {id: warehouseFk} + }; + this.$http.get('Warehouses/findOne', {filter}) + .then(res => { + this.warehouseText = this.$t('WarehouseFk', { + warehouseName: res.data.name + }); + }); + } +} + +ngModule.vnComponent('vnItemCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/card/index.spec.js b/modules/item/front/card/index.spec.js new file mode 100644 index 000000000..6ebce3d36 --- /dev/null +++ b/modules/item/front/card/index.spec.js @@ -0,0 +1,32 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemCard', () => { + let controller; + let $httpBackend; + let $state; + let data = {id: 1, name: 'fooName'}; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_, $stateParams, _$state_) => { + $httpBackend = _$httpBackend_; + $state = _$state_; + $state.getCurrentPath = () => [null, null, null, null, {state: {name: 'item.card.diary'}}]; + + let $element = angular.element('
'); + controller = $componentController('vnItemCard', {$element}); + + $stateParams.id = data.id; + $httpBackend.whenRoute('GET', 'Items/:id/getCard').respond(data); + })); + + it('should request data and set it on the controller', () => { + $httpBackend.expect('GET', `ItemConfigs/findOne`).respond({}); + controller.reload(); + $httpBackend.flush(); + + expect(controller.item).toEqual(data); + }); + }); +}); diff --git a/modules/item/front/create/index.html b/modules/item/front/create/index.html new file mode 100644 index 000000000..b284abf06 --- /dev/null +++ b/modules/item/front/create/index.html @@ -0,0 +1,95 @@ + + + + +
+ + + + + + + + + + + + +
+
{{::code}}
+
{{::name}}
+
+
+ {{category.name}} +
+
+
+ + +
{{::description}}
+
+ #{{::id}} +
+
+
+
+ + + + +
+ + + + + + +
diff --git a/modules/item/front/create/index.js b/modules/item/front/create/index.js new file mode 100644 index 000000000..4ca5f8f9f --- /dev/null +++ b/modules/item/front/create/index.js @@ -0,0 +1,40 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + this.fetchDefaultPriorityTag(); + } + + fetchDefaultPriorityTag() { + this.validPriorities = []; + const filter = {fields: ['defaultPriority', 'defaultTag', 'validPriorities'], limit: 1}; + this.$http.get(`ItemConfigs`, {filter}) + .then(res => { + if (res.data) { + const dataRow = res.data[0]; + dataRow.validPriorities.forEach(priority => { + this.validPriorities.push({priority}); + }); + this.item = { + priority: dataRow.defaultPriority, + tag: dataRow.defaultTag + }; + } + }); + } + + onSubmit() { + this.$.watcher.submit().then( + json => this.$state.go('item.card.basicData', {id: json.data.id}) + ); + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnItemCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/create/index.spec.js b/modules/item/front/create/index.spec.js new file mode 100644 index 000000000..9e54988d7 --- /dev/null +++ b/modules/item/front/create/index.spec.js @@ -0,0 +1,37 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemCreate', () => { + let $scope; + let $state; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$state_) => { + $scope = $rootScope.$new(); + $state = _$state_; + $scope.watcher = { + submit: () => { + return { + then: callback => { + callback({data: {id: 1}}); + } + }; + } + }; + const $element = angular.element(''); + controller = $componentController('vnItemCreate', {$element, $scope}); + })); + + describe('onSubmit()', () => { + it(`should call submit() on the watcher then expect a callback`, () => { + jest.spyOn($state, 'go'); + controller.onSubmit(); + + expect(controller.$state.go).toHaveBeenCalledWith('item.card.basicData', {id: 1}); + }); + }); + }); +}); + diff --git a/modules/item/front/create/locale/es.yml b/modules/item/front/create/locale/es.yml new file mode 100644 index 000000000..041826e1c --- /dev/null +++ b/modules/item/front/create/locale/es.yml @@ -0,0 +1 @@ +Temporal name: Nombre temporal \ No newline at end of file diff --git a/modules/item/front/diary/index.html b/modules/item/front/diary/index.html new file mode 100644 index 000000000..7fb3b870e --- /dev/null +++ b/modules/item/front/diary/index.html @@ -0,0 +1,2 @@ + + diff --git a/modules/item/front/diary/index.js b/modules/item/front/diary/index.js new file mode 100644 index 000000000..3d86b0b60 --- /dev/null +++ b/modules/item/front/diary/index.js @@ -0,0 +1,21 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + } + + async $onInit() { + this.$state.go('item.card.summary', {id: this.$params.id}); + window.location.href = await this.vnApp.getUrl(`item/${this.$params.id}/diary`); + } +} + +ngModule.vnComponent('vnItemDiary', { + template: require('./index.html'), + controller: Controller, + bindings: { + item: '<' + } +}); diff --git a/modules/item/front/fetched-tags/index.html b/modules/item/front/fetched-tags/index.html new file mode 100644 index 000000000..df5936871 --- /dev/null +++ b/modules/item/front/fetched-tags/index.html @@ -0,0 +1,40 @@ + + +
+ {{::$ctrl.item.value5}} +
+
+ {{::$ctrl.item.value6}} +
+
+ {{::$ctrl.item.value7}} +
+
+ {{::$ctrl.item.value8}} +
+
+ {{::$ctrl.item.value9}} +
+
+ {{::$ctrl.item.value10}} +
+
+
diff --git a/modules/item/front/fetched-tags/index.js b/modules/item/front/fetched-tags/index.js new file mode 100644 index 000000000..e3584be23 --- /dev/null +++ b/modules/item/front/fetched-tags/index.js @@ -0,0 +1,12 @@ +import ngModule from '../module'; +import Component from 'core/lib/component'; +import './style.scss'; + +ngModule.vnComponent('vnFetchedTags', { + template: require('./index.html'), + controller: Component, + bindings: { + maxLength: '<', + item: '<', + } +}); diff --git a/modules/item/front/fetched-tags/style.scss b/modules/item/front/fetched-tags/style.scss new file mode 100644 index 000000000..250ca07ab --- /dev/null +++ b/modules/item/front/fetched-tags/style.scss @@ -0,0 +1,61 @@ +@import "variables"; + +[vn-fetched-tags] { + min-width: 155px; + & [wide] { + width: 430px; + } + & div { + display: flex; + flex-wrap: wrap; + align-items: center; + & vn-one { + min-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1rem; + } + & vn-one h3 { + color: $color-font-secondary; + text-transform: uppercase; + line-height: initial; + font-size: 0.75rem; + margin-bottom: 0px; + } + } +} + +vn-fetched-tags { + & > vn-horizontal { + align-items: center; + max-width: 210px; + & > vn-auto { + flex-wrap: wrap; + + & > .inline-tag { + margin: 1px; + } + } + + & > vn-auto { + display: flex; + + & > .inline-tag { + color: $color-font-secondary; + text-align: center; + font-size: .8rem; + height: 13px; + padding: 1px; + width: 64px; + min-width: 64px; + max-width: 64px; + flex: 1; + border: 1px solid $color-font-secondary; + + &.empty { + border: 1px solid darken($color-font-secondary, 30%); + } + } + } + } +} diff --git a/modules/item/front/fixed-price-search-panel/index.html b/modules/item/front/fixed-price-search-panel/index.html new file mode 100644 index 000000000..347e65571 --- /dev/null +++ b/modules/item/front/fixed-price-search-panel/index.html @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + +
{{name}}
+
+ {{category.name}} +
+
+
+
+ + + + + + + + + + + + + + + + + + + + Tags + + + + + + + + + + + + + +
+ + Id/{{$ctrl.$t('Name')}}: {{$ctrl.filter.search}} + + + {{$ctrl.$t('Category')}}: {{category.selection.name}} + + + {{$ctrl.$t('Type')}}: {{type.selection.name}} + + + {{$ctrl.$t('Buyer')}}: {{buyer.selection.nickname}} + + + {{$ctrl.$t('Warehouse')}}: {{warehouse.selection.name}} + + + {{$ctrl.$t('Started')}}: {{$ctrl.filter.started | date:'dd/MM/yyyy'}} + + + {{$ctrl.$t('Ended')}}: {{$ctrl.filter.ended | date:'dd/MM/yyyy'}} + + + {{$ctrl.$t('For me')}}: {{$ctrl.filter.mine ? '✓' : '✗'}} + + + {{$ctrl.$t('Minimum price')}}: {{$ctrl.filter.hasMinPrice ? '✓' : '✗'}} + + + {{$ctrl.showTagInfo(chipTag)}} + +
+
diff --git a/modules/item/front/fixed-price-search-panel/index.js b/modules/item/front/fixed-price-search-panel/index.js new file mode 100644 index 000000000..0882eb5ac --- /dev/null +++ b/modules/item/front/fixed-price-search-panel/index.js @@ -0,0 +1,60 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; +import './style.scss'; + +class Controller extends SearchPanel { + constructor($element, $) { + super($element, $); + } + + $onInit() { + this.filter = { + tags: [] + }; + } + + changeCategory(id) { + if (this.filter.categoryFk != id) { + this.filter.categoryFk = id; + this.addFilters(); + } + } + + removeItemFilter(param) { + this.filter[param] = null; + if (param == 'categoryFk') this.filter['typeFk'] = null; + this.addFilters(); + } + + removeTag(tag) { + const index = this.filter.tags.indexOf(tag); + if (index > -1) this.filter.tags.splice(index, 1); + this.addFilters(); + } + + onKeyPress($event) { + if ($event.key === 'Enter') + this.addFilters(); + } + + addFilters() { + for (let i = 0; i < this.filter.tags.length; i++) { + if (!this.filter.tags[i].value) + this.filter.tags.splice(i, 1); + } + return this.model.addFilter({}, this.filter); + } + + showTagInfo(itemTag) { + if (!itemTag.tagFk) return itemTag.value; + return `${this.tags.find(tag => tag.id == itemTag.tagFk).name}: ${itemTag.value}`; + } +} + +ngModule.vnComponent('vnFixedPriceSearchPanel', { + template: require('./index.html'), + controller: Controller, + bindings: { + model: '<' + } +}); diff --git a/modules/item/front/fixed-price-search-panel/index.spec.js b/modules/item/front/fixed-price-search-panel/index.spec.js new file mode 100644 index 000000000..597bc108e --- /dev/null +++ b/modules/item/front/fixed-price-search-panel/index.spec.js @@ -0,0 +1,56 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnFixedPriceSearchPanel', () => { + let $element; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(angular.mock.inject($componentController => { + $element = angular.element(``); + controller = $componentController('vnFixedPriceSearchPanel', {$element}); + controller.model = {addFilter: () => {}}; + })); + + describe('removeItemFilter()', () => { + it(`should remove param from filter`, () => { + controller.filter = {tags: [], categoryFk: 1, typeFk: 1}; + const expectFilter = {tags: [], categoryFk: null, typeFk: null}; + + controller.removeItemFilter('categoryFk'); + + expect(controller.filter).toEqual(expectFilter); + }); + }); + + describe('removeTag()', () => { + it(`should remove tag from filter`, () => { + const tag = {tagFk: 1, value: 'Value'}; + controller.filter = {tags: [tag]}; + const expectFilter = {tags: []}; + + controller.removeTag(tag); + + expect(controller.filter).toEqual(expectFilter); + }); + }); + + describe('showTagInfo()', () => { + it(`should show tag value`, () => { + const tag = {value: 'Value'}; + const result = controller.showTagInfo(tag); + + expect(result).toEqual('Value'); + }); + + it(`should show tag name and value`, () => { + const tag = {tagFk: 1, value: 'Value'}; + controller.tags = [{id: 1, name: 'tagName'}]; + const result = controller.showTagInfo(tag); + + expect(result).toEqual('tagName: Value'); + }); + }); + }); +}); diff --git a/modules/item/front/fixed-price-search-panel/locale/es.yml b/modules/item/front/fixed-price-search-panel/locale/es.yml new file mode 100644 index 000000000..06e5e6b26 --- /dev/null +++ b/modules/item/front/fixed-price-search-panel/locale/es.yml @@ -0,0 +1,4 @@ +Started: Inicio +Ended: Fin +Minimum price: Precio mínimo +Item ID: ID Artículo \ No newline at end of file diff --git a/modules/item/front/fixed-price-search-panel/style.scss b/modules/item/front/fixed-price-search-panel/style.scss new file mode 100644 index 000000000..e386033dd --- /dev/null +++ b/modules/item/front/fixed-price-search-panel/style.scss @@ -0,0 +1,71 @@ +@import "variables"; + +vn-fixed-price-search-panel vn-side-menu { + .menu { + min-width: $menu-width; + } + & > div { + .input { + padding-left: $spacing-md; + padding-right: $spacing-md; + border-color: $color-spacer; + border-bottom: $border-thin; + } + .horizontal { + padding-left: $spacing-md; + padding-right: $spacing-md; + grid-auto-flow: column; + grid-column-gap: $spacing-sm; + align-items: center; + } + .tags { + padding: $spacing-md; + padding-bottom: 0%; + padding-top: 0%; + align-items: center; + } + .chips { + display: flex; + flex-wrap: wrap; + padding: $spacing-md; + overflow: hidden; + max-width: 100%; + border-color: $color-spacer; + border-top: $border-thin; + } + .item-category { + padding: $spacing-sm; + justify-content: flex-start; + align-items: flex-start; + flex-wrap: wrap; + + vn-autocomplete[vn-id="category"] { + display: none; + } + + & > vn-one { + padding: $spacing-sm; + min-width: 33.33%; + text-align: center; + box-sizing: border-box; + + & > vn-icon { + padding: $spacing-sm; + background-color: $color-font-secondary; + border-radius: 50%; + cursor: pointer; + + &.active { + background-color: $color-main; + color: #fff; + } + & > i:before { + font-size: 2.6rem; + width: 16px; + height: 16px; + } + } + } + } + } +} diff --git a/modules/item/front/fixed-price/index.html b/modules/item/front/fixed-price/index.html new file mode 100644 index 000000000..f1f6e1419 --- /dev/null +++ b/modules/item/front/fixed-price/index.html @@ -0,0 +1,284 @@ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Item ID + + Description + + Grouping price + + Packing price + + Min price + + Started + + Ended + + Warehouse +
+ + + + + +
{{id}}
+
+ {{name}} +
+
+
+
+
+ + {{itemFk.selection.name}} + + +

{{price.subName}}

+
+
+ + +
+ + + {{price.rate2 | currency: 'EUR':2}} + + + + + + + + + + {{price.rate3 | currency: 'EUR':2}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+
+ +
+ + + + +
+ + + Edit + + {{::$ctrl.totalChecked}} + + buy(s) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/item/front/fixed-price/index.js b/modules/item/front/fixed-price/index.js new file mode 100644 index 000000000..fe6788e9c --- /dev/null +++ b/modules/item/front/fixed-price/index.js @@ -0,0 +1,254 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.editedColumn; + this.checkAll = false; + this.checkedFixedPrices = []; + + this.smartTableOptions = { + activeButtons: { + search: true + }, + columns: [ + { + field: 'warehouseFk', + autocomplete: { + url: 'Warehouses', + showField: 'name', + valueField: 'id', + } + }, + { + field: 'started', + searchable: false + }, + { + field: 'ended', + searchable: false + } + ] + }; + + this.filterParams = { + warehouseFk: this.vnConfig.warehouseFk + }; + } + + getFilterParams() { + return { + warehouseFk: this.vnConfig.warehouseFk + }; + } + + get columns() { + if (this._columns) return this._columns; + + this._columns = [ + {field: 'rate2', displayName: this.$t('Grouping price')}, + {field: 'rate3', displayName: this.$t('Packing price')}, + {field: 'hasMinPrice', displayName: this.$t('Has min price')}, + {field: 'minPrice', displayName: this.$t('Min price')}, + {field: 'started', displayName: this.$t('Started')}, + {field: 'ended', displayName: this.$t('Ended')}, + {field: 'warehouseFk', displayName: this.$t('Warehouse')} + ]; + + return this._columns; + } + + get checked() { + const fixedPrices = this.$.model.data || []; + const checkedBuys = []; + for (let fixedPrice of fixedPrices) { + if (fixedPrice.checked) + checkedBuys.push(fixedPrice); + } + + return checkedBuys; + } + + uncheck() { + this.checkAll = false; + this.checkedFixedPrices = []; + } + + get totalChecked() { + if (this.checkedDummyCount) + return this.checkedDummyCount; + + return this.checked.length; + } + + saveChecked(fixedPriceId) { + const index = this.checkedFixedPrices.indexOf(fixedPriceId); + if (index !== -1) + return this.checkedFixedPrices.splice(index, 1); + return this.checkedFixedPrices.push(fixedPriceId); + } + + reCheck() { + if (!this.$.model.data) return; + if (!this.checkedFixedPrices.length) return; + + this.$.model.data.forEach(fixedPrice => { + if (this.checkedFixedPrices.includes(fixedPrice.id)) + fixedPrice.checked = true; + }); + } + + onEditAccept() { + const rowsToEdit = []; + for (let row of this.checked) + rowsToEdit.push({id: row.id, itemFk: row.itemFk}); + + const data = { + field: this.editedColumn.field, + newValue: this.editedColumn.newValue, + lines: rowsToEdit + }; + + if (this.checkedDummyCount && this.checkedDummyCount > 0) { + const params = {}; + if (this.$.model.userParams) { + const userParams = this.$.model.userParams; + for (let param in userParams) { + let newParam = this.exprBuilder(param, userParams[param]); + if (!newParam) + newParam = {[param]: userParams[param]}; + Object.assign(params, newParam); + } + } + if (this.$.model.userFilter) + Object.assign(params, this.$.model.userFilter.where); + + data.filter = params; + } + + return this.$http.post('FixedPrices/editFixedPrice', data) + .then(() => { + this.uncheck(); + this.$.model.refresh(); + }); + } + + isBigger(date) { + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + date = new Date(date); + date.setHours(0, 0, 0, 0); + + const timeDifference = today - date; + if (timeDifference < 0) return 'warning'; + } + + isLower(date) { + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + date = new Date(date); + date.setHours(0, 0, 0, 0); + + const timeDifference = today - date; + if (timeDifference > 0) return 'warning'; + } + + add() { + if (!this.$.model.data || this.$.model.data.length == 0) { + this.$.model.data = []; + this.$.model.proxiedData = []; + + const today = Date.vnNew(); + + const millisecsInDay = 86400000; + const daysInWeek = 7; + const nextWeek = new Date(today.getTime() + daysInWeek * millisecsInDay); + + this.$.model.insert({ + started: today, + ended: nextWeek + }); + return; + } + + const lastIndex = this.$.model.data.length - 1; + const lastItem = this.$.model.data[lastIndex]; + this.$.model.insert({ + itemFk: lastItem.itemFk, + name: lastItem.name, + subName: lastItem.subName, + value5: lastItem.value5, + value6: lastItem.value6, + value7: lastItem.value7, + value8: lastItem.value8, + value9: lastItem.value9, + value10: lastItem.value10, + warehouseFk: lastItem.warehouseFk, + rate2: lastItem.rate2, + rate3: lastItem.rate3, + hasMinPrice: lastItem.hasMinPrice, + minPrice: lastItem.minPrice, + started: lastItem.started, + ended: lastItem.ended, + }); + } + + upsertPrice(price, resetMinPrice) { + if (resetMinPrice) + delete price['minPrice']; + + const requiredFields = ['itemFk', 'started', 'ended', 'rate2', 'rate3']; + for (const field of requiredFields) + if (price[field] == undefined) return; + + const query = 'FixedPrices/upsertFixedPrice'; + this.$http.patch(query, price) + .then(res => { + this.vnApp.showSuccess(this.$t('Data saved!')); + Object.assign(price, res.data); + }); + } + + removePrice($index) { + const price = this.$.model.data[$index]; + if (price.id) { + this.$http.delete(`FixedPrices/${price.id}`) + .then(() => { + this.$.model.remove($index); + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } else + this.$.model.remove($index); + } + + itemSearchFunc($search) { + return /^\d+$/.test($search) + ? {id: $search} + : {name: {like: '%' + $search + '%'}}; + } + + exprBuilder(param, value) { + switch (param) { + case 'name': + return {'i.name': {like: `%${value}%`}}; + case 'itemFk': + case 'warehouseFk': + case 'rate2': + case 'rate3': + param = `fp.${param}`; + return {[param]: value}; + case 'minPrice': + param = `i.${param}`; + return {[param]: value}; + } + } +} + +ngModule.vnComponent('vnFixedPrice', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/fixed-price/index.spec.js b/modules/item/front/fixed-price/index.spec.js new file mode 100644 index 000000000..ae24da60b --- /dev/null +++ b/modules/item/front/fixed-price/index.spec.js @@ -0,0 +1,173 @@ +import './index'; + +describe('fixed price', () => { + describe('Component vnFixedPrice', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => { + $httpBackend = _$httpBackend_; + const $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnFixedPrice', {$element, $scope}); + controller.$ = { + model: {refresh: () => {}}, + edit: {hide: () => {}} + }; + })); + + describe('get columns', () => { + it(`should return a set of columns`, () => { + let result = controller.columns; + + let length = result.length; + let anyColumn = Object.keys(result[Math.floor(Math.random() * Math.floor(length))]); + + expect(anyColumn).toContain('field', 'displayName'); + }); + }); + + describe('get checked', () => { + it(`should return a set of checked lines`, () => { + controller.$.model.data = [ + {checked: true, id: 1}, + {checked: true, id: 2}, + {checked: true, id: 3}, + {checked: false, id: 4}, + ]; + + let result = controller.checked; + + expect(result.length).toEqual(3); + }); + }); + + describe('reCheck()', () => { + it(`should recheck buys`, () => { + controller.$.model.data = [ + {checked: false, id: 1}, + {checked: false, id: 2}, + {checked: false, id: 3}, + {checked: false, id: 4}, + ]; + controller.checkedFixedPrices = [1, 2]; + + controller.reCheck(); + + expect(controller.$.model.data[0].checked).toEqual(true); + expect(controller.$.model.data[1].checked).toEqual(true); + expect(controller.$.model.data[2].checked).toEqual(false); + expect(controller.$.model.data[3].checked).toEqual(false); + }); + }); + + describe('saveChecked()', () => { + it(`should check buy`, () => { + const buyCheck = 3; + controller.checkedFixedPrices = [1, 2]; + + controller.saveChecked(buyCheck); + + expect(controller.checkedFixedPrices[2]).toEqual(buyCheck); + }); + + it(`should uncheck buy`, () => { + const buyUncheck = 3; + controller.checkedFixedPrices = [1, 2, 3]; + + controller.saveChecked(buyUncheck); + + expect(controller.checkedFixedPrices[2]).toEqual(undefined); + }); + }); + + describe('onEditAccept()', () => { + it(`should perform a query to update columns`, () => { + controller.editedColumn = {field: 'my field', newValue: 'the new value'}; + const query = 'FixedPrices/editFixedPrice'; + + $httpBackend.expectPOST(query).respond(); + controller.onEditAccept(); + $httpBackend.flush(); + + const result = controller.checked; + + expect(result.length).toEqual(0); + }); + }); + + describe('upsertPrice()', () => { + it('should do nothing if one or more required arguments are missing', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.upsertPrice({}); + + expect(controller.vnApp.showSuccess).not.toHaveBeenCalled(); + }); + + it('should perform an http request to update the price', () => { + const now = Date.vnNew(); + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expectPATCH('FixedPrices/upsertFixedPrice').respond(); + controller.upsertPrice({ + itemFk: 1, + started: now, + ended: now, + rate2: 1, + rate3: 2 + }); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('removePrice()', () => { + it(`should only remove the created instance by the model as it doesn't have an ID yet`, () => { + const $index = 0; + controller.$ = { + model: { + remove: () => {}, + data: [{ + foo: 'bar' + }] + } + }; + jest.spyOn(controller.vnApp, 'showSuccess'); + jest.spyOn(controller.$.model, 'remove'); + + $httpBackend.expectGET('Warehouses').respond(); + + controller.removePrice($index); + + expect(controller.vnApp.showSuccess).not.toHaveBeenCalled(); + expect(controller.$.model.remove).toHaveBeenCalled(); + }); + + it('should remove the instance performing an delete http request', () => { + const $index = 0; + controller.$ = { + model: { + remove: () => {}, + data: [{ + id: '1' + }] + } + }; + jest.spyOn(controller.vnApp, 'showSuccess'); + jest.spyOn(controller.$.model, 'remove'); + + const query = `FixedPrices/${controller.$.model.data[0].id}`; + $httpBackend.expectDELETE(query).respond(); + controller.removePrice($index); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.$.model.remove).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/modules/item/front/fixed-price/locale/es.yml b/modules/item/front/fixed-price/locale/es.yml new file mode 100644 index 000000000..6dacf96c9 --- /dev/null +++ b/modules/item/front/fixed-price/locale/es.yml @@ -0,0 +1,7 @@ +Fixed prices: Precios fijados +Search prices by item ID or code: Buscar por ID de artículo o código +Search fixed prices: Buscar precios fijados +Add fixed price: Añadir precio fijado +This row will be removed: Esta linea se eliminará +Edit fixed price(s): Editar precio(s) fijado(s) +Has min price: Tiene precio mínimo diff --git a/modules/item/front/fixed-price/style.scss b/modules/item/front/fixed-price/style.scss new file mode 100644 index 000000000..f70ec2f50 --- /dev/null +++ b/modules/item/front/fixed-price/style.scss @@ -0,0 +1,46 @@ +@import "variables"; +vn-fixed-price{ + smart-table table{ + [shrink-field]{ + width: 90px; + max-width: 90px; + } + [shrink-field-expand]{ + width: 150px; + max-width: 150px; + } + } + + .minPrice { + align-items: center; + text-align: center; + vn-input-number { + width: 75px; + max-width: 75px; + } + } + + smart-table table tbody > * > td .chip { + padding: 0px; + } + + smart-table table tbody > * > td{ + padding: 0px; + padding-left: 5px; + padding-right: 5px; + } + + smart-table table tbody > * > td .chip.warning { + color: $color-font-bg + } + + .vn-field > .container > .infix > .control > input { + color: inherit; + } + + vn-input-number.inactive{ + input { + color: $color-font-light !important; + } + } +} diff --git a/modules/item/front/index.js b/modules/item/front/index.js index fb1aacd39..354477d4d 100644 --- a/modules/item/front/index.js +++ b/modules/item/front/index.js @@ -1,6 +1,27 @@ export * from './module'; import './main'; +import './index/'; +import './search-panel'; +import './diary'; +import './create'; +import './card'; import './descriptor'; import './descriptor-popover'; +import './basic-data'; +import './fetched-tags'; +import './tags'; +import './tax'; +import './log'; +import './request'; +import './request-search-panel'; +import './last-entries'; +import './botanical'; +import './barcode'; +import './summary'; +import './waste/index/'; +import './fixed-price'; +import './fixed-price-search-panel'; +import './item-type'; +import './item-shelving'; diff --git a/modules/item/front/index/index.html b/modules/item/front/index/index.html new file mode 100644 index 000000000..6f5cce7c0 --- /dev/null +++ b/modules/item/front/index/index.html @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Identifier + + Grouping + + Packing + + Description + + Stems + + Size + + Type + + Category + + Intrastat + + Origin + + Buyer + + Weight/Piece + + Multiplier + + Active + + Producer + + Landed +
+ + + + {{::item.id}} + + {{::item.grouping | dashIfEmpty}}{{::item.packing | dashIfEmpty}} +
+ {{::item.name}} + +

{{::item.subName}}

+
+
+ + +
{{::item.stems}}{{::item.size}} + {{::item.typeName}} + + {{::item.category}} + + {{::item.intrastat}} + {{::item.origin}} + + {{::item.userName}} + + {{::item.weightByPiece}}{{::item.stemMultiplier}} + + + {{::item.producer | dashIfEmpty}}{{::item.landed | date:'dd/MM/yyyy'}} + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + \ No newline at end of file diff --git a/modules/item/front/index/index.js b/modules/item/front/index/index.js new file mode 100644 index 000000000..2bcc2302a --- /dev/null +++ b/modules/item/front/index/index.js @@ -0,0 +1,112 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.smartTableOptions = { + activeButtons: { + search: true, + shownColumns: true, + }, + columns: [ + { + field: 'category', + autocomplete: { + url: 'ItemCategories', + valueField: 'name', + } + }, + { + field: 'origin', + autocomplete: { + url: 'Origins', + showField: 'code', + valueField: 'code' + } + }, + { + field: 'typeFk', + autocomplete: { + url: 'ItemTypes', + } + }, + { + field: 'intrastat', + autocomplete: { + url: 'Intrastats', + showField: 'description', + valueField: 'description' + } + }, + { + field: 'buyerFk', + autocomplete: { + url: 'TicketRequests/getItemTypeWorker', + searchFunction: '{firstName: $search}', + showField: 'nickname', + valueField: 'id', + } + }, + { + field: 'active', + searchable: false + }, + { + field: 'landed', + searchable: false + }, + ] + }; + } + + exprBuilder(param, value) { + switch (param) { + case 'category': + return {'ic.name': value}; + case 'buyerFk': + return {'it.workerFk': value}; + case 'grouping': + return {'b.grouping': value}; + case 'packing': + return {'b.packing': value}; + case 'origin': + return {'ori.code': value}; + case 'typeFk': + return {'i.typeFk': value}; + case 'intrastat': + return {'intr.description': value}; + case 'name': + return {'i.name': {like: `%${value}%`}}; + case 'producer': + return {'pr.name': {like: `%${value}%`}}; + case 'id': + case 'size': + case 'subname': + case 'isActive': + case 'weightByPiece': + case 'stemMultiplier': + case 'stems': + return {[`i.${param}`]: value}; + } + } + + onCloneAccept(itemFk) { + return this.$http.post(`Items/${itemFk}/clone`) + .then(res => { + this.$state.go('item.card.tags', {id: res.data.id}); + }); + } + + preview(item) { + this.itemSelected = item; + this.$.preview.show(); + } +} + +ngModule.vnComponent('vnItemIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/index/index.spec.js b/modules/item/front/index/index.spec.js new file mode 100644 index 000000000..18abde581 --- /dev/null +++ b/modules/item/front/index/index.spec.js @@ -0,0 +1,30 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemIndex', () => { + let controller; + let $httpBackend; + let $scope; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => { + $httpBackend = _$httpBackend_; + $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnItemIndex', {$element, $scope}); + })); + + describe('onCloneAccept()', () => { + it('should perform a post query and then call go() then update itemSelected in the controller', () => { + jest.spyOn(controller.$state, 'go'); + + $httpBackend.expectRoute('POST', `Items/:id/clone`).respond({id: 99}); + controller.onCloneAccept(1); + $httpBackend.flush(); + + expect(controller.$state.go).toHaveBeenCalledWith('item.card.tags', {id: 99}); + }); + }); + }); +}); diff --git a/modules/item/front/index/locale/es.yml b/modules/item/front/index/locale/es.yml new file mode 100644 index 000000000..0d72edd28 --- /dev/null +++ b/modules/item/front/index/locale/es.yml @@ -0,0 +1,2 @@ +picture: Foto +Buy requests: Peticiones de compra \ No newline at end of file diff --git a/modules/item/front/index/preview.svg b/modules/item/front/index/preview.svg new file mode 100644 index 000000000..5d56b5f34 --- /dev/null +++ b/modules/item/front/index/preview.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/modules/item/front/index/style.scss b/modules/item/front/index/style.scss new file mode 100644 index 000000000..e9fd9f935 --- /dev/null +++ b/modules/item/front/index/style.scss @@ -0,0 +1,34 @@ +@import "variables"; + +vn-item-product { + display: block; + + .id { + background-color: $color-main; + color: $color-font-dark; + margin-bottom: 0; + } + .image { + height: 112px; + width: 112px; + + & > img { + max-height: 100%; + max-width: 100%; + border-radius: 3px; + } + } + vn-label-value:first-of-type section{ + margin-top: 9px; + } +} + +vn-item-index { + table { + img { + border-radius: 50%; + width: 50px; + height: 50px; + } + } +} \ No newline at end of file diff --git a/modules/item/front/item-shelving/index.html b/modules/item/front/item-shelving/index.html new file mode 100644 index 000000000..fa7a70544 --- /dev/null +++ b/modules/item/front/item-shelving/index.html @@ -0,0 +1,118 @@ + + + + + +
+
+
Total
+ + +
+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Created + + Item + + Concept + + Parking + + Shelving + + Etiqueta + + Packing +
+ + + {{::itemShelvingPlacementSupplyStock.created | date: 'dd/MM/yyyy'}} + {{::itemShelvingPlacementSupplyStock.itemFk}} + + + {{itemShelvingPlacementSupplyStock.longName}} + + + {{::itemShelvingPlacementSupplyStock.parking}} + + {{::itemShelvingPlacementSupplyStock.shelving}} + + {{(itemShelvingPlacementSupplyStock.stock / itemShelvingPlacementSupplyStock.packing).toFixed(2)}} + + {{::itemShelvingPlacementSupplyStock.packing}} +
+
+
+
+ + + + + \ No newline at end of file diff --git a/modules/item/front/item-shelving/index.js b/modules/item/front/item-shelving/index.js new file mode 100644 index 000000000..b8584039b --- /dev/null +++ b/modules/item/front/item-shelving/index.js @@ -0,0 +1,89 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.smartTableOptions = { + activeButtons: { + search: true + }, + columns: [ + { + field: 'parking', + autocomplete: { + url: 'Parkings', + showField: 'code', + valueField: 'code' + } + }, + { + field: 'shelving', + autocomplete: { + url: 'Shelvings', + showField: 'code', + valueField: 'code' + } + }, + { + field: 'created', + searchable: false + }, + { + field: 'itemFk', + searchable: false + }, + { + field: 'longName', + searchable: false + } + ] + }; + } + + get checked() { + const itemShelvings = this.$.model.data || []; + const checkedLines = []; + for (let itemShelving of itemShelvings) { + if (itemShelving.checked) + checkedLines.push(itemShelving.itemShelvingFk); + } + + return checkedLines; + } + + calculateTotals() { + this.labelTotal = 0; + const itemShelvings = this.$.model.data || []; + itemShelvings.forEach(itemShelving => { + const label = itemShelving.stock / itemShelving.packing; + this.labelTotal += label; + }); + } + + onRemove() { + const params = {itemShelvingIds: this.checked}; + const query = `ItemShelvings/deleteItemShelvings`; + this.$http.post(query, params) + .then(() => { + this.vnApp.showSuccess(this.$t('ItemShelvings removed')); + this.$.model.refresh(); + }); + } + + exprBuilder(param, value) { + switch (param) { + case 'parking': + case 'shelving': + case 'label': + case 'packing': + return {[param]: value}; + } + } +} + +ngModule.vnComponent('vnItemShelving', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-shelving/index.spec.js b/modules/item/front/item-shelving/index.spec.js new file mode 100644 index 000000000..55df1c27d --- /dev/null +++ b/modules/item/front/item-shelving/index.spec.js @@ -0,0 +1,81 @@ +import './index'; +import crudModel from 'core/mocks/crud-model'; + +describe('item shelving', () => { + describe('Component vnItemShelving', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + const $element = angular.element(''); + controller = $componentController('vnItemShelving', {$element}); + controller.$.model = crudModel; + controller.$.model.data = [ + {itemShelvingFk: 1, packing: 10, stock: 1}, + {itemShelvingFk: 2, packing: 12, stock: 5}, + {itemShelvingFk: 4, packing: 20, stock: 10} + ]; + const modelData = controller.$.model.data; + modelData[0].checked = true; + modelData[1].checked = true; + })); + + describe('checked() getter', () => { + it('should return a the selected rows', () => { + const result = controller.checked; + + expect(result).toEqual(expect.arrayContaining([1, 2])); + }); + }); + + describe('calculateTotals()', () => { + it('should calculate the total of labels', () => { + controller.calculateTotals(); + + expect(controller.labelTotal).toEqual(1.0166666666666666); + }); + }); + + describe('onRemove()', () => { + it('shoud remove the selected lines', () => { + jest.spyOn(controller.$.model, 'refresh'); + const expectedParams = {itemShelvingIds: [1, 2]}; + + $httpBackend.expectPOST('ItemShelvings/deleteItemShelvings', expectedParams).respond(200); + controller.onRemove(); + $httpBackend.flush(); + + expect(controller.$.model.refresh).toHaveBeenCalled(); + }); + }); + + describe('exprBuilder()', () => { + it('should search by parking', () => { + const expr = controller.exprBuilder('parking', '700-01'); + + expect(expr).toEqual({'parking': '700-01'}); + }); + + it('should search by shelving', () => { + const expr = controller.exprBuilder('shelving', 'AAA'); + + expect(expr).toEqual({'shelving': 'AAA'}); + }); + + it('should search by label', () => { + const expr = controller.exprBuilder('label', 0.17); + + expect(expr).toEqual({'label': 0.17}); + }); + + it('should search by packing', () => { + const expr = controller.exprBuilder('packing', 10); + + expect(expr).toEqual({'packing': 10}); + }); + }); + }); +}); diff --git a/modules/item/front/item-shelving/locale/es.yml b/modules/item/front/item-shelving/locale/es.yml new file mode 100644 index 000000000..006363cfa --- /dev/null +++ b/modules/item/front/item-shelving/locale/es.yml @@ -0,0 +1,5 @@ +Shelving: Matrícula +Remove selected lines: Eliminar líneas seleccionadas +Selected lines will be deleted: Las líneas seleccionadas serán eliminadas +ItemShelvings removed: Carros eliminados +Total labels: Total etiquetas \ No newline at end of file diff --git a/modules/item/front/item-type/basic-data/index.html b/modules/item/front/item-type/basic-data/index.html new file mode 100644 index 000000000..c3f7a57f1 --- /dev/null +++ b/modules/item/front/item-type/basic-data/index.html @@ -0,0 +1,62 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/item/front/item-type/basic-data/index.js b/modules/item/front/item-type/basic-data/index.js new file mode 100644 index 000000000..ec280fdf8 --- /dev/null +++ b/modules/item/front/item-type/basic-data/index.js @@ -0,0 +1,12 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section {} + +ngModule.component('vnItemTypeBasicData', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/card/index.html b/modules/item/front/item-type/card/index.html new file mode 100644 index 000000000..80af6088e --- /dev/null +++ b/modules/item/front/item-type/card/index.html @@ -0,0 +1,5 @@ + + + + + diff --git a/modules/item/front/item-type/card/index.js b/modules/item/front/item-type/card/index.js new file mode 100644 index 000000000..fa6b37340 --- /dev/null +++ b/modules/item/front/item-type/card/index.js @@ -0,0 +1,23 @@ +import ngModule from '../../module'; +import ModuleCard from 'salix/components/module-card'; + +class Controller extends ModuleCard { + reload() { + const filter = { + include: [ + {relation: 'worker'}, + {relation: 'category'}, + {relation: 'itemPackingType'}, + {relation: 'temperature'} + ] + }; + + this.$http.get(`ItemTypes/${this.$params.id}`, {filter}) + .then(res => this.itemType = res.data); + } +} + +ngModule.vnComponent('vnItemTypeCard', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/card/index.spec.js b/modules/item/front/item-type/card/index.spec.js new file mode 100644 index 000000000..ab2314bb9 --- /dev/null +++ b/modules/item/front/item-type/card/index.spec.js @@ -0,0 +1,27 @@ +import './index'; + +describe('component vnItemTypeCard', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + controller = $componentController('vnItemTypeCard', {$element: null}); + })); + + describe('reload()', () => { + it('should reload the controller data', () => { + controller.$params.id = 1; + + const itemType = {id: 1}; + + $httpBackend.expectGET('ItemTypes/1').respond(itemType); + controller.reload(); + $httpBackend.flush(); + + expect(controller.itemType).toEqual(itemType); + }); + }); +}); diff --git a/modules/item/front/item-type/create/index.html b/modules/item/front/item-type/create/index.html new file mode 100644 index 000000000..4a199a1b1 --- /dev/null +++ b/modules/item/front/item-type/create/index.html @@ -0,0 +1,62 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/item/front/item-type/create/index.js b/modules/item/front/item-type/create/index.js new file mode 100644 index 000000000..ccf7744be --- /dev/null +++ b/modules/item/front/item-type/create/index.js @@ -0,0 +1,15 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + onSubmit() { + return this.$.watcher.submit().then(res => + this.$state.go('item.itemType.card.basicData', {id: res.data.id}) + ); + } +} + +ngModule.component('vnItemTypeCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/create/index.spec.js b/modules/item/front/item-type/create/index.spec.js new file mode 100644 index 000000000..4b000df9a --- /dev/null +++ b/modules/item/front/item-type/create/index.spec.js @@ -0,0 +1,34 @@ +import './index'; + +describe('component vnItemTypeCreate', () => { + let $scope; + let $state; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$state_) => { + $scope = $rootScope.$new(); + $state = _$state_; + $scope.watcher = { + submit: () => { + return { + then: callback => { + callback({data: {id: '1234'}}); + } + }; + } + }; + const $element = angular.element(''); + controller = $componentController('vnItemTypeCreate', {$element, $scope}); + })); + + describe('onSubmit()', () => { + it(`should call submit() on the watcher then expect a callback`, () => { + jest.spyOn($state, 'go'); + controller.onSubmit(); + + expect(controller.$state.go).toHaveBeenCalledWith('item.itemType.card.basicData', {id: '1234'}); + }); + }); +}); diff --git a/modules/item/front/item-type/descriptor/index.html b/modules/item/front/item-type/descriptor/index.html new file mode 100644 index 000000000..5a0e8ed49 --- /dev/null +++ b/modules/item/front/item-type/descriptor/index.html @@ -0,0 +1,25 @@ + + +
+ + + + + + + + +
+
+
\ No newline at end of file diff --git a/modules/item/front/item-type/descriptor/index.js b/modules/item/front/item-type/descriptor/index.js new file mode 100644 index 000000000..9322c599a --- /dev/null +++ b/modules/item/front/item-type/descriptor/index.js @@ -0,0 +1,20 @@ +import ngModule from '../../module'; +import Descriptor from 'salix/components/descriptor'; + +class Controller extends Descriptor { + get itemType() { + return this.entity; + } + + set itemType(value) { + this.entity = value; + } +} + +ngModule.component('vnItemTypeDescriptor', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/index.js b/modules/item/front/item-type/index.js new file mode 100644 index 000000000..5dcbe4097 --- /dev/null +++ b/modules/item/front/item-type/index.js @@ -0,0 +1,8 @@ +import './main'; +import './index/'; +import './summary'; +import './card'; +import './descriptor'; +import './create'; +import './basic-data'; +import './search-panel'; diff --git a/modules/item/front/item-type/index/index.html b/modules/item/front/item-type/index/index.html new file mode 100644 index 000000000..50b9eb172 --- /dev/null +++ b/modules/item/front/item-type/index/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/item-type/index/index.js b/modules/item/front/item-type/index/index.js new file mode 100644 index 000000000..54ecba997 --- /dev/null +++ b/modules/item/front/item-type/index/index.js @@ -0,0 +1,14 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + preview(itemType) { + this.selectedItemType = itemType; + this.$.summary.show(); + } +} + +ngModule.component('vnItemTypeIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/item-type/index/index.spec.js b/modules/item/front/item-type/index/index.spec.js new file mode 100644 index 000000000..887c03f7f --- /dev/null +++ b/modules/item/front/item-type/index/index.spec.js @@ -0,0 +1,34 @@ +import './index'; + +describe('Item', () => { + describe('Component vnItemTypeIndex', () => { + let controller; + let $window; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$window_) => { + $window = _$window_; + const $element = angular.element(''); + controller = $componentController('vnItemTypeIndex', {$element}); + })); + + describe('preview()', () => { + it('should show the dialog summary', () => { + controller.$.summary = {show: () => {}}; + jest.spyOn(controller.$.summary, 'show'); + + const itemType = {id: 1}; + + const event = new MouseEvent('click', { + view: $window, + bubbles: true, + cancelable: true + }); + controller.preview(event, itemType); + + expect(controller.$.summary.show).toHaveBeenCalledWith(); + }); + }); + }); +}); diff --git a/modules/item/front/item-type/index/locale/es.yml b/modules/item/front/item-type/index/locale/es.yml new file mode 100644 index 000000000..1a71ff212 --- /dev/null +++ b/modules/item/front/item-type/index/locale/es.yml @@ -0,0 +1,2 @@ +Item Type: Familia +New itemType: Nueva familia \ No newline at end of file diff --git a/modules/item/front/item-type/main/index.html b/modules/item/front/item-type/main/index.html new file mode 100644 index 000000000..faba696c0 --- /dev/null +++ b/modules/item/front/item-type/main/index.html @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/item-type/main/index.js b/modules/item/front/item-type/main/index.js new file mode 100644 index 000000000..0dea00abb --- /dev/null +++ b/modules/item/front/item-type/main/index.js @@ -0,0 +1,24 @@ +import ngModule from '../../module'; +import ModuleMain from 'salix/components/module-main'; + +export default class ItemType extends ModuleMain { + exprBuilder(param, value) { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {id: value} + : {or: [ + {name: {like: `%${value}%`}}, + {code: {like: `%${value}%`}} + ]}; + case 'name': + case 'code': + return {[param]: {like: `%${value}%`}}; + } + } +} + +ngModule.vnComponent('vnItemType', { + controller: ItemType, + template: require('./index.html') +}); diff --git a/modules/item/front/item-type/main/index.spec.js b/modules/item/front/item-type/main/index.spec.js new file mode 100644 index 000000000..dcb14ec0e --- /dev/null +++ b/modules/item/front/item-type/main/index.spec.js @@ -0,0 +1,31 @@ +import './index'; + +describe('Item', () => { + describe('Component vnItemType', () => { + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject($componentController => { + const $element = angular.element(''); + controller = $componentController('vnItemType', {$element}); + })); + + describe('exprBuilder()', () => { + it('should return a filter based on a search by id', () => { + const filter = controller.exprBuilder('search', '123'); + + expect(filter).toEqual({id: '123'}); + }); + + it('should return a filter based on a search by name or code', () => { + const filter = controller.exprBuilder('search', 'Alstroemeria'); + + expect(filter).toEqual({or: [ + {name: {like: '%Alstroemeria%'}}, + {code: {like: '%Alstroemeria%'}}, + ]}); + }); + }); + }); +}); diff --git a/modules/item/front/item-type/main/locale/es.yml b/modules/item/front/item-type/main/locale/es.yml new file mode 100644 index 000000000..7aceac46f --- /dev/null +++ b/modules/item/front/item-type/main/locale/es.yml @@ -0,0 +1 @@ +Search itemType by id, name or code: Buscar familia por id, nombre o código \ No newline at end of file diff --git a/modules/item/front/item-type/search-panel/index.html b/modules/item/front/item-type/search-panel/index.html new file mode 100644 index 000000000..4aa762900 --- /dev/null +++ b/modules/item/front/item-type/search-panel/index.html @@ -0,0 +1,22 @@ +
+
+ + + + + + + + + + + +
+
\ No newline at end of file diff --git a/modules/item/front/item-type/search-panel/index.js b/modules/item/front/item-type/search-panel/index.js new file mode 100644 index 000000000..17a439c39 --- /dev/null +++ b/modules/item/front/item-type/search-panel/index.js @@ -0,0 +1,7 @@ +import ngModule from '../../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +ngModule.component('vnItemTypeSearchPanel', { + template: require('./index.html'), + controller: SearchPanel +}); diff --git a/modules/item/front/item-type/summary/index.html b/modules/item/front/item-type/summary/index.html new file mode 100644 index 000000000..d003c8f38 --- /dev/null +++ b/modules/item/front/item-type/summary/index.html @@ -0,0 +1,50 @@ + +
+ {{summary.id}} - {{summary.name}} - {{summary.worker.firstName}} {{summary.worker.lastName}} +
+ + +

Basic data

+ + + + + + + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/modules/item/front/item-type/summary/index.js b/modules/item/front/item-type/summary/index.js new file mode 100644 index 000000000..7645de8b1 --- /dev/null +++ b/modules/item/front/item-type/summary/index.js @@ -0,0 +1,33 @@ +import ngModule from '../../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + set itemType(value) { + this._itemType = value; + this.$.summary = null; + if (!value) return; + + const filter = { + include: [ + {relation: 'worker'}, + {relation: 'category'}, + {relation: 'itemPackingType'}, + {relation: 'temperature'} + ] + }; + this.$http.get(`ItemTypes/${value.id}`, {filter}) + .then(res => this.$.summary = res.data); + } + + get itemType() { + return this._itemType; + } +} + +ngModule.component('vnItemTypeSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + itemType: '<' + } +}); diff --git a/modules/item/front/item-type/summary/locale/es.yml b/modules/item/front/item-type/summary/locale/es.yml new file mode 100644 index 000000000..8f4cef70f --- /dev/null +++ b/modules/item/front/item-type/summary/locale/es.yml @@ -0,0 +1,4 @@ +Life: Vida +Promo: Promoción +Item packing type: Tipo de embalaje +Is unconventional size: Es de tamaño poco convencional \ No newline at end of file diff --git a/modules/item/front/last-entries/index.html b/modules/item/front/last-entries/index.html new file mode 100644 index 000000000..e3b84655c --- /dev/null +++ b/modules/item/front/last-entries/index.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + Ig + Warehouse + Landed + Entry + PVP + Label + Packing + Grouping + Stems + Quantity + Cost + Kg. + Cube + Provider + + + + + + + + + {{::entry.warehouse| dashIfEmpty}} + {{::entry.landed | date:'dd/MM/yyyy HH:mm'}} + + + {{::entry.entryFk | dashIfEmpty}} + + + + {{::entry.price2 | currency: 'EUR':2 | dashIfEmpty}} / {{::entry.price3 | currency: 'EUR':2 | dashIfEmpty}} + + {{entry.stickers | dashIfEmpty}} + + + {{::entry.packing | dashIfEmpty}} + + + + + {{::entry.grouping | dashIfEmpty}} + + + {{::entry.stems | dashIfEmpty}} + {{::entry.quantity}} + + + {{::entry.cost | currency: 'EUR':3 | dashIfEmpty}} + + + {{::entry.weight | dashIfEmpty}} + {{::entry.packagingFk | dashIfEmpty}} + {{::entry.supplier | dashIfEmpty}} + + + + + + + + + + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + Copy value + + + diff --git a/modules/item/front/last-entries/index.js b/modules/item/front/last-entries/index.js new file mode 100644 index 000000000..a5f1f4d9d --- /dev/null +++ b/modules/item/front/last-entries/index.js @@ -0,0 +1,105 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + + const from = Date.vnNew(); + from.setDate(from.getDate() - 75); + from.setHours(0, 0, 0, 0); + + const to = Date.vnNew(); + to.setDate(to.getDate() + 10); + to.setHours(23, 59, 59, 59); + + this.filter = { + where: { + itemFk: this.$params.id, + landed: { + between: [from, to] + } + } + }; + this._dateFrom = from; + this._dateTo = to; + } + + set dateFrom(value) { + this._dateFrom = value; + + if (!value) return; + + const from = new Date(value); + from.setHours(0, 0, 0, 0); + + const to = new Date(this._dateTo); + to.setHours(23, 59, 59, 59); + + this.filter.where.landed = { + between: [from, to] + }; + this.$.model.refresh(); + } + + set dateTo(value) { + this._dateTo = value; + + if (!value) return; + + const from = new Date(this._dateFrom); + from.setHours(0, 0, 0, 0); + + const to = new Date(value); + to.setHours(23, 59, 59, 59); + + this.filter.where.landed = { + between: [from, to] + }; + this.$.model.refresh(); + } + + get dateFrom() { + return this._dateFrom; + } + + get dateTo() { + return this._dateTo; + } + + exprBuilder(param, value) { + switch (param) { + case 'id': + case 'quantity': + case 'packagingFk': + return {[`b.${param}`]: value}; + case 'supplierFk': + return {[`s.id`]: value}; + case 'warehouseFk': + return {'tr.warehouseInFk': value}; + case 'landed': + return {'tr.landed': { + between: this.dateRange(value)} + }; + } + } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnItemLastEntries', { + template: require('./index.html'), + controller: Controller, + bindings: { + item: '<' + } +}); diff --git a/modules/item/front/last-entries/locale/es.yml b/modules/item/front/last-entries/locale/es.yml new file mode 100644 index 000000000..f2917bb63 --- /dev/null +++ b/modules/item/front/last-entries/locale/es.yml @@ -0,0 +1,15 @@ +Since: Desde +Landed: F. entrega +Stems: Tallos +Quantity: Cantidad +Cost: Coste +Label: Etiquetas +Entry: Entrada +Ignored: Ignorado +Provider: Proveedor +Cube: Cubo +Price Per Unit: Precio Por Unidad +Price Per Package: Precio Por Paquete +Freight: Porte +Package: Embalaje +Comission: Comision \ No newline at end of file diff --git a/modules/item/front/main/index.html b/modules/item/front/main/index.html index e69de29bb..44d125758 100644 --- a/modules/item/front/main/index.html +++ b/modules/item/front/main/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/modules/item/front/main/index.js b/modules/item/front/main/index.js index 33e5822c3..1d99c91a1 100644 --- a/modules/item/front/main/index.js +++ b/modules/item/front/main/index.js @@ -3,10 +3,11 @@ import ModuleMain from 'salix/components/module-main'; export default class Items extends ModuleMain { constructor($element, $) { super($element, $); - } - async $onInit() { - this.$state.go('home'); - window.location.href = await this.vnApp.getUrl(`claim/`); + + this.filterParams = { + isActive: true, + isFloramondo: false + }; } } diff --git a/modules/item/front/request-search-panel/index.html b/modules/item/front/request-search-panel/index.html new file mode 100644 index 000000000..6a51a429d --- /dev/null +++ b/modules/item/front/request-search-panel/index.html @@ -0,0 +1,104 @@ +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + Or + + + + + +
+ + + + + {{name}} + + + + + +
+
diff --git a/modules/item/front/request-search-panel/index.js b/modules/item/front/request-search-panel/index.js new file mode 100644 index 000000000..556bdd874 --- /dev/null +++ b/modules/item/front/request-search-panel/index.js @@ -0,0 +1,48 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +class Controller extends SearchPanel { + constructor($element, $) { + super($element, $); + + this.states = [ + {code: 'pending', name: this.$t('Pending')}, + {code: 'accepted', name: this.$t('Accepted')}, + {code: 'denied', name: this.$t('Denied')} + ]; + } + + get from() { + return this._from; + } + + set from(value) { + this._from = value; + this.filter.scopeDays = null; + } + + get to() { + return this._to; + } + + set to(value) { + this._to = value; + this.filter.scopeDays = null; + } + + get scopeDays() { + return this._scopeDays; + } + + set scopeDays(value) { + this._scopeDays = value; + + this.filter.from = null; + this.filter.to = null; + } +} + +ngModule.vnComponent('vnRequestSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/request-search-panel/index.spec.js b/modules/item/front/request-search-panel/index.spec.js new file mode 100644 index 000000000..56c76eabf --- /dev/null +++ b/modules/item/front/request-search-panel/index.spec.js @@ -0,0 +1,48 @@ +import './index'; + +describe(' Component vnRequestSearchPanel', () => { + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject($componentController => { + controller = $componentController('vnRequestSearchPanel', {$element: null}); + controller.$t = () => {}; + controller.filter = {}; + })); + + describe('from() setter', () => { + it('should clear the scope days when setting the from property', () => { + controller.filter.scopeDays = 1; + + controller.from = Date.vnNew(); + + expect(controller.filter.scopeDays).toBeNull(); + expect(controller.from).toBeDefined(); + }); + }); + + describe('to() setter', () => { + it('should clear the scope days when setting the to property', () => { + controller.filter.scopeDays = 1; + + controller.to = Date.vnNew(); + + expect(controller.filter.scopeDays).toBeNull(); + expect(controller.to).toBeDefined(); + }); + }); + + describe('scopeDays() setter', () => { + it('should clear the date range when setting the scopeDays property', () => { + controller.filter.from = Date.vnNew(); + controller.filter.to = Date.vnNew(); + + controller.scopeDays = 1; + + expect(controller.filter.from).toBeNull(); + expect(controller.filter.to).toBeNull(); + expect(controller.scopeDays).toBeDefined(); + }); + }); +}); diff --git a/modules/item/front/request-search-panel/locale/es.yml b/modules/item/front/request-search-panel/locale/es.yml new file mode 100644 index 000000000..8d5276194 --- /dev/null +++ b/modules/item/front/request-search-panel/locale/es.yml @@ -0,0 +1,4 @@ +Ink: Tinta +Origin: Origen +Producer: Productor +For me: Para mi \ No newline at end of file diff --git a/modules/item/front/request/index.html b/modules/item/front/request/index.html new file mode 100644 index 000000000..03c8db8ec --- /dev/null +++ b/modules/item/front/request/index.html @@ -0,0 +1,172 @@ + + + + + + + + + + + + Ticket ID + Shipped + Description + Requester + Requested + Price + Atender + Item + Achieved + Concept + State + + + + + + + + {{request.ticketFk}} + + + + + {{::request.shipped | date: 'dd/MM/yyyy'}} + + + {{::request.description}} + + + {{::request.requesterName}} + + + {{::request.quantity}} + {{::request.price | currency: 'EUR':2}} + + + {{::request.attenderName}} + + + + {{request.itemFk}} + + + + + + + {{request.saleQuantity}} + + + + + + + + {{request.itemDescription}} + + + {{$ctrl.getState(request.isOk)}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Filter by selection + + + Exclude selection + + + Remove filter + + + Remove all filters + + + Copy value + + + diff --git a/modules/item/front/request/index.js b/modules/item/front/request/index.js new file mode 100644 index 000000000..747cbeff2 --- /dev/null +++ b/modules/item/front/request/index.js @@ -0,0 +1,143 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + + if (!this.$state.q) { + const today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + + const nextWeek = Date.vnNew(); + nextWeek.setHours(23, 59, 59, 59); + nextWeek.setDate(nextWeek.getDate() + 7); + + this.filterParams = { + from: today, + to: nextWeek, + state: 'pending' + }; + } + } + + fetchParams($params) { + if (!Object.entries($params).length) + $params.scopeDays = 1; + + if (typeof $params.scopeDays === 'number') { + const from = Date.vnNew(); + from.setHours(0, 0, 0, 0); + + const to = new Date(from.getTime()); + to.setDate(to.getDate() + $params.scopeDays); + to.setHours(23, 59, 59, 999); + + Object.assign($params, {from, to}); + } + + return $params; + } + + getState(isOk) { + if (isOk === null) + return 'Pending'; + else if (isOk) + return 'Accepted'; + else + return 'Denied'; + } + + confirmRequest(request) { + if (request.itemFk && request.saleQuantity) { + let params = { + itemFk: request.itemFk, + quantity: request.saleQuantity + }; + + let query = `TicketRequests/${request.id}/confirm`; + this.$http.post(query, params).then(res => { + request.itemDescription = res.data.concept; + request.isOk = true; + + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + } + + changeQuantity(request) { + if (request.saleFk) { + let params = { + quantity: request.saleQuantity + }; + + let endpoint = `Sales/${request.saleFk}`; + + this.$http.patch(endpoint, params) + .then(() => this.vnApp.showSuccess(this.$t('Data saved!'))) + .then(() => this.confirmRequest(request)); + } else + this.confirmRequest(request); + } + + compareDate(date) { + let today = Date.vnNew(); + today.setHours(0, 0, 0, 0); + let timeTicket = new Date(date); + timeTicket.setHours(0, 0, 0, 0); + + let comparation = today - timeTicket; + + if (comparation == 0) + return 'warning'; + if (comparation < 0) + return 'success'; + } + + onDenyAccept(request) { + let params = { + observation: this.denyObservation + }; + + return this.$http.post(`TicketRequests/${request.id}/deny`, params) + .then(res => { + const newRequest = res.data; + request.isOk = newRequest.isOk; + request.attenderFk = newRequest.attenderFk; + request.response = newRequest.response; + + this.vnApp.showSuccess(this.$t('Data saved!')); + }); + } + + exprBuilder(param, value) { + switch (param) { + case 'ticketFk': + case 'quantity': + case 'price': + case 'isOk': + return {[`tr.${param}`]: value}; + case 'attenderName': + return {[`ua.name`]: value}; + case 'shipped': + return {'t.shipped': { + between: this.dateRange(value)} + }; + } + } + + dateRange(value) { + const minHour = new Date(value); + minHour.setHours(0, 0, 0, 0); + const maxHour = new Date(value); + maxHour.setHours(23, 59, 59, 59); + + return [minHour, maxHour]; + } +} + +ngModule.vnComponent('vnItemRequest', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/request/index.spec.js b/modules/item/front/request/index.spec.js new file mode 100644 index 000000000..aadeaddca --- /dev/null +++ b/modules/item/front/request/index.spec.js @@ -0,0 +1,120 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemRequest', () => { + let $scope; + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + $scope = $rootScope.$new(); + controller = $componentController('vnItemRequest', {$element: null, $scope}); + })); + + afterAll(() => { + $scope.$destroy(); + $element.remove(); + }); + + describe('getState()', () => { + it(`should return an string depending to the isOK value`, () => { + let isOk = null; + let result = controller.getState(isOk); + + expect(result).toEqual('Pending'); + + isOk = 1; + result = controller.getState(isOk); + + expect(result).toEqual('Accepted'); + + isOk = 0; + result = controller.getState(isOk); + + expect(result).toEqual('Denied'); + }); + }); + + describe('confirmRequest()', () => { + it(`should do nothing if the request does't have itemFk or saleQuantity`, () => { + let request = {}; + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.confirmRequest(request); + + expect(controller.vnApp.showSuccess).not.toHaveBeenCalled(); + }); + + it('should perform a query and call vnApp.showSuccess() and refresh if the conditions are met', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + const expectedResult = {concept: 'Melee Weapon'}; + let request = {itemFk: 1, saleQuantity: 1, id: 1}; + + $httpBackend.expectPOST(`TicketRequests/${request.id}/confirm`).respond(expectedResult); + controller.confirmRequest(request); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('changeQuantity()', () => { + it(`should call confirmRequest() if there's no sale id in the request`, () => { + let request = {}; + jest.spyOn(controller, 'confirmRequest'); + + controller.changeQuantity(request); + + expect(controller.confirmRequest).toHaveBeenCalledWith(jasmine.any(Object)); + }); + + it(`should perform a query and call vnApp.showSuccess() if the conditions are met`, () => { + let request = {saleFk: 1, saleQuantity: 1}; + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expectPATCH(`Sales/${request.saleFk}`).respond(); + controller.changeQuantity(request); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + + describe('compareDate()', () => { + it(`should return "success" if receives a future date`, () => { + let date = '3019-02-18T11:00:00.000Z'; + let result = controller.compareDate(date); + + expect(result).toEqual('success'); + }); + + it(`should return "warning" if date is today`, () => { + let date = Date.vnNew(); + let result = controller.compareDate(date); + + expect(result).toEqual('warning'); + }); + }); + + describe('onDenyAccept()', () => { + it(`should deny the request`, () => { + const request = { + id: 1, + response: 'new' + }; + + $httpBackend.expectPOST(`TicketRequests/${request.id}/deny`) + .respond({response: 'denied'}); + controller.onDenyAccept(request); + $httpBackend.flush(); + + expect(request.response).toBe('denied'); + }); + }); + }); +}); + diff --git a/modules/item/front/request/locale/es.yml b/modules/item/front/request/locale/es.yml new file mode 100644 index 000000000..c61a00130 --- /dev/null +++ b/modules/item/front/request/locale/es.yml @@ -0,0 +1,9 @@ +Discard: Descartar +Specify the reasons to deny this request: Especifica las razones para descartar la petición +Buy requests: Peticiones de compra +Search request by id or alias: Buscar peticiones por identificador o alias +Requested: Solicitado +Achieved: Conseguido +Pending: Pendiente +Accepted: Aceptada +Denied: Rechazada \ No newline at end of file diff --git a/modules/item/front/request/style.scss b/modules/item/front/request/style.scss new file mode 100644 index 000000000..59612bbd6 --- /dev/null +++ b/modules/item/front/request/style.scss @@ -0,0 +1,15 @@ +@import "variables"; + +vn-item-request { + vn-dialog[vn-id="denyReason"] { + button.close { + display: none + } + vn-button { + margin: 0 auto + } + vn-textarea { + width: 100% + } + } +} \ No newline at end of file diff --git a/modules/item/front/routes.json b/modules/item/front/routes.json index 205930466..05b887a96 100644 --- a/modules/item/front/routes.json +++ b/modules/item/front/routes.json @@ -6,7 +6,25 @@ "dependencies": ["worker", "client", "ticket", "entry"], "menus": { "main": [ - {"state": "item.index", "icon": "icon-item"} + {"state": "item.index", "icon": "icon-item"}, + {"state": "item.request", "icon": "icon-buyrequest"}, + {"state": "item.waste.index", "icon": "icon-claims"}, + {"state": "item.fixedPrice", "icon": "icon-fixedPrice"}, + {"state": "item.itemType", "icon": "contact_support"} + ], + "card": [ + {"state": "item.card.basicData", "icon": "settings"}, + {"state": "item.card.tags", "icon": "icon-tags"}, + {"state": "item.card.last-entries", "icon": "icon-regentry"}, + {"state": "item.card.tax", "icon": "icon-tax"}, + {"state": "item.card.botanical", "icon": "local_florist"}, + {"state": "item.card.shelving", "icon": "icon-inventory"}, + {"state": "item.card.itemBarcode", "icon": "icon-barcode"}, + {"state": "item.card.diary", "icon": "icon-transaction"}, + {"state": "item.card.log", "icon": "history"} + ], + "itemType": [ + {"state": "item.itemType.card.basicData", "icon": "settings"} ] }, "keybindings": [ @@ -26,11 +44,187 @@ "component": "vn-item-index", "description": "Items" }, + { + "url": "/create", + "state": "item.create", + "component": "vn-item-create", + "description": "New item" + }, { "url": "/:id", "state": "item.card", "abstract": true, "component": "vn-item-card" + }, + { + "url" : "/basic-data", + "state": "item.card.basicData", + "component": "vn-item-basic-data", + "description": "Basic data", + "params": { + "item": "$ctrl.item" + }, + "acl": ["buyer"] + }, + { + "url" : "/tags", + "state": "item.card.tags", + "component": "vn-item-tags", + "description": "Tags", + "params": { + "item-tags": "$ctrl.itemTags" + }, + "acl": ["buyer", "replenisher"] + }, + { + "url" : "/tax", + "state": "item.card.tax", + "component": "vn-item-tax", + "description": "Tax", + "acl": ["administrative","buyer"] + }, + { + "url" : "/botanical", + "state": "item.card.botanical", + "component": "vn-item-botanical", + "description": "Botanical", + "params": { + "item": "$ctrl.item" + }, + "acl": ["buyer"] + }, + { + "url" : "/shelving", + "state": "item.card.shelving", + "component": "vn-item-shelving", + "description": "Shelvings", + "params": { + "item": "$ctrl.item" + }, + "acl": ["employee"] + }, + { + "url" : "/barcode", + "state": "item.card.itemBarcode", + "component": "vn-item-barcode", + "description": "Barcodes", + "params": { + "item": "$ctrl.item" + }, + "acl": ["buyer","replenisher"] + }, + { + "url" : "/summary", + "state": "item.card.summary", + "component": "vn-item-summary", + "description": "Summary", + "params": { + "item": "$ctrl.item" + } + }, + { + "url" : "/diary?warehouseFk&lineFk", + "state": "item.card.diary", + "component": "vn-item-diary", + "description": "Diary", + "params": { + "item": "$ctrl.item" + }, + "acl": ["employee"] + }, + { + "url" : "/last-entries", + "state": "item.card.last-entries", + "component": "vn-item-last-entries", + "description": "Last entries", + "params": { + "item": "$ctrl.item" + }, + "acl": ["employee"] + }, + { + "url" : "/log", + "state": "item.card.log", + "component": "vn-item-log", + "description": "Log" + }, + { + "url" : "/request?q", + "state": "item.request", + "component": "vn-item-request", + "description": "Buy requests", + "params": { + "item": "$ctrl.item" + }, + "acl": ["employee"] + }, + { + "url": "/waste", + "state": "item.waste", + "component": "ui-view", + "abstract": true + }, + { + "url" : "/index", + "state": "item.waste.index", + "component": "vn-item-waste-index", + "description": "Waste breakdown", + "acl": ["buyer"] + }, + { + "url" : "/detail?buyer&family", + "state": "item.waste.detail", + "component": "vn-item-waste-detail", + "description": "Waste breakdown by item", + "acl": ["buyer"] + }, + { + "url" : "/fixed-price?q", + "state": "item.fixedPrice", + "component": "vn-fixed-price", + "description": "Fixed prices", + "acl": ["buyer"] + }, + { + "url" : "/item-type?q", + "state": "item.itemType", + "component": "vn-item-type", + "description": "Item Type", + "acl": ["buyer"] + }, + { + "url": "/create", + "state": "item.itemType.create", + "component": "vn-item-type-create", + "description": "New itemType", + "acl": ["buyer"] + }, + { + "url": "/:id", + "state": "item.itemType.card", + "component": "vn-item-type-card", + "abstract": true, + "description": "Detail" + }, + { + "url": "/summary", + "state": "item.itemType.card.summary", + "component": "vn-item-type-summary", + "description": "Summary", + "params": { + "item-type": "$ctrl.itemType" + }, + "acl": ["buyer"] + }, + { + "url": "/basic-data", + "state": "item.itemType.card.basicData", + "component": "vn-item-type-basic-data", + "description": "Basic data", + "params": { + "item-type": "$ctrl.itemType" + }, + "acl": ["buyer"] } ] } diff --git a/modules/item/front/search-panel/index.html b/modules/item/front/search-panel/index.html new file mode 100644 index 000000000..33f141b18 --- /dev/null +++ b/modules/item/front/search-panel/index.html @@ -0,0 +1,181 @@ + +
+
+ + + + + + + + + +
{{name}}
+
+ {{category.name}} +
+
> +
+
+ + + + + {{name}}: {{nickname}} + + + + + Tags + + + + + + + + + + + + + + + + + More fields + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + + + + +
+
diff --git a/modules/item/front/search-panel/index.js b/modules/item/front/search-panel/index.js new file mode 100644 index 000000000..2448728be --- /dev/null +++ b/modules/item/front/search-panel/index.js @@ -0,0 +1,67 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +class Controller extends SearchPanel { + constructor($element, $) { + super($element, $); + let model = 'Item'; + let moreFields = ['id', 'description', 'name', 'isActive']; + + let properties; + let validations = window.validations; + + if (validations && validations[model]) + properties = validations[model].properties; + else + properties = {}; + + this.moreFields = []; + for (let field of moreFields) { + let prop = properties[field]; + this.moreFields.push({ + name: field, + label: prop ? prop.description : field, + type: prop ? prop.type : null + }); + } + } + + get filter() { + let filter = this.$.filter; + + for (let fieldFilter of this.fieldFilters) + filter[fieldFilter.name] = fieldFilter.value; + + return filter; + } + + set filter(value) { + if (!value) + value = {}; + if (!value.tags) + value.tags = [{}]; + + this.fieldFilters = []; + for (let field of this.moreFields) { + if (value[field.name] != undefined) { + this.fieldFilters.push({ + name: field.name, + value: value[field.name], + info: field + }); + } + } + + this.$.filter = value; + } + + removeField(index, field) { + this.fieldFilters.splice(index, 1); + delete this.$.filter[field]; + } +} + +ngModule.vnComponent('vnItemSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/search-panel/index.spec.js b/modules/item/front/search-panel/index.spec.js new file mode 100644 index 000000000..39b5b7aa5 --- /dev/null +++ b/modules/item/front/search-panel/index.spec.js @@ -0,0 +1,60 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemSearchPanel', () => { + let $element; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject($componentController => { + $element = angular.element(`
`); + controller = $componentController('vnItemSearchPanel', {$element}); + })); + + describe('filter() setter', () => { + it(`should set the tags property to the scope filter with an empty array`, () => { + const expectedFilter = { + tags: [{}] + }; + controller.filter = null; + + expect(controller.filter).toEqual(expectedFilter); + }); + + it(`should set the tags property to the scope filter with an array of tags`, () => { + const expectedFilter = { + description: 'My item', + tags: [{}] + }; + const expectedFieldFilter = [{ + info: { + label: 'description', + name: 'description', + type: null + }, + name: 'description', + value: 'My item' + }]; + controller.filter = { + description: 'My item' + }; + + expect(controller.filter).toEqual(expectedFilter); + expect(controller.fieldFilters).toEqual(expectedFieldFilter); + }); + }); + + describe('removeField()', () => { + it(`should remove the description property from the fieldFilters and from the scope filter`, () => { + const expectedFilter = {tags: [{}]}; + controller.filter = {description: 'My item'}; + + controller.removeField(0, 'description'); + + expect(controller.filter).toEqual(expectedFilter); + expect(controller.fieldFilters).toEqual([]); + }); + }); + }); +}); diff --git a/modules/item/front/search-panel/locale/es.yml b/modules/item/front/search-panel/locale/es.yml new file mode 100644 index 000000000..67a5200d7 --- /dev/null +++ b/modules/item/front/search-panel/locale/es.yml @@ -0,0 +1,8 @@ +Ink: Tinta +Origin: Origen +Producer: Productor +With visible: Con visible +Field: Campo +More fields: Más campos +Add field: Añadir campo +Remove field: Quitar campo \ No newline at end of file diff --git a/modules/item/front/summary/index.html b/modules/item/front/summary/index.html new file mode 100644 index 000000000..0e4cfc955 --- /dev/null +++ b/modules/item/front/summary/index.html @@ -0,0 +1,227 @@ + +
+ + + + {{$ctrl.item.id}} - {{$ctrl.summary.item.name}} +
+ + + + + +

Visible

+

{{$ctrl.summary.visible}}

+
+ +

Available

+

{{$ctrl.summary.available}}

+
+ +

+ + +

+
+
+
+ +

+ + Basic data + +

+

+ Basic data +

+ + + + + + + + + + + + + + + + + {{$ctrl.summary.item.itemType.worker.user.name}} + + + + + + +
+ +

+ + Other data + +

+

+ Other data +

+ + + + + + + + + + + + + + + + + + + + +
+ +

+ + Tags + +

+

+ Tags +

+ + +
+ +

+ + Description + +

+

+ Description +

+

+ {{$ctrl.summary.item.description}} +

+
+ +

+ + Tax + +

+

+ Tax +

+ + +
+ +

+ + Botanical + +

+

+ Botanical +

+ + + + +
+ +

+ + Barcode + +

+

+ Barcode +

+

+ {{barcode.code}} +

+
+
+
+ + diff --git a/modules/item/front/summary/index.js b/modules/item/front/summary/index.js new file mode 100644 index 000000000..e17a6a8c4 --- /dev/null +++ b/modules/item/front/summary/index.js @@ -0,0 +1,61 @@ +import ngModule from '../module'; +import Summary from 'salix/components/summary'; +import './style.scss'; + +class Controller extends Summary { + getSummary() { + this.$http.get(`Items/${this.item.id}/getSummary`).then(response => { + this.summary = response.data; + }); + + this.$http.get('ItemConfigs/findOne') + .then(res => { + if (this.card) this.card.warehouseFk = res.data.warehouseFk; + this.getWarehouseName(res.data.warehouseFk); + }); + } + + getWarehouseName(warehouseFk) { + const filter = { + where: {id: warehouseFk} + }; + this.$http.get('Warehouses/findOne', {filter}) + .then(res => { + this.warehouseText = this.$t('WarehouseFk', { + warehouseName: res.data.name + }); + }); + } + + $onChanges() { + if (this.item && this.item.id) + this.getSummary(); + } + + get isBuyer() { + return this.aclService.hasAny(['buyer']); + } + + get isReplenisher() { + return this.aclService.hasAny(['replenisher']); + } + + get isAdministrative() { + return this.aclService.hasAny(['administrative']); + } + + get isEmployee() { + return this.aclService.hasAny(['employee']); + } +} + +ngModule.vnComponent('vnItemSummary', { + template: require('./index.html'), + controller: Controller, + bindings: { + item: '<', + }, + require: { + card: '?^vnItemCard' + } +}); diff --git a/modules/item/front/summary/index.spec.js b/modules/item/front/summary/index.spec.js new file mode 100644 index 000000000..d7821bea0 --- /dev/null +++ b/modules/item/front/summary/index.spec.js @@ -0,0 +1,42 @@ +import './index.js'; + +describe('Item', () => { + describe('Component summary', () => { + let controller; + let $httpBackend; + let $scope; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, _$httpBackend_, $rootScope) => { + $httpBackend = _$httpBackend_; + $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnItemSummary', {$element, $scope}); + controller.item = {id: 1}; + controller.card = {}; + })); + + describe('getSummary()', () => { + it('should perform a query to set summary', () => { + let data = {id: 1, name: 'Gem of mind'}; + $httpBackend.expect('GET', `Items/1/getSummary`).respond(200, data); + $httpBackend.expect('GET', `ItemConfigs/findOne`).respond({}); + $httpBackend.expect('GET', `Warehouses/findOne`).respond({}); + controller.getSummary(); + $httpBackend.flush(); + + expect(controller.summary).toEqual(data); + }); + }); + + describe('$onChanges()', () => { + it('should call getSummary when item.id is defined', () => { + jest.spyOn(controller, 'getSummary'); + controller.$onChanges(); + + expect(controller.getSummary).toHaveBeenCalledWith(); + }); + }); + }); +}); diff --git a/modules/item/front/summary/locale/en.yml b/modules/item/front/summary/locale/en.yml new file mode 100644 index 000000000..0ec208720 --- /dev/null +++ b/modules/item/front/summary/locale/en.yml @@ -0,0 +1 @@ +WarehouseFk: Calculated on the warehouse of {{ warehouseName }} diff --git a/modules/item/front/summary/locale/es.yml b/modules/item/front/summary/locale/es.yml new file mode 100644 index 000000000..80988c491 --- /dev/null +++ b/modules/item/front/summary/locale/es.yml @@ -0,0 +1,5 @@ +Barcode: Códigos de barras +Other data: Otros datos +Go to the item: Ir al artículo +WarehouseFk: Calculado sobre el almacén de {{ warehouseName }} +Minimum sales quantity: Cantidad mínima de venta diff --git a/modules/item/front/summary/style.scss b/modules/item/front/summary/style.scss new file mode 100644 index 000000000..d047f3e36 --- /dev/null +++ b/modules/item/front/summary/style.scss @@ -0,0 +1,39 @@ +@import "./variables"; + +vn-item-summary { + p { + margin: 0; + } +} + +.item-state { + padding: 6px; + background-color: $color-main; + color: $color-font-dark; + + p { + font-size: .75rem; + text-align: center; + margin: 0; + + &:first-child { + text-transform: uppercase; + line-height: 1; + } + &:last-child { + font-size: 1.5rem; + font-weight: bold; + } + } + vn-one { + padding: 0; + + &:nth-child(1) { + border-right: 1px solid white; + } + + &:nth-child(2) { + border-right: 1px solid white; + } + } +} diff --git a/modules/item/front/tags/index.html b/modules/item/front/tags/index.html new file mode 100644 index 000000000..f9b5370fa --- /dev/null +++ b/modules/item/front/tags/index.html @@ -0,0 +1,82 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/item/front/tags/index.js b/modules/item/front/tags/index.js new file mode 100644 index 000000000..2c3b39d45 --- /dev/null +++ b/modules/item/front/tags/index.js @@ -0,0 +1,54 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +class Controller extends Section { + constructor($element, $) { + super($element, $); + this.include = { + relation: 'tag', + scope: { + fields: ['id', 'name', 'isFree', 'sourceTable'] + } + }; + } + + add() { + this.$.model.insert({ + itemFk: this.$params.id, + priority: this.getHighestPriority() + }); + } + + getHighestPriority() { + let max = 0; + this.$.model.data.forEach(tag => { + if (tag.priority > max) + max = tag.priority; + }); + return max + 1; + } + + onSubmit() { + const changes = this.$.model.getChanges(); + const data = { + creates: changes.creates, + deletes: changes.deletes, + updates: changes.updates, + maxPriority: this.getHighestPriority() + }; + this.$http.patch(`Tags/onSubmit`, data).then(() => { + this.$.model.refresh(); + this.$.watcher.notifySaved(); + this.$.watcher.updateOriginalData(); + this.card.reload(); + }); + } +} + +ngModule.vnComponent('vnItemTags', { + template: require('./index.html'), + controller: Controller, + require: { + card: '^vnItemCard' + } +}); diff --git a/modules/item/front/tags/index.spec.js b/modules/item/front/tags/index.spec.js new file mode 100644 index 000000000..8b4b8596b --- /dev/null +++ b/modules/item/front/tags/index.spec.js @@ -0,0 +1,34 @@ +import './index.js'; +import crudModel from 'core/mocks/crud-model'; + +describe('Item', () => { + describe('Component vnItemTags', () => { + let $scope; + let controller; + + beforeEach(ngModule('item')); + + beforeEach(inject(($componentController, $rootScope) => { + $scope = $rootScope.$new(); + $scope.model = crudModel; + const $element = angular.element(''); + controller = $componentController('vnItemTags', {$element, $scope}); + })); + + describe('getHighestPriority', () => { + it('should return the highest priority value + 1 from the array', () => { + $scope.model.data = [{priority: 1}, {priority: 2}, {priority: 1}]; + let result = controller.getHighestPriority(); + + expect(result).toEqual(3); + }); + + it('should return 1 when there is no priority defined', () => { + $scope.model.data = []; + let result = controller.getHighestPriority(); + + expect(result).toEqual(1); + }); + }); + }); +}); diff --git a/modules/item/front/tax/index.html b/modules/item/front/tax/index.html new file mode 100644 index 000000000..78858704f --- /dev/null +++ b/modules/item/front/tax/index.html @@ -0,0 +1,43 @@ + + + + +
+ + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/modules/item/front/tax/index.js b/modules/item/front/tax/index.js new file mode 100644 index 000000000..2d70414ab --- /dev/null +++ b/modules/item/front/tax/index.js @@ -0,0 +1,41 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + $onInit() { + this.getTaxes(); + } + + getTaxes() { + let filter = { + fields: ['id', 'countryFk', 'taxClassFk'], + include: [{ + relation: 'country', + scope: {fields: ['name']} + }] + }; + + let url = `Items/${this.$params.id}/taxes`; + this.$http.get(url, {params: {filter}}).then(json => { + this.taxes = json.data; + }); + } + + submit() { + let data = []; + for (let tax of this.taxes) + data.push({id: tax.id, taxClassFk: tax.taxClassFk}); + + this.$.watcher.check(); + let url = `Items/updateTaxes`; + this.$http.post(url, data).then(() => { + this.$.watcher.notifySaved(); + this.$.watcher.updateOriginalData(); + }); + } +} + +ngModule.vnComponent('vnItemTax', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/tax/index.spec.js b/modules/item/front/tax/index.spec.js new file mode 100644 index 000000000..9565a861d --- /dev/null +++ b/modules/item/front/tax/index.spec.js @@ -0,0 +1,64 @@ +import './index.js'; + +describe('Item', () => { + describe('Component vnItemTax', () => { + let $element; + let $stateParams; + let controller; + let $httpBackend; + + beforeEach(ngModule('item')); + + beforeEach(inject((_$httpBackend_, $rootScope, _$stateParams_, $compile) => { + $stateParams = _$stateParams_; + $stateParams.id = 1; + $httpBackend = _$httpBackend_; + + $httpBackend.whenRoute('GET', 'TaxClasses') + .respond([ + {id: 1, description: 'Reduced VAT', code: 'R'}, + {id: 2, description: 'General VAT', code: 'G'} + ]); + + $httpBackend.whenRoute('GET', 'Items/:id/taxes') + .respond([ + {id: 1, taxClassFk: 1} + ]); + + $element = $compile(` { + $element.remove(); + }); + + describe('getTaxes()', () => { + it('should perform a query to set the array of taxes into the controller', () => { + $httpBackend.flush(); + + expect(controller.taxes[0].id).toEqual(1); + expect(controller.taxes[0].taxClassFk).toEqual(1); + }); + }); + + describe('submit()', () => { + it('should perform a post to update taxes', () => { + jest.spyOn(controller.$.watcher, 'notifySaved'); + jest.spyOn(controller.$.watcher, 'updateOriginalData'); + + controller.$onInit(); + $httpBackend.flush(); + + controller.taxes.push({id: 3, description: 'General VAT', code: 'G'}); + + $httpBackend.whenPOST(`Items/updateTaxes`).respond(true); + controller.submit(); + $httpBackend.flush(); + + expect(controller.$.watcher.notifySaved).toHaveBeenCalledWith(); + expect(controller.$.watcher.updateOriginalData).toHaveBeenCalledWith(); + }); + }); + }); +}); diff --git a/modules/item/front/waste/index/index.html b/modules/item/front/waste/index/index.html new file mode 100644 index 000000000..7fb3b870e --- /dev/null +++ b/modules/item/front/waste/index/index.html @@ -0,0 +1,2 @@ + + diff --git a/modules/item/front/waste/index/index.js b/modules/item/front/waste/index/index.js new file mode 100644 index 000000000..86d9d3778 --- /dev/null +++ b/modules/item/front/waste/index/index.js @@ -0,0 +1,19 @@ +import ngModule from '../../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + } + + async $onInit() { + this.$state.go('item.index'); + window.location.href = 'https://grafana.verdnatura.es/d/TTNXQAxVk'; + } +} + +ngModule.vnComponent('vnItemWasteIndex', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/item/front/waste/index/style.scss b/modules/item/front/waste/index/style.scss new file mode 100644 index 000000000..36fac3311 --- /dev/null +++ b/modules/item/front/waste/index/style.scss @@ -0,0 +1,27 @@ +@import "variables"; +@import "effects"; + +vn-item-waste-index, +vn-item-waste-detail { + .header { + padding: 12px 0 5px 0; + background-color: $color-bg; + font-size: 1.2rem; + margin-bottom: 10px; + } + + vn-table vn-th.waste-family, + vn-table vn-td.waste-family { + max-width: 64px; + width: 64px + } + + .hidden { + display: none; + } + + .arrow.hidden { + display: block; + transform: rotate(180deg); + } +} diff --git a/modules/item/front/waste/locale/es.yml b/modules/item/front/waste/locale/es.yml new file mode 100644 index 000000000..b9cd33dec --- /dev/null +++ b/modules/item/front/waste/locale/es.yml @@ -0,0 +1,4 @@ +Family: Familia +Percentage: Porcentaje +Dwindle: Mermas +Minimize/Maximize: Minimizar/Maximizar \ No newline at end of file diff --git a/modules/order/back/methods/order/filter.js b/modules/order/back/methods/order/filter.js index ccc149958..2aeb1aac5 100644 --- a/modules/order/back/methods/order/filter.js +++ b/modules/order/back/methods/order/filter.js @@ -79,6 +79,7 @@ module.exports = Self => { const models = Self.app.models; const conn = Self.dataSource.connector; const myOptions = {}; + const userId = ctx.req.accessToken.userId; if (typeof options == 'object') Object.assign(myOptions, options); diff --git a/modules/supplier/back/methods/supplier/getWithPackaging.js b/modules/supplier/back/methods/supplier/getWithPackaging.js index 07e563412..41b080964 100644 --- a/modules/supplier/back/methods/supplier/getWithPackaging.js +++ b/modules/supplier/back/methods/supplier/getWithPackaging.js @@ -15,7 +15,7 @@ module.exports = Self => { Self.getWithPackaging = async options => { const models = Self.app.models; const myOptions = {}; - const oneYearAgo = new Date(); + const oneYearAgo = Date.vnNew(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); if (typeof options == 'object') diff --git a/modules/supplier/back/methods/supplier/specs/getWithPackaging.spec.js b/modules/supplier/back/methods/supplier/specs/getWithPackaging.spec.js index bd30d7437..b6a780a10 100644 --- a/modules/supplier/back/methods/supplier/specs/getWithPackaging.spec.js +++ b/modules/supplier/back/methods/supplier/specs/getWithPackaging.spec.js @@ -1,26 +1,12 @@ const {models} = require('vn-loopback/server/server'); describe('Supplier getWithPackaging()', () => { + beforeAll.mockLoopBackContext(); 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); diff --git a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js index 4d9f98cc3..688cbb634 100644 --- a/modules/worker/back/methods/worker-time-control-mail/checkInbox.js +++ b/modules/worker/back/methods/worker-time-control-mail/checkInbox.js @@ -16,7 +16,7 @@ module.exports = Self => { }); Self.checkInbox = async() => { - let imapConfig = await Self.app.models.WorkerTimeControlParams.findOne(); + let imapConfig = await Self.app.models.WorkerTimeControlConfig.findOne(); let imap = new Imap({ user: imapConfig.mailUser, password: imapConfig.mailPass, diff --git a/modules/worker/back/methods/worker/createAbsence.js b/modules/worker/back/methods/worker/createAbsence.js index 0397886cf..419659083 100644 --- a/modules/worker/back/methods/worker/createAbsence.js +++ b/modules/worker/back/methods/worker/createAbsence.js @@ -58,9 +58,8 @@ module.exports = Self => { if (!isSubordinate || (isSubordinate && userId == id && !isTeamBoss)) throw new UserError(`You don't have enough privileges`); - const labour = await models.WorkerLabour.findById(args.businessFk, { - include: {relation: 'department'} - }, myOptions); + const labour = await models.WorkerLabour.findById(args.businessFk, + {fields: ['started', 'ended', 'businessFk']}, myOptions); if (args.dated < labour.started || (labour.ended != null && args.dated > labour.ended)) throw new UserError(`The contract was not active during the selected date`); @@ -87,7 +86,8 @@ module.exports = Self => { `SELECT COUNT(*) halfHolidayCounter FROM vn.calendar c JOIN vn.business b ON b.id = c.businessFk - WHERE c.dayOffTypeFk = 6 + JOIN vn.absenceType at ON at.id = c.dayOffTypeFk + WHERE at.code = 'halfHoliday' AND b.workerFk = ? AND c.dated BETWEEN util.firstDayOfYear(?) AND LAST_DAY(DATE_ADD(?, INTERVAL 12 - MONTH(?) MONTH))`, [id, date, now, now]); @@ -119,25 +119,23 @@ module.exports = Self => { dated: args.dated }, myOptions); - const department = labour.department(); - if (department && department.notificationEmail) { - const absenceType = await models.AbsenceType.findById(args.absenceTypeId, null, myOptions); - const account = await models.VnUser.findById(userId, null, myOptions); - const subordinated = await models.VnUser.findById(id, null, myOptions); - const url = await Self.app.models.Url.getUrl(); - const body = $t('Created absence', { - author: account.nickname, - employee: subordinated.nickname, - absenceType: absenceType.name, - dated: formatDate(args.dated), - workerUrl: `${url}worker/${id}/calendar` - }); - await models.Mail.create({ - subject: $t('Absence change notification on the labour calendar'), - body: body, - receiver: department.notificationEmail - }, myOptions); - } + const account = await models.VnUser.findById(userId, null, myOptions); + const subordinated = await models.VnUser.findById(id, null, myOptions); + const worker = await models.Worker.findById(subordinated.id, null, myOptions); + const departmentBoss = await models.VnUser.findById(worker.bossFk, null, myOptions); + const url = await Self.app.models.Url.getUrl(); + const body = $t('Created absence', { + author: account.nickname, + employee: subordinated.nickname, + absenceType: absenceType.name, + dated: formatDate(args.dated), + workerUrl: `${url}worker/${id}/calendar` + }); + await models.Mail.create({ + subject: $t('Absence change notification on the labour calendar'), + body: body, + receiver: departmentBoss.email + }, myOptions); if (tx) await tx.commit(); diff --git a/modules/worker/back/model-config.json b/modules/worker/back/model-config.json index dc078f4d0..10e55d956 100644 --- a/modules/worker/back/model-config.json +++ b/modules/worker/back/model-config.json @@ -113,9 +113,6 @@ "WorkerTimeControlConfig": { "dataSource": "vn" }, - "WorkerTimeControlParams": { - "dataSource": "vn" - }, "WorkerTimeControlMail": { "dataSource": "vn" }, diff --git a/modules/worker/back/models/worker-time-control-config.json b/modules/worker/back/models/worker-time-control-config.json index b96e2ae3b..53f1a14ea 100644 --- a/modules/worker/back/models/worker-time-control-config.json +++ b/modules/worker/back/models/worker-time-control-config.json @@ -11,6 +11,15 @@ "id": true, "type": "number" }, + "mailPass": { + "type": "string" + }, + "mailHost": { + "type": "string" + }, + "mailUser": { + "type": "string" + }, "breakTime": { "type": "number" }, @@ -23,5 +32,13 @@ "teleworkingStartBreakTime": { "type": "number" } - } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] } diff --git a/modules/worker/back/models/worker-time-control-params.json b/modules/worker/back/models/worker-time-control-params.json deleted file mode 100644 index 14cabbfb0..000000000 --- a/modules/worker/back/models/worker-time-control-params.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "WorkerTimeControlParams", - "description": "imap config", - "base": "VnModel", - "options": { - "mysql": { - "table": "workerTimeControlParams" - } - }, - "properties": { - "mailHost": { - "type": "string" - }, - "mailUser": { - "type": "string" - }, - "mailPass": { - "type": "string" - }, - "mailSuccessFolder": { - "type": "string" - }, - "mailErrorFolder": { - "type": "string" - } - }, - "acls": [ - { - "accessType": "READ", - "principalType": "ROLE", - "principalId": "$everyone", - "permission": "ALLOW" - } - ] -} diff --git a/myt.config.yml b/myt.config.yml index 25f94f1bd..92936869e 100755 --- a/myt.config.yml +++ b/myt.config.yml @@ -385,7 +385,6 @@ localFixtures: - workerTimeControl - workerTimeControlConfig - workerTimeControlMail - - workerTimeControlParams - zone - zoneAgencyMode - zoneClosure