diff --git a/back/methods/chat/send.js b/back/methods/chat/send.js index b0a9431c79..53e5da7cc0 100644 --- a/back/methods/chat/send.js +++ b/back/methods/chat/send.js @@ -32,6 +32,8 @@ module.exports = Self => { if (sender.name != recipient) return sendMessage(sender, to, message); + + return false; }; async function sendMessage(sender, channel, message) { diff --git a/back/methods/chat/sendCheckingPresence.js b/back/methods/chat/sendCheckingPresence.js index 70b4da58f4..b2a3ca725f 100644 --- a/back/methods/chat/sendCheckingPresence.js +++ b/back/methods/chat/sendCheckingPresence.js @@ -36,7 +36,7 @@ module.exports = Self => { relation: 'department' } }); - const department = workerDepartment.department(); + const department = workerDepartment && workerDepartment.department(); const channelName = department.chatName; if (channelName) diff --git a/back/methods/chat/spec/send.spec.js b/back/methods/chat/spec/send.spec.js index b2585a9a15..56f2a9c275 100644 --- a/back/methods/chat/spec/send.spec.js +++ b/back/methods/chat/spec/send.spec.js @@ -9,10 +9,10 @@ describe('chat send()', () => { expect(response.message).toEqual('Fake notification sent'); }); - it('should not return a response', async() => { + it('should retrun false as response', async() => { let ctx = {req: {accessToken: {userId: 18}}}; let response = await app.models.Chat.send(ctx, '@salesPerson', 'I changed something'); - expect(response).toBeUndefined(); + expect(response).toBeFalsy(); }); }); diff --git a/db/changes/10141-zoneDoCalc/00-ticket.sql b/db/changes/10141-zoneDoCalc/00-ticket.sql index c116e51399..a756a11afe 100644 --- a/db/changes/10141-zoneDoCalc/00-ticket.sql +++ b/db/changes/10141-zoneDoCalc/00-ticket.sql @@ -3,9 +3,9 @@ ADD COLUMN `zonePrice` DECIMAL(10,2) NULL DEFAULT NULL AFTER `collectionFk`, ADD COLUMN `zoneBonus` DECIMAL(10,2) NULL DEFAULT NULL AFTER `zonePrice`, ADD COLUMN `zoneClosure` TIME NULL AFTER `zoneBonus`; -CREATE TABLE vn.`zoneCalcTicket` ( +CREATE TABLE `vn`.`zoneCalcTicket` ( `zoneFk` int(11) NOT NULL PRIMARY KEY, - CONSTRAINT `zoneCalcTicketfk_1` FOREIGN KEY (`zoneFk`) REFERENCES `zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE + CONSTRAINT `zoneCalcTicketfk_1` FOREIGN KEY (`zoneFk`) REFERENCES `vn`.`zone` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; DROP EVENT IF EXISTS vn.`zone_doCalc`; diff --git a/db/changes/10141-zoneDoCalc/01-zoneClosure_recalc.sql b/db/changes/10141-zoneDoCalc/01-zoneClosure_recalc.sql deleted file mode 100644 index f015eb894b..0000000000 --- a/db/changes/10141-zoneDoCalc/01-zoneClosure_recalc.sql +++ /dev/null @@ -1 +0,0 @@ -USE `vn`; diff --git a/db/changes/10141-zoneDoCalc/02-insertPastTickets.sql b/db/changes/10141-zoneDoCalc/02-insertPastTickets.sql new file mode 100644 index 0000000000..4314e5d7d9 --- /dev/null +++ b/db/changes/10141-zoneDoCalc/02-insertPastTickets.sql @@ -0,0 +1,62 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_doCalcInitialize`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_doCalcInitialize`() +proc: BEGIN +/** + * Initialize ticket + */ + DECLARE vDone BOOL; + DECLARE vTicketFk INT; + DECLARE vLanded DATE; + DECLARE vZoneFk INT; + + DECLARE cCur CURSOR FOR + SELECT t.id, t.landed, t.zoneFk + FROM ticket t + WHERE (zonePrice IS NULL OR zoneBonus IS NULL OR zoneClosure IS NULL) + AND landed >= '2019-01-01' AND shipped >= '2019-01-01' + GROUP BY landed, zoneFk; + + DECLARE CONTINUE HANDLER FOR NOT FOUND + SET vDone = TRUE; + + OPEN cCur; + + myLoop: LOOP + SET vDone = FALSE; + FETCH cCur INTO vTicketFk, vLanded, vZoneFk; + + IF vDone THEN + LEAVE myLoop; + END IF; + + DROP TEMPORARY TABLE IF EXISTS tmp.zone; + CREATE TEMPORARY TABLE tmp.zone + (INDEX (id)) + ENGINE = MEMORY + SELECT vZoneFk id; + + CALL zone_getOptionsForLanding(vLanded, TRUE); + + UPDATE ticket t + LEFT JOIN tmp.zoneOption zo ON TRUE + SET zonePrice = zo.price, zoneBonus = zo.bonus, zoneClosure = zo.`hour` + WHERE t.zoneFk = vZoneFk AND landed = vLanded; + + UPDATE ticket t + LEFT JOIN vn.zone z ON z.id = t.zoneFk + SET zonePrice = z.price, zoneBonus = z.bonus, zoneClosure = z.`hour` + WHERE t.zonePrice IS NULL AND z.id = vZoneFk + AND landed >= '2019-01-01' AND shipped >= '2019-01-01'; + + END LOOP; + + CLOSE cCur; + + DELETE FROM zoneCalcTicket; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10141-zoneDoCalc/03-getOptionsForLanding.sql b/db/changes/10141-zoneDoCalc/03-getOptionsForLanding.sql new file mode 100644 index 0000000000..e0f5f9a48b --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-getOptionsForLanding.sql @@ -0,0 +1,66 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_getOptionsForLanding`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_getOptionsForLanding`(vLanded DATE, vShowExpiredZones BOOLEAN) +BEGIN +/** + * Gets computed options for the passed zones and delivery date. + * + * @table tmp.zones(id) The zones ids + * @param vLanded The delivery date + * @return tmp.zoneOption The computed options + */ + DROP TEMPORARY TABLE IF EXISTS tmp.zoneOption; + CREATE TEMPORARY TABLE tmp.zoneOption + ENGINE = MEMORY + SELECT + zoneFk, + `hour`, + travelingDays, + price, + bonus, + TIMESTAMPADD(DAY, -travelingDays, vLanded) shipped + FROM ( + SELECT t.id zoneFk, + TIME(IFNULL(e.`hour`, z.`hour`)) `hour`, + IFNULL(e.travelingDays, z.travelingDays) travelingDays, + IFNULL(e.price, z.price) price, + IFNULL(e.bonus, z.bonus) bonus + FROM tmp.zone t + JOIN zone z ON z.id = t.id + JOIN zoneEvent e ON e.zoneFk = t.id + WHERE ( + e.`type` = 'day' + AND e.dated = vLanded + ) OR ( + e.`type` != 'day' + AND e.weekDays & (1 << WEEKDAY(vLanded)) + AND (e.`started` IS NULL OR vLanded >= e.`started`) + AND (e.`ended` IS NULL OR vLanded <= e.`ended`) + ) + ORDER BY + zoneFk, + CASE + WHEN e.`type` = 'day' + THEN 1 + WHEN e.`type` = 'range' + THEN 2 + ELSE 3 + END + ) t + GROUP BY zoneFk; + + DELETE t FROM tmp.zoneOption t + JOIN zoneExclusion e + ON e.zoneFk = t.zoneFk AND e.`dated` = vLanded; + + IF NOT vShowExpiredZones THEN + DELETE FROM tmp.zoneOption + WHERE shipped < CURDATE() + OR (shipped = CURDATE() AND CURTIME() > `hour`); + END IF; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10141-zoneDoCalc/03-rutasAnalyze.sql b/db/changes/10141-zoneDoCalc/03-rutasAnalyze.sql new file mode 100644 index 0000000000..313f2f797a --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-rutasAnalyze.sql @@ -0,0 +1,171 @@ +USE `vn`; +DROP procedure IF EXISTS `rutasAnalyze`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `rutasAnalyze`(vYear INT, vMonth INT) +BEGIN + +/* Analiza los costes de las rutas de reparto y lo almacena en la tabla Rutas_Master +* +* PAK 15/4/2019 +*/ + + DELETE FROM bi.rutasBoard + WHERE year = vYear AND month = vMonth; + + -- Rellenamos la tabla con los datos de las rutas VOLUMETRICAS, especialmente con los bultos "virtuales" + INSERT INTO bi.rutasBoard(year, + month, + warehouse_id, + Id_Ruta, + Id_Agencia, + km, + Dia, + Fecha, + Bultos, + Matricula, + Tipo, + Terceros) + SELECT YEAR(r.created), + MONTH(r.created), + GREATEST(1,a.warehouseFk), + r.id, + r.agencyModeFk, + r.kmEnd - r.kmStart, + DAYNAME(r.created), + r.created, + SUM(sv.volume / ebv.m3), + v.numberPlate, + IF(ISNULL(`r`.`cost`), 'P', 'A'), + r.cost + FROM vn.route r + JOIN vn.ticket t ON t.routeFk = r.id + LEFT JOIN vn.zone z ON z.id = t.zoneFk + LEFT JOIN vn.agencyMode am ON am.id = r.agencyModeFk + LEFT JOIN vn.agency a ON a.id = am.agencyFk + LEFT JOIN vn.vehicle v ON v.id = r.vehicleFk + JOIN vn.saleVolume sv ON sv.ticketFk = t.id + JOIN vn.expeditionBoxVol ebv ON ebv.boxFk = 71 + WHERE YEAR(r.created) = vYear AND MONTH(r.created) = vMonth + AND z.isVolumetric + GROUP BY r.id; + + -- Rellenamos la tabla con los datos de las rutas NO VOLUMETRICAS, especialmente con los bultos "virtuales" + INSERT INTO bi.rutasBoard(year, + month, + warehouse_id, + Id_Ruta, + Id_Agencia, + km, + Dia, + Fecha, + Bultos, + Matricula, + Tipo, + Terceros) + SELECT YEAR(r.created), + MONTH(r.created), + GREATEST(1,a.warehouseFk), + r.id, + r.agencyModeFk, + r.kmEnd - r.kmStart, + DAYNAME(r.created), + r.created, + SUM(t.packages), + v.numberPlate, + IF(ISNULL(`r`.`cost`), 'P', 'A'), + r.cost + FROM vn.route r + JOIN vn.ticket t ON t.routeFk = r.id + LEFT JOIN vn.zone z ON z.id = t.zoneFk + LEFT JOIN vn.agencyMode am ON am.id = r.agencyModeFk + LEFT JOIN vn.agency a ON a.id = am.agencyFk + LEFT JOIN vn.vehicle v ON v.id = r.vehicleFk + WHERE YEAR(r.created) = vYear AND MONTH(r.created) = vMonth + AND z.isVolumetric = FALSE + GROUP BY r.id + ON DUPLICATE KEY UPDATE Bultos = Bultos + VALUES(Bultos); + + -- Coste REAL de cada bulto "virtual", de acuerdo con el valor apuntado a mano en la ruta + UPDATE bi.rutasBoard r + INNER JOIN vn2008.Rutas_Master rm ON rm.año = r.year AND rm.mes = r.month AND rm.warehouse_id = r.warehouse_id + SET r.coste_bulto = IF(r.Tipo ='A', r.Terceros, r.km * rm.coste_km ) / r.Bultos + WHERE r.Bultos > 0 + AND rm.año = vYear + AND rm.mes = vMonth; + + -- Coste PRACTICO de cada bulto, de acuerdo con los componentes de tipo AGENCIA en cada linea de venta + UPDATE bi.rutasBoard r + JOIN ( + SELECT t.routeFk, sum(s.quantity * sc.value) practicoTotal + FROM vn.route r + JOIN vn.time tm ON tm.dated = r.created + JOIN vn.ticket t ON t.routeFk = r.id + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.saleComponent sc ON sc.saleFk = s.id + JOIN vn.`component` c ON c.id = sc.componentFk + JOIN vn.componentType ct ON ct.id = c.typeFk + WHERE ct.type = 'agencia' + AND tm.year = vYear + AND tm.month = vMonth + GROUP BY r.id + ) sub ON sub.routeFk = r.Id_Ruta + SET r.practico = sub.practicoTotal / r.Bultos; + + -- Coste TEORICO de una caja "virtual" para cada ruta, teniendo en cuenta que hay carros, pallets, etc + UPDATE bi.rutasBoard r + JOIN ( + SELECT t.routeFk, + SUM(t.zonePrice/ ebv.ratio)/ count(*) AS BultoTeoricoMedio + FROM vn.ticket t + JOIN vn.route r ON r.id = t.routeFk + JOIN vn.time tm ON tm.dated = r.created + JOIN vn.expedition e ON e.ticketFk = t.id + JOIN vn.expeditionBoxVol ebv ON ebv.boxFk = e.isBox + JOIN vn.address ad ON ad.id = t.addressFk + JOIN vn.client c ON c.id = ad.clientFk + LEFT JOIN vn.zone z ON z.id = t.zoneFk + WHERE tm.year = vYear + AND tm.month = vMonth + AND z.isVolumetric = FALSE + GROUP BY t.routeFk) sub ON r.Id_Ruta = sub.routeFk + SET r.teorico = sub.BultoTeoricoMedio; + + -- Coste VOLUMETRICO TEORICO de una caja "virtual" para cada ruta + UPDATE bi.rutasBoard r + JOIN ( + SELECT t.routeFk, + SUM(freight) AS BultoTeoricoMedio + FROM vn.ticket t + JOIN vn.route r ON r.id = t.routeFk + JOIN vn.time tm ON tm.dated = r.created + JOIN vn.saleVolume sf ON sf.ticketFk = t.id + JOIN vn.client c ON c.id = t.clientFk + JOIN vn.zone z ON z.id = t.zoneFk + WHERE tm.year = vYear + AND tm.month = vMonth + AND z.isVolumetric != FALSE + GROUP BY t.routeFk) sub ON r.Id_Ruta = sub.routeFk + SET r.teorico = sub.BultoTeoricoMedio / r.Bultos; + + -- La diferencia entre el teorico y el practico se deberia de cobrar en greuges, cada noche + UPDATE bi.rutasBoard r + JOIN ( + SELECT t.routeFk, + Sum(g.amount) AS greuge + FROM vn.ticket t + JOIN vn.route r ON r.id = t.routeFk + JOIN vn.time tm ON tm.dated = r.created + JOIN vn.greuge g ON g.ticketFk = t.id + JOIN vn.greugeType gt ON gt.id = g.greugeTypeFk + WHERE tm.year = vYear + AND tm.month = vMonth + AND gt.name = 'Diferencia portes' + GROUP BY t.routeFk) sub ON r.Id_Ruta = sub.routeFk + SET r.greuge = sub.greuge / r.Bultos; + +END$$ + +DELIMITER ; + diff --git a/db/changes/10141-zoneDoCalc/03-saleVolume.sql b/db/changes/10141-zoneDoCalc/03-saleVolume.sql new file mode 100644 index 0000000000..2ded49a8d3 --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-saleVolume.sql @@ -0,0 +1,26 @@ +USE `vn`; +CREATE + OR REPLACE ALGORITHM = UNDEFINED + DEFINER = `root`@`%` + SQL SECURITY DEFINER +VIEW `saleVolume` AS + SELECT + `s`.`ticketFk` AS `ticketFk`, + `s`.`id` AS `saleFk`, + IFNULL(ROUND(((((`i`.`compression` * (GREATEST(`i`.`density`, 167) / 167)) * `ic`.`cm3`) * `s`.`quantity`) / 1000), + 2), + 0) AS `litros`, + `t`.`routeFk` AS `routeFk`, + `t`.`shipped` AS `shipped`, + (((`s`.`quantity` * `ic`.`cm3`) * `i`.`compression`) / 1000000) AS `volume`, + ((((`s`.`quantity` * `ic`.`cm3`) * `i`.`compression`) * (GREATEST(`i`.`density`, 167) / 167)) / 1000000) AS `physicalWeight`, + (((`s`.`quantity` * `ic`.`cm3`) * `i`.`density`) / 1000000) AS `weight`, + (((`s`.`quantity` * `ic`.`cm3`) * `i`.`compression`) / 1000000) AS `physicalVolume`, + ((((`s`.`quantity` * `ic`.`cm3`) * `t`.`zonePrice`) * `i`.`compression`) / `cb`.`volume`) AS `freight` + FROM + ((((`sale` `s` + JOIN `item` `i` ON ((`i`.`id` = `s`.`itemFk`))) + JOIN `ticket` `t` ON ((`t`.`id` = `s`.`ticketFk`))) + JOIN `packaging` `cb` ON ((`cb`.`id` = '94'))) + JOIN `itemCost` `ic` ON (((`ic`.`itemFk` = `s`.`itemFk`) + AND (`ic`.`warehouseFk` = `t`.`warehouseFk`)))); diff --git a/db/changes/10141-zoneDoCalc/03-viewSaleFreight__.sql b/db/changes/10141-zoneDoCalc/03-viewSaleFreight__.sql new file mode 100644 index 0000000000..903c8b48ac --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-viewSaleFreight__.sql @@ -0,0 +1,24 @@ +DROP VIEW IF EXISTS `vn`.`saleFreight` ; +USE `vn`; +CREATE + OR REPLACE ALGORITHM = UNDEFINED + DEFINER = `root`@`%` + SQL SECURITY DEFINER +VIEW `saleFreight__` AS + SELECT + `s`.`ticketFk` AS `ticketFk`, + `t`.`clientFk` AS `clientFk`, + `t`.`routeFk` AS `routeFk`, + `s`.`id` AS `saleFk`, + `t`.`zoneFk` AS `zoneFk`, + `t`.`companyFk` AS `companyFk`, + `t`.`shipped` AS `shipped`, + `t`.`zonePrice` AS `price`, + ((((`s`.`quantity` * `r`.`cm3`) * `t`.`zonePrice`) * `i`.`compression`) / `cb`.`volume`) AS `freight` + FROM + ((((`vn`.`sale` `s` + JOIN `vn`.`item` `i` ON ((`i`.`id` = `s`.`itemFk`))) + JOIN `vn`.`ticket` `t` ON ((`t`.`id` = `s`.`ticketFk`))) + JOIN `vn`.`packaging` `cb` ON ((`cb`.`id` = '94'))) + JOIN `bi`.`rotacion` `r` ON (((`r`.`Id_Article` = `s`.`itemFk`) + AND (`r`.`warehouse_id` = `t`.`warehouseFk`)))); diff --git a/db/changes/10141-zoneDoCalc/03-zone_geShippedWarehouse.sql b/db/changes/10141-zoneDoCalc/03-zone_geShippedWarehouse.sql new file mode 100644 index 0000000000..14d5d8cd9a --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-zone_geShippedWarehouse.sql @@ -0,0 +1,41 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_getShippedWarehouse`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_getShippedWarehouse`(vLanded DATE, vAddressFk INT, vAgencyModeFk INT) +BEGIN +/** + * Devuelve la mínima fecha de envío para cada warehouse + * + * @param vLanded La fecha de recepcion + * @param vAddressFk Id del consignatario + * @param vAgencyModeFk Id de la agencia + * @return tmp.zoneGetShipped + */ + + CALL zone_getFromGeo(address_getGeo(vAddressFk)); + CALL zone_getOptionsForLanding(vLanded,TRUE); + + DROP TEMPORARY TABLE IF EXISTS tmp.zoneGetShipped; + CREATE TEMPORARY TABLE tmp.zoneGetShipped + ENGINE = MEMORY + SELECT * FROM ( + SELECT zo.zoneFk, + TIMESTAMPADD(DAY,-zo.travelingDays, vLanded) shipped, + zo.`hour`, + zw.warehouseFk, + z.agencyModeFk + FROM tmp.zoneOption zo + JOIN zoneWarehouse zw ON zw.zoneFk = zo.zoneFk + JOIN zone z ON z.id = zo.zoneFk + WHERE z.agencyModeFk = vAgencyModeFk + ORDER BY shipped) t + GROUP BY warehouseFk; + + DROP TEMPORARY TABLE + tmp.zone, + tmp.zoneOption; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10141-zoneDoCalc/03-zone_getAgency.sql b/db/changes/10141-zoneDoCalc/03-zone_getAgency.sql new file mode 100644 index 0000000000..b2837d43cd --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-zone_getAgency.sql @@ -0,0 +1,42 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_getAgency`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_getAgency`(vAddress INT, vLanded DATE) +BEGIN +/** + * Devuelve el listado de agencias disponibles para la fecha + * y dirección pasadas. + * + * @param vAddress Id de dirección de envío, %NULL si es recogida + * @param vLanded Fecha de recogida + * @select Listado de agencias disponibles + */ + + CALL zone_getFromGeo(address_getGeo(vAddress)); + CALL zone_getOptionsForLanding(vLanded, FALSE); + + DROP TEMPORARY TABLE IF EXISTS tmp.zoneGetAgency; + CREATE TEMPORARY TABLE tmp.zoneGetAgency + (INDEX (agencyModeFk)) ENGINE = MEMORY + SELECT am.name agencyMode, + am.description, + z.agencyModeFk, + am.deliveryMethodFk, + TIMESTAMPADD(DAY,-zo.travelingDays, vLanded) shipped, + TRUE isIncluded, + zo.zoneFk + FROM tmp.zoneOption zo + JOIN zone z ON z.id = zo.zoneFk + JOIN agencyMode am ON am.id = z.agencyModeFk + GROUP BY agencyModeFk; + + DROP TEMPORARY TABLE + tmp.zone, + tmp.zoneOption; + +END$$ + +DELIMITER ; + diff --git a/db/changes/10141-zoneDoCalc/03-zone_getAvailable.sql b/db/changes/10141-zoneDoCalc/03-zone_getAvailable.sql new file mode 100644 index 0000000000..2ef1a1ae96 --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-zone_getAvailable.sql @@ -0,0 +1,18 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_getAvailable`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_getAvailable`(vAddress INT, vLanded DATE) +BEGIN + CALL zone_getFromGeo(address_getGeo(vAddress)); + CALL zone_getOptionsForLanding(vLanded, FALSE); + + SELECT * FROM tmp.zoneOption; + + DROP TEMPORARY TABLE + tmp.zone, + tmp.zoneOption; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10141-zoneDoCalc/03-zone_getWarehouse.sql b/db/changes/10141-zoneDoCalc/03-zone_getWarehouse.sql new file mode 100644 index 0000000000..c1cea8b136 --- /dev/null +++ b/db/changes/10141-zoneDoCalc/03-zone_getWarehouse.sql @@ -0,0 +1,41 @@ +USE `vn`; +DROP procedure IF EXISTS `zone_getWarehouse`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `zone_getWarehouse`(vAddress INT, vLanded DATE, vWarehouse INT) +BEGIN +/** + * Devuelve el listado de agencias disponibles para la fecha, + * dirección y almacén pasados. + * + * @param vAddress + * @param vWarehouse warehouse + * @param vLanded Fecha de recogida + * @select Listado de agencias disponibles + */ + + CALL zone_getFromGeo(address_getGeo(vAddress)); + CALL zone_getOptionsForLanding(vLanded, FALSE); + + SELECT am.id agencyModeFk, + am.name agencyMode, + am.description, + am.deliveryMethodFk, + TIMESTAMPADD(DAY, -zo.travelingDays, vLanded) shipped, + zw.warehouseFk, + z.id zoneFk + FROM tmp.zoneOption zo + JOIN zone z ON z.id = zo.zoneFk + JOIN agencyMode am ON am.id = z.agencyModeFk + JOIN zoneWarehouse zw ON zw.zoneFk = zo.zoneFk + WHERE zw.warehouseFk + GROUP BY z.agencyModeFk + ORDER BY agencyMode; + + DROP TEMPORARY TABLE + tmp.zone, + tmp.zoneOption; +END$$ + +DELIMITER ; \ No newline at end of file diff --git a/db/changes/10160-postValentineDay/00-ACL.sql b/db/changes/10160-postValentineDay/00-ACL.sql new file mode 100644 index 0000000000..5b6301e3df --- /dev/null +++ b/db/changes/10160-postValentineDay/00-ACL.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES ('Intrastat', '*', '*', 'ALLOW', 'ROLE', 'buyer'); diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index f3dd36976b..f7d5d94f1c 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -684,8 +684,14 @@ INSERT INTO `vn`.`taxCode`(`id`, `dated`, `code`, `taxTypeFk`, `rate`, `equaliza INSERT INTO `vn`.`taxClass`(`id`, `description`, `code`) VALUES - (1, 'Reduced VAT','R'), + (1, 'Reduced VAT', 'R'), (2, 'General VAT', 'G'); + +INSERT INTO `vn`.`taxClassCode`(`taxClassFk`, `effectived`, `taxCodeFk`) + VALUES + (1, CURDATE(), 1), + (1, CURDATE(), 21), + (2, CURDATE(), 2); INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`) VALUES @@ -1123,17 +1129,19 @@ INSERT INTO `vn`.`travel`(`id`,`shipped`, `landed`, `warehouseInFk`, `warehouseO (4, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 2, 1, 50.00, 500, 'fourth travel', 0), (5, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 3, 2, 1, 50.00, 500, 'fifth travel', 1), (6, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 4, 2, 1, 50.00, 500, 'sixth travel', 1), - (7, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 5, 2, 1, 50.00, 500, 'seventh travel', 1); + (7, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 5, 2, 1, 50.00, 500, 'seventh travel', 1), + (8, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 5, 2, 1, 50.00, 500, 'eight travel', 1); -INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `companyFk`, `ref`, `notes`, `evaNotes`) +INSERT INTO `vn`.`entry`(`id`, `supplierFk`, `created`, `travelFk`, `companyFk`, `ref`,`isInventory`, `isRaid`, `notes`, `evaNotes`) VALUES - (1, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 442, 'Movement 1', 'this is the note one', 'observation one'), - (2, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 2, 442, 'Movement 2', 'this is the note two', 'observation two'), - (3, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 3, 442, 'Movement 3', 'this is the note three', 'observation three'), - (4, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 2, 69, 'Movement 4', 'this is the note four', 'observation four'), - (5, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 5, 442, 'Movement 5', 'this is the note five', 'observation five'), - (6, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 6, 442, 'Movement 6', 'this is the note six', 'observation six'), - (7, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 7, 442, 'Movement 7', 'this is the note seven', 'observation seven'); + (1, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 442, 'Movement 1', 0, 0, '', ''), + (2, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 2, 442, 'Movement 2', 0, 0, 'this is the note two', 'observation two'), + (3, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 3, 442, 'Movement 3', 0, 0, 'this is the note three', 'observation three'), + (4, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 2, 69, 'Movement 4', 0, 0, 'this is the note four', 'observation four'), + (5, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 5, 442, 'Movement 5', 0, 0, 'this is the note five', 'observation five'), + (6, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 6, 442, 'Movement 6', 0, 0, 'this is the note six', 'observation six'), + (7, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 7, 442, 'Movement 7', 0, 0, 'this is the note seven', 'observation seven'), + (8, 2, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 7, 442, 'Movement 8', 1, 1, '', ''); INSERT INTO `vn`.`claimRatio`(`clientFk`, `yearSale`, `claimAmount`, `claimingRate`, `priceIncreasing`, `packingRate`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 16a9bb3991..1e6a30bb5e 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -272,6 +272,10 @@ export default { longName: 'vn-textfield[ng-model="$ctrl.item.longName"]', isActiveCheckbox: 'vn-check[label="Active"]', priceInKgCheckbox: 'vn-check[label="Price in kg"]', + newIntrastatButton: 'vn-item-basic-data vn-icon-button[vn-tooltip="New intrastat"] > button', + newIntrastatId: '.vn-dialog.shown vn-input-number[ng-model="$ctrl.newIntrastat.intrastatId"]', + newIntrastatDescription: '.vn-dialog.shown vn-textfield[ng-model="$ctrl.newIntrastat.description"]', + acceptIntrastatButton: '.vn-dialog.shown button[response="accept"]', submitBasicDataButton: `button[type=submit]` }, itemTags: { @@ -627,8 +631,8 @@ export default { plantRealmButton: 'vn-order-catalog > vn-side-menu vn-icon[icon="icon-plant"]', type: 'vn-autocomplete[data="$ctrl.itemTypes"]', itemId: 'vn-order-catalog > vn-side-menu vn-textfield[ng-model="$ctrl.itemId"]', - itemTagValue: 'vn-order-catalog > vn-side-menu vn-textfield[ng-model="$ctrl.value"]', - openTagSearch: 'vn-order-catalog > vn-side-menu > div > vn-vertical > vn-textfield[ng-model="$ctrl.value"] .append i', + itemTagValue: 'vn-order-catalog > vn-side-menu vn-datalist[ng-model="$ctrl.value"]', + openTagSearch: 'vn-order-catalog > vn-side-menu > div > vn-vertical > vn-datalist[ng-model="$ctrl.value"] .append i', tag: 'vn-order-catalog-search-panel vn-autocomplete[ng-model="filter.tagFk"]', tagValue: 'vn-order-catalog-search-panel vn-textfield[ng-model="filter.value"]', searchTagButton: 'vn-order-catalog-search-panel button[type=submit]', diff --git a/e2e/paths/04-item/02_basic_data.spec.js b/e2e/paths/04-item/02_basic_data.spec.js index 2c8a8a7a10..64827ed9ba 100644 --- a/e2e/paths/04-item/02_basic_data.spec.js +++ b/e2e/paths/04-item/02_basic_data.spec.js @@ -39,6 +39,26 @@ describe('Item Edit basic data path', () => { expect(result).toEqual('Data saved!'); }, 20000); + it(`should create a new intrastat`, async() => { + await page.waitToClick(selectors.itemBasicData.newIntrastatButton); + await page.write(selectors.itemBasicData.newIntrastatId, '588420239'); + await page.write(selectors.itemBasicData.newIntrastatDescription, 'Tropical Flowers'); + await page.waitToClick(selectors.itemBasicData.acceptIntrastatButton); + await page.waitForTextInField(selectors.itemBasicData.intrastat, 'Tropical Flowers'); + let newcode = await page.waitToGetProperty(selectors.itemBasicData.intrastat, 'value'); + + expect(newcode).toEqual('588420239 Tropical Flowers'); + }); + + it(`should save with the new intrastat`, async() => { + await page.waitFor(250); + await page.waitForTextInField(selectors.itemBasicData.intrastat, 'Tropical Flowers'); + await page.waitToClick(selectors.itemBasicData.submitBasicDataButton); + const result = await page.waitForLastSnackbar(); + + expect(result).toEqual('Data saved!'); + }); + it(`should confirm the item name was edited`, async() => { await page.reloadSection('item.card.basicData'); const result = await page.waitToGetProperty(selectors.itemBasicData.name, 'value'); @@ -53,11 +73,11 @@ describe('Item Edit basic data path', () => { expect(result).toEqual('Anthurium'); }); - it(`should confirm the item intrastad was edited`, async() => { + it(`should confirm the item intrastat was edited`, async() => { const result = await page .waitToGetProperty(selectors.itemBasicData.intrastat, 'value'); - expect(result).toEqual('5080000 Coral y materiales similares'); + expect(result).toEqual('588420239 Tropical Flowers'); }); it(`should confirm the item relevancy was edited`, async() => { diff --git a/front/core/components/datalist/index.js b/front/core/components/datalist/index.js index d5feb18932..bf3cab9a18 100644 --- a/front/core/components/datalist/index.js +++ b/front/core/components/datalist/index.js @@ -3,6 +3,7 @@ import ArrayModel from '../array-model/array-model'; import CrudModel from '../crud-model/crud-model'; import {mergeWhere} from 'vn-loopback/util/filter'; import Textfield from '../textfield/textfield'; +import './style.scss'; export default class Datalist extends Textfield { constructor($element, $scope, $compile, $transclude) { @@ -12,7 +13,6 @@ export default class Datalist extends Textfield { this._selection = null; this.buildInput('text'); - this.input.setAttribute('autocomplete', 'off'); } @@ -157,8 +157,6 @@ export default class Datalist extends Textfield { this.destroyList(); } else this.buildList(); - - this.emit('select', {selection}); }); } diff --git a/front/core/components/datalist/style.scss b/front/core/components/datalist/style.scss new file mode 100755 index 0000000000..db4ed2bb09 --- /dev/null +++ b/front/core/components/datalist/style.scss @@ -0,0 +1,7 @@ +@import "effects"; + +vn-datalist { + input::-webkit-calendar-picker-indicator { + display: none + } +} \ No newline at end of file diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 23717ba8f3..49cd0f1719 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -61,7 +61,7 @@ "MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} (#{{itemId}}) for the ticket id [#{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} (#{{clientId}})]({{{url}}}) to *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} (#{{clientId}})]({{{url}}})", - "MESSAGE_CLAIM_ITEM_REGULARIZE": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to {{nickname}} coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})", + "Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} (#{{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [#{{ticketId}}]({{{ticketUrl}}})", "Customs agent is required for a non UEE member": "Customs agent is required for a non UEE member", "Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member", "Client checked as validated despite of duplication": "Client checked as validated despite of duplication from client id {{clientId}}" diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 90d4f8793d..7577c5349a 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -124,7 +124,8 @@ "MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} (#{{itemId}}) para el ticket id [#{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} (#{{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} (#{{clientId}})]({{{url}}})", - "MESSAGE_CLAIM_ITEM_REGULARIZE": "Envio *{{quantity}}* unidades de [{{concept}} (#{{itemId}})]({{{itemUrl}}}) a {{nickname}} provenientes del ticket id [#{{ticketId}}]({{{ticketUrl}}})", + "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} (#{{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [#{{ticketId}}]({{{ticketUrl}}})", "Client checked as validated despite of duplication": "Cliente comprobado a pesar de que existe el cliente id {{clientId}}", - "ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto" + "ORDER_ROW_UNAVAILABLE": "No hay disponibilidad de este producto", + "Distance must be lesser than 1000": "La distancia debe ser inferior a 1000" } \ No newline at end of file diff --git a/modules/claim/back/methods/claim/regularizeClaim.js b/modules/claim/back/methods/claim/regularizeClaim.js index 8a4f6dc957..adf1623c66 100644 --- a/modules/claim/back/methods/claim/regularizeClaim.js +++ b/modules/claim/back/methods/claim/regularizeClaim.js @@ -37,27 +37,43 @@ module.exports = Self => { for (let i = 0; i < claimEnds.length; i++) { const claimEnd = claimEnds[i]; const destination = claimEnd.claimDestination(); - const addressFk = destination && destination.addressFk; + const sale = await getSale(claimEnd.saleFk, options); + const addressId = destination && destination.addressFk; - if (!addressFk) continue; + let address; + if (addressId) + address = await models.Address.findById(addressId, null, options); + + const salesPerson = sale.ticket().client().salesPerson(); + if (salesPerson) { + const nickname = address && address.nickname || destination.description; + const origin = ctx.req.headers.origin; + const message = $t('Sent units from ticket', { + quantity: sale.quantity, + concept: sale.concept, + itemId: sale.itemFk, + ticketId: sale.ticketFk, + nickname: nickname, + ticketUrl: `${origin}/#!/ticket/${sale.ticketFk}/summary`, + itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + + if (!address) continue; - let sale = await getSale(claimEnd.saleFk, options); let ticketFk = await getTicketId({ - addressFk: addressFk, + addressFk: addressId, companyFk: sale.ticket().companyFk, warehouseFk: sale.ticket().warehouseFk }, options); - let address = await models.Address.findOne({ - where: {id: addressFk} - }, options); - if (!ticketFk) { ticketFk = await createTicket(ctx, { clientId: address.clientFk, warehouseId: sale.ticket().warehouseFk, companyId: sale.ticket().companyFk, - addressId: addressFk + addressId: addressId }, options); } @@ -69,21 +85,6 @@ module.exports = Self => { price: sale.price, discount: 100 }, options); - - const salesPerson = sale.ticket().client().salesPerson(); - if (salesPerson) { - const origin = ctx.req.headers.origin; - const message = $t('MESSAGE_CLAIM_ITEM_REGULARIZE', { - quantity: sale.quantity, - concept: sale.concept, - itemId: sale.itemFk, - ticketId: sale.ticketFk, - nickname: address.nickname, - ticketUrl: `${origin}/#!/ticket/${sale.ticketFk}/summary`, - itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` - }); - await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); - } } let claim = await Self.findById(params.claimFk, null, options); diff --git a/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js b/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js index fd3fb3c3de..c7aa1cf0a9 100644 --- a/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js +++ b/modules/claim/back/methods/claim/specs/regularizeClaim.spec.js @@ -5,6 +5,7 @@ describe('regularizeClaim()', () => { const pendentState = 1; const resolvedState = 3; const trashDestination = 2; + const okDestination = 1; const trashAddress = 12; let claimEnds = []; let trashTicket; @@ -21,15 +22,20 @@ describe('regularizeClaim()', () => { done(); }); - it('should change claim state to resolved', async() => { - const ctx = {req: { - accessToken: {userId: 18}, - headers: {origin: 'http://localhost'}} + it('should send a chat message with value "Trash" and then change claim state to resolved', async() => { + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'http://localhost'} + } }; - ctx.req.__ = value => { - return value; + ctx.req.__ = (value, params) => { + return params.nickname; }; + let params = {claimFk: claimFk}; + const chatModel = app.models.Chat; + spyOn(chatModel, 'sendCheckingPresence').and.callThrough(); claimEnds = await app.models.ClaimEnd.importTicketSales(ctx, { claimFk: claimFk, @@ -49,5 +55,32 @@ describe('regularizeClaim()', () => { expect(trashTicket.addressFk).toEqual(trashAddress); expect(claimBefore.claimStateFk).toEqual(pendentState); expect(claimAfter.claimStateFk).toEqual(resolvedState); + expect(chatModel.sendCheckingPresence).toHaveBeenCalledWith(ctx, 18, 'Trash'); + expect(chatModel.sendCheckingPresence).toHaveBeenCalledTimes(4); + }); + + it('should send a chat message with value "Bueno" and then change claim state to resolved', async() => { + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'http://localhost'} + } + }; + ctx.req.__ = (value, params) => { + return params.nickname; + }; + + let params = {claimFk: claimFk}; + const chatModel = app.models.Chat; + spyOn(chatModel, 'sendCheckingPresence').and.callThrough(); + + claimEnds.forEach(async claimEnd => { + claimEnd.updateAttributes({claimDestinationFk: okDestination}); + }); + + await app.models.Claim.regularizeClaim(ctx, params); + + expect(chatModel.sendCheckingPresence).toHaveBeenCalledWith(ctx, 18, 'Bueno'); + expect(chatModel.sendCheckingPresence).toHaveBeenCalledTimes(4); }); }); diff --git a/modules/client/front/balance/index/index.html b/modules/client/front/balance/index/index.html index f42998bbd9..8cdb5c9712 100644 --- a/modules/client/front/balance/index/index.html +++ b/modules/client/front/balance/index/index.html @@ -85,9 +85,9 @@ {{::balance.bankFk}} - {{::balance.debit | currency: 'EUR':2}} - {{::balance.credit | currency: 'EUR':2}} - {{balance.balance | currency: 'EUR':2}} + {{::balance.debit | currency: 'EUR':2}} + {{::balance.credit | currency: 'EUR':2}} + {{balance.balance | currency: 'EUR':2}} { expect(expectedBalances[2].balance).toEqual(213.24); }); }); + + describe('balances() setter', () => { + it('should set the balances data and not call the getBalances() method', () => { + spyOn(controller, 'getBalances'); + controller.$.riskModel.data = null; + controller.balances = [{ + id: 1, + debit: 1000, + credit: null + }, { + id: 2, + debit: null, + credit: 500 + }, { + id: 3, + debit: null, + credit: 300 + }]; + + expect(controller.balances).toBeDefined(); + expect(controller.getBalances).not.toHaveBeenCalledWith(); + }); + + it('should set the balances data and then call the getBalances() method', () => { + spyOn(controller, 'getBalances'); + controller.balances = [{ + id: 1, + debit: 1000, + credit: null + }, { + id: 2, + debit: null, + credit: 500 + }, { + id: 3, + debit: null, + credit: 300 + }]; + + expect(controller.balances).toBeDefined(); + expect(controller.getBalances).toHaveBeenCalledWith(); + }); + }); }); }); diff --git a/modules/client/front/sample/create/index.html b/modules/client/front/sample/create/index.html index 6a87d55b68..cd3412868b 100644 --- a/modules/client/front/sample/create/index.html +++ b/modules/client/front/sample/create/index.html @@ -15,7 +15,8 @@ + ng-model="$ctrl.clientSample.recipient" + info="Its only used when sample is sent"> @@ -30,7 +31,7 @@ { - this.$.showPreview.show(); - let dialog = document.body.querySelector('div.vn-dialog'); - let body = dialog.querySelector('tpl-body'); - let scroll = dialog.querySelector('div:first-child'); - - body.innerHTML = res.data; - scroll.scrollTop = 0; - }); + this.clientSample.companyId = value; } onSubmit() { @@ -73,28 +42,49 @@ class Controller extends Component { ); } + showPreview() { + this.send(true, res => { + this.$.showPreview.show(); + const dialog = document.body.querySelector('div.vn-dialog'); + const body = dialog.querySelector('tpl-body'); + const scroll = dialog.querySelector('div:first-child'); + + body.innerHTML = res.data; + scroll.scrollTop = 0; + }); + } + sendSample() { - let sampleType = this.$.sampleType.selection; - let params = { + this.send(false, () => { + this.vnApp.showSuccess(this.$translate.instant('Notification sent!')); + this.$state.go('client.card.sample.index'); + }); + } + + send(isPreview, cb) { + const sampleType = this.$.sampleType.selection; + const params = { clientId: this.$params.id, recipient: this.clientSample.recipient }; + if (!params.recipient) + return this.vnApp.showError(this.$translate.instant('Email cannot be blank')); + if (!sampleType) return this.vnApp.showError(this.$translate.instant('Choose a sample')); - if (sampleType.hasCompany && !this.clientSample.companyFk) + if (sampleType.hasCompany && !this.clientSample.companyId) return this.vnApp.showError(this.$translate.instant('Choose a company')); if (sampleType.hasCompany) - params.companyId = this.clientSample.companyFk; + params.companyId = this.clientSample.companyId; + + if (isPreview) params.isPreview = true; const serializedParams = this.$httpParamSerializer(params); const query = `email/${sampleType.code}?${serializedParams}`; - this.$http.get(query).then(res => { - this.vnApp.showSuccess(this.$translate.instant('Notification sent!')); - this.$state.go('client.card.sample.index'); - }); + this.$http.get(query).then(cb); } } Controller.$inject = ['$element', '$scope', 'vnApp', '$httpParamSerializer', 'vnConfig']; diff --git a/modules/client/front/sample/create/index.spec.js b/modules/client/front/sample/create/index.spec.js index efcda54012..da9a557f1e 100644 --- a/modules/client/front/sample/create/index.spec.js +++ b/modules/client/front/sample/create/index.spec.js @@ -40,84 +40,16 @@ describe('Client', () => { $httpParamSerializer = _$httpParamSerializer_; $element = angular.element(''); controller = $componentController('vnClientSampleCreate', {$element, $scope}); + const element = document.createElement('div'); + document.body.querySelector = () => { + return { + querySelector: () => { + return element; + } + }; + }; })); - describe('showPreview()', () => { - it(`should perform a query (GET) and open a sample preview`, () => { - spyOn(controller.$.showPreview, 'show'); - const element = document.createElement('div'); - document.body.querySelector = () => { - return { - querySelector: () => { - return element; - } - }; - }; - - controller.$.sampleType.selection = { - hasCompany: false, - code: 'MyReport' - }; - - controller.clientSample = { - clientFk: 101 - }; - - let event = {preventDefault: () => {}}; - - const params = { - clientId: 101, - isPreview: true - }; - const serializedParams = $httpParamSerializer(params); - - $httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true); - $httpBackend.expect('GET', `email/MyReport?${serializedParams}`); - controller.showPreview(event); - $httpBackend.flush(); - - expect(controller.$.showPreview.show).toHaveBeenCalledWith(); - }); - - it(`should perform a query (GET) with companyFk param and open a sample preview`, () => { - spyOn(controller.$.showPreview, 'show'); - const element = document.createElement('div'); - document.body.querySelector = () => { - return { - querySelector: () => { - return element; - } - }; - }; - - controller.$.sampleType.selection = { - hasCompany: true, - code: 'MyReport' - }; - - controller.clientSample = { - clientFk: 101, - companyFk: 442 - }; - - let event = {preventDefault: () => {}}; - - const params = { - clientId: 101, - companyId: 442, - isPreview: true - }; - const serializedParams = $httpParamSerializer(params); - - $httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true); - $httpBackend.expect('GET', `email/MyReport?${serializedParams}`); - controller.showPreview(event); - $httpBackend.flush(); - - expect(controller.$.showPreview.show).toHaveBeenCalledWith(); - }); - }); - describe('onSubmit()', () => { it(`should call sendSample() method`, () => { spyOn(controller, 'sendSample'); @@ -127,55 +59,113 @@ describe('Client', () => { }); }); - describe('sendSample()', () => { - it(`should perform a query (GET) and call go() method`, () => { - spyOn(controller.$state, 'go'); + describe('send()', () => { + it(`should not perform an HTTP query if no recipient is specified`, () => { + spyOn(controller.$http, 'get'); controller.$.sampleType.selection = { hasCompany: false, code: 'MyReport' }; - controller.clientSample = { - clientFk: 101 - }; - - const params = { clientId: 101 }; - const serializedParams = $httpParamSerializer(params); - $httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true); - $httpBackend.expect('GET', `email/MyReport?${serializedParams}`); - controller.sendSample(); - $httpBackend.flush(); + controller.send(false, () => {}); - expect(controller.$state.go).toHaveBeenCalledWith('client.card.sample.index'); + expect(controller.$http.get).not.toHaveBeenCalled(); }); - it(`should perform a query (GET) with companyFk param and call go() method`, () => { - spyOn(controller.$state, 'go'); + it(`should not perform an HTTP query if no sample is specified`, () => { + spyOn(controller.$http, 'get'); + + controller.$.sampleType.selection = null; + controller.clientSample = { + clientId: 101, + recipient: 'client@email.com' + }; + + controller.send(false, () => {}); + + expect(controller.$http.get).not.toHaveBeenCalled(); + }); + + it(`should not perform an HTTP query if company is required and not specified`, () => { + spyOn(controller.$http, 'get'); controller.$.sampleType.selection = { hasCompany: true, code: 'MyReport' }; - controller.clientSample = { - clientFk: 101, - companyFk: 442 + clientId: 101, + recipient: 'client@email.com' }; - const params = { + controller.send(false, () => {}); + + expect(controller.$http.get).not.toHaveBeenCalled(); + }); + + it(`should perform an HTTP query without passing companyId param`, () => { + controller.$.sampleType.selection = { + hasCompany: false, + code: 'MyReport' + }; + controller.clientSample = { clientId: 101, + recipient: 'client@email.com' + }; + + const serializedParams = $httpParamSerializer(controller.clientSample); + $httpBackend.expect('GET', `email/MyReport?${serializedParams}`).respond(true); + controller.send(false, () => {}); + $httpBackend.flush(); + }); + + it(`should perform an HTTP query passing companyId param`, () => { + controller.$.sampleType.selection = { + hasCompany: true, + code: 'MyReport' + }; + controller.clientSample = { + clientId: 101, + recipient: 'client@email.com', companyId: 442 }; - const serializedParams = $httpParamSerializer(params); - $httpBackend.when('GET', `email/MyReport?${serializedParams}`).respond(true); - $httpBackend.expect('GET', `email/MyReport?${serializedParams}`); - controller.sendSample(); + const serializedParams = $httpParamSerializer(controller.clientSample); + $httpBackend.expect('GET', `email/MyReport?${serializedParams}`).respond(true); + controller.send(false, () => {}); $httpBackend.flush(); + }); + }); + + describe('showPreview()', () => { + it(`should open a sample preview`, () => { + spyOn(controller.$.showPreview, 'show'); + + controller.send = (isPreview, cb) => { + cb({ + data: '
' + }); + }; + controller.showPreview(); + + expect(controller.$.showPreview.show).toHaveBeenCalledWith(); + }); + }); + + describe('sendSample()', () => { + it(`should perform a query (GET) and call go() method`, () => { + spyOn(controller.$state, 'go'); + + controller.send = (isPreview, cb) => { + cb({ + data: true + }); + }; + controller.sendSample(); expect(controller.$state.go).toHaveBeenCalledWith('client.card.sample.index'); }); diff --git a/modules/client/front/sample/create/locale/es.yml b/modules/client/front/sample/create/locale/es.yml index d1ef82c0ee..6828e3e489 100644 --- a/modules/client/front/sample/create/locale/es.yml +++ b/modules/client/front/sample/create/locale/es.yml @@ -1,3 +1,5 @@ Choose a sample: Selecciona una plantilla Choose a company: Selecciona una empresa -Recipient: Destinatario \ No newline at end of file +Email cannot be blank: Debes introducir un email +Recipient: Destinatario +Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla \ No newline at end of file diff --git a/modules/client/front/sms/index.html b/modules/client/front/sms/index.html index facbb76948..fb2f1dfff2 100644 --- a/modules/client/front/sms/index.html +++ b/modules/client/front/sms/index.html @@ -13,20 +13,27 @@ rule>
- + - {{'Characters remaining' | translate}}: {{$ctrl.charactersRemaining()}} + {{'Characters remaining' | translate}}: + + {{$ctrl.charactersRemaining()}} + diff --git a/modules/client/front/sms/index.js b/modules/client/front/sms/index.js index 851ce1a66e..c7d89e7170 100644 --- a/modules/client/front/sms/index.js +++ b/modules/client/front/sms/index.js @@ -17,13 +17,12 @@ class Controller extends Component { } charactersRemaining() { - let elementMaxLength; - let textAreaLength; const element = this.$scope.message; + const value = element.input.value; - textAreaLength = element.input.textLength; - elementMaxLength = element.maxlength; - return elementMaxLength - textAreaLength; + const maxLength = 160; + const textAreaLength = new Blob([value]).size; + return maxLength - textAreaLength; } onResponse(response) { @@ -33,6 +32,8 @@ class Controller extends Component { throw new Error(`The destination can't be empty`); if (!this.sms.message) throw new Error(`The message can't be empty`); + if (this.charactersRemaining() < 0) + throw new Error(`The message it's too long`); this.$http.post(`Clients/${this.$params.id}/sendSms`, this.sms).then(res => { this.vnApp.showMessage(this.$translate.instant('SMS sent!')); diff --git a/modules/client/front/sms/index.spec.js b/modules/client/front/sms/index.spec.js index 26a597c174..03d9cbf1d9 100644 --- a/modules/client/front/sms/index.spec.js +++ b/modules/client/front/sms/index.spec.js @@ -15,6 +15,11 @@ describe('Client', () => { controller = $componentController('vnClientSms', {$element, $scope}); controller.client = {id: 101}; controller.$params = {id: 101}; + controller.$scope.message = { + input: { + value: 'My SMS' + } + }; })); describe('onResponse()', () => { @@ -56,14 +61,13 @@ describe('Client', () => { it('should return the characters remaining in a element', () => { controller.$scope.message = { input: { - textLength: 50 - }, - maxlength: 150 + value: 'My message 0€' + } }; let result = controller.charactersRemaining(); - expect(result).toEqual(100); + expect(result).toEqual(145); }); }); }); diff --git a/modules/client/front/sms/locale/es.yml b/modules/client/front/sms/locale/es.yml index 4438e4fce4..64c3fcca67 100644 --- a/modules/client/front/sms/locale/es.yml +++ b/modules/client/front/sms/locale/es.yml @@ -4,4 +4,6 @@ Message: Mensaje SMS sent!: ¡SMS enviado! Characters remaining: Carácteres restantes The destination can't be empty: El destinatario no puede estar vacio -The message can't be empty: El mensaje no puede estar vacio \ No newline at end of file +The message can't be empty: El mensaje no puede estar vacio +The message it's too long: El mensaje es demasiado largo +Special characters like accents counts as a multiple: Carácteres especiales como los acentos cuentan como varios \ No newline at end of file diff --git a/modules/entry/back/methods/entry/filter.js b/modules/entry/back/methods/entry/filter.js index 8cbe5e15e6..93e9558a96 100644 --- a/modules/entry/back/methods/entry/filter.js +++ b/modules/entry/back/methods/entry/filter.js @@ -127,6 +127,7 @@ module.exports = Self => { e.companyFk, e.gestDocFk, e.invoiceInFk, + t.landed, s.name AS supplierName, co.code AS companyCode, cu.code AS currencyCode diff --git a/modules/entry/back/methods/entry/specs/filter.spec.js b/modules/entry/back/methods/entry/specs/filter.spec.js index 9b935d831a..25d2da0b42 100644 --- a/modules/entry/back/methods/entry/specs/filter.spec.js +++ b/modules/entry/back/methods/entry/specs/filter.spec.js @@ -23,7 +23,7 @@ describe('Entry filter()', () => { let result = await app.models.Entry.filter(ctx); - expect(result.length).toEqual(7); + expect(result.length).toEqual(8); }); it('should return the entry matching the supplier', async() => { @@ -35,7 +35,7 @@ describe('Entry filter()', () => { let result = await app.models.Entry.filter(ctx); - expect(result.length).toEqual(5); + expect(result.length).toEqual(6); }); it('should return the entry matching the company', async() => { @@ -47,7 +47,7 @@ describe('Entry filter()', () => { let result = await app.models.Entry.filter(ctx); - expect(result.length).toEqual(6); + expect(result.length).toEqual(7); }); it('should return the entries matching isBooked', async() => { diff --git a/modules/entry/front/index/index.html b/modules/entry/front/index/index.html index 8ddd4d3a39..f0f5404899 100644 --- a/modules/entry/front/index/index.html +++ b/modules/entry/front/index/index.html @@ -16,45 +16,68 @@ + Id - Created - Travel - Notes + Landed Reference - Booked - Is inventory - Confirmed - Ordered - Is raid - Commission Supplier Currency Company + Booked + Confirmed + Ordered + Notes + + + + + + {{::entry.id}} - {{::entry.created | date:'dd/MM/yyyy'}} - {{::entry.travelFk}} - {{::entry.notes}} + + + {{::entry.landed | date:'dd/MM/yyyy'}} + + {{::entry.ref}} - - - - - - {{::entry.commission}} {{::entry.supplierName}} {{::entry.currencyCode}} {{::entry.companyCode}} + + + + + + + + + diff --git a/modules/entry/front/index/index.js b/modules/entry/front/index/index.js index ec78c06dfa..53d2f45e07 100644 --- a/modules/entry/front/index/index.js +++ b/modules/entry/front/index/index.js @@ -1,5 +1,5 @@ import ngModule from '../module'; - +import './style.scss'; export default class Controller { constructor($scope) { this.$ = $scope; @@ -11,6 +11,16 @@ export default class Controller { else this.$.model.clear(); } + + showTravelDescriptor(event, travelFk) { + if (event.defaultPrevented) return; + event.preventDefault(); + event.stopPropagation(); + + this.selectedTravel = travelFk; + this.$.travelDescriptor.parent = event.target; + this.$.travelDescriptor.show(); + } } Controller.$inject = ['$scope']; diff --git a/modules/entry/front/index/locale/es.yml b/modules/entry/front/index/locale/es.yml new file mode 100644 index 0000000000..8ef9b2c7af --- /dev/null +++ b/modules/entry/front/index/locale/es.yml @@ -0,0 +1,15 @@ +Inventory entry: Es inventario +Virtual entry: Es una redada +Supplier: Proveedor +Currency: Moneda +Company: Empresa +Confirmed: Confirmada +Ordered: Pedida +Is raid: Redada +Commission: Comisión +Landed: F. entrega +Reference: Referencia +Created: Creado +Booked: Facturado +Is inventory: Inventario +Notes: Notas \ No newline at end of file diff --git a/modules/entry/front/index/style.scss b/modules/entry/front/index/style.scss new file mode 100644 index 0000000000..ab759d2ccb --- /dev/null +++ b/modules/entry/front/index/style.scss @@ -0,0 +1,5 @@ +@import "variables"; + + vn-icon[icon=insert_drive_file]{ + color: $color-font-secondary; + } diff --git a/modules/entry/front/locale/es.yml b/modules/entry/front/locale/es.yml index 214be93d4d..3de6c59a83 100644 --- a/modules/entry/front/locale/es.yml +++ b/modules/entry/front/locale/es.yml @@ -1,15 +1,3 @@ #Ordenar alfabeticamente -Reference: Referencia -Created: Creado -Booked: Facturado -Is inventory: Inventario -Notes: Notas -Travel: Envío -Supplier: Proveedor -Currency: Moneda -Company: Empresa -Confirmed: Confirmada -Ordered: Pedida -Is raid: Redada -Commission: Comisión + # Sections diff --git a/modules/entry/front/routes.json b/modules/entry/front/routes.json index f23ff02bfb..bd1ace3f25 100644 --- a/modules/entry/front/routes.json +++ b/modules/entry/front/routes.json @@ -2,6 +2,7 @@ "module": "entry", "name": "Entries", "icon": "icon-entry", + "dependencies": ["travel"], "validations": true, "menus": { "main": [ diff --git a/modules/item/back/methods/item/createIntrastat.js b/modules/item/back/methods/item/createIntrastat.js new file mode 100644 index 0000000000..1a88d16e25 --- /dev/null +++ b/modules/item/back/methods/item/createIntrastat.js @@ -0,0 +1,61 @@ +let UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethod('createIntrastat', { + description: 'Creates a new item intrastat', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'number', + required: true, + description: 'The item id', + http: {source: 'path'} + }, + { + arg: 'intrastatId', + type: 'number', + required: true + }, + { + arg: 'description', + type: 'string', + required: true + }], + returns: { + type: 'boolean', + root: true + }, + http: { + path: `/:id/createIntrastat`, + verb: 'PATCH' + } + }); + + Self.createIntrastat = async(id, intrastatId, description) => { + const models = Self.app.models; + const country = await models.Country.findOne({ + where: {code: 'ES'} + }); + + const itemTaxCountry = await models.ItemTaxCountry.findOne({ + where: { + itemFk: id, + countryFk: country.id + }, + order: 'effectived DESC' + }); + const taxClassCode = await models.TaxClassCode.findOne({ + where: { + taxClassFk: itemTaxCountry.taxClassFk + }, + order: 'effectived DESC' + }); + + return models.Intrastat.create({ + id: intrastatId, + description: description, + taxClassFk: itemTaxCountry.taxClassFk, + taxCodeFk: taxClassCode.taxCodeFk + }); + }; +}; diff --git a/modules/item/back/methods/item/getLastEntries.js b/modules/item/back/methods/item/getLastEntries.js index bab808cef1..a438afcb63 100644 --- a/modules/item/back/methods/item/getLastEntries.js +++ b/modules/item/back/methods/item/getLastEntries.js @@ -22,6 +22,7 @@ module.exports = Self => { let where = filter.where; let query = `CALL vn.itemLastEntries(?, ?)`; let [lastEntries] = await Self.rawSql(query, [where.itemFk, where.date]); + return lastEntries; }; }; diff --git a/modules/item/back/methods/item/specs/createIntrastat.spec.js b/modules/item/back/methods/item/specs/createIntrastat.spec.js new file mode 100644 index 0000000000..fb10de858a --- /dev/null +++ b/modules/item/back/methods/item/specs/createIntrastat.spec.js @@ -0,0 +1,22 @@ +const app = require('vn-loopback/server/server'); + +describe('createIntrastat()', () => { + let newIntrastat; + + afterAll(async done => { + await app.models.Intrastat.destroyById(newIntrastat.id); + + done(); + }); + + it('should create a new intrastat', async() => { + const intrastatId = 588420239; + const description = 'Tropical Flowers'; + const itemId = 9; + newIntrastat = await app.models.Item.createIntrastat(itemId, intrastatId, description); + + expect(newIntrastat.description).toEqual(description); + expect(newIntrastat.taxClassFk).toEqual(1); + expect(newIntrastat.taxCodeFk).toEqual(21); + }); +}); diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json index db8eed9d56..d8ec5914a1 100644 --- a/modules/item/back/model-config.json +++ b/modules/item/back/model-config.json @@ -62,6 +62,9 @@ "TaxClass": { "dataSource": "vn" }, + "TaxClassCode": { + "dataSource": "vn" + }, "TaxCode": { "dataSource": "vn" }, diff --git a/modules/item/back/models/item.js b/modules/item/back/models/item.js index 6c221e94df..01061ce999 100644 --- a/modules/item/back/models/item.js +++ b/modules/item/back/models/item.js @@ -12,6 +12,7 @@ module.exports = Self => { require('../methods/item/getVisibleAvailable')(Self); require('../methods/item/new')(Self); require('../methods/item/getWasteDetail')(Self); + require('../methods/item/createIntrastat')(Self); Self.validatesPresenceOf('originFk', {message: 'Cannot be blank'}); diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json index d8d1cb64d1..dbaa3a4090 100644 --- a/modules/item/back/models/item.json +++ b/modules/item/back/models/item.json @@ -1,13 +1,13 @@ { "name": "Item", - "base": "Loggable", - "log": { - "model":"ItemLog", + "base": "Loggable", + "log": { + "model": "ItemLog", "showField": "id" - }, + }, "options": { "mysql": { - "table": "item" + "table": "item" } }, "properties": { @@ -125,55 +125,55 @@ } }, "relations": { - "itemType": { - "type": "belongsTo", - "model": "ItemType", - "foreignKey": "typeFk" - }, - "ink": { - "type": "belongsTo", - "model": "Ink", - "foreignKey": "inkFk" - }, - "origin": { - "type": "belongsTo", - "model": "Origin", - "foreignKey": "originFk" - }, - "producer": { - "type": "belongsTo", - "model": "Producer", - "foreignKey": "producerFk" - }, - "intrastat": { - "type": "belongsTo", - "model": "Intrastat", - "foreignKey": "intrastatFk" - }, - "expense": { - "type": "belongsTo", - "model": "Expense", - "foreignKey": "expenseFk" - }, - "tags": { - "type": "hasMany", - "model": "ItemTag", - "foreignKey": "itemFk" - }, - "itemBarcode": { - "type": "hasMany", - "model": "ItemBarcode", - "foreignKey": "itemFk" - }, - "taxes": { - "type": "hasMany", - "model": "ItemTaxCountry", - "foreignKey": "itemFk" - }, - "itemNiche": { - "type": "hasMany", - "model": "ItemNiche", - "foreignKey": "itemFk" - } + "itemType": { + "type": "belongsTo", + "model": "ItemType", + "foreignKey": "typeFk" + }, + "ink": { + "type": "belongsTo", + "model": "Ink", + "foreignKey": "inkFk" + }, + "origin": { + "type": "belongsTo", + "model": "Origin", + "foreignKey": "originFk" + }, + "producer": { + "type": "belongsTo", + "model": "Producer", + "foreignKey": "producerFk" + }, + "intrastat": { + "type": "belongsTo", + "model": "Intrastat", + "foreignKey": "intrastatFk" + }, + "expense": { + "type": "belongsTo", + "model": "Expense", + "foreignKey": "expenseFk" + }, + "tags": { + "type": "hasMany", + "model": "ItemTag", + "foreignKey": "itemFk" + }, + "itemBarcode": { + "type": "hasMany", + "model": "ItemBarcode", + "foreignKey": "itemFk" + }, + "taxes": { + "type": "hasMany", + "model": "ItemTaxCountry", + "foreignKey": "itemFk" + }, + "itemNiche": { + "type": "hasMany", + "model": "ItemNiche", + "foreignKey": "itemFk" + } } - } \ No newline at end of file +} \ No newline at end of file diff --git a/modules/item/back/models/tax-class-code.json b/modules/item/back/models/tax-class-code.json new file mode 100644 index 0000000000..ef8c529d96 --- /dev/null +++ b/modules/item/back/models/tax-class-code.json @@ -0,0 +1,46 @@ +{ + "name": "TaxClassCode", + "base": "VnModel", + "options": { + "mysql": { + "table": "taxClassCode" + } + }, + "properties": { + "taxClassFk": { + "type": "number", + "required": true, + "id": 1 + }, + "taxCodeFk": { + "type": "number", + "required": true, + "id": 2 + }, + "effectived": { + "type": "date", + "required": true, + "id": 3 + } + }, + "relations": { + "taxClass": { + "type": "belongsTo", + "model": "TaxClass", + "foreignKey": "taxClassFk" + }, + "taxCode": { + "type": "belongsTo", + "model": "TaxCode", + "foreignKey": "taxCodeFk" + } + }, + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + } + ] +} \ No newline at end of file diff --git a/modules/item/front/basic-data/__snapshots__/index.spec.js.snap b/modules/item/front/basic-data/__snapshots__/index.spec.js.snap deleted file mode 100644 index 92219bb339..0000000000 --- a/modules/item/front/basic-data/__snapshots__/index.spec.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`vnItemBasicData Component vnItemBasicData $onChanges() should pass the data to the watcher 1`] = `"the current value of an item"`; diff --git a/modules/item/front/basic-data/index.html b/modules/item/front/basic-data/index.html index bd0cec86de..3cd879945d 100644 --- a/modules/item/front/basic-data/index.html +++ b/modules/item/front/basic-data/index.html @@ -55,6 +55,13 @@
{{::id}}
{{::description}}
+ + + + - +
@@ -134,3 +141,30 @@ + + + + +
New intrastat
+ + + + + + + + +
+ + + + +
\ No newline at end of file diff --git a/modules/item/front/basic-data/index.js b/modules/item/front/basic-data/index.js index 123aa59cdc..33a60b32d4 100644 --- a/modules/item/front/basic-data/index.js +++ b/modules/item/front/basic-data/index.js @@ -1,20 +1,23 @@ import ngModule from '../module'; +import Component from 'core/lib/component'; +class Controller extends Component { + showIntrastat(event) { + if (event.defaultPrevented) return; + event.preventDefault(); -class Controller { - constructor($scope, $timeout) { - this.$scope = $scope; - this.$timeout = $timeout; + this.newIntrastat = { + taxClassFk: this.item.taxClassFk + }; + this.$.intrastat.show(); } - $onChanges(data) { - this.$timeout(() => { - this.$scope.watcher.data = data.item.currentValue; - }); + onIntrastatAccept() { + const query = `Items/${this.$params.id}/createIntrastat`; + return this.$http.patch(query, this.newIntrastat) + .then(res => this.item.intrastatFk = res.data.id); } } -Controller.$inject = ['$scope', '$timeout']; - ngModule.component('vnItemBasicData', { template: require('./index.html'), bindings: { diff --git a/modules/item/front/basic-data/index.spec.js b/modules/item/front/basic-data/index.spec.js index e7578b54c0..178fac2784 100644 --- a/modules/item/front/basic-data/index.spec.js +++ b/modules/item/front/basic-data/index.spec.js @@ -2,26 +2,31 @@ import './index.js'; describe('vnItemBasicData', () => { describe('Component vnItemBasicData', () => { + let $httpBackend; let $scope; let controller; - let $timeout; + let $element; beforeEach(ngModule('item')); - beforeEach(angular.mock.inject(($componentController, $rootScope, _$timeout_) => { - $timeout = _$timeout_; + beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; $scope = $rootScope.$new(); - controller = $componentController('vnItemBasicData', {$scope, $timeout}); - controller.$scope.watcher = {}; + $element = angular.element(''); + controller = $componentController('vnItemBasicData', {$element, $scope}); + controller.$.watcher = {}; + controller.$params.id = 1; + controller.item = {id: 1, name: 'Rainbow Coral'}; })); - describe('$onChanges()', () => { + describe('onIntrastatAccept()', () => { it('should pass the data to the watcher', () => { - const data = {item: {currentValue: 'the current value of an item'}}; - controller.$onChanges(data); - $timeout.flush(); + const newIntrastatId = 20; + $httpBackend.expect('PATCH', 'Items/1/createIntrastat').respond({id: 20}); + controller.onIntrastatAccept(); + $httpBackend.flush(); - expect(controller.$scope.watcher.data).toMatchSnapshot(); + 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 index 67780557a3..07e6817709 100644 --- a/modules/item/front/basic-data/locale/es.yml +++ b/modules/item/front/basic-data/locale/es.yml @@ -5,4 +5,6 @@ Full name calculates based on tags 1-3. Is not recommended to change it manually No se recomienda cambiarlo manualmente Is active: Activo Expense: Gasto -Price in kg: Precio en kg \ No newline at end of file +Price in kg: Precio en kg +New intrastat: Nuevo intrastat +Identifier: Identificador \ No newline at end of file diff --git a/modules/order/back/methods/order/catalogFilter.js b/modules/order/back/methods/order/catalogFilter.js index 3814aa12d1..114cd27864 100644 --- a/modules/order/back/methods/order/catalogFilter.js +++ b/modules/order/back/methods/order/catalogFilter.js @@ -162,6 +162,7 @@ module.exports = Self => { `SELECT it.tagFk, it.itemFk, + it.value, t.name FROM tmp.ticketCalculateItem tci JOIN vn.itemTag it ON it.itemFk = tci.itemFk diff --git a/modules/order/front/catalog/index.html b/modules/order/front/catalog/index.html index 5cc72e3c8f..37767e81bb 100644 --- a/modules/order/front/catalog/index.html +++ b/modules/order/front/catalog/index.html @@ -85,12 +85,15 @@
- + show-field="value" + value-field="value" + label="Search tag"> @@ -101,7 +104,7 @@ style="cursor: pointer;"> - + { - // Add new tag filters - item.tags.forEach(itemTag => { - const alreadyAdded = newFilterList.findIndex(filter => { - return filter.field == itemTag.tagFk; - }); - - if (alreadyAdded == -1) { - newFilterList.push({ - name: itemTag.name, - field: itemTag.tagFk, - isTag: true - }); - } - }); - }); - - // Add default filters - Replaces tags with same name - this.defaultOrderFields.forEach(orderField => { - const index = newFilterList.findIndex(newfield => { - return newfield.name == orderField.name; - }); - - if (index > -1) - newFilterList[index] = orderField; - else - newFilterList.push(orderField); - }); - - this.orderFields = newFilterList; + this.buildTagsFilter(value); + this.buildOrderFilter(value); } get categoryId() { @@ -273,6 +244,43 @@ class Controller { this.$state.go(this.$state.current.name, params); } + + buildTagsFilter(items) { + const tagValues = []; + items.forEach(item => { + item.tags.forEach(itemTag => { + const alreadyAdded = tagValues.findIndex(tag => { + return tag.value == itemTag.value; + }); + + if (alreadyAdded == -1) + tagValues.push(itemTag); + }); + }); + this.tagValues = tagValues; + } + + buildOrderFilter(items) { + const tags = []; + items.forEach(item => { + item.tags.forEach(itemTag => { + const alreadyAdded = tags.findIndex(tag => { + return tag.field == itemTag.tagFk; + }); + + if (alreadyAdded == -1) { + tags.push({ + name: itemTag.name, + field: itemTag.tagFk, + isTag: true + }); + } + }); + }); + let newFilterList = [].concat(this.defaultOrderFields); + newFilterList = newFilterList.concat(tags); + this.orderFields = newFilterList; + } } Controller.$inject = ['$http', '$scope', '$state', '$compile', '$transitions']; diff --git a/modules/order/front/catalog/index.spec.js b/modules/order/front/catalog/index.spec.js index dd9e132577..01da618009 100644 --- a/modules/order/front/catalog/index.spec.js +++ b/modules/order/front/catalog/index.spec.js @@ -37,16 +37,19 @@ describe('Order', () => { describe('items() setter', () => { it(`should return an object with order params`, () => { - let expectedResult = [{field: 'showOrder, price', name: 'Color'}]; - let unexpectedResult = [{tagFk: 5, name: 'Color'}]; - controller.items = [{id: 1, name: 'My Item', tags: [ + spyOn(controller, 'buildTagsFilter'); + spyOn(controller, 'buildOrderFilter').and.callThrough(); + const expectedResult = [{field: 'showOrder, price', name: 'Color and price'}]; + const items = [{id: 1, name: 'My Item', tags: [ {tagFk: 4, name: 'Length'}, {tagFk: 5, name: 'Color'} ]}]; + controller.items = items; - expect(controller.orderFields.length).toEqual(5); + expect(controller.orderFields.length).toEqual(6); expect(controller.orderFields).toEqual(jasmine.arrayContaining(expectedResult)); - expect(controller.orderFields).not.toEqual(jasmine.arrayContaining(unexpectedResult)); + expect(controller.buildTagsFilter).toHaveBeenCalledWith(items); + expect(controller.buildOrderFilter).toHaveBeenCalledWith(items); }); }); @@ -222,6 +225,48 @@ describe('Order', () => { expect(controller.$.model.addFilter).toHaveBeenCalledWith(null, expectedOrder); }); }); + + describe('buildTagsFilter()', () => { + it(`should create an array of non repeated tag values and then set the tagValues property`, () => { + const items = [ + { + id: 1, name: 'My Item 1', tags: [ + {tagFk: 4, name: 'Length', value: 1}, + {tagFk: 5, name: 'Color', value: 'red'} + ] + }, + { + id: 2, name: 'My Item 2', tags: [ + {tagFk: 4, name: 'Length', value: 1}, + {tagFk: 5, name: 'Color', value: 'blue'} + ] + }]; + controller.buildTagsFilter(items); + + expect(controller.tagValues.length).toEqual(3); + }); + }); + + describe('buildOrderFilter()', () => { + it(`should create an array of non repeated tags plus default filters and then set the orderFields property`, () => { + const items = [ + { + id: 1, name: 'My Item 1', tags: [ + {tagFk: 4, name: 'Length'}, + {tagFk: 5, name: 'Color'} + ] + }, + { + id: 2, name: 'My Item 2', tags: [ + {tagFk: 5, name: 'Color'}, + {tagFk: 6, name: 'Relevancy'} + ] + }]; + controller.buildOrderFilter(items); + + expect(controller.orderFields.length).toEqual(7); + }); + }); }); }); diff --git a/modules/order/front/locale/es.yml b/modules/order/front/locale/es.yml index 0ada37bfd0..565d4f2518 100644 --- a/modules/order/front/locale/es.yml +++ b/modules/order/front/locale/es.yml @@ -16,6 +16,7 @@ Item id: Id de artículo Order by: Ordenar por Order: Orden Price: Precio +Color and price: Color y precio Ascendant: Ascendente Descendant: Descendente Created from: Creado desde diff --git a/modules/route/back/models/route.js b/modules/route/back/models/route.js index 4dd9f3dc02..6d93cfe405 100644 --- a/modules/route/back/models/route.js +++ b/modules/route/back/models/route.js @@ -5,4 +5,21 @@ module.exports = Self => { require('../methods/route/guessPriority')(Self); require('../methods/route/updateVolume')(Self); require('../methods/route/getDeliveryPoint')(Self); + + Self.validate('kmStart', validateDistance, { + message: 'Distance must be lesser than 1000' + }); + + Self.validate('kmEnd', validateDistance, { + message: 'Distance must be lesser than 1000' + }); + + function validateDistance(err) { + const routeTotalKm = this.kmEnd - this.kmStart; + const routeMaxKm = 1000; + if ( routeTotalKm > routeMaxKm || this.kmStart > this.kmEnd) + err(); + } }; + + diff --git a/modules/route/front/basic-data/index.js b/modules/route/front/basic-data/index.js index 810fd75112..d4a481dc5f 100644 --- a/modules/route/front/basic-data/index.js +++ b/modules/route/front/basic-data/index.js @@ -1,8 +1,10 @@ import ngModule from '../module'; class Controller { - constructor($scope) { + constructor($scope, vnApp, $translate) { this.$ = $scope; + this.vnApp = vnApp; + this.$translate = $translate; } onSubmit() { @@ -11,7 +13,7 @@ class Controller { }); } } -Controller.$inject = ['$scope']; +Controller.$inject = ['$scope', 'vnApp', '$translate']; ngModule.component('vnRouteBasicData', { template: require('./index.html'), diff --git a/modules/route/front/basic-data/locale/es.yml b/modules/route/front/basic-data/locale/es.yml index 442a4fa828..f0414b5b15 100644 --- a/modules/route/front/basic-data/locale/es.yml +++ b/modules/route/front/basic-data/locale/es.yml @@ -2,4 +2,4 @@ Date finished: Fecha fin Date started: Fecha inicio Km start: Km de inicio Km end: Km de fin -Description: Descripción \ No newline at end of file +Description: Descripción diff --git a/modules/ticket/front/create/card.js b/modules/ticket/front/create/card.js index 2e775c18b0..54cc56c684 100644 --- a/modules/ticket/front/create/card.js +++ b/modules/ticket/front/create/card.js @@ -14,7 +14,7 @@ class Controller { $onInit() { if (this.$stateParams && this.$stateParams.clientFk) - this.clientId = this.$stateParams.clientFk; + this.clientId = parseInt(this.$stateParams.clientFk); this.warehouseId = this.vnConfig.warehouseFk; } diff --git a/modules/ticket/front/sms/index.html b/modules/ticket/front/sms/index.html index f74bc29e5e..2003aa4fd4 100644 --- a/modules/ticket/front/sms/index.html +++ b/modules/ticket/front/sms/index.html @@ -18,15 +18,22 @@ vn-id="message" label="Message" ng-model="$ctrl.sms.message" + info="Special characters like accents counts as a multiple" rows="5" - maxlength="160" required="true" rule>
- {{'Characters remaining' | translate}}: {{$ctrl.charactersRemaining()}} + {{'Characters remaining' | translate}}: + + {{$ctrl.charactersRemaining()}} + diff --git a/modules/ticket/front/sms/index.js b/modules/ticket/front/sms/index.js index 0d639d46e2..ac11315132 100644 --- a/modules/ticket/front/sms/index.js +++ b/modules/ticket/front/sms/index.js @@ -17,13 +17,12 @@ class Controller extends Component { } charactersRemaining() { - let elementMaxLength; - let textAreaLength; const element = this.$scope.message; + const value = element.input.value; - textAreaLength = element.input.textLength; - elementMaxLength = element.maxlength; - return elementMaxLength - textAreaLength; + const maxLength = 160; + const textAreaLength = new Blob([value]).size; + return maxLength - textAreaLength; } onResponse(response) { @@ -33,6 +32,8 @@ class Controller extends Component { throw new Error(`The destination can't be empty`); if (!this.sms.message) throw new Error(`The message can't be empty`); + if (this.charactersRemaining() < 0) + throw new Error(`The message it's too long`); this.$http.post(`Tickets/${this.$params.id}/sendSms`, this.sms).then(res => { this.vnApp.showMessage(this.$translate.instant('SMS sent!')); diff --git a/modules/ticket/front/sms/index.spec.js b/modules/ticket/front/sms/index.spec.js index d02b3f3eb6..96c10edd10 100644 --- a/modules/ticket/front/sms/index.spec.js +++ b/modules/ticket/front/sms/index.spec.js @@ -14,6 +14,11 @@ describe('Ticket', () => { $element = angular.element(''); controller = $componentController('vnTicketSms', {$element, $scope}); controller.$params = {id: 11}; + controller.$scope.message = { + input: { + value: 'My SMS' + } + }; })); describe('onResponse()', () => { @@ -55,14 +60,13 @@ describe('Ticket', () => { it('should return the characters remaining in a element', () => { controller.$scope.message = { input: { - textLength: 50 - }, - maxlength: 150 + value: 'My message 0€' + } }; let result = controller.charactersRemaining(); - expect(result).toEqual(100); + expect(result).toEqual(145); }); }); }); diff --git a/modules/ticket/front/sms/locale/es.yml b/modules/ticket/front/sms/locale/es.yml index 4438e4fce4..64c3fcca67 100644 --- a/modules/ticket/front/sms/locale/es.yml +++ b/modules/ticket/front/sms/locale/es.yml @@ -4,4 +4,6 @@ Message: Mensaje SMS sent!: ¡SMS enviado! Characters remaining: Carácteres restantes The destination can't be empty: El destinatario no puede estar vacio -The message can't be empty: El mensaje no puede estar vacio \ No newline at end of file +The message can't be empty: El mensaje no puede estar vacio +The message it's too long: El mensaje es demasiado largo +Special characters like accents counts as a multiple: Carácteres especiales como los acentos cuentan como varios \ No newline at end of file diff --git a/modules/travel/back/methods/travel/specs/filter.spec.js b/modules/travel/back/methods/travel/specs/filter.spec.js index f2fe44989d..03849f2b0e 100644 --- a/modules/travel/back/methods/travel/specs/filter.spec.js +++ b/modules/travel/back/methods/travel/specs/filter.spec.js @@ -23,7 +23,7 @@ describe('Travel filter()', () => { let result = await app.models.Travel.filter(ctx); - expect(result.length).toEqual(7); + expect(result.length).toEqual(8); }); it('should return the travel matching "total entries"', async() => { diff --git a/modules/travel/front/descriptor-popover/index.html b/modules/travel/front/descriptor-popover/index.html new file mode 100644 index 0000000000..fc0fb0301d --- /dev/null +++ b/modules/travel/front/descriptor-popover/index.html @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/modules/travel/front/descriptor-popover/index.js b/modules/travel/front/descriptor-popover/index.js new file mode 100644 index 0000000000..cb13d7f068 --- /dev/null +++ b/modules/travel/front/descriptor-popover/index.js @@ -0,0 +1,88 @@ +import ngModule from '../module'; +import Component from 'core/lib/component'; + +class Controller extends Component { + constructor($element, $scope, $http, $timeout, $q) { + super($element, $scope); + this.$timeout = $timeout; + this.$http = $http; + this.$q = $q; + this.travel = null; + this._quicklinks = {}; + } + + set travelId(travelId) { + if (travelId == this._travelId) return; + + this._travelId = travelId; + this.travel = null; + this.loadData(); + } + + get travelId() { + return this._travelId; + } + + get quicklinks() { + return this._quicklinks; + } + + set quicklinks(value = {}) { + Object.keys(value).forEach(key => { + this._quicklinks[key] = value[key]; + }); + } + + show() { + this.$.popover.parent = this.parent; + this.$.popover.show(); + } + + loadData() { + let query = `Travels/findOne`; + let filter = { + fields: [ + 'id', + 'ref', + 'shipped', + 'landed', + 'totalEntries', + 'warehouseInFk', + 'warehouseOutFk' + ], + where: { + id: this._travelId + }, + include: [ + { + relation: 'warehouseIn', + scope: { + fields: ['name'] + } + }, { + relation: 'warehouseOut', + scope: { + fields: ['name'] + } + } + ] + }; + + this.$http.get(query, {params: {filter}}).then(res => { + this.travel = res.data; + this.$.$applyAsync(() => { + this.$.popover.relocate(); + }); + }); + } +} +Controller.$inject = ['$element', '$scope', '$http', '$timeout', '$q']; + +ngModule.component('vnTravelDescriptorPopover', { + template: require('./index.html'), + controller: Controller, + bindings: { + travelId: '<', + quicklinks: '<' + } +}); diff --git a/modules/travel/front/descriptor-popover/index.spec.js b/modules/travel/front/descriptor-popover/index.spec.js new file mode 100644 index 0000000000..12ad364bf4 --- /dev/null +++ b/modules/travel/front/descriptor-popover/index.spec.js @@ -0,0 +1,100 @@ +import './index.js'; + +describe('travel Component vnTravelDescriptorPopover', () => { + let $httpBackend; + let $httpParamSerializer; + let $scope; + let controller; + let $element; + + beforeEach(ngModule('travel')); + + beforeEach(angular.mock.inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => { + $httpBackend = _$httpBackend_; + $httpParamSerializer = _$httpParamSerializer_; + $element = angular.element(`
`); + $scope = $rootScope.$new(); + $scope.popover = {relocate: () => {}, show: () => {}}; + controller = $componentController('vnTravelDescriptorPopover', {$scope, $element}); + })); + + describe('travelId()', () => { + it(`should not apply any changes if the received id is the same stored in _travelId`, () => { + controller.travel = 'I exist!'; + controller._travelId = 1; + spyOn(controller, 'loadData'); + controller.travelId = 1; + + expect(controller.travel).toEqual('I exist!'); + expect(controller._travelId).toEqual(1); + expect(controller.loadData).not.toHaveBeenCalled(); + }); + + it(`should set the received id into _travelId, set the travel to null and then call loadData()`, () => { + controller.travel = `Please don't`; + controller._travelId = 1; + spyOn(controller, 'loadData'); + controller.travelId = 999; + + expect(controller.travel).toBeNull(); + expect(controller._travelId).toEqual(999); + expect(controller.loadData).toHaveBeenCalledWith(); + }); + }); + + describe('show()', () => { + it(`should call the show()`, () => { + spyOn(controller.$.popover, 'show'); + controller.show(); + + expect(controller.$.popover.show).toHaveBeenCalledWith(); + }); + }); + + describe('loadData()', () => { + it(`should perform a get query to store the worker data into the controller`, () => { + controller.travelId = 1; + controller.canceler = null; + let response = {}; + + let config = { + filter: { + fields: [ + 'id', + 'ref', + 'shipped', + 'landed', + 'totalEntries', + 'warehouseInFk', + 'warehouseOutFk' + ], + where: { + id: controller.travelId + }, + include: [ + { + relation: 'warehouseIn', + scope: { + fields: ['name'] + } + }, { + relation: 'warehouseOut', + scope: { + fields: ['name'] + } + } + ] + } + }; + + let json = $httpParamSerializer(config); + + $httpBackend.whenGET(`Travels/findOne?${json}`).respond(response); + $httpBackend.expectGET(`Travels/findOne?${json}`); + controller.loadData(); + $httpBackend.flush(); + + expect(controller.travel).toEqual(response); + }); + }); +}); diff --git a/modules/travel/front/index.js b/modules/travel/front/index.js index 1f5346e987..b72f9fd51c 100644 --- a/modules/travel/front/index.js +++ b/modules/travel/front/index.js @@ -11,4 +11,4 @@ import './log'; import './create'; import './thermograph/index/'; import './thermograph/create/'; - +import './descriptor-popover'; diff --git a/modules/worker/front/descriptor-popover/index.js b/modules/worker/front/descriptor-popover/index.js index b648e8bd20..55843a67da 100644 --- a/modules/worker/front/descriptor-popover/index.js +++ b/modules/worker/front/descriptor-popover/index.js @@ -1,6 +1,5 @@ import ngModule from '../module'; import Component from 'core/lib/component'; -import './style.scss'; class Controller extends Component { constructor($element, $scope, $http, $timeout, $q) { diff --git a/modules/worker/front/descriptor-popover/style.scss b/modules/worker/front/descriptor-popover/style.scss deleted file mode 100644 index 58e65d3206..0000000000 --- a/modules/worker/front/descriptor-popover/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -vn-ticket-descriptor-popover { - vn-ticket-descriptor { - display: block; - width: 16em; - max-height: 28em; - - & > vn-card { - margin: 0!important; - } - } -} \ No newline at end of file