diff --git a/back/methods/notification/clean.js b/back/methods/notification/clean.js index e6da58af8..bdc6737df 100644 --- a/back/methods/notification/clean.js +++ b/back/methods/notification/clean.js @@ -29,6 +29,9 @@ module.exports = Self => { try { const config = await models.NotificationConfig.findOne({}, myOptions); + + if (!config.cleanDays) return; + const cleanDate = new Date(); cleanDate.setDate(cleanDate.getDate() - config.cleanDays); @@ -36,11 +39,11 @@ module.exports = Self => { where: {status: {inq: status}}, created: {lt: cleanDate} }, myOptions); - - if (tx) await tx.commit(); } catch (e) { if (tx) await tx.rollback(); throw e; } + + if (tx) await tx.commit(); }; }; diff --git a/back/methods/notification/send.js b/back/methods/notification/send.js index 80faf0305..b2748477d 100644 --- a/back/methods/notification/send.js +++ b/back/methods/notification/send.js @@ -1,5 +1,4 @@ const {Email} = require('vn-print'); -const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethod('send', { @@ -35,7 +34,10 @@ module.exports = Self => { include: { relation: 'user', scope: { - fields: ['name', 'email', 'lang'] + fields: ['name', 'lang'], + include: { + relation: 'emailUser' + } } } } @@ -56,7 +58,7 @@ module.exports = Self => { for (const notificationUser of queue.notification().subscription()) { try { const sendParams = { - recipient: notificationUser.user().email, + recipient: notificationUser.user().emailUser().email, lang: notificationUser.user().lang }; diff --git a/db/changes/10503-november/00-alter_expedition_itemFk.sql b/db/changes/10503-november/00-alter_expedition_itemFk.sql new file mode 100644 index 000000000..d2849481b --- /dev/null +++ b/db/changes/10503-november/00-alter_expedition_itemFk.sql @@ -0,0 +1 @@ +Alter table `vn`.`expedition` RENAME COLUMN itemFk TO itemFk__; \ No newline at end of file diff --git a/db/changes/10503-november/00-collection_missingTrash.sql b/db/changes/10503-november/00-collection_missingTrash.sql new file mode 100644 index 000000000..d4467c699 --- /dev/null +++ b/db/changes/10503-november/00-collection_missingTrash.sql @@ -0,0 +1 @@ +DROP PROCEDURE IF EXISTS `vn`.`collection_missingTrash`; diff --git a/db/changes/10503-november/00-greuge.userFK_userFk.sql b/db/changes/10503-november/00-greuge.userFK_userFk.sql new file mode 100644 index 000000000..ec4bf3146 --- /dev/null +++ b/db/changes/10503-november/00-greuge.userFK_userFk.sql @@ -0,0 +1 @@ +ALTER TABLE `vn`.`greuge` CHANGE `userFK` `userFk` int(10) unsigned DEFAULT NULL NULL; \ No newline at end of file diff --git a/db/changes/10503-november/00-ticket_canMerge.sql b/db/changes/10503-november/00-ticket_canMerge.sql new file mode 100644 index 000000000..843237b64 --- /dev/null +++ b/db/changes/10503-november/00-ticket_canMerge.sql @@ -0,0 +1,14 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticket_canMerge`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canMerge`(vDated DATE, vScopeDays INT, vLitersMax INT, vLinesMax INT, vWarehouseFk INT) +BEGIN + CALL vn.ticket_canbePostponed(vDated,TIMESTAMPADD(DAY, vScopeDays, vDated),vLitersMax,vLinesMax,vWarehouseFk); +END $$ +DELIMITER ; + +INSERT INTO `salix`.`ACL` (model, property, accessType, permission, principalType, principalId) +VALUES + ('Ticket', 'getTicketsFuture', 'READ', 'ALLOW', 'ROLE', 'employee'), + ('Ticket', 'merge', 'WRITE', 'ALLOW', 'ROLE', 'employee'); diff --git a/db/changes/10503-november/00-ticket_canbePostponed.sql b/db/changes/10503-november/00-ticket_canbePostponed.sql new file mode 100644 index 000000000..c691fa4bd --- /dev/null +++ b/db/changes/10503-november/00-ticket_canbePostponed.sql @@ -0,0 +1,79 @@ +DROP PROCEDURE IF EXISTS `vn`.`ticket_canbePostponed`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`ticket_canbePostponed`(vOriginDated DATE, vFutureDated DATE, vLitersMax INT, vLinesMax INT, vWarehouseFk INT) +BEGIN +/** + * Devuelve un listado de tickets susceptibles de fusionarse con otros tickets en el futuro + * + * @param vOriginDated Fecha en cuestión + * @param vFutureDated Fecha en el futuro a sondear + * @param vLitersMax Volumen máximo de los tickets a catapultar + * @param vLinesMax Número máximo de lineas de los tickets a catapultar + * @param vWarehouseFk Identificador de vn.warehouse + */ + DROP TEMPORARY TABLE IF EXISTS tmp.filter; + CREATE TEMPORARY TABLE tmp.filter + (INDEX (id)) + SELECT sv.ticketFk id, + GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) ipt, + CAST(sum(litros) AS DECIMAL(10,0)) liters, + CAST(count(*) AS DECIMAL(10,0)) `lines`, + st.name state, + sub2.id ticketFuture, + t.landed originETD, + sub2.landed destETD, + sub2.iptd tfIpt, + sub2.state tfState, + t.clientFk, + t.warehouseFk, + ts.alertLevel, + t.shipped, + sub2.shipped tfShipped, + t.workerFk, + st.code code, + sub2.code tfCode + FROM vn.saleVolume sv + JOIN vn.sale s ON s.id = sv.saleFk + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.ticket t ON t.id = sv.ticketFk + JOIN vn.address a ON a.id = t.addressFk + JOIN vn.province p ON p.id = a.provinceFk + JOIN vn.country c ON c.id = p.countryFk + JOIN vn.ticketState ts ON ts.ticketFk = t.id + JOIN vn.state st ON st.id = ts.stateFk + JOIN vn.alertLevel al ON al.id = ts.alertLevel + LEFT JOIN vn.ticketParking tp ON tp.ticketFk = t.id + LEFT JOIN ( + SELECT * + FROM ( + SELECT + t.addressFk , + t.id, + t.landed, + t.shipped, + st.name state, + st.code code, + GROUP_CONCAT(DISTINCT i.itemPackingTypeFk ORDER BY i.itemPackingTypeFk) iptd + FROM vn.ticket t + JOIN vn.ticketState ts ON ts.ticketFk = t.id + JOIN vn.state st ON st.id = ts.stateFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.item i ON i.id = s.itemFk + WHERE t.shipped BETWEEN vFutureDated + AND util.dayend(vFutureDated) + AND t.warehouseFk = vWarehouseFk + GROUP BY t.id + ) sub + GROUP BY sub.addressFk + ) sub2 ON sub2.addressFk = t.addressFk AND t.id != sub2.id + WHERE t.shipped BETWEEN vOriginDated AND util.dayend(vOriginDated) + AND t.warehouseFk = vWarehouseFk + AND al.code = 'FREE' + AND tp.ticketFk IS NULL + GROUP BY sv.ticketFk + HAVING liters <= IFNULL(vLitersMax, 9999) AND `lines` <= IFNULL(vLinesMax, 9999) AND ticketFuture; +END$$ +DELIMITER ; + diff --git a/db/changes/10503-november/00-timeBusiness_calculate.sql b/db/changes/10503-november/00-timeBusiness_calculate.sql new file mode 100644 index 000000000..ea13c4a8a --- /dev/null +++ b/db/changes/10503-november/00-timeBusiness_calculate.sql @@ -0,0 +1,88 @@ +DROP PROCEDURE IF EXISTS `vn`.`timeBusiness_calculate`; + +DELIMITER $$ +$$ +CREATE DEFINER=`root`@`localhost` PROCEDURE `vn`.`timeBusiness_calculate`(vDatedFrom DATETIME, vDatedTo DATETIME) +BEGIN +/** + * Horas que debe trabajar un empleado según contrato y día. + * @param vDatedFrom workerTimeControl + * @param vDatedTo workerTimeControl + * @table tmp.user(userFk) + * @return tmp.timeBusinessCalculate + */ + DROP TEMPORARY TABLE IF EXISTS tmp.timeBusinessCalculate; + CREATE TEMPORARY TABLE tmp.timeBusinessCalculate + (INDEX (departmentFk)) + SELECT dated, + businessFk, + userFk, + departmentFk, + hourStart, + hourEnd, + timeTable, + timeWorkSeconds, + SEC_TO_TIME(timeWorkSeconds) timeWorkSexagesimal, + timeWorkSeconds / 3600 timeWorkDecimal, + timeWorkSeconds timeBusinessSeconds, + SEC_TO_TIME(timeWorkSeconds) timeBusinessSexagesimal, + timeWorkSeconds / 3600 timeBusinessDecimal, + name type, + permissionRate, + hoursWeek, + discountRate, + isAllowedToWork + FROM(SELECT t.dated, + b.id businessFk, + w.userFk, + b.departmentFk, + IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.start,5) ORDER BY j.start ASC SEPARATOR ' - ')) hourStart , + IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.end,5) ORDER BY j.end ASC SEPARATOR ' - ')) hourEnd, + IF(j.start = NULL, NULL, GROUP_CONCAT(DISTINCT LEFT(j.start,5), " - ", LEFT(j.end,5) ORDER BY j.end ASC SEPARATOR ' - ')) timeTable, + IF(j.start = NULL, 0, IFNULL(SUM(TIME_TO_SEC(j.end)) - SUM(TIME_TO_SEC(j.start)), 0)) timeWorkSeconds, + at2.name, + at2.permissionRate, + at2.discountRate, + cl.hours_week hoursWeek, + at2.isAllowedToWork + FROM time t + LEFT JOIN business b ON t.dated BETWEEN b.started AND IFNULL(b.ended, vDatedTo) + LEFT JOIN worker w ON w.id = b.workerFk + JOIN tmp.`user` u ON u.userFK = w.userFK + LEFT JOIN workCenter wc ON wc.id = b.workcenterFK + LEFT JOIN postgresql.calendar_labour_type cl ON cl.calendar_labour_type_id = b.calendarTypeFk + LEFT JOIN postgresql.journey j ON j.business_id = b.id AND j.day_id = WEEKDAY(t.dated) + 1 + LEFT JOIN postgresql.calendar_employee ce ON ce.businessFk = b.id AND ce.date = t.dated + LEFT JOIN absenceType at2 ON at2.id = ce.calendar_state_id + WHERE t.dated BETWEEN vDatedFrom AND vDatedTo + GROUP BY w.userFk, t.dated + )sub; + + UPDATE tmp.timeBusinessCalculate t + LEFT JOIN postgresql.journey j ON j.business_id = t.businessFk + SET t.timeWorkSeconds = t.hoursWeek / 5 * 3600, + t.timeWorkSexagesimal = SEC_TO_TIME( t.hoursWeek / 5 * 3600), + t.timeWorkDecimal = t.hoursWeek / 5, + t.timeBusinessSeconds = t.hoursWeek / 5 * 3600, + t.timeBusinessSexagesimal = SEC_TO_TIME( t.hoursWeek / 5 * 3600), + t.timeBusinessDecimal = t.hoursWeek / 5 + WHERE DAYOFWEEK(t.dated) IN(2,3,4,5,6) AND j.journey_id IS NULL ; + + UPDATE tmp.timeBusinessCalculate t + SET t.timeWorkSeconds = t.timeWorkSeconds - (t.timeWorkSeconds * permissionRate) , + t.timeWorkSexagesimal = SEC_TO_TIME ((t.timeWorkDecimal - (t.timeWorkDecimal * permissionRate)) * 3600), + t.timeWorkDecimal = t.timeWorkDecimal - (t.timeWorkDecimal * permissionRate) + WHERE permissionRate <> 0; + + UPDATE tmp.timeBusinessCalculate t + JOIN calendarHolidays ch ON ch.dated = t.dated + JOIN business b ON b.id = t.businessFk + AND b.workcenterFk = ch.workcenterFk + SET t.timeWorkSeconds = 0, + t.timeWorkSexagesimal = 0, + t.timeWorkDecimal = 0, + t.permissionrate = 1, + t.type = 'Festivo' + WHERE t.type IS NULL; +END$$ +DELIMITER ; diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 7d219eced..e0f946ee9 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -686,7 +686,10 @@ INSERT INTO `vn`.`ticket`(`id`, `priority`, `agencyModeFk`,`warehouseFk`,`routeF (24 ,NULL, 8, 1, 7, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 5, 5, 1, util.VN_CURDATE()), (25 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Bruce Wayne', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()), (26 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'An incredibly long alias for testing purposes', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()), - (27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()); + (27 ,NULL, 8, 1, NULL, util.VN_CURDATE(), util.VN_CURDATE(), 1101, 'Wolverine', 1, NULL, 0, 1, 5, 1, util.VN_CURDATE()), + (28, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()), + (29, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()), + (30, 1, 8, 1, 1, util.VN_CURDATE(), DATE_ADD(util.VN_CURDATE(), INTERVAL + 1 DAY), 1103, 'Phone Box', 123, NULL, 0, 1, 5, 1, util.VN_CURDATE()); INSERT INTO `vn`.`ticketObservation`(`id`, `ticketFk`, `observationTypeFk`, `description`) VALUES @@ -918,7 +921,7 @@ INSERT INTO `vn`.`expeditionStateType`(`id`, `description`, `code`) (3, 'Perdida', 'LOST'); -INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`) +INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `freightItemFk`, `created`, `itemFk__`, `counter`, `workerFk`, `externalId`, `packagingFk`, `stateTypeFk`, `hostFk`) VALUES (1, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 15, 1, 18, 'UR9000006041', 94, 1, 'pc1'), (2, 1, 1, 71, DATE_ADD(util.VN_CURDATE(), INTERVAL -1 MONTH), 16, 2, 18, 'UR9000006041', 94, 1, NULL), @@ -984,7 +987,10 @@ INSERT INTO `vn`.`sale`(`id`, `itemFk`, `ticketFk`, `concept`, `quantity`, `pric (30, 4, 18, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()), (31, 2, 23, 'Melee weapon combat fist 15cm', -5, 7.08, 0, 0, 0, util.VN_CURDATE()), (32, 1, 24, 'Ranged weapon longbow 2m', -1, 8.07, 0, 0, 0, util.VN_CURDATE()), - (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE()); + (33, 5, 14, 'Ranged weapon pistol 9mm', 50, 1.79, 0, 0, 0, util.VN_CURDATE()), + (34, 4, 28, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()), + (35, 4, 29, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()), + (36, 4, 30, 'Melee weapon heavy shield 1x0.5m', 20, 1.72, 0, 0, 0, util.VN_CURDATE()); INSERT INTO `vn`.`saleChecked`(`saleFk`, `isChecked`) VALUES @@ -2701,7 +2707,7 @@ INSERT INTO `util`.`notificationSubscription` (`notificationFk`, `userFk`) INSERT INTO `vn`.`routeConfig` (`id`, `defaultWorkCenterFk`) VALUES (1, 9); - + INSERT INTO `vn`.`productionConfig` (`isPreviousPreparationRequired`, `ticketPrintedMax`, `ticketTrolleyMax`, `rookieDays`, `notBuyingMonths`, `id`, `isZoneClosedByExpeditionActivated`, `maxNotReadyCollections`, `minTicketsToCloseZone`, `movingTicketDelRoute`, `defaultZone`, `defautlAgencyMode`, `hasUniqueCollectionTime`, `maxCollectionWithoutUser`, `pendingCollectionsOrder`, `pendingCollectionsAge`) VALUES (0, 8, 80, 0, 0, 1, 0, 15, 25, -1, 697, 1328, 0, 1, 8, 6); diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 1391731de..1a87b5bf0 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -727,6 +727,34 @@ export default { saveImport: 'button[response="accept"]', anyDocument: 'vn-ticket-dms-index > vn-data-viewer vn-tbody vn-tr' }, + ticketFuture: { + openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', + originDated: 'vn-date-picker[label="Origin ETD"]', + futureDated: 'vn-date-picker[label="Destination ETD"]', + shipped: 'vn-date-picker[label="Origin date"]', + tfShipped: 'vn-date-picker[label="Destination date"]', + linesMax: 'vn-textfield[label="Max Lines"]', + litersMax: 'vn-textfield[label="Max Liters"]', + ipt: 'vn-autocomplete[label="Origin IPT"]', + tfIpt: 'vn-autocomplete[label="Destination IPT"]', + tableIpt: 'vn-autocomplete[name="ipt"]', + tableTfIpt: 'vn-autocomplete[name="tfIpt"]', + state: 'vn-autocomplete[label="Origin Grouped State"]', + tfState: 'vn-autocomplete[label="Destination Grouped State"]', + warehouseFk: 'vn-autocomplete[label="Warehouse"]', + problems: 'vn-check[label="With problems"]', + tableButtonSearch: 'vn-button[vn-tooltip="Search"]', + moveButton: 'vn-button[vn-tooltip="Future tickets"]', + acceptButton: '.vn-confirm.shown button[response="accept"]', + firstCheck: 'tbody > tr:nth-child(1) > td > vn-check', + multiCheck: 'vn-multi-check', + tableId: 'vn-textfield[name="id"]', + tableTfId: 'vn-textfield[name="ticketFuture"]', + tableLiters: 'vn-textfield[name="litersMax"]', + tableLines: 'vn-textfield[name="linesMax"]', + submit: 'vn-submit[label="Search"]', + table: 'tbody > tr:not(.empty-rows)' + }, createStateView: { state: 'vn-autocomplete[ng-model="$ctrl.stateFk"]', worker: 'vn-autocomplete[ng-model="$ctrl.workerFk"]', diff --git a/e2e/paths/04-item/08_regularize.spec.js b/e2e/paths/04-item/08_regularize.spec.js index 400df666f..2e09a9f63 100644 --- a/e2e/paths/04-item/08_regularize.spec.js +++ b/e2e/paths/04-item/08_regularize.spec.js @@ -127,8 +127,8 @@ describe('Item regularize path', () => { await page.waitForState('ticket.index'); }); - it('should search for the ticket with id 28 once again', async() => { - await page.accessToSearchResult('28'); + it('should search for the ticket with id 31 once again', async() => { + await page.accessToSearchResult('31'); await page.waitForState('ticket.card.summary'); }); 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 index d776f417d..67dfd83bf 100644 --- a/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js +++ b/e2e/paths/05-ticket/01-sale/02_edit_sale.spec.js @@ -291,7 +291,7 @@ describe('Ticket Edit sale path', () => { it('should confirm the transfered quantity is the correct one', async() => { const result = await page.waitToGetProperty(selectors.ticketSales.secondSaleQuantityCell, 'innerText'); - expect(result).toContain('10'); + expect(result).toContain('20'); }); it('should go back to the original ticket sales section', async() => { diff --git a/e2e/paths/05-ticket/02_expeditions_and_log.spec.js b/e2e/paths/05-ticket/02_expeditions_and_log.spec.js index 66df3e9e6..f970247e5 100644 --- a/e2e/paths/05-ticket/02_expeditions_and_log.spec.js +++ b/e2e/paths/05-ticket/02_expeditions_and_log.spec.js @@ -1,7 +1,7 @@ import selectors from '../../helpers/selectors.js'; import getBrowser from '../../helpers/puppeteer'; -fdescribe('Ticket expeditions and log path', () => { +describe('Ticket expeditions and log path', () => { let browser; let page; diff --git a/e2e/paths/05-ticket/06_basic_data_steps.spec.js b/e2e/paths/05-ticket/06_basic_data_steps.spec.js index 46cbf29b8..fa901e325 100644 --- a/e2e/paths/05-ticket/06_basic_data_steps.spec.js +++ b/e2e/paths/05-ticket/06_basic_data_steps.spec.js @@ -104,7 +104,6 @@ describe('Ticket Edit basic data path', () => { await page.waitToClick(selectors.ticketBasicData.nextStepButton); - await page.waitToClick(selectors.ticketBasicData.withoutNegatives); await page.waitToClick(selectors.ticketBasicData.finalizeButton); await page.waitForState('ticket.card.summary'); diff --git a/e2e/paths/05-ticket/20_future.spec.js b/e2e/paths/05-ticket/20_future.spec.js new file mode 100644 index 000000000..4fee9523b --- /dev/null +++ b/e2e/paths/05-ticket/20_future.spec.js @@ -0,0 +1,266 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Ticket Future path', () => { + let browser; + let page; + + beforeAll(async () => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('employee', 'ticket'); + await page.accessToSection('ticket.future'); + }); + + afterAll(async () => { + await browser.close(); + }); + + const now = new Date(); + const tomorrow = new Date(now.getDate() + 1); + const ticket = { + originDated: now, + futureDated: now, + linesMax: '9999', + litersMax: '9999', + warehouseFk: 'Warehouse One' + }; + + it('should show errors snackbar because of the required data', 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.litersMax); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + expect(message.text).toContain('litersMax is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.linesMax); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + expect(message.text).toContain('linesMax is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.futureDated); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + expect(message.text).toContain('futureDated is a required argument'); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.originDated); + await page.waitToClick(selectors.ticketFuture.submit); + message = await page.waitForSnackbar(); + expect(message.text).toContain('originDated is a required argument'); + }); + + it('should search with the required data', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search with the origin shipped today', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.pickDate(selectors.ticketFuture.shipped, now); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search with the origin shipped tomorrow', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.pickDate(selectors.ticketFuture.shipped, tomorrow); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); + }); + + it('should search with the destination shipped today', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.shipped); + await page.pickDate(selectors.ticketFuture.tfShipped, now); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search with the destination shipped tomorrow', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.pickDate(selectors.ticketFuture.tfShipped, tomorrow); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); + }); + + it('should search with the origin IPT', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.shipped); + await page.clearInput(selectors.ticketFuture.tfShipped); + await page.clearInput(selectors.ticketFuture.ipt); + await page.clearInput(selectors.ticketFuture.tfIpt); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.tfState); + + await page.autocompleteSearch(selectors.ticketFuture.ipt, 'Horizontal'); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); + }); + + it('should search with the destination IPT', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.shipped); + await page.clearInput(selectors.ticketFuture.tfShipped); + await page.clearInput(selectors.ticketFuture.ipt); + await page.clearInput(selectors.ticketFuture.tfIpt); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.tfState); + + await page.autocompleteSearch(selectors.ticketFuture.tfIpt, 'Horizontal'); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); + }); + + it('should search with the origin grouped state', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.shipped); + await page.clearInput(selectors.ticketFuture.tfShipped); + await page.clearInput(selectors.ticketFuture.ipt); + await page.clearInput(selectors.ticketFuture.tfIpt); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.tfState); + + await page.autocompleteSearch(selectors.ticketFuture.state, 'Free'); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 3); + }); + + it('should search with the destination grouped state', async () => { + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + + await page.clearInput(selectors.ticketFuture.shipped); + await page.clearInput(selectors.ticketFuture.tfShipped); + await page.clearInput(selectors.ticketFuture.ipt); + await page.clearInput(selectors.ticketFuture.tfIpt); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.tfState); + + await page.autocompleteSearch(selectors.ticketFuture.tfState, 'Free'); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 0); + + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.clearInput(selectors.ticketFuture.shipped); + await page.clearInput(selectors.ticketFuture.tfShipped); + await page.clearInput(selectors.ticketFuture.ipt); + await page.clearInput(selectors.ticketFuture.tfIpt); + await page.clearInput(selectors.ticketFuture.state); + await page.clearInput(selectors.ticketFuture.tfState); + + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with an ID Origin', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableId, "13"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 2); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with an ID Destination', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableTfId, "12"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with an IPT Origin', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.autocompleteSearch(selectors.ticketFuture.tableIpt, 'Vertical'); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with an IPT Destination', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.autocompleteSearch(selectors.ticketFuture.tableTfIpt, 'Vertical'); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with especified Lines', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableLines, "0"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableLines, "1"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should search in smart-table with especified Liters', async () => { + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableLiters, "0"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 1); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.write(selectors.ticketFuture.tableLiters, "28"); + await page.keyboard.press("Enter"); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 5); + + await page.waitToClick(selectors.ticketFuture.tableButtonSearch); + await page.waitToClick(selectors.ticketFuture.openAdvancedSearchButton); + await page.waitToClick(selectors.ticketFuture.submit); + await page.waitForNumberOfElements(selectors.ticketFuture.table, 4); + }); + + it('should check the three last tickets and move to the future', async () => { + await page.waitToClick(selectors.ticketFuture.multiCheck); + await page.waitToClick(selectors.ticketFuture.firstCheck); + await page.waitToClick(selectors.ticketFuture.moveButton); + await page.waitToClick(selectors.ticketFuture.acceptButton); + const message = await page.waitForSnackbar(); + expect(message.text).toContain('Tickets moved successfully!'); + }); + +}); diff --git a/front/core/styles/fonts/MaterialIcons-Regular.woff2 b/front/core/styles/fonts/MaterialIcons-Regular.woff2 index 9d1dfcc70..2eb4fb499 100644 Binary files a/front/core/styles/fonts/MaterialIcons-Regular.woff2 and b/front/core/styles/fonts/MaterialIcons-Regular.woff2 differ diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 887a657a3..36da9f661 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -135,7 +135,8 @@ "You don't have privileges to change the zone": "You don't have privileges to change the zone or for these parameters there are more than one shipping options, talk to agencies", "Not enough privileges to edit a client": "Not enough privileges to edit a client", "Claim pickup order sent": "Claim pickup order sent [({{claimId}})]({{{claimUrl}}}) to client *{{clientName}}*", - "You don't have grant privilege": "You don't have grant privilege", + "You don't have grant privilege": "You don't have grant privilege", "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user", + "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production" } diff --git a/loopback/locale/es.json b/loopback/locale/es.json index b85edb5fe..747007713 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -241,6 +241,7 @@ "Claim pickup order sent": "Reclamación Orden de recogida enviada [({{claimId}})]({{{claimUrl}}}) al cliente *{{clientName}}*", "You don't have grant privilege": "No tienes privilegios para dar privilegios", "You don't own the role and you can't assign it to another user": "No eres el propietario del rol y no puedes asignarlo a otro usuario", + "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) fusionado con [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Already has this status": "Ya tiene este estado", "There aren't records for this week": "No existen registros para esta semana", "Empty data source": "Origen de datos vacio" diff --git a/modules/client/back/methods/client/setPassword.js b/modules/client/back/methods/client/setPassword.js index 2f0ebca5b..e3fc9bbf8 100644 --- a/modules/client/back/methods/client/setPassword.js +++ b/modules/client/back/methods/client/setPassword.js @@ -23,12 +23,6 @@ module.exports = Self => { Self.setPassword = async function(ctx, id, newPassword) { const models = Self.app.models; - const userId = ctx.req.accessToken.userId; - - const isSalesPerson = await models.Account.hasRole(userId, 'salesPerson'); - - if (!isSalesPerson) - throw new UserError(`Not enough privileges to edit a client`); const isClient = await models.Client.findById(id, null); const isUserAccount = await models.UserAccount.findById(id, null); diff --git a/modules/client/back/methods/client/specs/setPassword.spec.js b/modules/client/back/methods/client/specs/setPassword.spec.js index 13065ca12..03334918b 100644 --- a/modules/client/back/methods/client/specs/setPassword.spec.js +++ b/modules/client/back/methods/client/specs/setPassword.spec.js @@ -6,21 +6,6 @@ describe('Client setPassword', () => { req: {accessToken: {userId: salesPersonId}} }; - it(`should throw an error if you don't have enough permissions`, async() => { - let error; - const employeeId = 1; - const ctx = { - req: {accessToken: {userId: employeeId}} - }; - try { - await models.Client.setPassword(ctx, 1, 't0pl3v3l.p455w0rd!'); - } catch (e) { - error = e; - } - - expect(error.message).toEqual(`Not enough privileges to edit a client`); - }); - it('should throw an error the setPassword target is not just a client but a worker', async() => { let error; diff --git a/modules/client/back/models/greuge.js b/modules/client/back/models/greuge.js index bd5f2865d..42820fd60 100644 --- a/modules/client/back/models/greuge.js +++ b/modules/client/back/models/greuge.js @@ -1,6 +1,21 @@ +const LoopBackContext = require('loopback-context'); + module.exports = function(Self) { require('../methods/greuge/sumAmount')(Self); + Self.observe('before save', function(ctx, next) { + const loopBackContext = LoopBackContext.getCurrentContext(); + + let userFk = loopBackContext.active.accessToken.userId; + + if (ctx.instance) + ctx.instance.userFk = userFk; + else + ctx.data.userFk = userFk; + + next(); + }); + Self.validatesLengthOf('description', { max: 45, message: 'Description should have maximum of 45 characters' diff --git a/modules/client/back/models/greuge.json b/modules/client/back/models/greuge.json index 918ff0ca5..625bf4e28 100644 --- a/modules/client/back/models/greuge.json +++ b/modules/client/back/models/greuge.json @@ -2,9 +2,9 @@ "name": "Greuge", "base": "Loggable", "log": { - "model": "ClientLog", - "relation": "client", - "showField": "description" + "model": "ClientLog", + "relation": "client", + "showField": "description" }, "options": { "mysql": { @@ -35,7 +35,6 @@ "type": "number", "required": true } - }, "relations": { "client": { @@ -52,6 +51,11 @@ "type": "belongsTo", "model": "GreugeType", "foreignKey": "greugeTypeFk" + }, + "user": { + "type": "belongsTo", + "model": "Account", + "foreignKey": "userFk" } } } \ No newline at end of file diff --git a/modules/client/front/greuge/index/index.html b/modules/client/front/greuge/index/index.html index b48fe9466..459d92fc7 100644 --- a/modules/client/front/greuge/index/index.html +++ b/modules/client/front/greuge/index/index.html @@ -29,6 +29,7 @@ Date + Created by Comment Type Amount @@ -37,6 +38,8 @@ {{::greuge.shipped | date:'dd/MM/yyyy HH:mm' }} + {{::greuge.user.name}} {{::greuge.description}} @@ -57,3 +60,4 @@ vn-bind="+" fixed-bottom-right> + \ No newline at end of file diff --git a/modules/client/front/greuge/index/index.js b/modules/client/front/greuge/index/index.js index 2451167a4..7a5ccc531 100644 --- a/modules/client/front/greuge/index/index.js +++ b/modules/client/front/greuge/index/index.js @@ -8,6 +8,12 @@ class Controller extends Section { include: [ { relation: 'greugeType', + scope: { + fields: ['id', 'name'] + }, + }, + { + relation: 'user', scope: { fields: ['id', 'name'] } diff --git a/modules/client/front/greuge/index/locale/es.yml b/modules/client/front/greuge/index/locale/es.yml index 513e6ff7b..d1f202862 100644 --- a/modules/client/front/greuge/index/locale/es.yml +++ b/modules/client/front/greuge/index/locale/es.yml @@ -1,4 +1,5 @@ Date: Fecha Comment: Comentario Amount: Importe -Type: Tipo \ No newline at end of file +Type: Tipo +Created by: Creado por \ No newline at end of file diff --git a/modules/invoiceIn/front/descriptor/index.html b/modules/invoiceIn/front/descriptor/index.html index 819615c20..a2b48b5cd 100644 --- a/modules/invoiceIn/front/descriptor/index.html +++ b/modules/invoiceIn/front/descriptor/index.html @@ -26,11 +26,13 @@ Clone Invoice Show agricultural invoice as PDF Send agricultural invoice as PDF diff --git a/modules/item/back/methods/item/new.js b/modules/item/back/methods/item/new.js index 0771b6b14..fae37836f 100644 --- a/modules/item/back/methods/item/new.js +++ b/modules/item/back/methods/item/new.js @@ -49,6 +49,10 @@ module.exports = Self => { const provisionalName = params.provisionalName; delete params.provisionalName; + const itemType = await models.ItemType.findById(params.typeFk, myOptions); + + params.isLaid = itemType.isLaid; + const item = await models.Item.create(params, myOptions); const typeTags = await models.ItemTypeTag.find({ diff --git a/modules/item/back/methods/item/specs/new.spec.js b/modules/item/back/methods/item/specs/new.spec.js index 049ad0ff2..7364faa7d 100644 --- a/modules/item/back/methods/item/specs/new.spec.js +++ b/modules/item/back/methods/item/specs/new.spec.js @@ -15,6 +15,11 @@ describe('item new()', () => { }; let item = await models.Item.new(itemParams, options); + + let itemType = await models.ItemType.findById(item.typeFk, options); + + item.isLaid = itemType.isLaid; + const temporalNameTag = await models.Tag.findOne({where: {name: 'Nombre temporal'}}, options); const temporalName = await models.ItemTag.findOne({ @@ -26,9 +31,14 @@ describe('item new()', () => { item = await models.Item.findById(item.id, null, options); + itemType = await models.ItemType.findById(item.typeFk, options); + + item.isLaid = itemType.isLaid; + expect(item.intrastatFk).toEqual(5080000); expect(item.originFk).toEqual(1); expect(item.typeFk).toEqual(2); + expect(item.isLaid).toBeFalse(); expect(item.name).toEqual('planta'); expect(temporalName.value).toEqual('planta'); diff --git a/modules/item/back/models/item-type.json b/modules/item/back/models/item-type.json index 74cdf3fc8..c5c920b2f 100644 --- a/modules/item/back/models/item-type.json +++ b/modules/item/back/models/item-type.json @@ -26,6 +26,9 @@ }, "isUnconventionalSize": { "type": "number" + }, + "isLaid": { + "type": "boolean" } }, "relations": { diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json index 704c97434..2f58c30a9 100644 --- a/modules/item/back/models/item.json +++ b/modules/item/back/models/item.json @@ -143,6 +143,9 @@ }, "packingShelve": { "type": "number" + }, + "isLaid": { + "type": "boolean" } }, "relations": { diff --git a/modules/monitor/back/methods/sales-monitor/ordersFilter.js b/modules/monitor/back/methods/sales-monitor/ordersFilter.js index 6ae4042b6..a80ea822e 100644 --- a/modules/monitor/back/methods/sales-monitor/ordersFilter.js +++ b/modules/monitor/back/methods/sales-monitor/ordersFilter.js @@ -53,7 +53,7 @@ module.exports = Self => { JOIN client c ON c.id = o.customer_id JOIN address a ON a.id = o.address_id JOIN agencyMode am ON am.id = o.agency_id - JOIN user u ON u.id = c.salesPersonFk + JOIN account.user u ON u.id = c.salesPersonFk JOIN workerTeamCollegues wtc ON c.salesPersonFk = wtc.collegueFk`); if (!filter.where) filter.where = {}; diff --git a/modules/monitor/back/methods/sales-monitor/specs/salesFilter.spec.js b/modules/monitor/back/methods/sales-monitor/specs/salesFilter.spec.js index 0682cef09..d2e1a5bec 100644 --- a/modules/monitor/back/methods/sales-monitor/specs/salesFilter.spec.js +++ b/modules/monitor/back/methods/sales-monitor/specs/salesFilter.spec.js @@ -11,7 +11,7 @@ describe('SalesMonitor salesFilter()', () => { const filter = {order: 'id DESC'}; const result = await models.SalesMonitor.salesFilter(ctx, filter, options); - expect(result.length).toEqual(27); + expect(result.length).toBeGreaterThan(25); await tx.rollback(); } catch (e) { @@ -39,7 +39,7 @@ describe('SalesMonitor salesFilter()', () => { const filter = {}; const result = await models.SalesMonitor.salesFilter(ctx, filter, options); - expect(result.length).toEqual(16); + expect(result.length).toBeGreaterThan(15); await tx.rollback(); } catch (e) { @@ -87,7 +87,7 @@ describe('SalesMonitor salesFilter()', () => { const filter = {}; const result = await models.SalesMonitor.salesFilter(ctx, filter, options); - expect(result.length).toEqual(27); + expect(result.length).toBeGreaterThan(20); await tx.rollback(); } catch (e) { @@ -130,7 +130,7 @@ describe('SalesMonitor salesFilter()', () => { const length = result.length; const anyResult = result[Math.floor(Math.random() * Math.floor(length))]; - expect(length).toEqual(10); + expect(length).toBeGreaterThan(10); expect(anyResult.state).toMatch(/(Libre|Arreglar)/); await tx.rollback(); @@ -171,7 +171,7 @@ describe('SalesMonitor salesFilter()', () => { const filter = {}; const result = await models.SalesMonitor.salesFilter(ctx, filter, options); - expect(result.length).toEqual(23); + expect(result.length).toBeGreaterThan(20); await tx.rollback(); } catch (e) { diff --git a/modules/route/back/methods/route/sendSms.js b/modules/route/back/methods/route/sendSms.js new file mode 100644 index 000000000..d1c3efa35 --- /dev/null +++ b/modules/route/back/methods/route/sendSms.js @@ -0,0 +1,39 @@ + +module.exports = Self => { + Self.remoteMethodCtx('sendSms', { + description: 'Sends a SMS to each client of the routes, each client only recieves the SMS once', + accessType: 'WRITE', + accepts: [ + { + arg: 'destination', + type: 'string', + description: 'A comma separated string of destinations', + required: true, + }, + { + arg: 'message', + type: 'string', + required: true, + }], + returns: { + type: 'object', + root: true + }, + http: { + path: `/sendSms`, + verb: 'POST' + } + }); + + Self.sendSms = async(ctx, destination, message) => { + const targetClients = destination.split(','); + + const allSms = []; + for (let client of targetClients) { + let sms = await Self.app.models.Sms.send(ctx, client, message); + allSms.push(sms); + } + + return allSms; + }; +}; diff --git a/modules/route/back/models/route.js b/modules/route/back/models/route.js index f5406728a..08cabd30e 100644 --- a/modules/route/back/models/route.js +++ b/modules/route/back/models/route.js @@ -12,6 +12,7 @@ module.exports = Self => { require('../methods/route/updateWorkCenter')(Self); require('../methods/route/driverRoutePdf')(Self); require('../methods/route/driverRouteEmail')(Self); + require('../methods/route/sendSms')(Self); Self.validate('kmStart', validateDistance, { message: 'Distance must be lesser than 1000' diff --git a/modules/route/front/descriptor/index.html b/modules/route/front/descriptor/index.html index fc1d3419c..6d6fb082e 100644 --- a/modules/route/front/descriptor/index.html +++ b/modules/route/front/descriptor/index.html @@ -20,6 +20,13 @@ translate> Update volume + + Delete route +
diff --git a/modules/route/front/descriptor/index.js b/modules/route/front/descriptor/index.js index 2dc512b67..aa47044b1 100644 --- a/modules/route/front/descriptor/index.js +++ b/modules/route/front/descriptor/index.js @@ -34,6 +34,14 @@ class Controller extends Descriptor { }); } + deleteCurrentRoute() { + this.$http.delete(`Routes/${this.id}`) + .then(() => { + this.vnApp.showSuccess(this.$t('Route deleted')); + this.$state.go('route.index'); + }); + } + loadData() { const filter = { fields: [ diff --git a/modules/route/front/descriptor/index.spec.js b/modules/route/front/descriptor/index.spec.js index ab996d9b0..f43666c8b 100644 --- a/modules/route/front/descriptor/index.spec.js +++ b/modules/route/front/descriptor/index.spec.js @@ -23,4 +23,20 @@ describe('vnRouteDescriptorPopover', () => { expect(controller.route).toEqual(response); }); }); + + describe('deleteCurrentRoute()', () => { + it(`should perform a delete query to delete the current route`, () => { + const id = 1; + + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller._id = id; + $httpBackend.expectDELETE(`Routes/${id}`).respond(200); + controller.deleteCurrentRoute(); + $httpBackend.flush(); + + expect(controller.route).toBeUndefined(); + expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Route deleted'); + }); + }); }); diff --git a/modules/route/front/descriptor/locale/es.yml b/modules/route/front/descriptor/locale/es.yml index 63fa7202b..23068fbf8 100644 --- a/modules/route/front/descriptor/locale/es.yml +++ b/modules/route/front/descriptor/locale/es.yml @@ -4,4 +4,6 @@ Send route report: Enviar informe de ruta Show route report: Ver informe de ruta Update volume: Actualizar volumen Volume updated: Volumen actualizado +Delete route: Borrar ruta +Route deleted: Ruta borrada Are you sure you want to update the volume?: Estas seguro que quieres actualizar el volumen? \ No newline at end of file diff --git a/modules/route/front/index.js b/modules/route/front/index.js index 55cb745e1..c43048df5 100644 --- a/modules/route/front/index.js +++ b/modules/route/front/index.js @@ -15,3 +15,4 @@ import './agency-term/index'; import './agency-term/createInvoiceIn'; import './agency-term-search-panel'; import './ticket-popup'; +import './sms'; diff --git a/modules/route/front/index/locale/es.yml b/modules/route/front/index/locale/es.yml index e4c5c8c55..3db84d81e 100644 --- a/modules/route/front/index/locale/es.yml +++ b/modules/route/front/index/locale/es.yml @@ -8,4 +8,6 @@ Hour finished: Hora fin Go to route: Ir a la ruta You must select a valid time: Debe seleccionar una hora válida You must select a valid date: Debe seleccionar una fecha válida -Mark as served: Marcar como servidas \ No newline at end of file +Mark as served: Marcar como servidas +Retrieving data from the routes: Recuperando datos de las rutas +Send SMS to all clients: Mandar sms a todos los clientes de las rutas \ No newline at end of file diff --git a/modules/route/front/sms/index.html b/modules/route/front/sms/index.html new file mode 100644 index 000000000..0d7dd7c11 --- /dev/null +++ b/modules/route/front/sms/index.html @@ -0,0 +1,36 @@ + + +
+ + + + + + + {{'Characters remaining' | translate}}: + + {{$ctrl.charactersRemaining()}} + + + +
+
+ + + + +
\ No newline at end of file diff --git a/modules/route/front/sms/index.js b/modules/route/front/sms/index.js new file mode 100644 index 000000000..d8b1fc134 --- /dev/null +++ b/modules/route/front/sms/index.js @@ -0,0 +1,47 @@ +import ngModule from '../module'; +import Component from 'core/lib/component'; +import './style.scss'; + +class Controller extends Component { + open() { + this.$.SMSDialog.show(); + } + + charactersRemaining() { + const element = this.$.message; + const value = element.input.value; + + const maxLength = 160; + const textAreaLength = new Blob([value]).size; + return maxLength - textAreaLength; + } + + onResponse() { + try { + if (!this.sms.destination) + 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(`Routes/sendSms`, this.sms).then(res => { + this.vnApp.showMessage(this.$t('SMS sent!')); + + if (res.data) this.emit('send', {response: res.data}); + }); + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + return true; + } +} + +ngModule.vnComponent('vnRouteSms', { + template: require('./index.html'), + controller: Controller, + bindings: { + sms: '<', + } +}); diff --git a/modules/route/front/sms/index.spec.js b/modules/route/front/sms/index.spec.js new file mode 100644 index 000000000..42bf30931 --- /dev/null +++ b/modules/route/front/sms/index.spec.js @@ -0,0 +1,71 @@ +import './index'; + +describe('Route', () => { + describe('Component vnRouteSms', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('route')); + + beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + let $scope = $rootScope.$new(); + const $element = angular.element(''); + controller = $componentController('vnRouteSms', {$element, $scope}); + controller.$.message = { + input: { + value: 'My SMS' + } + }; + })); + + describe('onResponse()', () => { + it('should perform a POST query and show a success snackbar', () => { + let params = {destinationFk: 1101, destination: 111111111, message: 'My SMS'}; + controller.sms = {destinationFk: 1101, destination: 111111111, message: 'My SMS'}; + + jest.spyOn(controller.vnApp, 'showMessage'); + $httpBackend.expect('POST', `Routes/sendSms`, params).respond(200, params); + + controller.onResponse(); + $httpBackend.flush(); + + expect(controller.vnApp.showMessage).toHaveBeenCalledWith('SMS sent!'); + }); + + it('should call onResponse without the destination and show an error snackbar', () => { + controller.sms = {destinationFk: 1101, message: 'My SMS'}; + + jest.spyOn(controller.vnApp, 'showError'); + + controller.onResponse(); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`The destination can't be empty`); + }); + + it('should call onResponse without the message and show an error snackbar', () => { + controller.sms = {destinationFk: 1101, destination: 222222222}; + + jest.spyOn(controller.vnApp, 'showError'); + + controller.onResponse(); + + expect(controller.vnApp.showError).toHaveBeenCalledWith(`The message can't be empty`); + }); + }); + + describe('charactersRemaining()', () => { + it('should return the characters remaining in a element', () => { + controller.$.message = { + input: { + value: 'My message 0€' + } + }; + + let result = controller.charactersRemaining(); + + expect(result).toEqual(145); + }); + }); + }); +}); diff --git a/modules/route/front/sms/locale/es.yml b/modules/route/front/sms/locale/es.yml new file mode 100644 index 000000000..0168a6eb6 --- /dev/null +++ b/modules/route/front/sms/locale/es.yml @@ -0,0 +1,9 @@ +Send SMS to the selected tickets: Enviar SMS a los tickets seleccionados +Routes to notify: Rutas a notificar +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 +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/route/front/sms/style.scss b/modules/route/front/sms/style.scss new file mode 100644 index 000000000..84571a5f4 --- /dev/null +++ b/modules/route/front/sms/style.scss @@ -0,0 +1,5 @@ +@import "variables"; + +.SMSDialog { + min-width: 400px +} \ No newline at end of file diff --git a/modules/route/front/tickets/index.html b/modules/route/front/tickets/index.html index 1f91276e7..dae894ac7 100644 --- a/modules/route/front/tickets/index.html +++ b/modules/route/front/tickets/index.html @@ -29,13 +29,21 @@ disabled="!$ctrl.isChecked" ng-click="$ctrl.deletePriority()" vn-tooltip="Delete priority" - icon="filter_alt_off"> + icon="filter_alt"> + + @@ -149,19 +157,29 @@ route="$ctrl.$params" parent-reload="$ctrl.$.model.refresh()"> - - - +
+ + + + + + +
- \ No newline at end of file + + + + \ No newline at end of file diff --git a/modules/route/front/tickets/index.js b/modules/route/front/tickets/index.js index e78d9b8b7..80f8ad4f4 100644 --- a/modules/route/front/tickets/index.js +++ b/modules/route/front/tickets/index.js @@ -161,6 +161,37 @@ class Controller extends Section { throw error; }); } + + async sendSms() { + try { + const clientsFk = []; + const clientsName = []; + const clients = []; + + const selectedTickets = this.getSelectedItems(this.$.$ctrl.tickets); + + for (let ticket of selectedTickets) { + clientsFk.push(ticket.clientFk); + let userContact = await this.$http.get(`Clients/${ticket.clientFk}`); + clientsName.push(userContact.data.name); + clients.push(userContact.data.phone); + } + + const destinationFk = String(clientsFk); + const destination = String(clients); + + this.newSMS = Object.assign({ + destinationFk: destinationFk, + destination: destination + }); + + this.$.sms.open(); + return true; + } catch (e) { + this.vnApp.showError(this.$t(e.message)); + return false; + } + } } ngModule.vnComponent('vnRouteTickets', { diff --git a/modules/ticket/back/methods/expedition/filter.js b/modules/ticket/back/methods/expedition/filter.js index 65f840d80..fcf0bd1b3 100644 --- a/modules/ticket/back/methods/expedition/filter.js +++ b/modules/ticket/back/methods/expedition/filter.js @@ -37,7 +37,6 @@ module.exports = Self => { i1.name packageItemName, e.counter, i2.name freightItemName, - e.itemFk, u.name userName, e.created, e.externalId, @@ -50,10 +49,10 @@ module.exports = Self => { est.description state FROM vn.expedition e LEFT JOIN vn.expeditionStateType est ON est.id = e.stateTypeFk - LEFT JOIN vn.item i2 ON i2.id = e.itemFk INNER JOIN vn.item i1 ON i1.id = e.freightItemFk LEFT JOIN vn.packaging p ON p.id = e.packagingFk LEFT JOIN vn.item i3 ON i3.id = p.itemFk + LEFT JOIN vn.item i2 ON i2.id = p.itemFk LEFT JOIN account.user u ON u.id = e.workerFk LEFT JOIN vn.expeditionScan es ON es.expeditionFk = e.id LEFT JOIN account.user su ON su.id = es.workerFk diff --git a/modules/ticket/back/methods/ticket-future/getTicketsFuture.js b/modules/ticket/back/methods/ticket-future/getTicketsFuture.js new file mode 100644 index 000000000..0fcc21182 --- /dev/null +++ b/modules/ticket/back/methods/ticket-future/getTicketsFuture.js @@ -0,0 +1,224 @@ +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('vn-loopback/util/filter').buildFilter; +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('getTicketsFuture', { + description: 'Find all tickets that can be moved to the future', + accessType: 'READ', + accepts: [ + { + arg: 'originDated', + type: 'date', + description: 'The date in question', + required: true + }, + { + arg: 'futureDated', + type: 'date', + description: 'The date to probe', + required: true + }, + { + arg: 'litersMax', + type: 'number', + description: 'Maximum volume of tickets to catapult', + required: true + }, + { + arg: 'linesMax', + type: 'number', + description: 'Maximum number of lines of tickets to catapult', + required: true + }, + { + arg: 'warehouseFk', + type: 'number', + description: 'Warehouse identifier', + required: true + }, + { + arg: 'shipped', + type: 'date', + description: 'Origin shipped', + required: false + }, + { + arg: 'tfShipped', + type: 'date', + description: 'Destination shipped', + required: false + }, + { + arg: 'ipt', + type: 'string', + description: 'Origin Item Packaging Type', + required: false + }, + { + arg: 'tfIpt', + type: 'string', + description: 'Destination Item Packaging Type', + required: false + }, + { + arg: 'id', + type: 'number', + description: 'Origin id', + required: false + }, + { + arg: 'tfId', + type: 'number', + description: 'Destination id', + required: false + }, + { + arg: 'state', + type: 'string', + description: 'Origin state', + required: false + }, + { + arg: 'tfState', + type: 'string', + description: 'Destination state', + required: false + }, + { + arg: 'problems', + type: 'boolean', + description: `Whether to show only tickets with problems`, + required: false + }, + { + arg: 'filter', + type: 'object', + description: `Filter defining where, order, offset, and limit - must be a JSON-encoded string` + } + ], + returns: { + type: ['object'], + root: true + }, + http: { + path: `/getTicketsFuture`, + verb: 'GET' + } + }); + + Self.getTicketsFuture = async (ctx, options) => { + const args = ctx.args; + const conn = Self.dataSource.connector; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + const where = buildFilter(ctx.args, (param, value) => { + switch (param) { + case 'id': + return { 'f.id': value }; + case 'tfId': + return { 'f.ticketFuture': value }; + case 'shipped': + return { 'f.shipped': value }; + case 'tfShipped': + return { 'f.tfShipped': value }; + case 'ipt': + return { 'f.ipt': value }; + case 'tfIpt': + return { 'f.tfIpt': value }; + case 'state': + return { 'f.code': { like: `%${value}%` } }; + case 'tfState': + return { 'f.tfCode': { like: `%${value}%` } }; + } + }); + + let filter = mergeFilters(ctx.args.filter, { where }); + const stmts = []; + let stmt; + + stmt = new ParameterizedSQL( + `CALL vn.ticket_canbePostponed(?,?,?,?,?)`, + [args.originDated, args.futureDated, args.litersMax, args.linesMax, args.warehouseFk]); + + stmts.push(stmt); + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.sale_getProblems'); + + stmt = new ParameterizedSQL(` + CREATE TEMPORARY TABLE tmp.sale_getProblems + (INDEX (ticketFk)) + ENGINE = MEMORY + SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped + FROM tmp.filter f + LEFT JOIN alertLevel al ON al.id = f.alertLevel + WHERE (al.code = 'FREE' OR f.alertLevel IS NULL)`); + stmts.push(stmt); + + stmts.push('CALL ticket_getProblems(FALSE)'); + + stmt = new ParameterizedSQL(` + SELECT f.*, tp.* + FROM tmp.filter f + LEFT JOIN tmp.ticket_problems tp ON tp.ticketFk = f.id`); + + if (args.problems != undefined && (!args.originDated && !args.futureDated)) + throw new UserError('Choose a date range or days forward'); + + let condition; + let hasProblem; + let range; + let hasWhere; + switch (args.problems) { + case true: + condition = `or`; + hasProblem = true; + range = { neq: null }; + hasWhere = true; + break; + + case false: + condition = `and`; + hasProblem = null; + range = null; + hasWhere = true; + break; + } + + const problems = { + [condition]: [ + { 'tp.isFreezed': hasProblem }, + { 'tp.risk': hasProblem }, + { 'tp.hasTicketRequest': hasProblem }, + { 'tp.itemShortage': range }, + { 'tp.hasComponentLack': hasProblem }, + { 'tp.isTooLittle': hasProblem } + ] + }; + + if (hasWhere) { + filter = mergeFilters(filter, { where: problems }); + } + + stmt.merge(conn.makeWhere(filter.where)); + stmt.merge(conn.makeOrderBy(filter.order)); + stmt.merge(conn.makeLimit(filter)); + + const ticketsIndex = stmts.push(stmt) - 1; + + stmts.push( + `DROP TEMPORARY TABLE + tmp.filter, + tmp.ticket_problems`); + + const sql = ParameterizedSQL.join(stmts, ';'); + + const result = await conn.executeStmt(sql, myOptions); + + return result[ticketsIndex]; + }; +}; diff --git a/modules/ticket/back/methods/ticket-future/spec/getTicketsFuture.spec.js b/modules/ticket/back/methods/ticket-future/spec/getTicketsFuture.spec.js new file mode 100644 index 000000000..502ea3074 --- /dev/null +++ b/modules/ticket/back/methods/ticket-future/spec/getTicketsFuture.spec.js @@ -0,0 +1,438 @@ +const models = require('vn-loopback/server/server').models; + +describe('TicketFuture getTicketsFuture()', () => { + const today = new Date(); + today.setHours(0, 0, 0, 0); + const tomorrow = new Date(today.getDate() + 1); + + it('should return the tickets passing the required data', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the problems on true', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + problems: true + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the problems on false', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + problems: false + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the problems on null', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + problems: null + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the correct origin shipped', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + shipped: today + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the an incorrect origin shipped', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + shipped: tomorrow + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the correct destination shipped', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfShipped: today + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the an incorrect destination shipped', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfShipped: tomorrow + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the OK State in origin date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + state: "OK" + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(1); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the OK State in destination date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfState: "OK" + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the correct IPT in origin date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + ipt: null + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the incorrect IPT in origin date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + ipt: 0 + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the correct IPT in destination date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfIpt: null + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the incorrect IPT in destination date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfIpt: 0 + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(0); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the ID in origin date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + id: 13 + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(1); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + + it('should return the tickets matching the ID in destination date', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + + const args = { + originDated: today, + futureDated: today, + litersMax: 9999, + linesMax: 9999, + warehouseFk: 1, + tfId: 12 + }; + + const ctx = { req: { accessToken: { userId: 9 } }, args }; + const result = await models.Ticket.getTicketsFuture(ctx, options); + + expect(result.length).toEqual(4); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); + +}); diff --git a/modules/ticket/back/methods/ticket/merge.js b/modules/ticket/back/methods/ticket/merge.js new file mode 100644 index 000000000..04f8d83af --- /dev/null +++ b/modules/ticket/back/methods/ticket/merge.js @@ -0,0 +1,65 @@ +const dateUtil = require('vn-loopback/util/date'); + +module.exports = Self => { + Self.remoteMethodCtx('merge', { + description: 'Merge one ticket into another', + accessType: 'WRITE', + accepts: [ + { + arg: 'tickets', + type: ['object'], + description: 'The array of tickets', + required: false + } + ], + returns: { + type: 'string', + root: true + }, + http: { + path: `/merge`, + verb: 'POST' + } + }); + + Self.merge = async(ctx, tickets, options) => { + const httpRequest = ctx.req; + const $t = httpRequest.__; + const origin = httpRequest.headers.origin; + const models = Self.app.models; + const myOptions = {}; + let tx; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + for (let ticket of tickets) { + const fullPath = `${origin}/#!/ticket/${ticket.id}/summary`; + const fullPathFuture = `${origin}/#!/ticket/${ticket.ticketFuture}/summary`; + const message = $t('Ticket merged', { + originDated: dateUtil.toString(new Date(ticket.originETD)), + futureDated: dateUtil.toString(new Date(ticket.destETD)), + id: ticket.id, + tfId: ticket.ticketFuture, + fullPath, + fullPathFuture + }); + if (!ticket.id || !ticket.ticketFuture) continue; + await models.Sale.updateAll({ticketFk: ticket.id}, {ticketFk: ticket.ticketFuture}, myOptions); + await models.Ticket.setDeleted(ctx, ticket.id, myOptions); + await models.Chat.sendCheckingPresence(ctx, ticket.workerFk, message); + } + if (tx) + await tx.commit(); + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/ticket/back/methods/ticket/specs/filter.spec.js b/modules/ticket/back/methods/ticket/specs/filter.spec.js index c3dc40092..008068ff2 100644 --- a/modules/ticket/back/methods/ticket/specs/filter.spec.js +++ b/modules/ticket/back/methods/ticket/specs/filter.spec.js @@ -11,7 +11,7 @@ describe('ticket filter()', () => { const filter = {order: 'id DESC'}; const result = await models.Ticket.filter(ctx, filter, options); - expect(result.length).toEqual(27); + expect(result.length).toBeGreaterThan(25); await tx.rollback(); } catch (e) { @@ -87,7 +87,7 @@ describe('ticket filter()', () => { const filter = {}; const result = await models.Ticket.filter(ctx, filter, options); - expect(result.length).toEqual(27); + expect(result.length).toBeGreaterThan(25); await tx.rollback(); } catch (e) { @@ -130,7 +130,7 @@ describe('ticket filter()', () => { const length = result.length; const anyResult = result[Math.floor(Math.random() * Math.floor(length))]; - expect(length).toEqual(10); + expect(length).toBeGreaterThan(10); expect(anyResult.state).toMatch(/(Libre|Arreglar)/); await tx.rollback(); @@ -175,7 +175,7 @@ describe('ticket filter()', () => { const filter = {}; const result = await models.Ticket.filter(ctx, filter, options); - expect(result.length).toEqual(23); + expect(result.length).toEqual(26); await tx.rollback(); } catch (e) { @@ -232,7 +232,7 @@ describe('ticket filter()', () => { const filter = {}; const result = await models.Ticket.filter(ctx, filter, options); - expect(result.length).toEqual(22); + expect(result.length).toBeGreaterThan(20); await tx.rollback(); } catch (e) { @@ -270,7 +270,7 @@ describe('ticket filter()', () => { const filter = {}; const result = await models.Ticket.filter(ctx, filter, options); - expect(result.length).toEqual(27); + expect(result.length).toBeGreaterThan(25); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/methods/ticket/specs/merge.spec.js b/modules/ticket/back/methods/ticket/specs/merge.spec.js new file mode 100644 index 000000000..713f86ad6 --- /dev/null +++ b/modules/ticket/back/methods/ticket/specs/merge.spec.js @@ -0,0 +1,58 @@ +const models = require('vn-loopback/server/server').models; +const LoopBackContext = require('loopback-context'); + +describe('ticket merge()', () => { + const tickets = [{ + id: 13, + ticketFuture: 12, + workerFk: 1, + originETD: new Date(), + destETD: new Date() + }]; + + const activeCtx = { + accessToken: { userId: 9 }, + }; + + beforeEach(() => { + spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({ + active: activeCtx + }); + }); + + const ctx = { + req: { + accessToken: { userId: 9 }, + headers: { origin: 'http://localhost:5000' }, + } + }; + ctx.req.__ = value => { + return value; + }; + + it('should merge two tickets', async () => { + const tx = await models.Ticket.beginTransaction({}); + + try { + const options = { transaction: tx }; + const chatNotificationBeforeMerge = await models.Chat.find(); + + await models.Ticket.merge(ctx, tickets, options); + + const createdTicketLog = await models.TicketLog.find({ where: { originFk: tickets[0].id } }, options); + const deletedTicket = await models.Ticket.findOne({ where: { id: tickets[0].id } }, options); + const salesTicketFuture = await models.Sale.find({ where: { ticketFk: tickets[0].ticketFuture } }, options); + const chatNotificationAfterMerge = await models.Chat.find(); + + expect(createdTicketLog.length).toEqual(1); + expect(deletedTicket.isDeleted).toEqual(true); + expect(salesTicketFuture.length).toEqual(2); + expect(chatNotificationBeforeMerge.length).toEqual(chatNotificationAfterMerge.length - 2); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js index d8c785baa..96d29c46f 100644 --- a/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js +++ b/modules/ticket/back/methods/ticket/specs/priceDifference.spec.js @@ -87,7 +87,7 @@ describe('sale priceDifference()', () => { const secondtItem = result.items[1]; expect(firstItem.movable).toEqual(410); - expect(secondtItem.movable).toEqual(1870); + expect(secondtItem.movable).toEqual(1810); await tx.rollback(); } catch (e) { diff --git a/modules/ticket/back/model-config.json b/modules/ticket/back/model-config.json index 17bd72949..baaca595e 100644 --- a/modules/ticket/back/model-config.json +++ b/modules/ticket/back/model-config.json @@ -94,5 +94,8 @@ }, "TicketConfig": { "dataSource": "vn" + }, + "TicketFuture": { + "dataSource": "vn" } } diff --git a/modules/ticket/back/models/expedition.json b/modules/ticket/back/models/expedition.json index 324ad4609..d74c56d2c 100644 --- a/modules/ticket/back/models/expedition.json +++ b/modules/ticket/back/models/expedition.json @@ -37,11 +37,6 @@ "model": "agency-mode", "foreignKey": "agencyModeFk" }, - "packageItem": { - "type": "belongsTo", - "model": "Item", - "foreignKey": "itemFk" - }, "worker": { "type": "belongsTo", "model": "Worker", diff --git a/modules/ticket/back/models/ticket-future.json b/modules/ticket/back/models/ticket-future.json new file mode 100644 index 000000000..00277ab8a --- /dev/null +++ b/modules/ticket/back/models/ticket-future.json @@ -0,0 +1,12 @@ +{ + "name": "TicketFuture", + "base": "PersistedModel", + "acls": [ + { + "accessType": "READ", + "principalType": "ROLE", + "principalId": "employee", + "permission": "ALLOW" + } + ] + } diff --git a/modules/ticket/back/models/ticket-methods.js b/modules/ticket/back/models/ticket-methods.js index 3ac03e1de..82a1ac862 100644 --- a/modules/ticket/back/models/ticket-methods.js +++ b/modules/ticket/back/models/ticket-methods.js @@ -33,6 +33,8 @@ module.exports = function(Self) { require('../methods/ticket/closeByTicket')(Self); require('../methods/ticket/closeByAgency')(Self); require('../methods/ticket/closeByRoute')(Self); + require('../methods/ticket-future/getTicketsFuture')(Self); + require('../methods/ticket/merge')(Self); require('../methods/ticket/isRoleAdvanced')(Self); require('../methods/ticket/collectionLabel')(Self); }; diff --git a/modules/ticket/front/future-search-panel/index.html b/modules/ticket/front/future-search-panel/index.html new file mode 100644 index 000000000..1b3ae453e --- /dev/null +++ b/modules/ticket/front/future-search-panel/index.html @@ -0,0 +1,111 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + {{name}} + + + + + {{name}} + + + + + + + {{name}} + + + + + {{name}} + + + + + + + + + + + + +
+
diff --git a/modules/ticket/front/future-search-panel/index.js b/modules/ticket/front/future-search-panel/index.js new file mode 100644 index 000000000..1a1f0e4c5 --- /dev/null +++ b/modules/ticket/front/future-search-panel/index.js @@ -0,0 +1,44 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +class Controller extends SearchPanel { + constructor($, $element) { + super($, $element); + this.filter = this.$.filter; + this.getGroupedStates(); + this.getItemPackingTypes(); + } + + getGroupedStates() { + let groupedStates = []; + this.$http.get('AlertLevels').then(res => { + for (let state of res.data) { + groupedStates.push({ + id: state.id, + code: state.code, + name: this.$t(state.code) + }); + } + this.groupedStates = groupedStates; + }); + } + + getItemPackingTypes() { + let itemPackingTypes = []; + this.$http.get('ItemPackingTypes').then(res => { + for (let ipt of res.data) { + itemPackingTypes.push({ + id: ipt.id, + code: ipt.code, + name: this.$t(ipt.code) + }); + } + this.itemPackingTypes = itemPackingTypes; + }); + } +} + +ngModule.vnComponent('vnFutureTicketSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/ticket/front/future-search-panel/locale/en.yml b/modules/ticket/front/future-search-panel/locale/en.yml new file mode 100644 index 000000000..fe71865cb --- /dev/null +++ b/modules/ticket/front/future-search-panel/locale/en.yml @@ -0,0 +1,9 @@ +Future tickets: Tickets a futuro +FREE: Free +DELIVERED: Delivered +ON_PREPARATION: On preparation +PACKED: Packed +F: Fruits and vegetables +V: Vertical +H: Horizontal +P: Feed diff --git a/modules/ticket/front/future-search-panel/locale/es.yml b/modules/ticket/front/future-search-panel/locale/es.yml new file mode 100644 index 000000000..82deba538 --- /dev/null +++ b/modules/ticket/front/future-search-panel/locale/es.yml @@ -0,0 +1,23 @@ +Future tickets: Tickets a futuro +Origin date: Fecha origen +Destination date: Fecha destino +Origin ETD: ETD origen +Destination ETD: ETD destino +Max Lines: Líneas máx. +Max Liters: Litros máx. +Origin IPT: IPT origen +Destination IPT: IPT destino +With problems: Con problemas +Warehouse: Almacén +Origin Grouped State: Estado agrupado origen +Destination Grouped State: Estado agrupado destino +FREE: Libre +DELIVERED: Servido +ON_PREPARATION: En preparacion +PACKED: Encajado +F: Frutas y verduras +V: Vertical +H: Horizontal +P: Pienso +ETD: Tiempo estimado de entrega +IPT: Encajado diff --git a/modules/ticket/front/future/index.html b/modules/ticket/front/future/index.html new file mode 100644 index 000000000..d30cbaf19 --- /dev/null +++ b/modules/ticket/front/future/index.html @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Problems + + Origin ID + + Origin ETD + + Origin State + + IPT + + Liters + + Available Lines + + Destination ID + + Destination ETD + + Destination State + + IPT +
+ + + + + + + + + + + + + + + + + {{::ticket.id}} + + + {{::ticket.originETD | date: 'dd/MM/yyyy'}} + + + + {{::ticket.state}} + + {{::ticket.ipt}}{{::ticket.liters}}{{::ticket.lines}} + + {{::ticket.ticketFuture}} + + + + {{::ticket.destETD | date: 'dd/MM/yyyy'}} + + + + {{::ticket.tfState}} + + {{::ticket.tfIpt}}
+
+
+
+ + + + diff --git a/modules/ticket/front/future/index.js b/modules/ticket/front/future/index.js new file mode 100644 index 000000000..311b9c307 --- /dev/null +++ b/modules/ticket/front/future/index.js @@ -0,0 +1,147 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.$checkAll = false; + + this.smartTableOptions = { + activeButtons: { + search: true, + }, + columns: [{ + field: 'problems', + searchable: false + }, + { + field: 'originETD', + searchable: false + }, + { + field: 'destETD', + searchable: false + }, + { + field: 'state', + searchable: false + }, + { + field: 'tfState', + searchable: false + }, + { + field: 'ipt', + autocomplete: { + url: 'ItemPackingTypes', + showField: 'description', + valueField: 'code' + } + }, + { + field: 'tfIpt', + autocomplete: { + url: 'ItemPackingTypes', + showField: 'description', + valueField: 'code' + } + }, + ] + }; + this.setDefaultFilter(); + } + + setDefaultFilter() { + const today = new Date(); + + this.filterParams = { + originDated: today, + futureDated: today, + linesMax: '9999', + litersMax: '9999', + warehouseFk: 1 + }; + } + + compareDate(date) { + let today = new Date(); + 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'; + } + + get checked() { + const tickets = this.$.model.data || []; + const checkedLines = []; + for (let ticket of tickets) { + if (ticket.checked) + checkedLines.push(ticket); + } + + return checkedLines; + } + + stateColor(state) { + if (state === 'OK') + return 'success'; + else if (state === 'Libre') + return 'notice'; + } + + 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]; + } + + get confirmationMessage() { + if (!this.$.model) return 0; + + return this.$t(`Move confirmation`, { + checked: this.checked.length + }); + } + + moveTicketsFuture() { + let params = { tickets: this.checked }; + return this.$http.post('Tickets/merge', params) + .then(() => { + this.$.model.refresh(); + this.vnApp.showSuccess(this.$t('Success')); + }); + } + + exprBuilder(param, value) { + switch (param) { + case 'id': + return { 'id': value }; + case 'ticketFuture': + return { 'ticketFuture': value }; + case 'litersMax': + return { 'liters': value }; + case 'linesMax': + return { 'lines': value }; + case 'ipt': + return { 'ipt': value }; + case 'tfIpt': + return { 'tfIpt': value }; + } + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnTicketFuture', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/ticket/front/future/index.spec.js b/modules/ticket/front/future/index.spec.js new file mode 100644 index 000000000..63deebc4f --- /dev/null +++ b/modules/ticket/front/future/index.spec.js @@ -0,0 +1,113 @@ +import './index.js'; +import crudModel from 'core/mocks/crud-model'; + +describe('Component vnTicketFuture', () => { + let controller; + let $httpBackend; + let $window; + + beforeEach(ngModule('ticket') + ); + + beforeEach(inject(($componentController, _$window_, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + $window = _$window_; + const $element = angular.element(''); + controller = $componentController('vnTicketFuture', { $element }); + controller.$.model = crudModel; + controller.$.model.data = [{ + id: 1, + checked: true, + state: "OK" + }, { + id: 2, + checked: true, + state: "Libre" + }]; + })); + + describe('compareDate()', () => { + it('should return warning when the date is the present', () => { + let today = new Date(); + let result = controller.compareDate(today); + + expect(result).toEqual('warning'); + }); + + it('should return sucess when the date is in the future', () => { + let futureDate = new Date(); + futureDate = futureDate.setDate(futureDate.getDate() + 10); + let result = controller.compareDate(futureDate); + + expect(result).toEqual('success'); + }); + + it('should return undefined when the date is in the past', () => { + let pastDate = new Date(); + pastDate = pastDate.setDate(pastDate.getDate() - 10); + let result = controller.compareDate(pastDate); + + expect(result).toEqual(undefined); + }); + }); + + describe('checked()', () => { + it('should return an array of checked tickets', () => { + const result = controller.checked; + const firstRow = result[0]; + const secondRow = result[1]; + + expect(result.length).toEqual(2); + expect(firstRow.id).toEqual(1); + expect(secondRow.id).toEqual(2); + }); + }); + + describe('stateColor()', () => { + it('should return success to the OK tickets', () => { + const ok = controller.stateColor(controller.$.model.data[0].state); + const notOk = controller.stateColor(controller.$.model.data[1].state); + expect(ok).toEqual('success'); + expect(notOk).not.toEqual('success'); + }); + + it('should return success to the FREE tickets', () => { + const notFree = controller.stateColor(controller.$.model.data[0].state); + const free = controller.stateColor(controller.$.model.data[1].state); + expect(free).toEqual('notice'); + expect(notFree).not.toEqual('notice'); + }); + }); + + describe('dateRange()', () => { + it('should return two dates with the hours at the start and end of the given date', () => { + const now = new Date(); + + const today = now.getDate(); + + const dateRange = controller.dateRange(now); + const start = dateRange[0].toString(); + const end = dateRange[1].toString(); + + expect(start).toContain(today); + expect(start).toContain('00:00:00'); + + expect(end).toContain(today); + expect(end).toContain('23:59:59'); + }); + }); + + describe('moveTicketsFuture()', () => { + it('should make an HTTP Post query', () => { + jest.spyOn(controller.$.model, 'refresh'); + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expectPOST(`Tickets/merge`).respond(); + controller.moveTicketsFuture(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + expect(controller.$.model.refresh).toHaveBeenCalledWith(); + }); + }); +}); diff --git a/modules/ticket/front/future/locale/en.yml b/modules/ticket/front/future/locale/en.yml new file mode 100644 index 000000000..66d3ce269 --- /dev/null +++ b/modules/ticket/front/future/locale/en.yml @@ -0,0 +1,6 @@ +Move confirmation: Do you want to move {{checked}} tickets to the future? +FREE: Free +DELIVERED: Delivered +ON_PREPARATION: On preparation +PACKED: Packed +Success: Tickets moved successfully! diff --git a/modules/ticket/front/future/locale/es.yml b/modules/ticket/front/future/locale/es.yml new file mode 100644 index 000000000..9be0be6a4 --- /dev/null +++ b/modules/ticket/front/future/locale/es.yml @@ -0,0 +1,22 @@ +Future tickets: Tickets a futuro +Search tickets: Buscar tickets +Search future tickets by date: Buscar tickets por fecha +Problems: Problemas +Origin ID: ID origen +Closing: Cierre +Origin State: Estado origen +Destination State: Estado destino +Liters: Litros +Available Lines: Líneas disponibles +Destination ID: ID destino +Destination ETD: ETD Destino +Origin ETD: ETD Origen +Move tickets: Mover tickets +Move confirmation: ¿Desea mover {{checked}} tickets hacia el futuro? +Success: Tickets movidos correctamente +ETD: Tiempo estimado de entrega +IPT: Encajado +FREE: Libre +DELIVERED: Servido +ON_PREPARATION: En preparacion +PACKED: Encajado diff --git a/modules/ticket/front/index.js b/modules/ticket/front/index.js index 0558d251d..6106a22eb 100644 --- a/modules/ticket/front/index.js +++ b/modules/ticket/front/index.js @@ -34,3 +34,5 @@ import './dms/create'; import './dms/edit'; import './sms'; import './boxing'; +import './future'; +import './future-search-panel'; diff --git a/modules/ticket/front/routes.json b/modules/ticket/front/routes.json index 4be8e2183..2963d54c4 100644 --- a/modules/ticket/front/routes.json +++ b/modules/ticket/front/routes.json @@ -7,7 +7,8 @@ "menus": { "main": [ {"state": "ticket.index", "icon": "icon-ticket"}, - {"state": "ticket.weekly.index", "icon": "schedule"} + {"state": "ticket.weekly.index", "icon": "schedule"}, + {"state": "ticket.future", "icon": "keyboard_double_arrow_right"} ], "card": [ {"state": "ticket.card.basicData.stepOne", "icon": "settings"}, @@ -283,6 +284,12 @@ "params": { "ticket": "$ctrl.ticket" } + }, + { + "url": "/future", + "state": "ticket.future", + "component": "vn-ticket-future", + "description": "Future tickets" } ] } diff --git a/modules/ticket/front/sale/index.js b/modules/ticket/front/sale/index.js index 85a862bbb..f64d0b61b 100644 --- a/modules/ticket/front/sale/index.js +++ b/modules/ticket/front/sale/index.js @@ -89,7 +89,7 @@ class Controller extends Section { getUsesMana() { this.$http.get(`Sales/usesMana`) .then(res => { - this.useMana = res.data; + this.usesMana = res.data; }); } diff --git a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js index d0afd45b9..4cc6e54e3 100644 --- a/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js +++ b/modules/worker/back/methods/worker-time-control/specs/sendMail.spec.js @@ -12,11 +12,6 @@ describe('workerTimeControl sendMail()', () => { }; - beforeAll(function() { - originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; - jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; - }); - it('should fill time control of a worker without records in Journey and with rest', async() => { const tx = await models.WorkerTimeControl.beginTransaction({}); @@ -124,9 +119,5 @@ describe('workerTimeControl sendMail()', () => { throw e; } }); - - afterAll(function() { - jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; - }); }); diff --git a/print/package-lock.json b/print/package-lock.json index 02c5fa77d..2a657269f 100644 --- a/print/package-lock.json +++ b/print/package-lock.json @@ -12,6 +12,7 @@ "fs-extra": "^7.0.1", "intl": "^1.2.5", "js-yaml": "^3.13.1", + "jsbarcode": "^3.11.5", "jsonexport": "^3.2.0", "juice": "^5.2.0", "log4js": "^6.7.0", @@ -22,7 +23,8 @@ "strftime": "^0.10.0", "vue": "^2.6.10", "vue-i18n": "^8.15.0", - "vue-server-renderer": "^2.6.10" + "vue-server-renderer": "^2.6.10", + "xmldom": "^0.6.0" } }, "node_modules/@babel/parser": { @@ -866,6 +868,66 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==", + "bin": { + "auto.js": "bin/barcodes/CODE128/auto.js", + "Barcode.js": "bin/barcodes/Barcode.js", + "barcodes": "bin/barcodes", + "canvas.js": "bin/renderers/canvas.js", + "checksums.js": "bin/barcodes/MSI/checksums.js", + "codabar": "bin/barcodes/codabar", + "CODE128": "bin/barcodes/CODE128", + "CODE128_AUTO.js": "bin/barcodes/CODE128/CODE128_AUTO.js", + "CODE128.js": "bin/barcodes/CODE128/CODE128.js", + "CODE128A.js": "bin/barcodes/CODE128/CODE128A.js", + "CODE128B.js": "bin/barcodes/CODE128/CODE128B.js", + "CODE128C.js": "bin/barcodes/CODE128/CODE128C.js", + "CODE39": "bin/barcodes/CODE39", + "constants.js": "bin/barcodes/ITF/constants.js", + "defaults.js": "bin/options/defaults.js", + "EAN_UPC": "bin/barcodes/EAN_UPC", + "EAN.js": "bin/barcodes/EAN_UPC/EAN.js", + "EAN13.js": "bin/barcodes/EAN_UPC/EAN13.js", + "EAN2.js": "bin/barcodes/EAN_UPC/EAN2.js", + "EAN5.js": "bin/barcodes/EAN_UPC/EAN5.js", + "EAN8.js": "bin/barcodes/EAN_UPC/EAN8.js", + "encoder.js": "bin/barcodes/EAN_UPC/encoder.js", + "ErrorHandler.js": "bin/exceptions/ErrorHandler.js", + "exceptions": "bin/exceptions", + "exceptions.js": "bin/exceptions/exceptions.js", + "fixOptions.js": "bin/help/fixOptions.js", + "GenericBarcode": "bin/barcodes/GenericBarcode", + "getOptionsFromElement.js": "bin/help/getOptionsFromElement.js", + "getRenderProperties.js": "bin/help/getRenderProperties.js", + "help": "bin/help", + "index.js": "bin/renderers/index.js", + "index.tmp.js": "bin/barcodes/index.tmp.js", + "ITF": "bin/barcodes/ITF", + "ITF.js": "bin/barcodes/ITF/ITF.js", + "ITF14.js": "bin/barcodes/ITF/ITF14.js", + "JsBarcode.js": "bin/JsBarcode.js", + "linearizeEncodings.js": "bin/help/linearizeEncodings.js", + "merge.js": "bin/help/merge.js", + "MSI": "bin/barcodes/MSI", + "MSI.js": "bin/barcodes/MSI/MSI.js", + "MSI10.js": "bin/barcodes/MSI/MSI10.js", + "MSI1010.js": "bin/barcodes/MSI/MSI1010.js", + "MSI11.js": "bin/barcodes/MSI/MSI11.js", + "MSI1110.js": "bin/barcodes/MSI/MSI1110.js", + "object.js": "bin/renderers/object.js", + "options": "bin/options", + "optionsFromStrings.js": "bin/help/optionsFromStrings.js", + "pharmacode": "bin/barcodes/pharmacode", + "renderers": "bin/renderers", + "shared.js": "bin/renderers/shared.js", + "svg.js": "bin/renderers/svg.js", + "UPC.js": "bin/barcodes/EAN_UPC/UPC.js", + "UPCE.js": "bin/barcodes/EAN_UPC/UPCE.js" + } + }, "node_modules/jsbn": { "version": "0.1.1", "license": "MIT" @@ -2092,6 +2154,14 @@ } } }, + "node_modules/xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "license": "MIT", @@ -2685,6 +2755,11 @@ "esprima": "^4.0.0" } }, + "jsbarcode": { + "version": "3.11.5", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.11.5.tgz", + "integrity": "sha512-zv3KsH51zD00I/LrFzFSM6dst7rDn0vIMzaiZFL7qusTjPZiPtxg3zxetp0RR7obmjTw4f6NyGgbdkBCgZUIrA==" + }, "jsbn": { "version": "0.1.1" }, @@ -3464,6 +3539,11 @@ "peer": true, "requires": {} }, + "xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==" + }, "xtend": { "version": "4.0.2" }, diff --git a/print/package.json b/print/package.json index a6c53a28f..65a8687b3 100755 --- a/print/package.json +++ b/print/package.json @@ -16,6 +16,7 @@ "fs-extra": "^7.0.1", "intl": "^1.2.5", "js-yaml": "^3.13.1", + "jsbarcode": "^3.11.5", "jsonexport": "^3.2.0", "juice": "^5.2.0", "log4js": "^6.7.0", @@ -26,6 +27,7 @@ "strftime": "^0.10.0", "vue": "^2.6.10", "vue-i18n": "^8.15.0", - "vue-server-renderer": "^2.6.10" + "vue-server-renderer": "^2.6.10", + "xmldom": "^0.6.0" } } diff --git a/print/templates/email/client-welcome/client-welcome.html b/print/templates/email/client-welcome/client-welcome.html index acd49dc87..3554b6e92 100644 --- a/print/templates/email/client-welcome/client-welcome.html +++ b/print/templates/email/client-welcome/client-welcome.html @@ -9,7 +9,7 @@
{{$t('clientId')}}: {{client.id}}
{{$t('user')}}: {{client.userName}}
{{$t('password')}}: ******** - ({{$t('passwordResetText')}}) + ({{$t('passwordResetText')}})

@@ -53,4 +53,4 @@

- \ No newline at end of file + diff --git a/print/templates/email/client-welcome/locale/es.yml b/print/templates/email/client-welcome/locale/es.yml index 1257113ac..478fd242c 100644 --- a/print/templates/email/client-welcome/locale/es.yml +++ b/print/templates/email/client-welcome/locale/es.yml @@ -1,8 +1,8 @@ subject: Bienvenido a Verdnatura title: "¡Te damos la bienvenida!" dearClient: Estimado cliente -clientData: 'Tus datos para poder comprar en la web de Verdnatura (https://verdnatura.es) +clientData: 'Tus datos para poder comprar en la web de Verdnatura (https://shop.verdnatura.es) o en nuestras aplicaciones para iOS y Android, son' diff --git a/print/templates/email/vehicle-event-expired/assets/css/import.js b/print/templates/email/vehicle-event-expired/assets/css/import.js new file mode 100644 index 000000000..4b4bb7086 --- /dev/null +++ b/print/templates/email/vehicle-event-expired/assets/css/import.js @@ -0,0 +1,11 @@ +const Stylesheet = require(`vn-print/core/stylesheet`); + +const path = require('path'); +const vnPrintPath = path.resolve('print'); + +module.exports = new Stylesheet([ + `${vnPrintPath}/common/css/spacing.css`, + `${vnPrintPath}/common/css/misc.css`, + `${vnPrintPath}/common/css/layout.css`, + `${vnPrintPath}/common/css/email.css`]) + .mergeStyles(); diff --git a/print/templates/email/vehicle-event-expired/attachments.json b/print/templates/email/vehicle-event-expired/attachments.json new file mode 100644 index 000000000..6b56392a0 --- /dev/null +++ b/print/templates/email/vehicle-event-expired/attachments.json @@ -0,0 +1,6 @@ +[ + { + "filename": "vehicle-event-expired.pdf", + "component": "vehicle-event-expired" + } +] diff --git a/print/templates/email/vehicle-event-expired/locale/es.yml b/print/templates/email/vehicle-event-expired/locale/es.yml new file mode 100644 index 000000000..f6fff468c --- /dev/null +++ b/print/templates/email/vehicle-event-expired/locale/es.yml @@ -0,0 +1,3 @@ +subject: Expiración Tarjetas Vehículos +title: Expiración Tarjetas Vehículos +description: A continuación se adjunta el informe de expiración de tarjetas vehículos diff --git a/print/templates/email/vehicle-event-expired/vehicle-event-expired.html b/print/templates/email/vehicle-event-expired/vehicle-event-expired.html new file mode 100644 index 000000000..f4c1ebc1b --- /dev/null +++ b/print/templates/email/vehicle-event-expired/vehicle-event-expired.html @@ -0,0 +1,8 @@ + +
+
+

{{ $t('title') }}

+

+
+
+
diff --git a/print/templates/email/vehicle-event-expired/vehicle-event-expired.js b/print/templates/email/vehicle-event-expired/vehicle-event-expired.js new file mode 100755 index 000000000..1c228d2b5 --- /dev/null +++ b/print/templates/email/vehicle-event-expired/vehicle-event-expired.js @@ -0,0 +1,9 @@ +const Component = require(`vn-print/core/component`); +const emailBody = new Component('email-body'); + +module.exports = { + name: 'vehicle-event-expired', + components: { + 'email-body': emailBody.build() + } +}; diff --git a/print/templates/reports/collection-label/collection-label.html b/print/templates/reports/collection-label/collection-label.html index 35c0786b6..eeb82ac4a 100644 --- a/print/templates/reports/collection-label/collection-label.html +++ b/print/templates/reports/collection-label/collection-label.html @@ -2,7 +2,7 @@ - +
diff --git a/print/templates/reports/vehicle-event-expired/assets/css/import.js b/print/templates/reports/vehicle-event-expired/assets/css/import.js index fd8796c2b..37a98dfdd 100644 --- a/print/templates/reports/vehicle-event-expired/assets/css/import.js +++ b/print/templates/reports/vehicle-event-expired/assets/css/import.js @@ -1,9 +1,12 @@ -const Stylesheet = require(`${appPath}/core/stylesheet`); +const Stylesheet = require(`vn-print/core/stylesheet`); + +const path = require('path'); +const vnPrintPath = path.resolve('print'); module.exports = new Stylesheet([ - `${appPath}/common/css/spacing.css`, - `${appPath}/common/css/misc.css`, - `${appPath}/common/css/layout.css`, - `${appPath}/common/css/report.css`, + `${vnPrintPath}/common/css/spacing.css`, + `${vnPrintPath}/common/css/misc.css`, + `${vnPrintPath}/common/css/layout.css`, + `${vnPrintPath}/common/css/report.css`, `${__dirname}/style.css`]) .mergeStyles(); diff --git a/print/templates/reports/vehicle-event-expired/vehicle-event-expired.js b/print/templates/reports/vehicle-event-expired/vehicle-event-expired.js index 278caeabf..ab6cffc00 100755 --- a/print/templates/reports/vehicle-event-expired/vehicle-event-expired.js +++ b/print/templates/reports/vehicle-event-expired/vehicle-event-expired.js @@ -1,4 +1,4 @@ -const Component = require(`${appPath}/core/component`); +const Component = require(`vn-print/core/component`); const reportBody = new Component('report-body'); module.exports = { diff --git a/print/templates/reports/zone/zone.js b/print/templates/reports/zone/zone.js index 463c28acf..720542cd6 100755 --- a/print/templates/reports/zone/zone.js +++ b/print/templates/reports/zone/zone.js @@ -1,4 +1,4 @@ -const Component = require(`${appPath}/core/component`); +const Component = require(`vn-print/core/component`); const reportBody = new Component('report-body'); module.exports = {
{{labelData.levelV}}