diff --git a/db/changes/10210-summer/00-ACL.sql b/db/changes/10210-summer/00-ACL.sql new file mode 100644 index 000000000..755b148d7 --- /dev/null +++ b/db/changes/10210-summer/00-ACL.sql @@ -0,0 +1,2 @@ +INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) + VALUES ('Buy', '*', '*', 'ALLOW', 'ROLE', 'buyer'); diff --git a/db/changes/10210-summer/00-accountingType.sql b/db/changes/10210-summer/00-accountingType.sql index 1dbe29952..7cb21ec31 100644 --- a/db/changes/10210-summer/00-accountingType.sql +++ b/db/changes/10210-summer/00-accountingType.sql @@ -1,2 +1,2 @@ ALTER TABLE `vn`.`accountingType` -ADD COLUMN `receiptDescription` VARCHAR(50) NULL AFTER `description`; +ADD COLUMN `receiptDescription` VARCHAR(50) NULL COMMENT 'Descripción por defecto al crear nuevo recibo' AFTER `description`; diff --git a/db/changes/10210-summer/00-ticket_close.sql b/db/changes/10210-summer/00-ticket_close.sql new file mode 100644 index 000000000..96f9c5528 --- /dev/null +++ b/db/changes/10210-summer/00-ticket_close.sql @@ -0,0 +1,119 @@ +USE `vn`; +DROP procedure IF EXISTS `ticket_close`; + +DELIMITER $$ +USE `vn`$$ +CREATE DEFINER=`root`@`%` PROCEDURE `ticket_close`(vTicketFk INT) +BEGIN +/** + * Realiza el cierre de todos los + * tickets de la tabla ticketClosure. + * + * @param vTicketFk Id del ticket + */ + DECLARE vDone BOOL; + DECLARE vClientFk INT; + DECLARE vCurTicketFk INT; + DECLARE vIsTaxDataChecked BOOL; + DECLARE vCompanyFk INT; + DECLARE vShipped DATE; + DECLARE vNewInvoiceId INT; + DECLARE vHasDailyInvoice BOOL; + DECLARE vWithPackage BOOL; + DECLARE vHasToInvoice BOOL; + + DECLARE cur CURSOR FOR + SELECT ticketFk FROM tmp.ticketClosure; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN + RESIGNAL; + END; + + DROP TEMPORARY TABLE IF EXISTS tmp.ticketClosure; + CREATE TEMPORARY TABLE tmp.ticketClosure + SELECT vTicketFk AS ticketFk; + + INSERT INTO tmp.ticketClosure + SELECT id FROM stowaway s + WHERE s.shipFk = vTicketFk; + OPEN cur; + + proc: LOOP + SET vDone = FALSE; + + FETCH cur INTO vCurTicketFk; + + IF vDone THEN + LEAVE proc; + END IF; + + -- ticketClosure start + SELECT + c.id, + c.isTaxDataChecked, + t.companyFk, + t.shipped, + co.hasDailyInvoice, + w.isManaged, + c.hasToInvoice + INTO vClientFk, + vIsTaxDataChecked, + vCompanyFk, + vShipped, + vHasDailyInvoice, + vWithPackage, + vHasToInvoice + FROM ticket t + JOIN `client` c ON c.id = t.clientFk + JOIN province p ON p.id = c.provinceFk + JOIN country co ON co.id = p.countryFk + JOIN warehouse w ON w.id = t.warehouseFk + WHERE t.id = vCurTicketFk; + + INSERT INTO ticketPackaging (ticketFk, packagingFk, quantity) + (SELECT vCurTicketFk, p.id, COUNT(*) + FROM expedition e + JOIN packaging p ON p.itemFk = e.itemFk + WHERE e.ticketFk = vCurTicketFk AND p.isPackageReturnable + AND vWithPackage + GROUP BY p.itemFk); + + -- No retornables o no catalogados + INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, isPriceFixed) + (SELECT e.itemFk, vCurTicketFk, i.name, COUNT(*) AS amount, getSpecialPrice(e.itemFk, vClientFk), 1 + FROM expedition e + JOIN item i ON i.id = e.itemFk + LEFT JOIN packaging p ON p.itemFk = i.id + WHERE e.ticketFk = vCurTicketFk AND IFNULL(p.isPackageReturnable, 0) = 0 + AND getSpecialPrice(e.itemFk, vClientFk) > 0 + GROUP BY e.itemFk); + + CALL vn.zonePromo_Make(); + + IF(vHasDailyInvoice) AND vHasToInvoice THEN + + -- Facturacion rapida + CALL ticketTrackingAdd(vCurTicketFk, 'DELIVERED', NULL); + -- Facturar si está contabilizado + IF vIsTaxDataChecked THEN + CALL invoiceOut_newFromClient( + vClientFk, + (SELECT invoiceSerial(vClientFk, vCompanyFk, 'M')), + vShipped, + vCompanyFk, + NULL, + vNewInvoiceId); + END IF; + ELSE + CALL ticketTrackingAdd(vCurTicketFk, (SELECT vn.getAlert3State(vCurTicketFk)), NULL); + END IF; + END LOOP; + + CLOSE cur; + + DROP TEMPORARY TABLE IF EXISTS tmp.ticketClosure; +END$$ + +DELIMITER ; + diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 4351cd867..22fa2076a 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -733,6 +733,39 @@ INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`) (05080000, 'Coral y materiales similares', 2, 2), (06021010, 'Plantas vivas: Esqueje/injerto, Vid', 1, 1); +INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`) + VALUES + (1, 2, 70, 'AMA', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 67, 1, NULL, 0), + (2, 2, 70, 'AZL', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 66, 1, NULL, 0), + (3, 1, 60, 'AMR', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 65, 1, NULL, 0), + (4, 1, 60, 'AMR', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, 69, 2, NULL, 0), + (5, 3, 30, 'GRE', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 74, 2, NULL, 0), + (6, 5, 30, 'GRE', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 62, 2, NULL, 0), + (7, 5, 90, 'AZL', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 64, 2, NULL, 0), + (8, 2, 70, 'AMA', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 75, 1, NULL, 0), + (9, 2, 70, 'AZL', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 76, 1, NULL, 0), + (10, 1, 60, 'AMR', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 77, 1, NULL, 0), + (11, 1, 60, 'AMR', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, 78, 2, NULL, 0), + (12, 3, 30, 'GRE', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 82, 2, NULL, 0), + (13, 5, 30, 'GRE', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 83, 2, NULL, 0), + (14, 5, 90, 'AZL', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 84, 2, NULL, 0), + (15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 67350, 2, NULL, 0), + (16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 67350, 2, NULL, 0), + (71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, 88, 2, NULL, 0); + +INSERT INTO `vn`.`expedition`(`id`, `agencyModeFk`, `ticketFk`, `isBox`, `created`, `itemFk`, `counter`, `checked`, `workerFk`) + VALUES + (1, 1, 1, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1, 1, 18), + (2, 1, 1, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 2, 1, 18), + (3, 1, 1, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 2, 3, 1, 18), + (4, 1, 1, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 4, 4, 1, 18), + (5, 1, 2, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1, 1, 18), + (6, 7, 3, 71, DATE_ADD(CURDATE(), INTERVAL -2 MONTH), 1, 1, 1, 18), + (7, 2, 4, 71, DATE_ADD(CURDATE(), INTERVAL -3 MONTH), 1, 1, 1, 18), + (8, 3, 5, 71, DATE_ADD(CURDATE(), INTERVAL -4 MONTH), 1, 1, 1, 18), + (9, 3, 6, 71, DATE_ADD(CURDATE(), INTERVAL -1 MONTH), 1, 1, 1, 18), + (10, 7, 7, 71, CURDATE(), 1, 1, 1, 18); + INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`) VALUES (1, 2, 70, 'AMA', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 67, 1, NULL), @@ -1233,23 +1266,23 @@ INSERT INTO `bs`.`waste`(`buyer`, `year`, `week`, `family`, `saleTotal`, `saleWa ('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Miscellaneous Accessories', '186', '0', '0.0'), ('HankPym', YEAR(DATE_ADD(CURDATE(), INTERVAL -1 WEEK)), WEEK(DATE_ADD(CURDATE(), INTERVAL -1 WEEK), 1), 'Adhesives', '277', '0', '0.0'); -INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`minPrice`,`producer`,`printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`) +INSERT INTO `vn`.`buy`(`id`,`entryFk`,`itemFk`,`buyingValue`,`quantity`,`packageFk`,`stickers`,`freightValue`,`packageValue`,`comissionValue`,`packing`,`grouping`,`groupingMode`,`location`,`price1`,`price2`,`price3`,`producer`,`printedStickers`,`isChecked`,`isIgnored`,`weight`, `created`) VALUES - (1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 1, DATE_ADD(CURDATE(), INTERVAL -2 MONTH)), - (2, 2, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH)), - (3, 3, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 0, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 1, CURDATE()), - (4, 2, 2, 5, 450, 3, 1, 1.000, 1.000, 0.000, 10, 10, 0, NULL, 0.00, 7.30, 7.00, 0.00, NULL, 0, 1, 0, 2.5, CURDATE()), - (5, 3, 3, 55, 500, 5, 1, 1.000, 1.000, 0.000, 1, 1, 0, NULL, 0.00, 78.3, 75.6, 0.00, NULL, 0, 1, 0, 2.5, CURDATE()), - (6, 4, 8, 50, 1000, 4, 1, 1.000, 1.000, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 2.5, CURDATE()), - (7, 4, 9, 20, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 30.50, 29.00, 0.00, NULL, 0, 1, 0, 2.5, CURDATE()), - (8, 4, 4, 1.25, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, 0.00, NULL, 0, 1, 0, 2.5, CURDATE()), - (9, 4, 4, 1.25, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (10, 5, 1, 50, 10, 4, 1, 2.500, 2.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (11, 5, 4, 1.25, 10, 3, 1, 2.500, 2.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (12, 6, 4, 1.25, 0, 3, 1, 2.500, 2.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (13, 7, 1, 50, 0, 3, 1, 2.000, 2.000, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (14, 7, 2, 5, 0, 3, 1, 2.000, 2.000, 0.000, 10, 10, 1, NULL, 0.00, 7.30, 7.00, 0.00, NULL, 0, 1, 0, 4, CURDATE()), - (15, 7, 4, 1.25, 0, 3, 1, 2.000, 2.000, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, 0.00, NULL, 0, 1, 0, 4, CURDATE()); + (1, 1, 1, 50, 5000, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 1, DATE_ADD(CURDATE(), INTERVAL -2 MONTH)), + (2, 2, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 1, DATE_ADD(CURDATE(), INTERVAL -1 MONTH)), + (3, 3, 1, 50, 100, 4, 1, 1.500, 1.500, 0.000, 1, 1, 0, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 1, CURDATE()), + (4, 2, 2, 5, 450, 3, 1, 1.000, 1.000, 0.000, 10, 10, 0, NULL, 0.00, 7.30, 7.00, NULL, 0, 1, 0, 2.5, CURDATE()), + (5, 3, 3, 55, 500, 5, 1, 1.000, 1.000, 0.000, 1, 1, 0, NULL, 0.00, 78.3, 75.6, NULL, 0, 1, 0, 2.5, CURDATE()), + (6, 4, 8, 50, 1000, 4, 1, 1.000, 1.000, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 2.5, CURDATE()), + (7, 4, 9, 20, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 30.50, 29.00, NULL, 0, 1, 0, 2.5, CURDATE()), + (8, 4, 4, 1.25, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, NULL, 0, 1, 0, 2.5, CURDATE()), + (9, 4, 4, 1.25, 1000, 3, 1, 0.500, 0.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, NULL, 0, 1, 0, 4, CURDATE()), + (10, 5, 1, 50, 10, 4, 1, 2.500, 2.500, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 4, CURDATE()), + (11, 5, 4, 1.25, 10, 3, 1, 2.500, 2.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, NULL, 0, 1, 0, 4, CURDATE()), + (12, 6, 4, 1.25, 0, 3, 1, 2.500, 2.500, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, NULL, 0, 1, 0, 4, CURDATE()), + (13, 7, 1, 50, 0, 3, 1, 2.000, 2.000, 0.000, 1, 1, 1, NULL, 0.00, 99.6, 99.4, NULL, 0, 1, 0, 4, CURDATE()), + (14, 7, 2, 5, 0, 3, 1, 2.000, 2.000, 0.000, 10, 10, 1, NULL, 0.00, 7.30, 7.00, NULL, 0, 1, 0, 4, CURDATE()), + (15, 7, 4, 1.25, 0, 3, 1, 2.000, 2.000, 0.000, 10, 10, 1, NULL, 0.00, 1.75, 1.67, NULL, 0, 1, 0, 4, CURDATE()); INSERT INTO `hedera`.`order`(`id`, `date_send`, `customer_id`, `delivery_method_id`, `agency_id`, `address_id`, `company_id`, `note`, `source_app`, `confirmed`,`total`, `date_make`, `first_row_stamp`, `confirm_date`) VALUES diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index d93b72483..c1e13d889 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -220,23 +220,23 @@ export default { topbarSearch: 'vn-topbar', searchButton: 'vn-searchbar vn-icon[icon="search"]', closeItemSummaryPreview: '.vn-popup.shown', - fieldsToShowButton: 'vn-item-index vn-table > div > div > vn-icon-button[icon="menu"]', - fieldsToShowForm: '.vn-dialog.shown form', + fieldsToShowButton: 'vn-item-index vn-table > div > div > vn-icon-button[icon="more_vert"]', + fieldsToShowForm: '.vn-popover.shown .content', firstItemImage: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(1) > img', firstItemImageTd: 'vn-item-index vn-table a:nth-child(1) vn-td:nth-child(1)', firstItemId: 'vn-item-index vn-tbody > a:nth-child(1) > vn-td:nth-child(2)', - idCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(2) > vn-check', - stemsCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(3) > vn-check', - sizeCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(4) > vn-check', - nicheCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(5) > vn-check', - typeCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(6) > vn-check', - categoryCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(7) > vn-check', - intrastadCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(8) > vn-check', - originCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(9) > vn-check', - buyerCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(10) > vn-check', - destinyCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(11) > vn-check', - taxClassCheckbox: '.vn-dialog.shown form vn-horizontal:nth-child(12) > vn-check', - saveFieldsButton: '.vn-dialog.shown vn-horizontal:nth-child(16) > vn-button > button' + idCheckbox: '.vn-popover.shown vn-horizontal:nth-child(1) > vn-check', + stemsCheckbox: '.vn-popover.shown vn-horizontal:nth-child(2) > vn-check', + sizeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(3) > vn-check', + nicheCheckbox: '.vn-popover.shown vn-horizontal:nth-child(4) > vn-check', + typeCheckbox: '.vn-popover.shown vn-horizontal:nth-child(5) > vn-check', + categoryCheckbox: '.vn-popover.shown vn-horizontal:nth-child(6) > vn-check', + intrastadCheckbox: '.vn-popover.shown vn-horizontal:nth-child(7) > vn-check', + originCheckbox: '.vn-popover.shown vn-horizontal:nth-child(8) > vn-check', + buyerCheckbox: '.vn-popover.shown vn-horizontal:nth-child(9) > vn-check', + destinyCheckbox: '.vn-popover.shown vn-horizontal:nth-child(10) > vn-check', + taxClassCheckbox: '.vn-popover.shown vn-horizontal:nth-child(11) > vn-check', + saveFieldsButton: '.vn-popover.shown vn-button[label="Save"] > button' }, itemCreateView: { temporalName: 'vn-item-create vn-textfield[ng-model="$ctrl.item.provisionalName"]', @@ -372,6 +372,8 @@ export default { ticketsIndex: { openAdvancedSearchButton: 'vn-searchbar .append vn-icon[icon="arrow_drop_down"]', advancedSearchInvoiceOut: 'vn-ticket-search-panel vn-textfield[ng-model="filter.refFk"]', + advancedSearchDaysOnward: 'vn-ticket-search-panel vn-input-number[ng-model="filter.scopeDays"]', + advancedSearchButton: 'vn-ticket-search-panel button[type=submit]', newTicketButton: 'vn-ticket-index a[ui-sref="ticket.create"]', searchResult: 'vn-ticket-index vn-card > vn-table > div > vn-tbody > a.vn-tr', secondTicketCheckbox: 'vn-ticket-index vn-tbody > a:nth-child(2) > vn-td:nth-child(1) > vn-check', @@ -384,7 +386,6 @@ export default { searchWeeklyResult: 'vn-ticket-weekly-index vn-table vn-tbody > vn-tr', searchResultDate: 'vn-ticket-summary [label=Landed] span', topbarSearch: 'vn-searchbar', - advancedSearchButton: 'vn-ticket-search-panel button[type=submit]', searchButton: 'vn-searchbar vn-icon[icon="search"]', moreMenu: 'vn-ticket-index vn-icon-button[icon=more_vert]', sixthWeeklyTicket: 'vn-ticket-weekly-index vn-table vn-tr:nth-child(6)', @@ -898,5 +899,22 @@ export default { agency: 'vn-entry-descriptor div.body vn-label-value:nth-child(1) span', travelsQuicklink: 'vn-entry-descriptor vn-quick-link[icon="local_airport"] > a', entriesQuicklink: 'vn-entry-descriptor vn-quick-link[icon="icon-entry"] > a' + }, + entryLatestBuys: { + firstBuy: 'vn-entry-latest-buys vn-tbody > a:nth-child(1)', + allBuysCheckBox: 'vn-entry-latest-buys vn-thead vn-check', + secondBuyCheckBox: 'vn-entry-latest-buys a:nth-child(2) vn-check[ng-model="buy.checked"]', + editBuysButton: 'vn-entry-latest-buys vn-button[icon="edit"]', + fieldAutocomplete: 'vn-autocomplete[ng-model="$ctrl.editedColumn.field"]', + newValueInput: 'vn-textfield[ng-model="$ctrl.editedColumn.newValue"]', + latestBuysSectionButton: 'a[ui-sref="entry.latestBuys"]', + acceptEditBuysDialog: 'button[response="accept"]' + }, + entryIndex: { + createEntryButton: 'vn-entry-index vn-button[icon="add"]', + newEntrySupplier: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.supplierFk"]', + newEntryTravel: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.travelFk"]', + newEntryCompany: 'vn-entry-create vn-autocomplete[ng-model="$ctrl.entry.companyFk"]', + saveNewEntry: 'vn-entry-create button[type="submit"]' } }; diff --git a/e2e/paths/02-client/05_add_address.spec.js b/e2e/paths/02-client/05_add_address.spec.js index f16408b34..c946bc774 100644 --- a/e2e/paths/02-client/05_add_address.spec.js +++ b/e2e/paths/02-client/05_add_address.spec.js @@ -17,6 +17,7 @@ describe('Client Add address path', () => { }); it(`should click on the add new address button to access to the new address form`, async() => { + await page.waitFor(500); await page.waitToClick(selectors.clientAddresses.createAddress); await page.waitForState('client.card.address.create'); }); diff --git a/e2e/paths/04-item/10_index.spec.js b/e2e/paths/04-item/10_index.spec.js index b4c4b636e..a6c0d4919 100644 --- a/e2e/paths/04-item/10_index.spec.js +++ b/e2e/paths/04-item/10_index.spec.js @@ -55,7 +55,7 @@ describe('Item index path', () => { }); it('should mark all unchecked boxes to leave the index as it was', async() => { - await page.waitFor(3000); // otherwise the snackbar doesnt appear some times. + await page.waitFor(500); // otherwise the snackbar doesnt appear some times. await page.waitToClick(selectors.itemsIndex.fieldsToShowButton); await page.waitToClick(selectors.itemsIndex.idCheckbox); await page.waitToClick(selectors.itemsIndex.stemsCheckbox); diff --git a/e2e/paths/05-ticket/10_request.spec.js b/e2e/paths/05-ticket/10_request.spec.js index afab9b9e9..c01964d2b 100644 --- a/e2e/paths/05-ticket/10_request.spec.js +++ b/e2e/paths/05-ticket/10_request.spec.js @@ -18,7 +18,7 @@ describe('Ticket purchase request path', () => { }); it('should add a new request', async() => { - await page.waitFor('.vn-popup', {hidden: true}), + await page.waitFor(500); await page.waitToClick(selectors.ticketRequests.addRequestButton); await page.write(selectors.ticketRequests.descriptionInput, 'New stuff'); await page.write(selectors.ticketRequests.quantity, '9'); diff --git a/e2e/paths/08-route/03_create.spec.js b/e2e/paths/08-route/03_create.spec.js index 80c0071b6..7c6c3f75d 100644 --- a/e2e/paths/08-route/03_create.spec.js +++ b/e2e/paths/08-route/03_create.spec.js @@ -17,6 +17,7 @@ describe('Route create path', () => { describe('as employee', () => { it('should click on the add new route button and open the creation form', async() => { + await page.waitFor(500); await page.waitToClick(selectors.routeIndex.addNewRouteButton); await page.waitForState('route.create'); }); diff --git a/e2e/paths/09-invoice-out/02_descriptor.spec.js b/e2e/paths/09-invoice-out/02_descriptor.spec.js index ade121a8b..cb60fd4a7 100644 --- a/e2e/paths/09-invoice-out/02_descriptor.spec.js +++ b/e2e/paths/09-invoice-out/02_descriptor.spec.js @@ -18,6 +18,7 @@ describe('InvoiceOut descriptor path', () => { describe('as Administrative', () => { it('should search for tickets with an specific invoiceOut', async() => { await page.waitToClick(selectors.ticketsIndex.openAdvancedSearchButton); + await page.clearInput(selectors.ticketsIndex.advancedSearchDaysOnward); await page.write(selectors.ticketsIndex.advancedSearchInvoiceOut, 'T2222222'); await page.waitToClick(selectors.ticketsIndex.advancedSearchButton); await page.waitForState('ticket.card.summary'); @@ -63,7 +64,7 @@ describe('InvoiceOut descriptor path', () => { await page.waitForState('ticket.index'); }); - it('should search for tickets with an specific invoiceOut to find no results', async() => { + it('should search now for tickets with an specific invoiceOut to find no results', async() => { await page.doSearch('T2222222'); const nResults = await page.countElement(selectors.ticketsIndex.searchResult); diff --git a/e2e/paths/12-entry/03_latestBuys.spec.js b/e2e/paths/12-entry/03_latestBuys.spec.js new file mode 100644 index 000000000..e3cfadbcc --- /dev/null +++ b/e2e/paths/12-entry/03_latestBuys.spec.js @@ -0,0 +1,46 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Entry lastest buys path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'entry'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should access the latest buys seccion and search not seeing the edit buys button yet', async() => { + await page.waitToClick(selectors.entryLatestBuys.latestBuysSectionButton); + await page.waitFor(250); + await page.keyboard.press('Enter'); + await page.waitForSelector(selectors.entryLatestBuys.editBuysButton, {visible: false}); + }); + + it('should select all lines but one and then check the edit buys button appears', async() => { + await page.waitToClick(selectors.entryLatestBuys.allBuysCheckBox); + await page.waitToClick(selectors.entryLatestBuys.secondBuyCheckBox); + await page.waitForSelector(selectors.entryLatestBuys.editBuysButton, {visible: true}); + }); + + it('should open the edit dialog', async() => { + await page.waitToClick(selectors.entryLatestBuys.editBuysButton); + await page.waitForSelector(selectors.entryLatestBuys.fieldAutocomplete, {visible: true}); + }); + + it('should search for the "Description" field and type a new description for the items in each selected buy', async() => { + await page.autocompleteSearch(selectors.entryLatestBuys.fieldAutocomplete, 'Description'); + await page.write(selectors.entryLatestBuys.newValueInput, 'Crafted item'); + await page.waitToClick(selectors.entryLatestBuys.acceptEditBuysDialog); + }); + + it('should navigate to the entry.buy section by clicking one of the buys', async() => { + await page.waitToClick(selectors.entryLatestBuys.firstBuy); + await page.waitForState('entry.card.buy'); + }); +}); diff --git a/e2e/paths/12-entry/04_create.spec.js b/e2e/paths/12-entry/04_create.spec.js new file mode 100644 index 000000000..90dac618a --- /dev/null +++ b/e2e/paths/12-entry/04_create.spec.js @@ -0,0 +1,33 @@ +import selectors from '../../helpers/selectors.js'; +import getBrowser from '../../helpers/puppeteer'; + +describe('Entry create path', () => { + let browser; + let page; + + beforeAll(async() => { + browser = await getBrowser(); + page = browser.page; + await page.loginAndModule('buyer', 'entry'); + }); + + afterAll(async() => { + await browser.close(); + }); + + it('should click the create entry button to open the form', async() => { + await page.waitToClick(selectors.entryIndex.createEntryButton); + await page.waitForState('entry.create'); + }); + + it('should fill the form to create a valid entry', async() => { + await page.autocompleteSearch(selectors.entryIndex.newEntrySupplier, '2'); + await page.autocompleteSearch(selectors.entryIndex.newEntryTravel, 'Warehouse Three'); + await page.autocompleteSearch(selectors.entryIndex.newEntryCompany, 'ORN'); + await page.waitToClick(selectors.entryIndex.saveNewEntry); + }); + + it('should be redirected to entry basic data', async() => { + await page.waitForState('entry.card.basicData'); + }); +}); diff --git a/front/core/components/table/index.js b/front/core/components/table/index.js index a61b97485..eadb10cb5 100644 --- a/front/core/components/table/index.js +++ b/front/core/components/table/index.js @@ -7,7 +7,6 @@ export default class Table { this.table = $element[0]; this.field = null; this.order = null; - this.autoLoad = true; } setOrder(field, order) { @@ -25,9 +24,7 @@ export default class Table { } $onChanges() { - // FIXME: The autoload property should be removed from vnTable - // because it's already implemented at vnModel - if (this.autoLoad && this.model && !this.model.data) + if (this.model && !this.model.data) this.applyOrder(); } diff --git a/front/core/directives/index.js b/front/core/directives/index.js index 4377afe8c..af05c9b38 100644 --- a/front/core/directives/index.js +++ b/front/core/directives/index.js @@ -11,7 +11,7 @@ import './visible-by'; import './bind'; import './repeat-last'; import './title'; -import './uvc'; +import './smart-table'; import './droppable'; import './http-click'; import './http-submit'; diff --git a/front/core/directives/uvc.html b/front/core/directives/smart-table.html similarity index 77% rename from front/core/directives/uvc.html rename to front/core/directives/smart-table.html index 9de13b675..6808b5618 100644 --- a/front/core/directives/uvc.html +++ b/front/core/directives/smart-table.html @@ -1,12 +1,13 @@
-
+
+ class="vn-pt-sm" + icon="more_vert" + ng-click="$ctrl.showSmartTable($event)"> -
@@ -24,6 +25,6 @@
-
+
diff --git a/front/core/directives/uvc.js b/front/core/directives/smart-table.js similarity index 70% rename from front/core/directives/uvc.js rename to front/core/directives/smart-table.js index e464a93ab..08d1b6463 100644 --- a/front/core/directives/uvc.js +++ b/front/core/directives/smart-table.js @@ -1,23 +1,28 @@ import ngModule from '../module'; -import template from './uvc.html'; -import './uvc.scss'; +import template from './smart-table.html'; +import './smart-table.scss'; +/** + * Directive to hide/show selected columns of a table, don't use with rowspan. + * Property smart-table-ignore ignores one or more vn-th with prop field. + */ directive.$inject = ['$http', '$compile', 'vnApp', '$translate']; export function directive($http, $compile, vnApp, $translate) { function getHeaderList($element, $scope) { - let allHeaders = $element[0].querySelectorAll(`vn-th[field], vn-th[th-id]`); - let headerList = Array.from(allHeaders); + let filtrableHeaders = $element[0].querySelectorAll('vn-th[field]:not([smart-table-ignore])'); + let headerList = Array.from(filtrableHeaders); let ids = []; let titles = {}; headerList.forEach(header => { - let id = header.getAttribute('th-id') || header.getAttribute('field'); + let id = header.getAttribute('field'); ids.push(id); titles[id] = header.innerText || id.charAt(0).toUpperCase() + id.slice(1); }); $scope.fields = ids; $scope.titles = titles; + $scope.allHeaders = Array.from($element[0].querySelectorAll('vn-th')); return headerList; } @@ -38,34 +43,33 @@ export function directive($http, $compile, vnApp, $translate) { Object.keys(userConfig.configuration).forEach(key => { let index; if (userConfig.configuration[key] === false) { - index = headerList.findIndex(el => { - return (el.getAttribute('th-id') == key || el.getAttribute('field') == key); + index = $scope.allHeaders.findIndex(el => { + return el.getAttribute('field') == key; }); - let baseSelector = `vn-table[vn-uvc=${userConfig.tableCode}] > div`; + let baseSelector = `vn-table[vn-smart-table=${userConfig.tableCode}] > div`; selectors.push(`${baseSelector} vn-thead > vn-tr > vn-th:nth-child(${index + 1})`); selectors.push(`${baseSelector} vn-tbody > vn-tr > vn-td:nth-child(${index + 1})`); selectors.push(`${baseSelector} vn-tbody > .vn-tr > vn-td:nth-child(${index + 1})`); } }); - - if (document.querySelector('style[id="uvc"]')) { - let child = document.querySelector('style[id="uvc"]'); + if (document.querySelector('style[id="smart-table"]')) { + let child = document.querySelector('style[id="smart-table"]'); child.parentNode.removeChild(child); } if (selectors.length) { let rule = selectors.join(', ') + '{display: none;}'; $scope.css = document.createElement('style'); - $scope.css.setAttribute('id', 'uvc'); + $scope.css.setAttribute('id', 'smart-table'); document.head.appendChild($scope.css); $scope.css.appendChild(document.createTextNode(rule)); } $scope.$on('$destroy', () => { - if ($scope.css && document.querySelector('style[id="uvc"]')) { - let child = document.querySelector('style[id="uvc"]'); + if ($scope.css && document.querySelector('style[id="smart-table"]')) { + let child = document.querySelector('style[id="smart-table"]'); child.parentNode.removeChild(child); } }); @@ -80,13 +84,13 @@ export function directive($http, $compile, vnApp, $translate) { restrict: 'A', link: async function($scope, $element, $attrs) { let cTemplate = $compile(template)($scope)[0]; - $scope.$ctrl.showUvc = event => { + $scope.$ctrl.showSmartTable = event => { event.preventDefault(); event.stopImmediatePropagation(); - $scope.uvc.show(); + $scope.smartTable.show(event.target); }; - let tableCode = $attrs.vnUvc.trim(); + let tableCode = $attrs.vnSmartTable.trim(); let headerList = getHeaderList($element, $scope); let defaultConfigView = {tableCode: tableCode, configuration: {}}; @@ -98,16 +102,16 @@ export function directive($http, $compile, vnApp, $translate) { addHideClass($scope, headerList, config); - let table = document.querySelector(`vn-table[vn-uvc=${tableCode}]`); + let table = document.querySelector(`vn-table[vn-smart-table=${tableCode}]`); table.insertBefore(cTemplate, table.firstChild); $scope.$ctrl.saveConfiguration = async tableConfiguration => { let newConfig = await saveConfiguration(tableConfiguration); vnApp.showSuccess($translate.instant('Data saved!')); addHideClass($scope, headerList, newConfig.data); - $scope.uvc.hide(); + $scope.smartTable.hide(); }; } }; } -ngModule.directive('vnUvc', directive); +ngModule.directive('vnSmartTable', directive); diff --git a/front/core/directives/uvc.scss b/front/core/directives/smart-table.scss similarity index 76% rename from front/core/directives/uvc.scss rename to front/core/directives/smart-table.scss index 0cdf0ba1a..a156c060b 100644 --- a/front/core/directives/uvc.scss +++ b/front/core/directives/smart-table.scss @@ -1,4 +1,4 @@ -vn-table vn-dialog[vn-id="uvc"]{ +vn-table vn-popover[vn-id="smart-table"]{ & > div { min-width: 288px; align-items: center; diff --git a/front/core/styles/icons/salixfont.css b/front/core/styles/icons/salixfont.css index 8805815e8..52a2308b4 100644 --- a/front/core/styles/icons/salixfont.css +++ b/front/core/styles/icons/salixfont.css @@ -22,7 +22,9 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - +.icon-latestBuys:before { + content: "\e95f"; +} .icon-zone:before { content: "\e95d"; } diff --git a/front/core/styles/icons/salixfont.svg b/front/core/styles/icons/salixfont.svg index 9ca57000d..7e8695d63 100644 --- a/front/core/styles/icons/salixfont.svg +++ b/front/core/styles/icons/salixfont.svg @@ -102,6 +102,7 @@ + diff --git a/front/core/styles/icons/salixfont.ttf b/front/core/styles/icons/salixfont.ttf index ab5de35ff..608dd7c80 100644 Binary files a/front/core/styles/icons/salixfont.ttf and b/front/core/styles/icons/salixfont.ttf differ diff --git a/front/core/styles/icons/salixfont.woff b/front/core/styles/icons/salixfont.woff index d9ade1e31..ecea37f50 100644 Binary files a/front/core/styles/icons/salixfont.woff and b/front/core/styles/icons/salixfont.woff differ diff --git a/modules/entry/back/methods/entry/editLatestBuys.js b/modules/entry/back/methods/entry/editLatestBuys.js new file mode 100644 index 000000000..bd5358e31 --- /dev/null +++ b/modules/entry/back/methods/entry/editLatestBuys.js @@ -0,0 +1,87 @@ +module.exports = Self => { + Self.remoteMethod('editLatestBuys', { + description: 'Updates a column for one or more buys', + accessType: 'WRITE', + accepts: [{ + arg: 'field', + type: 'String', + required: true, + description: `the column to edit` + }, + { + arg: 'newValue', + type: 'Any', + required: true, + description: `The new value to save` + }, + { + arg: 'lines', + type: ['Object'], + required: true, + description: `the buys which will be modified` + }], + returns: { + type: 'Object', + root: true + }, + http: { + path: `/editLatestBuys`, + verb: 'POST' + } + }); + + Self.editLatestBuys = async(field, newValue, lines) => { + let modelName; + let identifier; + + switch (field) { + case 'size': + case 'density': + case 'minPrice': + case 'description': + modelName = 'Item'; + identifier = 'itemFk'; + break; + case 'quantity': + case 'buyingValue': + case 'freightValue': + case 'packing': + case 'grouping': + case 'groupingMode': + case 'comissionValue': + case 'packageValue': + case 'price2': + case 'price3': + case 'weight': + modelName = 'Buy'; + identifier = 'id'; + } + + const models = Self.app.models; + const model = models[modelName]; + + let tx = await model.beginTransaction({}); + + try { + let promises = []; + let options = {transaction: tx}; + + let targets = lines.map(line => { + return line[identifier]; + }); + + let value = {}; + value[field] = newValue; + + // intentarlo con updateAll + for (let target of targets) + promises.push(model.upsertWithWhere({id: target}, value, options)); + + await Promise.all(promises); + await tx.commit(); + } catch (error) { + await tx.rollback(); + throw error; + } + }; +}; diff --git a/modules/entry/back/methods/entry/latestBuysFilter.js b/modules/entry/back/methods/entry/latestBuysFilter.js new file mode 100644 index 000000000..dffe5793d --- /dev/null +++ b/modules/entry/back/methods/entry/latestBuysFilter.js @@ -0,0 +1,171 @@ + +const ParameterizedSQL = require('loopback-connector').ParameterizedSQL; +const buildFilter = require('vn-loopback/util/filter').buildFilter; +const mergeFilters = require('vn-loopback/util/filter').mergeFilters; + +module.exports = Self => { + Self.remoteMethodCtx('latestBuysFilter', { + description: 'Find all instances of the model matched by filter from the data source.', + accessType: 'READ', + accepts: [ + { + arg: 'filter', + type: 'Object', + description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', + }, + { + arg: 'search', + type: 'String', + description: `If it's and integer searchs by id, otherwise it searchs by name`, + }, { + arg: 'id', + type: 'Integer', + description: 'Item id', + }, { + arg: 'tags', + type: ['Object'], + description: 'List of tags to filter with', + http: {source: 'query'} + }, { + arg: 'description', + type: 'String', + description: 'The item description', + }, { + arg: 'salesPersonFk', + type: 'Integer', + description: 'The buyer of the item', + }, { + arg: 'active', + type: 'Boolean', + description: 'Whether the the item is or not active', + }, { + arg: 'visible', + type: 'Boolean', + description: 'Whether the the item is or not visible', + }, { + arg: 'typeFk', + type: 'Integer', + description: 'Type id', + }, { + arg: 'categoryFk', + type: 'Integer', + description: 'Category id', + } + ], + returns: { + type: ['Object'], + root: true + }, + http: { + path: `/latestBuysFilter`, + verb: 'GET' + } + }); + + Self.latestBuysFilter = async(ctx, filter) => { + let conn = Self.dataSource.connector; + let where = buildFilter(ctx.args, (param, value) => { + switch (param) { + case 'search': + return /^\d+$/.test(value) + ? {'i.id': value} + : {'i.name': {like: `%${value}%`}}; + case 'id': + return {'i.id': value}; + case 'description': + return {'i.description': {like: `%${value}%`}}; + case 'categoryFk': + return {'ic.id': value}; + case 'salesPersonFk': + return {'it.workerFk': value}; + case 'typeFk': + return {'i.typeFk': value}; + case 'active': + return {'i.isActive': value}; + case 'visible': + if (value) + return {'v.visible': {gt: 0}}; + else if (!value) + return {'v.visible': {lte: 0}}; + } + }); + filter = mergeFilters(ctx.args.filter, {where}); + + let stmts = []; + let stmt; + + stmts.push('CALL cache.last_buy_refresh(FALSE)'); + stmts.push('CALL cache.visible_refresh(@calc_id, FALSE, 1)'); + + stmt = new ParameterizedSQL(` + SELECT + i.image, + i.id AS itemFk, + i.size, + i.density, + i.typeFk, + i.family, + i.isActive, + i.minPrice, + i.description, + t.name AS type, + intr.description AS intrastat, + ori.code AS origin, + b.entryFk, + b.id, + b.quantity, + b.buyingValue, + b.freightValue, + b.isIgnored, + b.packing, + b.grouping, + b.groupingMode, + b.comissionValue, + b.packageValue, + b.price2, + b.price3, + b.ektFk, + b.weight + FROM cache.last_buy lb + LEFT JOIN cache.visible v ON v.item_id = lb.item_id + AND v.calc_id = @calc_id + JOIN item i ON i.id = lb.item_id + JOIN itemType it ON it.id = i.typeFk AND lb.warehouse_id = it.warehouseFk + JOIN buy b ON b.id = lb.buy_id + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN itemType t ON t.id = i.typeFk + LEFT JOIN intrastat intr ON intr.id = i.intrastatFk + LEFT JOIN origin ori ON ori.id = i.originFk` + ); + + if (ctx.args.tags) { + let i = 1; + for (const tag of ctx.args.tags) { + const tAlias = `it${i++}`; + + if (tag.tagFk) { + stmt.merge({ + sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id + AND ${tAlias}.tagFk = ? + AND ${tAlias}.value LIKE ?`, + params: [tag.tagFk, `%${tag.value}%`], + }); + } else { + stmt.merge({ + sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id + AND ${tAlias}.value LIKE ?`, + params: [`%${tag.value}%`], + }); + } + } + } + + stmt.merge(conn.makeSuffix(filter)); + let buysIndex = stmts.push(stmt) - 1; + + let sql = ParameterizedSQL.join(stmts, ';'); + let result = await conn.executeStmt(sql); + return buysIndex === 0 ? result : result[buysIndex]; + }; +}; + diff --git a/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js new file mode 100644 index 000000000..5d1bd5a0d --- /dev/null +++ b/modules/entry/back/methods/entry/specs/editLatestBuys.spec.js @@ -0,0 +1,31 @@ +const app = require('vn-loopback/server/server'); +const model = app.models.Buy; + +describe('Buy editLatestsBuys()', () => { + it('should change the value of a given column for the selected buys', async() => { + let ctx = { + args: { + search: 'Ranged weapon longbow 2m' + } + }; + + let [original] = await model.latestBuysFilter(ctx); + + const field = 'size'; + let newValue = 99; + const lines = [{itemFk: original.itemFk, id: original.id}]; + + await model.editLatestBuys(field, newValue, lines); + + let [result] = await model.latestBuysFilter(ctx); + + expect(result.size).toEqual(99); + + newValue = original.size; + await model.editLatestBuys(field, newValue, lines); + + let [restoredFixture] = await model.latestBuysFilter(ctx); + + expect(restoredFixture.size).toEqual(original.size); + }); +}); diff --git a/modules/entry/back/methods/entry/specs/latestBuysFilter.spec.js b/modules/entry/back/methods/entry/specs/latestBuysFilter.spec.js new file mode 100644 index 000000000..ba18fcf57 --- /dev/null +++ b/modules/entry/back/methods/entry/specs/latestBuysFilter.spec.js @@ -0,0 +1,139 @@ +const app = require('vn-loopback/server/server'); + +describe('Buy latests buys filter()', () => { + it('should return the entry matching "search"', async() => { + let ctx = { + args: { + search: 'Ranged weapon longbow 2m' + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + let firstBuy = results[0]; + + expect(results.length).toEqual(1); + expect(firstBuy.size).toEqual(70); + }); + + it('should return the entry matching "id"', async() => { + let ctx = { + args: { + id: 1 + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toEqual(1); + }); + + it('should return results matching "tags"', async() => { + let ctx = { + args: { + tags: [ + {tagFk: 27, value: '2m'} + ] + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(2); + }); + + it('should return results matching "categoryFk"', async() => { + let ctx = { + args: { + categoryFk: 1 + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(4); + }); + + it('should return results matching "typeFk"', async() => { + let ctx = { + args: { + typeFk: 2 + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(4); + }); + + it('should return results matching "active"', async() => { + let ctx = { + args: { + active: true + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(6); + }); + + it('should return results matching "not active"', async() => { + let ctx = { + args: { + active: false + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(0); + }); + + it('should return results matching "visible"', async() => { + let ctx = { + args: { + visible: true + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(5); + }); + + it('should return results matching "not visible"', async() => { + let ctx = { + args: { + visible: false + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(0); + }); + + it('should return results matching "salesPersonFk"', async() => { + let ctx = { + args: { + salesPersonFk: 35 + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(6); + }); + + it('should return results matching "description"', async() => { + let ctx = { + args: { + description: 'Increases block' + } + }; + + let results = await app.models.Buy.latestBuysFilter(ctx); + + expect(results.length).toBe(1); + }); +}); diff --git a/modules/entry/back/model-config.json b/modules/entry/back/model-config.json index c8c8babad..0e37b947f 100644 --- a/modules/entry/back/model-config.json +++ b/modules/entry/back/model-config.json @@ -2,6 +2,9 @@ "Entry": { "dataSource": "vn" }, + "Buy": { + "dataSource": "vn" + }, "EntryLog": { "dataSource": "vn" } diff --git a/modules/entry/back/models/buy.js b/modules/entry/back/models/buy.js new file mode 100644 index 000000000..e110164e8 --- /dev/null +++ b/modules/entry/back/models/buy.js @@ -0,0 +1,4 @@ +module.exports = Self => { + require('../methods/entry/editLatestBuys')(Self); + require('../methods/entry/latestBuysFilter')(Self); +}; diff --git a/modules/entry/back/models/buy.json b/modules/entry/back/models/buy.json new file mode 100644 index 000000000..1f3f1b862 --- /dev/null +++ b/modules/entry/back/models/buy.json @@ -0,0 +1,61 @@ +{ + "name": "Buy", + "base": "Loggable", + "log": { + "model": "EntryLog", + "relation": "entry" + }, + "options": { + "mysql": { + "table": "buy" + } + }, + "properties": { + "id": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "quantity": { + "type": "number" + }, + "buyingValue": { + "type": "number" + }, + "freightValue": { + "type": "number" + }, + "packing": { + "type": "number" + }, + "grouping": { + "type": "number" + }, + "groupingMode": { + "type": "number" + }, + "comissionValue": { + "type": "number" + }, + "packageValue": { + "type": "number" + }, + "price2": { + "type": "number" + }, + "price3": { + "type": "number" + }, + "weight": { + "type": "number" + } + }, + "relations": { + "entry": { + "type": "belongsTo", + "model": "Entry", + "foreignKey": "entryFk", + "required": true + } + } +} \ No newline at end of file diff --git a/modules/entry/back/models/entry.js b/modules/entry/back/models/entry.js index b1f71b4bd..713154d1b 100644 --- a/modules/entry/back/models/entry.js +++ b/modules/entry/back/models/entry.js @@ -1,4 +1,3 @@ - module.exports = Self => { require('../methods/entry/filter')(Self); require('../methods/entry/getEntry')(Self); diff --git a/modules/entry/back/models/entry.json b/modules/entry/back/models/entry.json index d3f149680..a67641b0c 100644 --- a/modules/entry/back/models/entry.json +++ b/modules/entry/back/models/entry.json @@ -1,6 +1,6 @@ { "name": "Entry", - "base": "VnModel", + "base": "Loggable", "log": { "model":"EntryLog" }, @@ -62,6 +62,18 @@ }, "loadPriority": { "type": "number" + }, + "supplierFk": { + "type": "number", + "required": true + }, + "travelFk": { + "type": "number", + "required": true + }, + "companyFk": { + "type": "number", + "required": true } }, "relations": { diff --git a/modules/entry/front/basic-data/index.html b/modules/entry/front/basic-data/index.html new file mode 100644 index 000000000..e69de29bb diff --git a/modules/entry/front/basic-data/index.js b/modules/entry/front/basic-data/index.js new file mode 100644 index 000000000..141a365fa --- /dev/null +++ b/modules/entry/front/basic-data/index.js @@ -0,0 +1,10 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +ngModule.vnComponent('vnEntryBasicData', { + template: require('./index.html'), + controller: Section, + bindings: { + entry: '<' + } +}); diff --git a/modules/entry/front/card/index.js b/modules/entry/front/card/index.js index f9ab6187c..eafed171b 100644 --- a/modules/entry/front/card/index.js +++ b/modules/entry/front/card/index.js @@ -10,7 +10,8 @@ class Controller extends ModuleCard { scope: { fields: ['id', 'code'] } - }, { + }, + { relation: 'travel', scope: { fields: ['id', 'landed', 'agencyFk', 'warehouseOutFk'], @@ -35,12 +36,14 @@ class Controller extends ModuleCard { } ] } - }, { + }, + { relation: 'supplier', scope: { fields: ['id', 'nickname'] } - }, { + }, + { relation: 'currency' } ] diff --git a/modules/entry/front/create/index.html b/modules/entry/front/create/index.html new file mode 100644 index 000000000..7b5dfc928 --- /dev/null +++ b/modules/entry/front/create/index.html @@ -0,0 +1,61 @@ + + + +
+ + + + + + + {{::id}} - {{::nickname}} + + + + + + + {{::agencyModeName}} - {{::warehouseInName}} ({{::shipped | date: 'dd/MM/yyyy'}}) → + {{::warehouseOutName}} ({{::landed | date: 'dd/MM/yyyy'}}) + + + + + + + + + + + + +
diff --git a/modules/entry/front/create/index.js b/modules/entry/front/create/index.js new file mode 100644 index 000000000..5c61730f9 --- /dev/null +++ b/modules/entry/front/create/index.js @@ -0,0 +1,43 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; +import './style.scss'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + + this.entry = { + companyFk: this.vnConfig.companyFk + }; + + if (this.$params && this.$params.supplierFk) + this.entry.supplierFk = parseInt(this.$params.supplierFk); + if (this.$params && this.$params.travelFk) + this.entry.travelFk = parseInt(this.$params.travelFk); + if (this.$params && this.$params.companyFk) + this.entry.companyFk = parseInt(this.$params.companyFk); + } + + onSubmit() { + this.$.watcher.submit().then( + res => this.$state.go('entry.card.basicData', {id: res.data.id}) + ); + } + + searchFunction($search) { + return {or: [ + {'agencyModeName': {like: `%${$search}%`}}, + {'warehouseInName': {like: `%${$search}%`}}, + {'warehouseOutName': {like: `%${$search}%`}}, + {'shipped': new Date($search)}, + {'landed': new Date($search)} + ]}; + } +} + +Controller.$inject = ['$element', '$scope']; + +ngModule.vnComponent('vnEntryCreate', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/entry/front/create/locale/es.yml b/modules/entry/front/create/locale/es.yml new file mode 100644 index 000000000..aa269ed15 --- /dev/null +++ b/modules/entry/front/create/locale/es.yml @@ -0,0 +1,2 @@ +New entry: Nueva entrada +Required fields (*): Campos requeridos (*) \ No newline at end of file diff --git a/modules/entry/front/create/style.scss b/modules/entry/front/create/style.scss new file mode 100644 index 000000000..2dc52b1ff --- /dev/null +++ b/modules/entry/front/create/style.scss @@ -0,0 +1,10 @@ +vn-entry-create { + vn-card { + position: relative + } + vn-icon[icon="info"] { + position: absolute; + top: 16px; + right: 16px + } +} \ No newline at end of file diff --git a/modules/entry/front/descriptor-popover/index.html b/modules/entry/front/descriptor-popover/index.html new file mode 100644 index 000000000..465a9bf51 --- /dev/null +++ b/modules/entry/front/descriptor-popover/index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/entry/front/descriptor-popover/index.js b/modules/entry/front/descriptor-popover/index.js new file mode 100644 index 000000000..d79aed03e --- /dev/null +++ b/modules/entry/front/descriptor-popover/index.js @@ -0,0 +1,9 @@ +import ngModule from '../module'; +import DescriptorPopover from 'salix/components/descriptor-popover'; + +class Controller extends DescriptorPopover {} + +ngModule.vnComponent('vnEntryDescriptorPopover', { + slotTemplate: require('./index.html'), + controller: Controller +}); diff --git a/modules/entry/front/descriptor/index.js b/modules/entry/front/descriptor/index.js index dd417a842..fed3787d4 100644 --- a/modules/entry/front/descriptor/index.js +++ b/modules/entry/front/descriptor/index.js @@ -11,15 +11,23 @@ class Controller extends Descriptor { } get travelFilter() { - return this.entry && JSON.stringify({ - agencyFk: this.entry.travel.agencyFk - }); + let travelFilter; + const entryTravel = this.entry && this.entry.travel; + + if (entryTravel && entryTravel.agencyFk) { + travelFilter = this.entry && JSON.stringify({ + agencyFk: entryTravel.agencyFk + }); + } + return travelFilter; } get entryFilter() { - if (!this.entry) return null; + let entryTravel = this.entry && this.entry.travel; - const date = new Date(this.entry.travel.landed); + if (!entryTravel || !entryTravel.landed) return null; + + const date = new Date(entryTravel.landed); date.setHours(0, 0, 0, 0); const from = new Date(date.getTime()); @@ -35,6 +43,48 @@ class Controller extends Descriptor { }); } + loadData() { + const filter = { + include: [ + { + relation: 'travel', + scope: { + fields: ['id', 'landed', 'agencyFk', 'warehouseOutFk'], + include: [ + { + relation: 'agency', + scope: { + fields: ['name'] + } + }, + { + relation: 'warehouseOut', + scope: { + fields: ['name'] + } + }, + { + relation: 'warehouseIn', + scope: { + fields: ['name'] + } + } + ] + } + }, + { + relation: 'supplier', + scope: { + fields: ['id', 'nickname'] + } + } + ] + }; + + return this.getData(`Entries/${this.id}`, {filter}) + .then(res => this.entity = res.data); + } + showEntryReport() { this.vnReport.show('entry-order', { entryId: this.entry.id diff --git a/modules/entry/front/descriptor/index.spec.js b/modules/entry/front/descriptor/index.spec.js index 513425c76..84defea3b 100644 --- a/modules/entry/front/descriptor/index.spec.js +++ b/modules/entry/front/descriptor/index.spec.js @@ -1,12 +1,14 @@ import './index.js'; describe('Entry Component vnEntryDescriptor', () => { + let $httpBackend; let controller; const entry = {id: 2}; beforeEach(ngModule('entry')); - beforeEach(inject($componentController => { + beforeEach(inject(($componentController, _$httpBackend_) => { + $httpBackend = _$httpBackend_; controller = $componentController('vnEntryDescriptor', {$element: null}, {entry}); })); @@ -24,4 +26,18 @@ describe('Entry Component vnEntryDescriptor', () => { expect(controller.vnReport.show).toHaveBeenCalledWith('entry-order', params); }); }); + + describe('loadData()', () => { + it('should perform ask for the entry', () => { + let query = `Entries/${entry.id}`; + jest.spyOn(controller, 'getData'); + + $httpBackend.expectGET(query).respond(); + controller.loadData(); + $httpBackend.flush(); + + expect(controller.getData).toHaveBeenCalledTimes(1); + expect(controller.getData).toHaveBeenCalledWith(query, jasmine.any(Object)); + }); + }); }); diff --git a/modules/entry/front/index.js b/modules/entry/front/index.js index f0c845b14..484d8f718 100644 --- a/modules/entry/front/index.js +++ b/modules/entry/front/index.js @@ -2,8 +2,12 @@ export * from './module'; import './main'; import './index/'; +import './create'; +import './latest-buys'; import './search-panel'; +import './latest-buys-search-panel'; import './descriptor'; +import './descriptor-popover'; import './card'; import './summary'; import './log'; diff --git a/modules/entry/front/index/index.html b/modules/entry/front/index/index.html index e3d174cbf..8a28888b0 100644 --- a/modules/entry/front/index/index.html +++ b/modules/entry/front/index/index.html @@ -69,4 +69,16 @@ - \ No newline at end of file + + +
+ + + + + + +
\ No newline at end of file diff --git a/modules/entry/front/index/locale/es.yml b/modules/entry/front/index/locale/es.yml index 8ef9b2c7a..2770ccfe9 100644 --- a/modules/entry/front/index/locale/es.yml +++ b/modules/entry/front/index/locale/es.yml @@ -12,4 +12,6 @@ Reference: Referencia Created: Creado Booked: Facturado Is inventory: Inventario -Notes: Notas \ No newline at end of file +Notes: Notas +Status: Estado +Selection: Selección \ No newline at end of file diff --git a/modules/entry/front/latest-buys-search-panel/index.html b/modules/entry/front/latest-buys-search-panel/index.html new file mode 100644 index 000000000..67fa7f0c2 --- /dev/null +++ b/modules/entry/front/latest-buys-search-panel/index.html @@ -0,0 +1,167 @@ + +
+
+ + + + + + + + + +
{{name}}
+
+ {{category.name}} +
+
> +
+
+ + + + + + + + + + + + + Tags + + + + + + + + + + + + + + + + + More fields + + + + + + + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + +
+
diff --git a/modules/entry/front/latest-buys-search-panel/index.js b/modules/entry/front/latest-buys-search-panel/index.js new file mode 100644 index 000000000..adc95f43d --- /dev/null +++ b/modules/entry/front/latest-buys-search-panel/index.js @@ -0,0 +1,79 @@ +import ngModule from '../module'; +import SearchPanel from 'core/components/searchbar/search-panel'; + +class Controller extends SearchPanel { + constructor($element, $) { + super($element, $); + let model = 'Item'; + let moreFields = ['description', 'name']; + + let properties; + let validations = window.validations; + + if (validations && validations[model]) + properties = validations[model].properties; + else + properties = {}; + + this.moreFields = []; + for (let field of moreFields) { + let prop = properties[field]; + this.moreFields.push({ + name: field, + label: prop ? prop.description : field, + type: prop ? prop.type : null + }); + } + } + + get filter() { + let filter = this.$.filter; + + for (let fieldFilter of this.fieldFilters) + filter[fieldFilter.name] = fieldFilter.value; + + return filter; + } + + set filter(value) { + if (!value) + value = {}; + if (!value.tags) + value.tags = [{}]; + + this.fieldFilters = []; + for (let field of this.moreFields) { + if (value[field.name] != undefined) { + this.fieldFilters.push({ + name: field.name, + value: value[field.name], + info: field + }); + } + } + + this.$.filter = value; + } + + getSourceTable(selection) { + if (!selection || selection.isFree === true) + return null; + + if (selection.sourceTable) { + return '' + + selection.sourceTable.charAt(0).toUpperCase() + + selection.sourceTable.substring(1) + 's'; + } else if (selection.sourceTable == null) + return `ItemTags/filterItemTags/${selection.id}`; + } + + removeField(index, field) { + this.fieldFilters.splice(index, 1); + this.$.filter[field] = undefined; + } +} + +ngModule.component('vnLatestBuysSearchPanel', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/entry/front/latest-buys-search-panel/index.spec.js b/modules/entry/front/latest-buys-search-panel/index.spec.js new file mode 100644 index 000000000..6d403b9fb --- /dev/null +++ b/modules/entry/front/latest-buys-search-panel/index.spec.js @@ -0,0 +1,45 @@ +import './index.js'; + +describe('Entry', () => { + describe('Component vnLatestBuysSearchPanel', () => { + let $element; + let controller; + + beforeEach(ngModule('entry')); + + beforeEach(angular.mock.inject($componentController => { + $element = angular.element(``); + controller = $componentController('vnLatestBuysSearchPanel', {$element}); + })); + + describe('getSourceTable()', () => { + it(`should return null if there's no selection`, () => { + let selection = null; + let result = controller.getSourceTable(selection); + + expect(result).toBeNull(); + }); + + it(`should return null if there's a selection but its isFree property is truthy`, () => { + let selection = {isFree: true}; + let result = controller.getSourceTable(selection); + + expect(result).toBeNull(); + }); + + it(`should return the formated sourceTable concatenated to a path`, () => { + let selection = {sourceTable: 'hello guy'}; + let result = controller.getSourceTable(selection); + + expect(result).toEqual('Hello guys'); + }); + + it(`should return a path if there's no sourceTable and the selection has an id`, () => { + let selection = {id: 99}; + let result = controller.getSourceTable(selection); + + expect(result).toEqual(`ItemTags/filterItemTags/${selection.id}`); + }); + }); + }); +}); diff --git a/modules/entry/front/latest-buys/index.html b/modules/entry/front/latest-buys/index.html new file mode 100644 index 000000000..4aa3aeae2 --- /dev/null +++ b/modules/entry/front/latest-buys/index.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + Picture + Id + Packing + Grouping + Description + Size + Type + Intrastat + Origin + Density + Active + Family + Entry + Quantity + Buying value + Freight value + Commission value + Package value + Is ignored + Grouping price + Packing price + Min price + Ekt + Weight + + + + + + + + + + + + + + {{::buy.itemFk | zeroFill:6}} + + + + + {{::buy.packing | dashIfEmpty}} + + + + + {{::buy.grouping | dashIfEmpty}} + + + + {{::buy.description | dashIfEmpty}} + + {{::buy.size}} + + {{::buy.type}} + + + {{::buy.intrastat}} + + {{::buy.origin}} + {{::buy.density}} + + + + + {{::buy.family}} + + + {{::buy.entryFk}} + + + {{::buy.quantity}} + {{::buy.buyingValue | currency: 'EUR':2}} + {{::buy.freightValue | currency: 'EUR':2}} + {{::buy.comissionValue | currency: 'EUR':2}} + {{::buy.packageValue | currency: 'EUR':2}} + {{::buy.isIgnored}} + {{::buy.price2 | currency: 'EUR':2}} + {{::buy.price3 | currency: 'EUR':2}} + {{::buy.minPrice | currency: 'EUR':2}} + {{::buy.ektFk | dashIfEmpty}} + {{::buy.weight}} + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + diff --git a/modules/entry/front/latest-buys/index.js b/modules/entry/front/latest-buys/index.js new file mode 100644 index 000000000..8d2c5fe13 --- /dev/null +++ b/modules/entry/front/latest-buys/index.js @@ -0,0 +1,78 @@ +import ngModule from '../module'; +import Section from 'salix/components/section'; + +export default class Controller extends Section { + constructor($element, $) { + super($element, $); + this.showFields = { + id: false, + actions: false + }; + this.editedColumn; + } + + get columns() { + if (this._columns) return this._columns; + + this._columns = [ + {field: 'quantity', displayName: this.$t('Quantity')}, + {field: 'buyingValue', displayName: this.$t('Buying value')}, + {field: 'freightValue', displayName: this.$t('Freight value')}, + {field: 'packing', displayName: this.$t('Packing')}, + {field: 'grouping', displayName: this.$t('Grouping')}, + {field: 'comissionValue', displayName: this.$t('Commission value')}, + {field: 'packageValue', displayName: this.$t('Package value')}, + {field: 'price2', displayName: this.$t('Grouping price')}, + {field: 'price3', displayName: this.$t('Packing price')}, + {field: 'weight', displayName: this.$t('Weight')}, + {field: 'description', displayName: this.$t('Description')}, + {field: 'minPrice', displayName: this.$t('Min price')}, + {field: 'size', displayName: this.$t('Size')}, + {field: 'density', displayName: this.$t('Density')} + ]; + + return this._columns; + } + + get checked() { + const buys = this.$.model.data || []; + const checkedBuys = []; + for (let buy of buys) { + if (buy.checked) + checkedBuys.push(buy); + } + + return checkedBuys; + } + + uncheck() { + const lines = this.checked; + for (let line of lines) { + if (line.checked) + line.checked = false; + } + } + + get totalChecked() { + return this.checked.length; + } + + onEditAccept() { + let data = { + field: this.editedColumn.field, + newValue: this.editedColumn.newValue, + lines: this.checked + }; + + return this.$http.post('Buys/editLatestBuys', data) + .then(() => { + this.uncheck(); + this.$.model.refresh(); + }); + } +} + +ngModule.component('vnEntryLatestBuys', { + template: require('./index.html'), + controller: Controller +}); diff --git a/modules/entry/front/latest-buys/index.spec.js b/modules/entry/front/latest-buys/index.spec.js new file mode 100644 index 000000000..658a2dc86 --- /dev/null +++ b/modules/entry/front/latest-buys/index.spec.js @@ -0,0 +1,82 @@ +import './index.js'; + +describe('Entry', () => { + describe('Component vnEntryLatestBuys', () => { + let controller; + let $httpBackend; + + beforeEach(ngModule('entry')); + + beforeEach(angular.mock.inject(($componentController, $compile, $rootScope, _$httpBackend_) => { + $httpBackend = _$httpBackend_; + let $element = $compile(' {}}, + edit: {hide: () => {}} + }; + })); + + describe('get columns', () => { + it(`should return a set of columns`, () => { + let result = controller.columns; + + let length = result.length; + let anyColumn = Object.keys(result[Math.floor(Math.random() * Math.floor(length))]); + + expect(anyColumn).toContain('field', 'displayName'); + }); + }); + + describe('get checked', () => { + it(`should return a set of checked lines`, () => { + controller.$.model.data = [ + {checked: true, id: 1}, + {checked: true, id: 2}, + {checked: true, id: 3}, + {checked: false, id: 4}, + ]; + + let result = controller.checked; + + expect(result.length).toEqual(3); + }); + }); + + describe('uncheck()', () => { + it(`should clear the selection of lines on the controller`, () => { + controller.$.model.data = [ + {checked: true, id: 1}, + {checked: true, id: 2}, + {checked: true, id: 3}, + {checked: false, id: 4}, + ]; + + let result = controller.checked; + + expect(result.length).toEqual(3); + + controller.uncheck(); + + result = controller.checked; + + expect(result.length).toEqual(0); + }); + }); + + describe('onEditAccept()', () => { + it(`should perform a query to update columns`, () => { + controller.editedColumn = {field: 'my field', newValue: 'the new value'}; + let query = 'Buys/editLatestBuys'; + + $httpBackend.expectPOST(query).respond(); + controller.onEditAccept(); + $httpBackend.flush(); + + const result = controller.checked; + + expect(result.length).toEqual(0); + }); + }); + }); +}); diff --git a/modules/entry/front/latest-buys/locale/en.yml b/modules/entry/front/latest-buys/locale/en.yml new file mode 100644 index 000000000..4f53d6126 --- /dev/null +++ b/modules/entry/front/latest-buys/locale/en.yml @@ -0,0 +1 @@ +Minimun amount: Minimun purchase quantity \ No newline at end of file diff --git a/modules/entry/front/latest-buys/locale/es.yml b/modules/entry/front/latest-buys/locale/es.yml new file mode 100644 index 000000000..7144caa8a --- /dev/null +++ b/modules/entry/front/latest-buys/locale/es.yml @@ -0,0 +1,13 @@ +Edit buy(s): Editar compra(s) +Buying value: Precio +Freight value: Porte +Commission value: Comisión +Package value: Embalaje +Is ignored: Ignorado +Grouping price: Precio grouping +Packing price: Precio packing +Min price: Precio min +Ekt: Ekt +Weight: Peso +Minimun amount: Cantidad mínima de compra +Field to edit: Campo a editar \ No newline at end of file diff --git a/modules/entry/front/locale/es.yml b/modules/entry/front/locale/es.yml index 0858bb7f9..b28cbe735 100644 --- a/modules/entry/front/locale/es.yml +++ b/modules/entry/front/locale/es.yml @@ -1,5 +1,6 @@ #Ordenar alfabeticamente entry: entrada +Latest buys: Últimas compras # Sections diff --git a/modules/entry/front/routes.json b/modules/entry/front/routes.json index 084ff7bb2..a430a95fa 100644 --- a/modules/entry/front/routes.json +++ b/modules/entry/front/routes.json @@ -2,13 +2,15 @@ "module": "entry", "name": "Entries", "icon": "icon-entry", - "dependencies": ["travel"], + "dependencies": ["travel", "item"], "validations": true, "menus": { "main": [ - {"state": "entry.index", "icon": "icon-entry"} + {"state": "entry.index", "icon": "icon-entry"}, + {"state": "entry.latestBuys", "icon": "icon-latestBuys"} ], "card": [ + {"state": "entry.card.basicData", "icon": "settings"}, {"state": "entry.card.buy", "icon": "icon-lines"}, {"state": "entry.card.log", "icon": "history"} ] @@ -24,7 +26,20 @@ "url": "/index?q", "state": "entry.index", "component": "vn-entry-index", - "description": "Entries" + "description": "Entries", + "acl": ["buyer"] + }, { + "url": "/latest-buys?q", + "state": "entry.latestBuys", + "component": "vn-entry-latest-buys", + "description": "Latest buys", + "acl": ["buyer"] + }, { + "url": "/create?supplierFk&travelFk&companyFk", + "state": "entry.create", + "component": "vn-entry-create", + "description": "New entry", + "acl": ["buyer"] }, { "url": "/:id", "state": "entry.card", @@ -38,6 +53,14 @@ "params": { "entry": "$ctrl.entry" } + }, { + "url": "/basic-data", + "state": "entry.card.basicData", + "component": "vn-entry-basic-data", + "description": "Basic data", + "params": { + "entry": "$ctrl.entry" + } }, { "url" : "/log", "state": "entry.card.log", diff --git a/modules/item/back/methods/item-image-queue/downloadImages.js b/modules/item/back/methods/item-image-queue/downloadImages.js index 420b357a5..d953d1938 100644 --- a/modules/item/back/methods/item-image-queue/downloadImages.js +++ b/modules/item/back/methods/item-image-queue/downloadImages.js @@ -26,7 +26,9 @@ module.exports = Self => { await fs.mkdir(tempPath, {recursive: true}); const timer = setInterval(async() => { - const image = await Self.findOne({where: {error: null}}); + const image = await Self.findOne({ + where: {error: null, url: {neq: null}} + }); // Exit loop if (!image) return clearInterval(timer); diff --git a/modules/item/back/methods/item/filter.js b/modules/item/back/methods/item/filter.js index ad7edfa8c..a38a06713 100644 --- a/modules/item/back/methods/item/filter.js +++ b/modules/item/back/methods/item/filter.js @@ -12,42 +12,38 @@ module.exports = Self => { arg: 'filter', type: 'Object', description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string', - http: {source: 'query'} }, { arg: 'tags', type: ['Object'], description: 'List of tags to filter with', - http: {source: 'query'} }, { arg: 'search', type: 'String', description: `If it's and integer searchs by id, otherwise it searchs by name`, - http: {source: 'query'} }, { arg: 'id', type: 'Integer', description: 'Item id', - http: {source: 'query'} }, { arg: 'categoryFk', type: 'Integer', description: 'Category id', - http: {source: 'query'} }, { arg: 'typeFk', type: 'Integer', description: 'Type id', - http: {source: 'query'} }, { arg: 'isActive', type: 'Boolean', - description: 'Whether the the item is o not active', - http: {source: 'query'} + description: 'Whether the the item is or not active', }, { arg: 'salesPersonFk', type: 'Integer', description: 'The buyer of the item', - http: {source: 'query'} + }, { + arg: 'description', + type: 'String', + description: 'The item description', } ], returns: { diff --git a/modules/item/back/models/item.json b/modules/item/back/models/item.json index 5d2e47d2a..c5af8890b 100644 --- a/modules/item/back/models/item.json +++ b/modules/item/back/models/item.json @@ -122,6 +122,9 @@ "mysql": { "columnName": "expenceFk" } + }, + "minPrice": { + "type": "number" } }, "relations": { diff --git a/modules/item/back/models/supplier.json b/modules/item/back/models/supplier.json index bc13e79b9..41fc9c45c 100644 --- a/modules/item/back/models/supplier.json +++ b/modules/item/back/models/supplier.json @@ -33,13 +33,13 @@ "retAccount": { "type": "Number" }, - "commision": { + "commission": { "type": "Boolean" }, "created": { "type": "Date" }, - "poscodeFk": { + "postcodeFk": { "type": "Number" }, "isActive": { diff --git a/modules/item/front/index/index.html b/modules/item/front/index/index.html index 0ee6a8815..eaef0f34f 100644 --- a/modules/item/front/index/index.html +++ b/modules/item/front/index/index.html @@ -8,24 +8,24 @@ + vn-smart-table="itemIndex"> - + Id - Grouping - Packing - Description - Stems - Size - Niche - Type - Category - Intrastat - Origin - Buyer - Density - Active + Grouping + Packing + Description + Stems + Size + Niche + Type + Category + Intrastat + Origin + Buyer + Density + Active diff --git a/modules/route/front/index/index.html b/modules/route/front/index/index.html index 7258018f1..76938d9cf 100644 --- a/modules/route/front/index/index.html +++ b/modules/route/front/index/index.html @@ -79,10 +79,9 @@ tooltip-position="left"> - + diff --git a/modules/ticket/back/methods/ticket/filter.js b/modules/ticket/back/methods/ticket/filter.js index 3a34be442..878b4278e 100644 --- a/modules/ticket/back/methods/ticket/filter.js +++ b/modules/ticket/back/methods/ticket/filter.js @@ -245,7 +245,7 @@ module.exports = Self => { SELECT f.id ticketFk, f.clientFk, f.warehouseFk, f.shipped FROM tmp.filter f LEFT JOIN alertLevel al ON al.alertLevel = f.alertLevel - WHERE (f.alertLevelCode = 'FREE' OR f.alertLevel IS NULL) + WHERE (al.code = 'FREE' OR f.alertLevel IS NULL) AND f.shipped >= CURDATE()`); stmts.push('CALL ticketGetProblems()'); diff --git a/modules/ticket/front/descriptor/index.spec.js b/modules/ticket/front/descriptor/index.spec.js index fe7f60c14..a725a2d4a 100644 --- a/modules/ticket/front/descriptor/index.spec.js +++ b/modules/ticket/front/descriptor/index.spec.js @@ -70,7 +70,7 @@ describe('Ticket Component vnTicketDescriptor', () => { window.open = jasmine.createSpy('open'); const params = { - clientId: ticket.client.id, + recipientId: ticket.client.id, ticketId: ticket.id }; controller.showDeliveryNote(); @@ -85,7 +85,7 @@ describe('Ticket Component vnTicketDescriptor', () => { const params = { recipient: ticket.client.email, - clientId: ticket.client.id, + recipientId: ticket.client.id, ticketId: ticket.id }; controller.sendDeliveryNote(); @@ -135,7 +135,7 @@ describe('Ticket Component vnTicketDescriptor', () => { }); describe('canStowaway()', () => { - fit('should make a query and return if the ticket can be stowawayed', () => { + it('should make a query and return if the ticket can be stowawayed', () => { controller.canStowaway(); $httpBackend.flush(); @@ -179,13 +179,10 @@ describe('Ticket Component vnTicketDescriptor', () => { describe('loadData()', () => { it(`should perform a get query to store the ticket data into the controller`, () => { - controller.ticket = null; - - $httpBackend.expectRoute('GET', `Tickets/${ticket.id}`).respond(ticket); - controller.id = ticket.id; + $httpBackend.when('GET', `Tickets/${ticket.id}/isEditable`).respond(); + $httpBackend.expectRoute('GET', `Tickets/${ticket.id}`).respond(); + controller.loadData(); $httpBackend.flush(); - - expect(controller.ticket).toEqual(ticket); }); }); }); diff --git a/modules/ticket/front/index/index.html b/modules/ticket/front/index/index.html index b0aff5b91..2c598ca03 100644 --- a/modules/ticket/front/index/index.html +++ b/modules/ticket/front/index/index.html @@ -143,10 +143,9 @@ vn-tooltip="Payment on account..." tooltip-position="left"> - + diff --git a/modules/travel/back/methods/travel/filter.js b/modules/travel/back/methods/travel/filter.js index 0cfafd7ba..024448bfe 100644 --- a/modules/travel/back/methods/travel/filter.js +++ b/modules/travel/back/methods/travel/filter.js @@ -112,7 +112,8 @@ module.exports = Self => { let stmts = []; let stmt; stmt = new ParameterizedSQL( - `SELECT + `SELECT * FROM + (SELECT t.id, t.shipped, t.landed, @@ -132,7 +133,7 @@ module.exports = Self => { FROM vn.travel t JOIN vn.agencyMode am ON am.id = t.agencyFk JOIN vn.warehouse win ON win.id = t.warehouseInFk - JOIN vn.warehouse wout ON wout.id = t.warehouseOutFk` + JOIN vn.warehouse wout ON wout.id = t.warehouseOutFk) AS t` ); stmt.merge(conn.makeSuffix(filter)); diff --git a/modules/worker/front/calendar/index.js b/modules/worker/front/calendar/index.js index 006725172..eb2ea35cc 100644 --- a/modules/worker/front/calendar/index.js +++ b/modules/worker/front/calendar/index.js @@ -91,7 +91,7 @@ class Controller extends Section { if (data.holidays) { data.holidays.forEach(holiday => { - const holidayDetail = holiday.detail && holiday.detail.description; + const holidayDetail = holiday.detail && holiday.detail.name; const holidayType = holiday.type && holiday.type.name; const holidayName = holidayDetail || holidayType; diff --git a/modules/worker/front/calendar/index.spec.js b/modules/worker/front/calendar/index.spec.js index ebf52dc66..cb42fb316 100644 --- a/modules/worker/front/calendar/index.spec.js +++ b/modules/worker/front/calendar/index.spec.js @@ -81,8 +81,8 @@ describe('Worker', () => { $httpBackend.whenRoute('GET', 'Calendars/absences') .respond({ holidays: [ - {dated: today, detail: {description: 'New year'}}, - {dated: tomorrow, detail: {description: 'Easter'}} + {dated: today, detail: {name: 'New year'}}, + {dated: tomorrow, detail: {name: 'Easter'}} ], absences: [ {dated: today, absenceType: {name: 'Holiday', rgb: '#aaa'}}, diff --git a/print/methods/closure.js b/print/methods/closure.js index 3bcca9d4e..a1450f446 100644 --- a/print/methods/closure.js +++ b/print/methods/closure.js @@ -4,42 +4,179 @@ const smtp = require('../core/smtp'); const config = require('../core/config'); module.exports = app => { - app.get('/api/closure/by-ticket', async function(req, res) { + app.get('/api/closure/all', async function(req, res, next) { + try { + res.status(200).json({ + message: 'Task executed successfully' + }); + + await db.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close`); + await db.rawSql(` + CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY ( + SELECT + t.id AS ticketFk + FROM expedition e + JOIN ticket t ON t.id = e.ticketFk + JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.alertLevel = ts.alertLevel + WHERE al.code = 'PACKED' + AND DATE(t.shipped) BETWEEN DATE_ADD(CURDATE(), INTERVAL -2 DAY) AND CURDATE() + AND t.refFk IS NULL + GROUP BY e.ticketFk)`); + + await closeAll(req.args); + + await db.rawSql(` + UPDATE ticket t + JOIN ticketState ts ON t.id = ts.ticketFk + JOIN alertLevel al ON al.alertLevel = ts.alertLevel + JOIN agencyMode am ON am.id = t.agencyModeFk + JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk + JOIN zone z ON z.id = t.zoneFk + SET t.routeFk = NULL + WHERE shipped BETWEEN CURDATE() AND util.dayEnd(CURDATE()) + AND al.code NOT IN('DELIVERED','PACKED') + AND t.routeFk + AND z.name LIKE '%MADRID%'`); + } catch (error) { + next(error); + } }); - app.get('/api/closure/all', async function(req, res) { - res.status(200).json({ - message: 'Task executed successfully' - }); + app.get('/api/closure/by-ticket', async function(req, res, next) { + try { + const reqArgs = req.args; + if (!reqArgs.ticketId) + throw new Error('The argument ticketId is required'); + res.status(200).json({ + message: 'Task executed successfully' + }); + + await db.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close`); + await db.rawSql(` + CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY ( + SELECT + t.id AS ticketFk + FROM expedition e + JOIN ticket t ON t.id = e.ticketFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.alertLevel = ts.alertLevel + WHERE al.code = 'PACKED' + AND t.id = :ticketId + AND t.refFk IS NULL + GROUP BY e.ticketFk)`, { + ticketId: reqArgs.ticketId + }); + + await closeAll(reqArgs); + } catch (error) { + next(error); + } + }); + + app.get('/api/closure/by-agency', async function(req, res) { + try { + const reqArgs = req.args; + if (!reqArgs.agencyModeId) + throw new Error('The argument agencyModeId is required'); + + if (!reqArgs.warehouseId) + throw new Error('The argument warehouseId is required'); + + if (!reqArgs.to) + throw new Error('The argument to is required'); + + res.status(200).json({ + message: 'Task executed successfully' + }); + + await db.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close`); + await db.rawSql(` + CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY ( + SELECT + t.id AS ticketFk + FROM expedition e + JOIN ticket t ON t.id = e.ticketFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.alertLevel = ts.alertLevel + WHERE al.code = 'PACKED' + AND t.agencyModeFk = :agencyModeId + AND t.warehouseFk = :warehouseId + AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY) AND :to + AND t.refFk IS NULL + GROUP BY e.ticketFk)`, { + agencyModeId: reqArgs.agencyModeId, + warehouseId: reqArgs.warehouseId, + to: reqArgs.to + }); + + await closeAll(reqArgs); + } catch (error) { + next(error); + } + }); + + app.get('/api/closure/by-route', async function(req, res) { + try { + const reqArgs = req.args; + if (!reqArgs.routeId) + throw new Error('The argument routeId is required'); + + res.status(200).json({ + message: 'Task executed successfully' + }); + + await db.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close`); + await db.rawSql(` + CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY ( + SELECT + t.id AS ticketFk + FROM expedition e + JOIN ticket t ON t.id = e.ticketFk + JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.alertLevel = ts.alertLevel + WHERE al.code = 'PACKED' + AND t.routeFk = :routeId + AND t.refFk IS NULL + GROUP BY e.ticketFk)`, { + routeId: reqArgs.routeId + }); + + await closeAll(reqArgs); + } catch (error) { + next(error); + } + }); + + async function closeAll(reqArgs) { const failedtickets = []; const tickets = await db.rawSql(` SELECT t.id, t.clientFk, c.email recipient, - c.isToBeMailed, c.salesPersonFk, + c.isToBeMailed, + c.hasToInvoice, + co.hasDailyInvoice, eu.email salesPersonEmail - FROM expedition e - JOIN ticket t ON t.id = e.ticketFk + FROM tmp.ticket_close tt + JOIN ticket t ON t.id = tt.ticketFk JOIN client c ON c.id = t.clientFk - JOIN warehouse wh ON wh.id = t.warehouseFk AND wh.hasComission - JOIN ticketState ts ON ts.ticketFk = t.id - JOIN alertLevel al ON al.alertLevel = ts.alertLevel - LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk - WHERE al.code = 'PACKED' - AND DATE(t.shipped) BETWEEN DATE_ADD(CURDATE(), INTERVAL -2 DAY) AND CURDATE() - AND t.refFk IS NULL - GROUP BY e.ticketFk`); + JOIN province p ON p.id = c.provinceFk + JOIN country co ON co.id = p.countryFk + LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk`); for (const ticket of tickets) { try { - await db.rawSql(`CALL vn.ticket_closeByTicket(:ticketId)`, { + await db.rawSql(`CALL vn.ticket_close(:ticketId)`, { ticketId: ticket.id }); - if (!ticket.salesPersonFk || !ticket.isToBeMailed) continue; + const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice; + if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue; if (!ticket.recipient) { const body = `No se ha podido enviar el albarán ${ticket.id} @@ -54,7 +191,6 @@ module.exports = app => { continue; } - const reqArgs = req.args; const args = Object.assign({ ticketId: ticket.id, recipientId: ticket.clientFk, @@ -88,5 +224,7 @@ module.exports = app => { html: body }); } - }); + + await db.rawSql(`DROP TEMPORARY TABLE tmp.ticket_close`); + } }; diff --git a/print/templates/email/delivery-note-link/locale/en.yml b/print/templates/email/delivery-note-link/locale/en.yml index 5f1526828..aaa545525 100644 --- a/print/templates/email/delivery-note-link/locale/en.yml +++ b/print/templates/email/delivery-note-link/locale/en.yml @@ -1,5 +1,5 @@ subject: Your delivery note -title: "Here is your delivery note!" +title: Your delivery note dear: Dear client description: The delivery note from the order {0} is now available.
You can download it by clicking
this link. diff --git a/print/templates/email/delivery-note-link/locale/es.yml b/print/templates/email/delivery-note-link/locale/es.yml index 47c7f11da..0bafd459a 100644 --- a/print/templates/email/delivery-note-link/locale/es.yml +++ b/print/templates/email/delivery-note-link/locale/es.yml @@ -1,5 +1,5 @@ -subject: Aquí tienes tu albarán -title: "Aquí tienes tu albarán" +subject: Tu albarán +title: Tu albarán dear: Estimado cliente description: Ya está disponible el albarán correspondiente al pedido {0}.
Puedes verlo haciendo clic en este enlace. diff --git a/print/templates/email/delivery-note-link/locale/fr.yml b/print/templates/email/delivery-note-link/locale/fr.yml index 3ecf357e1..bcb16c09f 100644 --- a/print/templates/email/delivery-note-link/locale/fr.yml +++ b/print/templates/email/delivery-note-link/locale/fr.yml @@ -1,5 +1,5 @@ -subject: Voici votre bon de livraison -title: "Voici votre bon de livraison" +subject: Votre bon de livraison +title: Votre bon de livraison dear: Cher client, description: Le bon de livraison correspondant à la commande {0} est maintenant disponible.
Vous pouvez le voir en cliquant sur ce lien. diff --git a/print/templates/email/delivery-note-link/locale/pt.yml b/print/templates/email/delivery-note-link/locale/pt.yml index c008ea2c7..cff3ea52b 100644 --- a/print/templates/email/delivery-note-link/locale/pt.yml +++ b/print/templates/email/delivery-note-link/locale/pt.yml @@ -1,5 +1,5 @@ -subject: Vossa Nota de Entrega -title: "Esta é vossa nota de entrega!" +subject: Vossa nota de entrega +title: Vossa nota de entrega dear: Estimado cliente description: Já está disponível sua nota de entrega correspondente a encomenda numero {0}.
Para ver-lo faça um clique neste link. diff --git a/print/templates/email/delivery-note/locale/en.yml b/print/templates/email/delivery-note/locale/en.yml index fcabe11ec..50d39e8cf 100644 --- a/print/templates/email/delivery-note/locale/en.yml +++ b/print/templates/email/delivery-note/locale/en.yml @@ -1,5 +1,5 @@ subject: Your delivery note -title: "Here is your delivery note!" +title: Your delivery note dear: Dear client description: The delivery note from the order {0} is now available.
You can download it by clicking on the attachment of this email. diff --git a/print/templates/email/delivery-note/locale/es.yml b/print/templates/email/delivery-note/locale/es.yml index 3294b2316..ffa99e12f 100644 --- a/print/templates/email/delivery-note/locale/es.yml +++ b/print/templates/email/delivery-note/locale/es.yml @@ -1,5 +1,5 @@ -subject: Aquí tienes tu albarán -title: "¡Este es tu albarán!" +subject: Tu albarán +title: Tu albarán dear: Estimado cliente description: Ya está disponible el albarán correspondiente al pedido {0}.
Puedes descargarlo haciendo clic en el adjunto de este correo. diff --git a/print/templates/email/delivery-note/locale/fr.yml b/print/templates/email/delivery-note/locale/fr.yml index fdaf6e320..f8fb5e7cd 100644 --- a/print/templates/email/delivery-note/locale/fr.yml +++ b/print/templates/email/delivery-note/locale/fr.yml @@ -1,5 +1,5 @@ -subject: Voici votre bon de livraison -title: "Voici votre bon de livraison!" +subject: Votre bon de livraison +title: Votre bon de livraison dear: Cher client, description: Le bon de livraison correspondant à la commande {0} est maintenant disponible.
Vous pouvez le télécharger en cliquant sur la pièce jointe dans cet email. diff --git a/print/templates/email/delivery-note/locale/pt.yml b/print/templates/email/delivery-note/locale/pt.yml index cbca170a3..818a4de4c 100644 --- a/print/templates/email/delivery-note/locale/pt.yml +++ b/print/templates/email/delivery-note/locale/pt.yml @@ -1,5 +1,5 @@ -subject: Vossa Nota de Entrega -title: "Esta é vossa nota de entrega!" +subject: Vossa nota de entrega +title: Vossa nota de entrega dear: Estimado cliente description: Já está disponível sua nota de entrega correspondente a encomenda {0}.
Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail.