diff --git a/back/models/sage-withholding.json b/back/models/sage-withholding.json index 8d93daeae..dddbcfd74 100644 --- a/back/models/sage-withholding.json +++ b/back/models/sage-withholding.json @@ -6,6 +6,9 @@ "table": "sage.TiposRetencion" } }, + "log": { + "showField": "withholding" + }, "properties": { "id": { "type": "Number", diff --git a/db/changes/10340-summer/00-buy_importReference.sql b/db/changes/10340-summer/00-buy_importReference.sql new file mode 100644 index 000000000..f6bdc059c --- /dev/null +++ b/db/changes/10340-summer/00-buy_importReference.sql @@ -0,0 +1,14 @@ +create table `vn`.`itemMatchProperties` +( + itemFk int not null, + name varchar(80) not null, + producer varchar(80) not null, + size int not null, + constraint itemMatchProperties_pk + primary key (itemFk, name, producer, size), + constraint itemFk___fk + foreign key (itemFk) references item (id) + on update cascade on delete cascade +) +comment 'Propiedades para encontrar articulos equivalentes en verdnatura'; + diff --git a/db/changes/10340-summer/00-sample.sql b/db/changes/10340-summer/00-sample.sql new file mode 100644 index 000000000..d4858ac72 --- /dev/null +++ b/db/changes/10340-summer/00-sample.sql @@ -0,0 +1,7 @@ +ALTER TABLE `vn`.`sample` ADD COLUMN + (`datepickerEnabled` TINYINT(1) NOT NULL DEFAULT 0); + +ALTER TABLE `vn`.`sample` MODIFY code VARCHAR(25) charset utf8 NOT NULL; + +INSERT INTO `vn`.`sample` (code, description, isVisible, hasCompany, hasPreview, datepickerEnabled) + VALUES ('client-debt-statement', 'Extracto del cliente', 1, 0, 1, 1); \ No newline at end of file diff --git a/db/changes/10340-summer/00-ticket_close.sql b/db/changes/10340-summer/00-ticket_close.sql new file mode 100644 index 000000000..a8086549c --- /dev/null +++ b/db/changes/10340-summer/00-ticket_close.sql @@ -0,0 +1,109 @@ +drop procedure `vn`.`ticket_close`; + +DELIMITER $$ +$$ +create + definer = root@`%` procedure `vn`.`ticket_close`() +BEGIN +/** + * Realiza el cierre de todos los + * tickets de la tabla tmp.ticket_close. + * + * @table tmp.ticket_close(ticketFk) Identificadores de los tickets a cerrar + */ + 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.ticket_close; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE; + DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN + RESIGNAL; + END; + + OPEN cur; + + proc: LOOP + SET vDone = FALSE; + + FETCH cur INTO vCurTicketFk; + + IF vDone THEN + LEAVE proc; + END IF; + + -- Fetch ticket data + 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, + NULL, + vNewInvoiceId); + END IF; + ELSE + CALL ticketTrackingAdd(vCurTicketFk, (SELECT vn.getAlert3State(vCurTicketFk)), NULL); + END IF; + END LOOP; + + CLOSE cur; +END;;$$ +DELIMITER ; diff --git a/db/changes/10340-summer/00-ticket_closeAll.sql b/db/changes/10340-summer/00-ticket_closeAll.sql new file mode 100644 index 000000000..6441f2c1a --- /dev/null +++ b/db/changes/10340-summer/00-ticket_closeAll.sql @@ -0,0 +1,118 @@ +drop procedure `vn`.`ticket_closeAll`; + +DELIMITER $$ +$$ +create definer = root@`%` procedure `vn`.`ticket_closeAll__`() +BEGIN +/** + * Realiza el cierre de todos los + * tickets de la tabla tmp.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.ticketClosure2; + CREATE TEMPORARY TABLE tmp.ticketClosure2 + SELECT ticketFk FROM tmp.ticketClosure; + + INSERT INTO tmp.ticketClosure + SELECT id FROM stowaway s + JOIN tmp.ticketClosure2 tc ON s.shipFk = tc.ticketFk; + 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, + 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/changes/10340-summer/00-ticket_closeByTicket.sql b/db/changes/10340-summer/00-ticket_closeByTicket.sql new file mode 100644 index 000000000..a24ea5ad9 --- /dev/null +++ b/db/changes/10340-summer/00-ticket_closeByTicket.sql @@ -0,0 +1,34 @@ +drop procedure `vn`.`ticket_closeByTicket`; + +DELIMITER $$ +$$ +create + definer = root@`%` procedure `vn`.`ticket_closeByTicket`(IN vTicketFk int) +BEGIN + +/** + * Inserta el ticket en la tabla temporal + * para ser cerrado. + * + * @param vTicketFk Id del ticket + */ + + DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close; + CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY ( + SELECT + t.id AS ticketFk + FROM expedition e + INNER JOIN ticket t ON t.id = e.ticketFk + LEFT JOIN ticketState ts ON ts.ticketFk = t.id + JOIN alertLevel al ON al.id = ts.alertLevel + WHERE + al.code = 'PACKED' + AND t.id = vTicketFk + AND t.refFk IS NULL + GROUP BY e.ticketFk); + + CALL ticket_close(); + + DROP TEMPORARY TABLE tmp.ticket_close; +END;;$$ +DELIMITER ; diff --git a/db/changes/10340-summer/01-credit_request.sql b/db/changes/10340-summer/01-credit_request.sql new file mode 100644 index 000000000..821623326 --- /dev/null +++ b/db/changes/10340-summer/01-credit_request.sql @@ -0,0 +1,2 @@ +INSERT INTO `vn`.`sample` (`code`, `description`, `isVisible`, `hasCompany`, `hasPreview`) + VALUES ('credit-request', 'Solicitud de crédito', 1, 1, 1); \ No newline at end of file diff --git a/db/dump/fixtures.sql b/db/dump/fixtures.sql index 25e855bd7..89d5d463a 100644 --- a/db/dump/fixtures.sql +++ b/db/dump/fixtures.sql @@ -2349,6 +2349,14 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`) (9, 9, 9), (10, 10, 10); +INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxCodeFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`, `created`) + VALUES + (1, 4, 99.99, '2000000000', null, null, null, CURDATE()), + (2, 4, 999.99, '2000000000', null, null, null, CURDATE()), + (3, 4, 1000.50, '2000000000', null, null, null, CURDATE()), + (4, 4, 0.50, '2000000000', null, null, null, CURDATE()), + (5, 4, 150.50, '2000000000', null, null, null, CURDATE()); + INSERT INTO `vn`.`ticketRecalc`(`ticketFk`) SELECT `id` FROM `vn`.`ticket` t diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index e2ff0f0e4..a561a08cf 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -258,7 +258,9 @@ export default { }, clientLog: { lastModificationPreviousValue: 'vn-client-log vn-table vn-td.before', - lastModificationCurrentValue: 'vn-client-log vn-table vn-td.after' + lastModificationCurrentValue: 'vn-client-log vn-table vn-td.after', + penultimateModificationPreviousValue: 'vn-client-log vn-table vn-tr:nth-child(2) vn-td.before', + penultimateModificationCurrentValue: 'vn-client-log vn-table vn-tr:nth-child(2) vn-td.after' }, clientBalance: { @@ -462,8 +464,8 @@ export default { itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor', itemDescriptorPopoverItemDiaryButton: 'vn-item-descriptor a[href="#!/item/2/diary?warehouseFk=5&lineFk=16"]', popoverDiaryButton: '.vn-popover.shown vn-item-descriptor vn-icon[icon="icon-transaction"]', - firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(4)', - firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(7)', + firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(5)', + firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(8)', invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span', setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button', descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div' @@ -566,18 +568,18 @@ export default { moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input', transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text', transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable', - firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(5) > span', + firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > span', firstSaleClaimIcon: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) vn-icon[icon="icon-claims"]', firstSaleDescriptorImage: '.vn-popover.shown vn-item-descriptor img', firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img', firstSaleZoomedImage: 'body > div > div > img', firstSaleQuantity: 'vn-ticket-sale [ng-model="sale.quantity"]', - firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(6)', - firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(8) > span', + firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(7)', + firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span', firstSalePriceInput: '.vn-popover.shown input[ng-model="$ctrl.field"]', - firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span', + firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(10) > span', firstSaleDiscountInput: '.vn-popover.shown [ng-model="$ctrl.field"]', - firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(10)', + firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(11)', firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)', firstSaleColour: 'vn-ticket-sale vn-tr:nth-child(1) vn-fetched-tags section', firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[ng-model="sale.checked"]', @@ -585,8 +587,8 @@ export default { secondSaleId: 'vn-ticket-sale:nth-child(2) vn-td-editable:nth-child(4) text > span', secondSaleIdAutocomplete: 'vn-ticket-sale vn-tr:nth-child(2) vn-autocomplete[ng-model="sale.itemFk"]', secondSaleQuantity: 'vn-ticket-sale vn-table vn-tr:nth-child(2) vn-input-number', - secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(6)', - secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(7)', + secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(7)', + secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(8)', secondSaleConceptInput: 'vn-ticket-sale vn-tbody > :nth-child(2) > vn-td-editable.ng-isolate-scope.selected vn-textfield', totalImport: 'vn-ticket-sale vn-one.taxes > p:nth-child(3) > strong', selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check', @@ -936,7 +938,8 @@ export default { invoiceInDescriptor: { moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]', moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]', - acceptDeleteButton: '.vn-confirm.shown button[response="accept"]' + moreMenuCloneInvoiceIn: '.vn-menu [name="cloneInvoice"]', + acceptButton: '.vn-confirm.shown button[response="accept"]' }, invoiceInBasicData: { issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]', diff --git a/e2e/paths/02-client/07_edit_web_access.spec.js b/e2e/paths/02-client/07_edit_web_access.spec.js index b1af95638..bcd476f6b 100644 --- a/e2e/paths/02-client/07_edit_web_access.spec.js +++ b/e2e/paths/02-client/07_edit_web_access.spec.js @@ -16,8 +16,15 @@ describe('Client Edit web access path', () => { await browser.close(); }); - it(`should uncheck the Enable web access checkbox and update the name`, async() => { + it('should uncheck the Enable web access checkbox', async() => { await page.waitToClick(selectors.clientWebAccess.enableWebAccessCheckbox); + await page.waitToClick(selectors.clientWebAccess.saveButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('Data saved!'); + }); + + it(`should update the name`, async() => { await page.clearInput(selectors.clientWebAccess.userName); await page.write(selectors.clientWebAccess.userName, 'Hulk'); await page.waitToClick(selectors.clientWebAccess.saveButton); @@ -26,9 +33,8 @@ describe('Client Edit web access path', () => { expect(message.text).toContain('Data saved!'); }); - it('should confirm web access is now unchecked', async() => { - await page.accessToSection('client.card.basicData'); - await page.accessToSection('client.card.webAccess'); + it('should reload the section and confirm web access is now unchecked', async() => { + await page.reloadSection('client.card.webAccess'); const result = await page.checkboxState(selectors.clientWebAccess.enableWebAccessCheckbox); expect(result).toBe('unchecked'); @@ -44,13 +50,23 @@ describe('Client Edit web access path', () => { await page.accessToSection('client.card.log'); }); - it(`should confirm the log is showing the updated data for the client`, async() => { + it(`should confirm the last log is showing the updated client name and no modifications on the active checkbox`, async() => { let lastModificationPreviousValue = await page .waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText'); let lastModificationCurrentValue = await page .waitToGetProperty(selectors.clientLog.lastModificationCurrentValue, 'innerText'); - expect(lastModificationPreviousValue).toEqual('name BruceBanner active true'); + expect(lastModificationPreviousValue).toEqual('name BruceBanner active false'); expect(lastModificationCurrentValue).toEqual('name Hulk active false'); }); + + it(`should confirm the penultimate log is showing the updated avtive field and no modifications on the client name`, async() => { + let penultimateModificationPreviousValue = await page + .waitToGetProperty(selectors.clientLog.penultimateModificationPreviousValue, 'innerText'); + let penultimateModificationCurrentValue = await page + .waitToGetProperty(selectors.clientLog.penultimateModificationCurrentValue, 'innerText'); + + expect(penultimateModificationPreviousValue).toEqual('name BruceBanner active true'); + expect(penultimateModificationCurrentValue).toEqual('name BruceBanner active false'); + }); }); diff --git a/e2e/paths/09-invoice-in/02_descriptor.spec.js b/e2e/paths/09-invoice-in/02_descriptor.spec.js index 2386dada4..02bbce7ac 100644 --- a/e2e/paths/09-invoice-in/02_descriptor.spec.js +++ b/e2e/paths/09-invoice-in/02_descriptor.spec.js @@ -10,16 +10,30 @@ describe('InvoiceIn descriptor path', () => { page = browser.page; await page.loginAndModule('administrative', 'invoiceIn'); await page.accessToSearchResult('10'); + await page.accessToSection('invoiceIn.card.basicData'); }); afterAll(async() => { await browser.close(); }); - it('should delete the invoiceIn using the descriptor more menu', async() => { + it('should clone the invoiceIn using the descriptor more menu', async() => { + await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); + await page.waitToClick(selectors.invoiceInDescriptor.moreMenuCloneInvoiceIn); + await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); + const message = await page.waitForSnackbar(); + + expect(message.text).toContain('InvoiceIn cloned'); + }); + + it('should have been redirected to the created invoiceIn summary', async() => { + await page.waitForState('invoiceIn.card.summary'); + }); + + it('should delete the cloned invoiceIn using the descriptor more menu', async() => { await page.waitToClick(selectors.invoiceInDescriptor.moreMenu); await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn); - await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton); + await page.waitToClick(selectors.invoiceInDescriptor.acceptButton); const message = await page.waitForSnackbar(); expect(message.text).toContain('InvoiceIn deleted'); diff --git a/e2e/paths/12-entry/07_buys.spec.js b/e2e/paths/12-entry/07_buys.spec.js index e5617b8bd..4042c99b6 100644 --- a/e2e/paths/12-entry/07_buys.spec.js +++ b/e2e/paths/12-entry/07_buys.spec.js @@ -29,9 +29,6 @@ describe('Entry import, create and edit buys path', () => { }); it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => { - await page.write(selectors.entryBuys.ref, 'a reference'); - await page.write(selectors.entryBuys.observation, 'an observation'); - let currentDir = process.cwd(); let filePath = `${currentDir}/e2e/assets/07_import_buys.json`; @@ -41,6 +38,9 @@ describe('Entry import, create and edit buys path', () => { ]); await fileChooser.accept([filePath]); + await page.waitForTextInField(selectors.entryBuys.ref, '200573095, 200573106, 200573117, 200573506'); + await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846'); + await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm'); await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m'); await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m'); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 75804ba21..d77b0c26d 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -57,7 +57,14 @@ "The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket", "Swift / BIC can't be empty": "Swift / BIC can't be empty", - "Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})", + "Deleted sales from ticket": "I have deleted the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}", + "Added sale to ticket": "I have added the following line to the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}", + "Changed sale discount": "I have changed the following lines discounts from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Created claim": "I have created the claim [{{claimId}}]({{{claimUrl}}}) for the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Changed sale price": "I have changed the price of [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) from {{oldPrice}}€ ➔ *{{newPrice}}€* of the ticket [{{ticketId}}]({{{ticketUrl}}})", + "Changed sale quantity": "I have changed the quantity of [{{itemId}} {{concept}}]({{{itemUrl}}}) from {{oldQuantity}} ➔ *{{newQuantity}}* of the ticket [{{ticketId}}]({{{ticketUrl}}})", + "Changed sale reserved state": "I have changed the following lines reserved state from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Bought units from buy request": "Bought {{quantity}} units of [{{itemId}} {{concept}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*", "Changed client paymethod": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})", "Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})", @@ -99,5 +106,8 @@ "None": "None", "error densidad = 0": "error densidad = 0", "nickname": "nickname", - "This document already exists on this ticket": "This document already exists on this ticket" + "This document already exists on this ticket": "This document already exists on this ticket", + "State": "State", + "regular": "regular", + "reserved": "reserved" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index ff30a61ff..f301df8cc 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -54,7 +54,7 @@ "This address doesn't exist": "Este consignatario no existe", "You can't create an order for a inactive client": "You can't create an order for a inactive client", "You can't create an order for a client that doesn't has tax data verified": "You can't create an order for a client that doesn't has tax data verified", - "You must delete the claim id %d first": "Antes debes borrar la reclamacion %d", + "You must delete the claim id %d first": "Antes debes borrar la reclamación %d", "You don't have enough privileges": "No tienes suficientes permisos", "Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF", "You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos", @@ -122,7 +122,17 @@ "Swift / BIC can't be empty": "Swift / BIC no puede estar vacío", "Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios", "Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios", - "Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", + "Deleted sales from ticket": "He eliminado las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}", + "Added sale to ticket": "He añadido la siguiente linea al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}", + "Changed sale discount": "He cambiado el descuento de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Created claim": "He creado la reclamación [{{claimId}}]({{{claimUrl}}}) de las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Changed sale price": "He cambiado el precio de [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) de {{oldPrice}}€ ➔ *{{newPrice}}€* del ticket [{{ticketId}}]({{{ticketUrl}}})", + "Changed sale quantity": "He cambiado la cantidad de [{{itemId}} {{concept}}]({{{itemUrl}}}) de {{oldQuantity}} ➔ *{{newQuantity}}* del ticket [{{ticketId}}]({{{ticketUrl}}})", + "State": "Estado", + "regular": "normal", + "reserved": "reservado", + "Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", + "Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", diff --git a/modules/claim/back/methods/claim/createFromSales.js b/modules/claim/back/methods/claim/createFromSales.js index 2dd1b75c2..f22aabbf3 100644 --- a/modules/claim/back/methods/claim/createFromSales.js +++ b/modules/claim/back/methods/claim/createFromSales.js @@ -3,17 +3,20 @@ const UserError = require('vn-loopback/util/user-error'); module.exports = Self => { Self.remoteMethodCtx('createFromSales', { description: 'Create a claim', - accepts: [{ - arg: 'ticketId', - type: 'Number', - required: true, - description: 'The origin ticket id' - }, { - arg: 'sales', - type: ['Object'], - required: true, - description: 'The claimed sales' - }], + accepts: [ + { + arg: 'ticketId', + type: 'number', + required: true, + description: 'The origin ticket id' + }, + { + arg: 'sales', + type: ['object'], + required: true, + description: 'The claimed sales' + } + ], returns: { type: 'object', root: true @@ -25,6 +28,7 @@ module.exports = Self => { }); Self.createFromSales = async(ctx, ticketId, sales, options) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; let tx; const myOptions = {}; @@ -39,7 +43,20 @@ module.exports = Self => { const userId = ctx.req.accessToken.userId; try { - const ticket = await models.Ticket.findById(ticketId, null, myOptions); + const ticket = await models.Ticket.findById(ticketId, { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + }, myOptions); + if (ticket.isDeleted) throw new UserError(`You can't create a claim for a removed ticket`); @@ -49,6 +66,8 @@ module.exports = Self => { ticketCreated: ticket.shipped, workerFk: userId }, myOptions); + + let changesMade = ''; const promises = []; for (const sale of sales) { @@ -59,10 +78,25 @@ module.exports = Self => { }, myOptions); promises.push(newClaimBeginning); + changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`; } await Promise.all(promises); + const salesPerson = ticket.client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + + const message = $t('Created claim', { + claimId: newClaim.id, + ticketId: ticketId, + ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, + claimUrl: `${origin}/#!/claim/${newClaim.id}/summary`, + changes: changesMade + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + if (tx) await tx.commit(); return newClaim; diff --git a/modules/claim/back/methods/claim/specs/createFromSales.spec.js b/modules/claim/back/methods/claim/specs/createFromSales.spec.js index f08914025..849ccf8f5 100644 --- a/modules/claim/back/methods/claim/specs/createFromSales.spec.js +++ b/modules/claim/back/methods/claim/specs/createFromSales.spec.js @@ -7,7 +7,13 @@ describe('Claim createFromSales()', () => { instance: 0, quantity: 10 }]; - const ctx = {req: {accessToken: {userId: 1}}}; + const ctx = { + req: { + accessToken: {userId: 1}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; it('should create a new claim', async() => { const tx = await app.models.Claim.beginTransaction({}); diff --git a/modules/client/back/models/sample.json b/modules/client/back/models/sample.json index 725bfb9c7..cfb127ab2 100644 --- a/modules/client/back/models/sample.json +++ b/modules/client/back/models/sample.json @@ -9,23 +9,26 @@ "properties": { "id": { "id": true, - "type": "Number", + "type": "number", "description": "Identifier" }, "code": { - "type": "String" + "type": "string" }, "description": { - "type": "String" + "type": "string" }, "isVisible": { - "type": "Boolean" + "type": "boolean" }, "hasCompany": { - "type": "Boolean" + "type": "boolean" }, "hasPreview": { - "type": "Boolean" + "type": "boolean" + }, + "datepickerEnabled": { + "type": "boolean" } }, "scopes": { diff --git a/modules/client/front/sample/create/index.html b/modules/client/front/sample/create/index.html index e6733a656..2d0f3d29c 100644 --- a/modules/client/front/sample/create/index.html +++ b/modules/client/front/sample/create/index.html @@ -25,38 +25,48 @@
- - - - - - - - + label="Sample" + required="true"> + + + + + + + + + ng-if="sampleType.selection.hasCompany" + required="true"> + + diff --git a/modules/client/front/sample/create/index.js b/modules/client/front/sample/create/index.js index 0eaeddc20..19fe1fecf 100644 --- a/modules/client/front/sample/create/index.js +++ b/modules/client/front/sample/create/index.js @@ -80,6 +80,12 @@ class Controller extends Section { if (sampleType.hasCompany) params.companyId = this.clientSample.companyFk; + if (sampleType.datepickerEnabled && !this.clientSample.from) + return this.vnApp.showError(this.$t('Choose a date')); + + if (sampleType.datepickerEnabled) + params.from = this.clientSample.from; + let query = `email/${sampleType.code}`; if (isPreview) query = `email/${sampleType.code}/preview`; diff --git a/modules/client/front/sample/create/locale/es.yml b/modules/client/front/sample/create/locale/es.yml index a534e05d4..b72d456d8 100644 --- a/modules/client/front/sample/create/locale/es.yml +++ b/modules/client/front/sample/create/locale/es.yml @@ -1,5 +1,6 @@ Choose a sample: Selecciona una plantilla Choose a company: Selecciona una empresa +Choose a date: Selecciona una fecha Email cannot be blank: Debes introducir un email Recipient: Destinatario Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla diff --git a/modules/client/front/summary/index.html b/modules/client/front/summary/index.html index 8a71599e1..b724f9a6b 100644 --- a/modules/client/front/summary/index.html +++ b/modules/client/front/summary/index.html @@ -293,6 +293,8 @@ Id Nickname + Agency + Route Packages Date State @@ -313,6 +315,17 @@ {{::ticket.nickname}} + + {{::ticket.agencyMode.name}} + + + + {{::ticket.routeFk}} + + {{::ticket.packages}} @@ -371,6 +384,9 @@ + + diff --git a/modules/client/front/summary/index.js b/modules/client/front/summary/index.js index 8a927e205..e50de7da9 100644 --- a/modules/client/front/summary/index.js +++ b/modules/client/front/summary/index.js @@ -22,7 +22,13 @@ class Controller extends Summary { scope: { fields: ['id'] } - } + }, + { + relation: 'agencyMode', + scope: { + fields: ['name'] + } + }, ] }; } diff --git a/modules/client/front/web-access/index.html b/modules/client/front/web-access/index.html index 07a2615dd..610497994 100644 --- a/modules/client/front/web-access/index.html +++ b/modules/client/front/web-access/index.html @@ -9,6 +9,8 @@ { description: 'The entry id', http: {source: 'path'} }, - { - arg: 'options', - type: 'object', - description: 'Callback options', - }, { arg: 'ref', type: 'string', @@ -28,11 +23,11 @@ module.exports = Self => { }, { arg: 'buys', - type: ['Object'], + type: ['object'], description: 'The buys', }], returns: { - type: ['Object'], + type: ['object'], root: true }, http: { @@ -41,23 +36,27 @@ module.exports = Self => { } }); - Self.importBuys = async(ctx, id, options = {}) => { + Self.importBuys = async(ctx, id, options) => { const conn = Self.dataSource.connector; const args = ctx.args; const models = Self.app.models; let tx; + const myOptions = {}; - if (!options.transaction) { + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { tx = await Self.beginTransaction({}); - options.transaction = tx; + myOptions.transaction = tx; } try { - const entry = await models.Entry.findById(id, null, options); + const entry = await models.Entry.findById(id, null, myOptions); await entry.updateAttributes({ observation: args.observation, ref: args.ref - }, options); + }, myOptions); const buys = []; for (let buy of args.buys) { @@ -71,9 +70,16 @@ module.exports = Self => { buyingValue: buy.buyingValue, packageFk: buy.packageFk }); + + await models.ItemMatchProperties.upsert({ + itemFk: buy.itemFk, + name: buy.description, + producer: buy.companyName, + size: buy.size + }, myOptions); } - const createdBuys = await models.Buy.create(buys, options); + const createdBuys = await models.Buy.create(buys, myOptions); const buyIds = createdBuys.map(buy => buy.id); const stmts = []; @@ -90,7 +96,7 @@ module.exports = Self => { stmts.push('CALL buy_recalcPrices()'); const sql = ParameterizedSQL.join(stmts, ';'); - await conn.executeStmt(sql, options); + await conn.executeStmt(sql, myOptions); if (tx) await tx.commit(); } catch (e) { if (tx) await tx.rollback(); diff --git a/modules/entry/back/methods/entry/importBuysPreview.js b/modules/entry/back/methods/entry/importBuysPreview.js index 9ba2b58ed..790d33364 100644 --- a/modules/entry/back/methods/entry/importBuysPreview.js +++ b/modules/entry/back/methods/entry/importBuysPreview.js @@ -37,7 +37,21 @@ module.exports = Self => { where: {volume: {gte: buy.volume}}, order: 'volume ASC' }, myOptions); - buy.packageFk = packaging.id; + + if (packaging) + buy.packageFk = packaging.id; + + const reference = await models.ItemMatchProperties.findOne({ + fields: ['itemFk'], + where: { + name: buy.description, + producer: buy.companyName, + size: buy.size + } + }, myOptions); + + if (reference) + buy.itemFk = reference.itemFk; } return buys; diff --git a/modules/entry/back/model-config.json b/modules/entry/back/model-config.json index eddef9c41..ad5a9063e 100644 --- a/modules/entry/back/model-config.json +++ b/modules/entry/back/model-config.json @@ -5,6 +5,9 @@ "Buy": { "dataSource": "vn" }, + "ItemMatchProperties": { + "dataSource": "vn" + }, "EntryLog": { "dataSource": "vn" }, diff --git a/modules/entry/back/models/buy-import-reference.json b/modules/entry/back/models/buy-import-reference.json new file mode 100644 index 000000000..ab64dad73 --- /dev/null +++ b/modules/entry/back/models/buy-import-reference.json @@ -0,0 +1,32 @@ +{ + "name": "ItemMatchProperties", + "base": "VnModel", + "options": { + "mysql": { + "table": "itemMatchProperties" + } + }, + "properties": { + "itemFk": { + "type": "number", + "id": true, + "description": "Identifier" + }, + "name": { + "type": "string" + }, + "producer": { + "type": "string" + }, + "size": { + "type": "string" + } + }, + "relations": { + "item": { + "type": "belongsTo", + "model": "Item", + "foreignKey": "itemFk" + } + } +} \ No newline at end of file diff --git a/modules/entry/front/buy/import/index.html b/modules/entry/front/buy/import/index.html index 74b6c708a..179657dae 100644 --- a/modules/entry/front/buy/import/index.html +++ b/modules/entry/front/buy/import/index.html @@ -9,20 +9,6 @@ class="vn-ma-md">
- - - - - - - - + + + + + + + + @@ -51,7 +51,6 @@ - @@ -70,20 +69,19 @@ {{::id}} - {{::name}} + + + + - - + -
Grouping Buying value BoxVolume
{{::buy.description | dashIfEmpty}} {{::buy.size | dashIfEmpty}} - - {{::buy.packing | dashIfEmpty}} - - - - {{::buy.grouping | dashIfEmpty}} - - + {{::buy.packing | dashIfEmpty}}{{::buy.grouping | dashIfEmpty}} {{::buy.buyingValue | currency: 'EUR':2}} {{::buy.volume | number}}
@@ -110,7 +107,95 @@ label="Cancel" ui-sref="entry.card.buy.index"> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + ID + Item + Size + Producer + Color + + + + + + + {{::item.id}} + + + {{::item.name}} + {{::item.size}} + {{::item.producer.name}} + {{::item.ink.name}} + + + + + + + + \ No newline at end of file diff --git a/modules/entry/front/buy/import/index.js b/modules/entry/front/buy/import/index.js index b5ff92a89..2f13b2746 100644 --- a/modules/entry/front/buy/import/index.js +++ b/modules/entry/front/buy/import/index.js @@ -29,6 +29,7 @@ class Controller extends Section { this.$.$applyAsync(() => { this.import.observation = invoice.tx_awb; + const companyName = invoice.tx_company; const boxes = invoice.boxes; const buys = []; for (let box of boxes) { @@ -37,11 +38,12 @@ class Controller extends Section { const packing = product.nu_stems_bunch * product.nu_bunches; buys.push({ description: product.nm_product, + companyName: companyName, size: product.nu_length, packing: packing, grouping: product.nu_stems_bunch, buyingValue: parseFloat(product.mny_rate_stem), - volume: boxVolume + volume: boxVolume, }); } } @@ -86,6 +88,59 @@ class Controller extends Section { ? {id: $search} : {name: {like: '%' + $search + '%'}}; } + + showFilterDialog(buy) { + this.activeBuy = buy; + this.itemFilterParams = {}; + this.itemFilter = { + include: [ + { + relation: 'producer', + scope: { + fields: ['name'] + } + }, + { + relation: 'ink', + scope: { + fields: ['name'] + } + } + ] + }; + + this.$.filterDialog.show(); + } + + selectItem(id) { + this.activeBuy['itemFk'] = id; + this.$.filterDialog.hide(); + } + + filter() { + const filter = this.itemFilter; + const params = this.itemFilterParams; + const where = {}; + + for (let key in params) { + const value = params[key]; + if (!value) continue; + + switch (key) { + case 'name': + where[key] = {like: `%${value}%`}; + break; + case 'producerFk': + case 'typeFk': + case 'size': + case 'ink': + where[key] = value; + } + } + + filter.where = where; + this.$.itemsModel.applyFilter(filter); + } } Controller.$inject = ['$element', '$scope']; diff --git a/modules/entry/front/buy/import/style.scss b/modules/entry/front/buy/import/style.scss index dba069616..8426d4169 100644 --- a/modules/entry/front/buy/import/style.scss +++ b/modules/entry/front/buy/import/style.scss @@ -2,4 +2,10 @@ vn-entry-buy-import { .vn-table > tbody td:nth-child(1) { width: 250px } +} + +.itemFilter { + vn-table.scrollable { + height: 500px + } } \ No newline at end of file diff --git a/modules/entry/front/buy/locale/es.yml b/modules/entry/front/buy/locale/es.yml index c77587758..55828a3c6 100644 --- a/modules/entry/front/buy/locale/es.yml +++ b/modules/entry/front/buy/locale/es.yml @@ -3,4 +3,6 @@ Observation: Observación Box: Embalaje Import buys: Importar compras Some of the imported buys doesn't have an item: Algunas de las compras importadas no tienen un artículo -JSON files only: Solo ficheros JSON \ No newline at end of file +JSON files only: Solo ficheros JSON +Filter item: Filtrar artículo +Filter...: Filtrar... \ No newline at end of file diff --git a/modules/invoiceIn/back/methods/invoice-in/clone.js b/modules/invoiceIn/back/methods/invoice-in/clone.js new file mode 100644 index 000000000..c1bf0f3ac --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/clone.js @@ -0,0 +1,120 @@ + +const loggable = require('vn-loopback/util/log'); + +module.exports = Self => { + Self.remoteMethodCtx('clone', { + description: 'Clone the invoiceIn and as many invoiceInTax and invoiceInDueDay referencing it', + accessType: 'WRITE', + accepts: { + arg: 'id', + type: 'number', + required: true, + description: 'The invoiceIn id', + http: {source: 'path'} + }, + returns: { + type: 'object', + root: true + }, + http: { + path: '/:id/clone', + verb: 'POST' + } + }); + + Self.clone = async(ctx, id, options) => { + const userId = ctx.req.accessToken.userId; + const models = Self.app.models; + let tx; + const myOptions = {}; + + if (typeof options == 'object') + Object.assign(myOptions, options); + + if (!myOptions.transaction) { + tx = await Self.beginTransaction({}); + myOptions.transaction = tx; + } + + try { + const sourceInvoiceIn = await Self.findById(id, { + fields: [ + 'id', + 'serial', + 'supplierRef', + 'supplierFk', + 'issued', + 'currencyFk', + 'companyFk', + 'isVatDeductible', + 'withholdingSageFk', + 'deductibleExpenseFk', + ] + }, myOptions); + const sourceInvoiceInTax = await models.InvoiceInTax.find({where: {invoiceInFk: id}}, myOptions); + const sourceInvoiceInDueDay = await models.InvoiceInDueDay.find({where: {invoiceInFk: id}}, myOptions); + + const issued = new Date(sourceInvoiceIn.issued); + issued.setMonth(issued.getMonth() + 1); + + const clone = await models.InvoiceIn.create({ + serial: sourceInvoiceIn.serial, + supplierRef: sourceInvoiceIn.supplierRef, + supplierFk: sourceInvoiceIn.supplierFk, + issued: issued, + currencyFk: sourceInvoiceIn.currencyFk, + companyFk: sourceInvoiceIn.companyFk, + isVatDeductible: sourceInvoiceIn.isVatDeductible, + withholdingSageFk: sourceInvoiceIn.withholdingSageFk, + deductibleExpenseFk: sourceInvoiceIn.deductibleExpenseFk, + }, myOptions); + + const oldProperties = await loggable.translateValues(Self, sourceInvoiceIn); + const newProperties = await loggable.translateValues(Self, clone); + await models.InvoiceInLog.create({ + originFk: clone.id, + userFk: userId, + action: 'insert', + changedModel: 'InvoiceIn', + changedModelId: clone.id, + oldInstance: oldProperties, + newInstance: newProperties + }, myOptions); + + const promises = []; + + for (let tax of sourceInvoiceInTax) { + promises.push(models.InvoiceInTax.create({ + invoiceInFk: clone.id, + taxableBase: tax.taxableBase, + expenceFk: tax.expenceFk, + foreignValue: tax.foreignValue, + taxTypeSageFk: tax.taxTypeSageFk, + transactionTypeSageFk: tax.transactionTypeSageFk + }, myOptions)); + } + + for (let dueDay of sourceInvoiceInDueDay) { + const dueDated = dueDay.dueDated; + dueDated.setMonth(dueDated.getMonth() + 1); + + promises.push(models.InvoiceInDueDay.create({ + invoiceInFk: clone.id, + dueDated: dueDated, + bankFk: dueDay.bankFk, + amount: dueDay.amount, + foreignValue: dueDated.foreignValue, + }, myOptions)); + } + + await Promise.all(promises); + + if (tx) await tx.commit(); + + return clone; + } catch (e) { + if (tx) await tx.rollback(); + throw e; + } + }; +}; diff --git a/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js b/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js new file mode 100644 index 000000000..09b7e6019 --- /dev/null +++ b/modules/invoiceIn/back/methods/invoice-in/specs/clone.spec.js @@ -0,0 +1,36 @@ +const models = require('vn-loopback/server/server').models; + +describe('invoiceIn clone()', () => { + it('should return the cloned invoiceIn and also clone invoiceInDueDays and invoiceInTaxes if there are any referencing the invoiceIn', async() => { + const userId = 1; + const ctx = { + req: { + + accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, + } + }; + + const tx = await models.InvoiceIn.beginTransaction({}); + const options = {transaction: tx}; + + try { + const clone = await models.InvoiceIn.clone(ctx, 1, options); + + expect(clone.supplierRef).toEqual('1234'); + + const invoiceInTaxes = await models.InvoiceInTax.find({where: {invoiceInFk: clone.id}}, options); + + expect(invoiceInTaxes.length).toEqual(1); + + const invoiceInDueDays = await models.InvoiceInDueDay.find({where: {invoiceInFk: clone.id}}, options); + + expect(invoiceInDueDays.length).toEqual(2); + + await tx.rollback(); + } catch (e) { + await tx.rollback(); + throw e; + } + }); +}); diff --git a/modules/invoiceIn/back/models/invoice-in-due-day.json b/modules/invoiceIn/back/models/invoice-in-due-day.json index 6c27dcd6c..5a66ecd8b 100644 --- a/modules/invoiceIn/back/models/invoice-in-due-day.json +++ b/modules/invoiceIn/back/models/invoice-in-due-day.json @@ -24,6 +24,9 @@ "amount": { "type": "number" }, + "foreignValue": { + "type": "number" + }, "created": { "type": "date" } diff --git a/modules/invoiceIn/back/models/invoice-in-tax.json b/modules/invoiceIn/back/models/invoice-in-tax.json index 19ab8be37..af93a05bc 100644 --- a/modules/invoiceIn/back/models/invoice-in-tax.json +++ b/modules/invoiceIn/back/models/invoice-in-tax.json @@ -30,6 +30,7 @@ "created": { "type": "date" } + }, "relations": { "invoiceIn": { diff --git a/modules/invoiceIn/back/models/invoice-in.js b/modules/invoiceIn/back/models/invoice-in.js index 7c5b16358..7754890ca 100644 --- a/modules/invoiceIn/back/models/invoice-in.js +++ b/modules/invoiceIn/back/models/invoice-in.js @@ -1,4 +1,5 @@ module.exports = Self => { require('../methods/invoice-in/filter')(Self); require('../methods/invoice-in/summary')(Self); + require('../methods/invoice-in/clone')(Self); }; diff --git a/modules/invoiceIn/back/models/invoice-in.json b/modules/invoiceIn/back/models/invoice-in.json index 6e6cea4e8..feb2d9aec 100644 --- a/modules/invoiceIn/back/models/invoice-in.json +++ b/modules/invoiceIn/back/models/invoice-in.json @@ -12,7 +12,7 @@ "properties": { "id": { "id": true, - "type": "Number", + "type": "number", "description": "Identifier" }, "serialNumber": { @@ -36,6 +36,9 @@ "booked": { "type": "date" }, + "isVatDeductible": { + "type": "boolean" + }, "operated": { "type": "date" }, diff --git a/modules/invoiceIn/front/descriptor/index.html b/modules/invoiceIn/front/descriptor/index.html index 42a946913..6829a0daf 100644 --- a/modules/invoiceIn/front/descriptor/index.html +++ b/modules/invoiceIn/front/descriptor/index.html @@ -8,6 +8,13 @@ translate> Delete Invoice + + Clone Invoice +
@@ -42,8 +49,16 @@
- - + + + diff --git a/modules/invoiceIn/front/descriptor/index.js b/modules/invoiceIn/front/descriptor/index.js index be507e0d4..a767f4b5c 100644 --- a/modules/invoiceIn/front/descriptor/index.js +++ b/modules/invoiceIn/front/descriptor/index.js @@ -30,6 +30,12 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('InvoiceIn deleted'))); } + cloneInvoiceIn() { + return this.$http.post(`InvoiceIns/${this.id}/clone`) + .then(res => this.$state.go('invoiceIn.card.summary', {id: res.data.id})) + .then(() => this.vnApp.showSuccess(this.$t('InvoiceIn cloned'))); + } + loadData() { const filter = { include: [ diff --git a/modules/invoiceIn/front/locale/es.yml b/modules/invoiceIn/front/locale/es.yml index 931be5dc7..f837c834b 100644 --- a/modules/invoiceIn/front/locale/es.yml +++ b/modules/invoiceIn/front/locale/es.yml @@ -8,4 +8,6 @@ Sage tax: Sage iva Sage transaction: Sage transaccion Foreign value: Divisa Due day: Vencimiento -Invoice list: Listado de facturas recibidas \ No newline at end of file +Invoice list: Listado de facturas recibidas +InvoiceIn cloned: Factura clonada + diff --git a/modules/invoiceOut/front/card/index.js b/modules/invoiceOut/front/card/index.js index f718f22ae..093fcdf66 100644 --- a/modules/invoiceOut/front/card/index.js +++ b/modules/invoiceOut/front/card/index.js @@ -21,7 +21,7 @@ class Controller extends ModuleCard { }, { relation: 'client', scope: { - fields: ['id', 'socialName', 'name'] + fields: ['id', 'socialName', 'name', 'email'] } } ] diff --git a/modules/invoiceOut/front/descriptor/index.html b/modules/invoiceOut/front/descriptor/index.html index b4c76d808..7e8d6b8d9 100644 --- a/modules/invoiceOut/front/descriptor/index.html +++ b/modules/invoiceOut/front/descriptor/index.html @@ -9,6 +9,12 @@ translate> Show invoice PDF + + Send invoice PDF + + + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js index 3e859478d..e6305db93 100644 --- a/modules/invoiceOut/front/descriptor/index.js +++ b/modules/invoiceOut/front/descriptor/index.js @@ -59,6 +59,14 @@ class Controller extends Descriptor { return this.getData(`InvoiceOuts/${this.id}`, {filter}) .then(res => this.entity = res.data); } + + sendInvoice() { + return this.vnEmail.send('invoice', { + recipientId: this.invoiceOut.client.id, + recipient: this.invoiceOut.client.email, + invoiceId: this.id + }); + } } ngModule.vnComponent('vnInvoiceOutDescriptor', { diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml index dd67660ee..ec9cd3310 100644 --- a/modules/invoiceOut/front/descriptor/locale/es.yml +++ b/modules/invoiceOut/front/descriptor/locale/es.yml @@ -3,9 +3,12 @@ Volume: Volumen Client card: Ficha del cliente Invoice ticket list: Listado de tickets de la factura Show invoice PDF: Ver factura en PDF +Send invoice PDF: Enviar factura en PDF Delete Invoice: Eliminar factura +Clone Invoice: Clonar factura InvoiceOut deleted: Factura eliminada Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? +Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura? Book invoice: Asentar factura InvoiceOut booked: Factura asentada Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? diff --git a/modules/invoiceOut/front/summary/index.html b/modules/invoiceOut/front/summary/index.html index 0406a39c5..924972299 100644 --- a/modules/invoiceOut/front/summary/index.html +++ b/modules/invoiceOut/front/summary/index.html @@ -1,6 +1,7 @@ diff --git a/modules/invoiceOut/front/summary/index.js b/modules/invoiceOut/front/summary/index.js index 2446e986a..af0b5365e 100644 --- a/modules/invoiceOut/front/summary/index.js +++ b/modules/invoiceOut/front/summary/index.js @@ -7,7 +7,7 @@ class Controller extends Summary { this._invoiceOut = value; if (value && value.id) { this.getSummary(); - this.getTickets(); + this.$.ticketsModel.url = `InvoiceOuts/${this.invoiceOut.id}/getTickets`; } } @@ -19,14 +19,6 @@ class Controller extends Summary { return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`) .then(res => this.summary = res.data); } - - getTickets() { - this.$.$applyAsync(() => { - const query = `InvoiceOuts/${this.invoiceOut.id}/getTickets`; - this.$.ticketsModel.url = query; - this.$.ticketsModel.refresh(); - }); - } } ngModule.vnComponent('vnInvoiceOutSummary', { diff --git a/modules/invoiceOut/front/summary/index.spec.js b/modules/invoiceOut/front/summary/index.spec.js index be6ad0a18..675cc02c3 100644 --- a/modules/invoiceOut/front/summary/index.spec.js +++ b/modules/invoiceOut/front/summary/index.spec.js @@ -27,17 +27,5 @@ describe('InvoiceOut', () => { expect(controller.summary).toEqual('the data you are looking for'); }); }); - - describe('getTickets()', () => { - it('should perform a and then call to the ticketModel refresh() method', () => { - jest.spyOn(controller.$.ticketsModel, 'refresh'); - - controller.getTickets(); - $scope.$apply(); - - expect(controller.$.ticketsModel.url).toEqual('InvoiceOuts/1/getTickets'); - expect(controller.$.ticketsModel.refresh).toHaveBeenCalledWith(); - }); - }); }); }); diff --git a/modules/item/back/methods/item/clone.js b/modules/item/back/methods/item/clone.js index 04d018961..3e6f3ab5a 100644 --- a/modules/item/back/methods/item/clone.js +++ b/modules/item/back/methods/item/clone.js @@ -71,7 +71,7 @@ module.exports = Self => { const models = Self.app.models; const originalTaxes = await models.ItemTaxCountry.find({ where: {itemFk: originalId}, - fields: ['botanical', 'countryFk', 'taxClassFk'] + fields: ['countryFk', 'taxClassFk'] }, options); const promises = []; @@ -99,7 +99,7 @@ module.exports = Self => { const models = Self.app.models; const botanical = await models.ItemBotanical.findOne({ where: {itemFk: originalId}, - fields: ['botanical', 'genusFk', 'specieFk'] + fields: ['genusFk', 'specieFk'] }, options); if (botanical) { diff --git a/modules/item/back/models/item-botanical.json b/modules/item/back/models/item-botanical.json index 541218145..6c465f0ee 100644 --- a/modules/item/back/models/item-botanical.json +++ b/modules/item/back/models/item-botanical.json @@ -3,8 +3,7 @@ "base": "Loggable", "log": { "model": "ItemLog", - "relation": "item", - "showField": "botanical" + "relation": "item" }, "options": { "mysql": { diff --git a/modules/item/front/search-panel/locale/es.yml b/modules/item/front/search-panel/locale/es.yml index 197da0695..67a5200d7 100644 --- a/modules/item/front/search-panel/locale/es.yml +++ b/modules/item/front/search-panel/locale/es.yml @@ -1,6 +1,6 @@ Ink: Tinta Origin: Origen -Producer: Productor. +Producer: Productor With visible: Con visible Field: Campo More fields: Más campos diff --git a/modules/order/back/methods/order/getItemTypeAvailable.js b/modules/order/back/methods/order/getItemTypeAvailable.js index 56f6a8c0e..906095f41 100644 --- a/modules/order/back/methods/order/getItemTypeAvailable.js +++ b/modules/order/back/methods/order/getItemTypeAvailable.js @@ -28,30 +28,57 @@ module.exports = Self => { }); Self.getItemTypeAvailable = async(orderId, itemCategoryId) => { - let stmts = []; + const stmts = []; let stmt; - let order = await app.models.Order.findById(orderId); + const order = await app.models.Order.findById(orderId); stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [ order.landed, order.addressFk, order.agencyModeFk ]); stmts.push(stmt); - stmt = new ParameterizedSQL(` - SELECT it.id, it.name, ic.name categoryName + + stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item'); + + stmt = new ParameterizedSQL( + `CREATE TEMPORARY TABLE tmp.item + (PRIMARY KEY (itemFk)) ENGINE = MEMORY + SELECT DISTINCT + i.id AS itemFk, + it.id AS typeFk, + it.name, + ic.name AS categoryName FROM tmp.availableCalc ac JOIN cache.available a ON a.calc_id = ac.calcFk - JOIN item i ON i.id = a.item_id - JOIN itemType it ON it.id = i.typeFk - JOIN itemCategory ic ON ic.id = it.categoryFk - WHERE it.categoryFk = ? - GROUP BY it.id`, [itemCategoryId] + JOIN vn.item i ON i.id = a.item_id + JOIN vn.itemType it ON it.id = i.typeFk + JOIN vn.itemCategory ic ON ic.id = it.categoryFk + WHERE it.categoryFk = ?`, [itemCategoryId] ); - let categoriesIndex = stmts.push(stmt) - 1; + stmts.push(stmt); - let sql = ParameterizedSQL.join(stmts, ';'); - let result = await Self.rawStmt(sql); + stmt = new ParameterizedSQL( + 'CALL vn.catalog_calculate(?, ?, ?)', [ + order.landed, + order.addressFk, + order.agencyModeFk, + ] + ); + stmts.push(stmt); + + stmt = new ParameterizedSQL(` + SELECT i.typeFk AS id, i.name, i.categoryName + FROM tmp.item i + JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.itemFk + GROUP BY i.typeFk` + ); + const categoriesIndex = stmts.push(stmt) - 1; + + stmts.push('DROP TEMPORARY TABLE tmp.item'); + + const sql = ParameterizedSQL.join(stmts, ';'); + const result = await Self.rawStmt(sql); return result[categoriesIndex]; }; diff --git a/modules/ticket/back/methods/sale/deleteSales.js b/modules/ticket/back/methods/sale/deleteSales.js index 31899a501..a604da858 100644 --- a/modules/ticket/back/methods/sale/deleteSales.js +++ b/modules/ticket/back/methods/sale/deleteSales.js @@ -6,18 +6,18 @@ module.exports = Self => { accessType: 'WRITE', accepts: [{ arg: 'sales', - type: ['Object'], + type: ['object'], required: true, description: 'The sales to remove' }, { arg: 'ticketId', - type: 'Number', + type: 'number', required: true, description: 'The ticket id' }], returns: { - type: ['Object'], + type: ['object'], root: true }, http: { @@ -27,10 +27,25 @@ module.exports = Self => { }); Self.deleteSales = async(ctx, sales, ticketId) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; const canEditSales = await models.Sale.canEdit(ctx, sales); + const ticket = await models.Ticket.findById(ticketId, { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + }); + const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); if (!isTicketEditable) throw new UserError(`The sales of this ticket can't be modified`); @@ -39,11 +54,26 @@ module.exports = Self => { throw new UserError(`Sale(s) blocked, please contact production`); const promises = []; + let deletions = ''; for (let sale of sales) { const deletedSale = models.Sale.destroyById(sale.id); + deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`; + promises.push(deletedSale); } + const salesPerson = ticket.client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + + const message = $t('Deleted sales from ticket', { + ticketId: ticketId, + ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, + deletions: deletions + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + return Promise.all(promises); }; }; diff --git a/modules/ticket/back/methods/sale/reserve.js b/modules/ticket/back/methods/sale/reserve.js index 96c794e7c..639827fa7 100644 --- a/modules/ticket/back/methods/sale/reserve.js +++ b/modules/ticket/back/methods/sale/reserve.js @@ -5,24 +5,27 @@ module.exports = Self => { Self.remoteMethodCtx('reserve', { description: 'Change the state of a ticket', accessType: 'WRITE', - accepts: [{ - arg: 'ticketId', - type: 'Number', - required: true, - description: 'The ticket id' - }, { - arg: 'sales', - type: ['Object'], - required: true, - description: 'The sale to reserve' - }, - { - arg: 'reserved', - type: 'Boolean', - required: true - }], + accepts: [ + { + arg: 'ticketId', + type: 'number', + required: true, + description: 'The ticket id' + }, + { + arg: 'sales', + type: ['object'], + required: true, + description: 'The sale to reserve' + }, + { + arg: 'reserved', + type: 'boolean', + required: true + } + ], returns: { - type: ['Object'], + type: ['object'], root: true }, http: { @@ -32,7 +35,9 @@ module.exports = Self => { }); Self.reserve = async(ctx, ticketId, sales, reserved) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; + const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); if (!isTicketEditable) throw new UserError(`The sales of this ticket can't be modified`); @@ -42,12 +47,50 @@ module.exports = Self => { if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); + let changesMade = ''; const promises = []; + for (let sale of sales) { - const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved}); - promises.push(reservedSale); + if (sale.reserved != reserved) { + const oldState = sale.reserved ? 'reserved' : 'regular'; + const newState = reserved ? 'reserved' : 'regular'; + + const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved}); + + promises.push(reservedSale); + + changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`; + } } - return Promise.all(promises); + const result = await Promise.all(promises); + + const ticket = await models.Ticket.findById(ticketId, { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + }); + + const salesPerson = ticket.client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + + const message = $t('Changed sale reserved state', { + ticketId: ticketId, + ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`, + changes: changesMade + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + + return result; }; }; diff --git a/modules/ticket/back/methods/sale/specs/deleteSales.spec.js b/modules/ticket/back/methods/sale/specs/deleteSales.spec.js index 22ac49452..aabc38375 100644 --- a/modules/ticket/back/methods/sale/specs/deleteSales.spec.js +++ b/modules/ticket/back/methods/sale/specs/deleteSales.spec.js @@ -17,7 +17,14 @@ describe('sale deleteSales()', () => { }); it('should throw an error if the ticket of the given sales is not editable', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + let ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + let error; const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}]; @@ -33,7 +40,13 @@ describe('sale deleteSales()', () => { }); it('should delete the sale', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + let ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const sales = [{id: newSale.id, instance: 0}]; const ticketId = 16; diff --git a/modules/ticket/back/methods/sale/specs/reserve.spec.js b/modules/ticket/back/methods/sale/specs/reserve.spec.js index fce7bd95e..3752a2b6b 100644 --- a/modules/ticket/back/methods/sale/specs/reserve.spec.js +++ b/modules/ticket/back/methods/sale/specs/reserve.spec.js @@ -1,7 +1,14 @@ const app = require('vn-loopback/server/server'); describe('sale reserve()', () => { - const ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + afterAll(async done => { let ctx = {req: {accessToken: {userId: 9}}}; let params = { diff --git a/modules/ticket/back/methods/sale/specs/updatePrice.spec.js b/modules/ticket/back/methods/sale/specs/updatePrice.spec.js index ec4376adb..ea8a65c27 100644 --- a/modules/ticket/back/methods/sale/specs/updatePrice.spec.js +++ b/modules/ticket/back/methods/sale/specs/updatePrice.spec.js @@ -17,7 +17,13 @@ describe('sale updatePrice()', () => { }); it('should throw an error if the ticket is not editable', async() => { - let ctx = {req: {accessToken: {userId: 18}}}; + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; let immutableSaleId = 1; let price = 5; @@ -31,7 +37,14 @@ describe('sale updatePrice()', () => { }); it('should return 0 if the price is an empty string', async() => { - let ctx = {req: {accessToken: {userId: 18}}}; + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + let price = ''; await app.models.Sale.updatePrice(ctx, saleId, price); @@ -46,7 +59,14 @@ describe('sale updatePrice()', () => { }); it('should now set price as a number in a string', async() => { - let ctx = {req: {accessToken: {userId: 18}}}; + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; + let price = '8'; await app.models.Sale.updatePrice(ctx, saleId, price); diff --git a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js index 16221b55c..dabdac384 100644 --- a/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js +++ b/modules/ticket/back/methods/sale/specs/updateQuantity.spec.js @@ -1,7 +1,13 @@ const app = require('vn-loopback/server/server'); describe('sale updateQuantity()', () => { - const ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; it('should throw an error if the quantity is not a number', async() => { let error; diff --git a/modules/ticket/back/methods/sale/updatePrice.js b/modules/ticket/back/methods/sale/updatePrice.js index 2195c2b7b..cb86296c9 100644 --- a/modules/ticket/back/methods/sale/updatePrice.js +++ b/modules/ticket/back/methods/sale/updatePrice.js @@ -19,7 +19,7 @@ module.exports = Self => { } ], returns: { - type: 'Number', + type: 'number', root: true }, http: { @@ -29,29 +29,37 @@ module.exports = Self => { }); Self.updatePrice = async(ctx, id, newPrice) => { - let models = Self.app.models; - let tx = await Self.beginTransaction({}); + const $t = ctx.req.__; // $translate + const models = Self.app.models; + const tx = await Self.beginTransaction({}); try { - let options = {transaction: tx}; + const options = {transaction: tx}; - let filter = { + const filter = { include: { relation: 'ticket', scope: { include: { relation: 'client', scope: { - fields: ['salesPersonFk'] + fields: ['salesPersonFk'], + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } } }, fields: ['id', 'clientFk'] } } }; - let sale = await models.Sale.findById(id, filter, options); - let isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk); + const sale = await models.Sale.findById(id, filter, options); + + const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk); if (!isEditable) throw new UserError(`The sales of this ticket can't be modified`); @@ -60,21 +68,19 @@ module.exports = Self => { if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); + const oldPrice = sale.price; const userId = ctx.req.accessToken.userId; + const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options); + const componentCode = usesMana ? 'mana' : 'buyerDiscount'; + const discount = await models.Component.findOne({where: {code: componentCode}}, options); + const componentId = discount.id; + const componentValue = newPrice - sale.price; - let usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options); - - let componentCode = usesMana ? 'mana' : 'buyerDiscount'; - - let discount = await models.Component.findOne({where: {code: componentCode}}, options); - let componentId = discount.id; - let componentValue = newPrice - sale.price; - - let where = { + const where = { componentFk: componentId, saleFk: id }; - let saleComponent = await models.SaleComponent.findOne({where}, options); + const saleComponent = await models.SaleComponent.findOne({where}, options); if (saleComponent) { await models.SaleComponent.updateAll(where, { @@ -92,6 +98,22 @@ module.exports = Self => { query = `CALL vn.manaSpellersRequery(?)`; await Self.rawSql(query, [userId], options); + const salesPerson = sale.ticket().client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + const message = $t('Changed sale price', { + ticketId: sale.ticket().id, + itemId: sale.itemFk, + concept: sale.concept, + quantity: sale.quantity, + oldPrice: oldPrice, + newPrice: newPrice, + ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, + itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + await tx.commit(); return sale; diff --git a/modules/ticket/back/methods/sale/updateQuantity.js b/modules/ticket/back/methods/sale/updateQuantity.js index 00df49b9f..62e09f1f5 100644 --- a/modules/ticket/back/methods/sale/updateQuantity.js +++ b/modules/ticket/back/methods/sale/updateQuantity.js @@ -26,7 +26,8 @@ module.exports = Self => { } }); - Self.updateQuantity = async(ctx, id, quantity) => { + Self.updateQuantity = async(ctx, id, newQuantity) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; const canEditSale = await models.Sale.canEdit(ctx, [id]); @@ -34,13 +35,51 @@ module.exports = Self => { if (!canEditSale) throw new UserError(`Sale(s) blocked, please contact production`); - if (isNaN(quantity)) + if (isNaN(newQuantity)) throw new UserError(`The value should be a number`); - let currentLine = await models.Sale.findOne({where: {id: id}}); - if (quantity > currentLine.quantity) + const filter = { + include: { + relation: 'ticket', + scope: { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + } + } + }; + + const sale = await models.Sale.findById(id, filter); + + if (newQuantity > sale.quantity) throw new UserError('The new quantity should be smaller than the old one'); - return await currentLine.updateAttributes({quantity: quantity}); + const oldQuantity = sale.quantity; + const result = await sale.updateAttributes({quantity: newQuantity}); + + const salesPerson = sale.ticket().client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + const message = $t('Changed sale quantity', { + ticketId: sale.ticket().id, + itemId: sale.itemFk, + concept: sale.concept, + oldQuantity: oldQuantity, + newQuantity: newQuantity, + ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`, + itemUrl: `${origin}/#!/item/${sale.itemFk}/summary` + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + + return result; }; }; diff --git a/modules/ticket/back/methods/ticket/addSale.js b/modules/ticket/back/methods/ticket/addSale.js index 365355df2..dc45e5de9 100644 --- a/modules/ticket/back/methods/ticket/addSale.js +++ b/modules/ticket/back/methods/ticket/addSale.js @@ -33,6 +33,7 @@ module.exports = Self => { }); Self.addSale = async(ctx, id, itemId, quantity) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; const isEditable = await models.Ticket.isEditable(ctx, id); @@ -40,7 +41,19 @@ module.exports = Self => { throw new UserError(`The sales of this ticket can't be modified`); const item = await models.Item.findById(itemId); - const ticket = await models.Ticket.findById(id); + const ticket = await models.Ticket.findById(id, { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + }); const res = await models.Item.getVisibleAvailable(itemId, ticket.warehouseFk, ticket.shipped); @@ -63,6 +76,20 @@ module.exports = Self => { } }); + const addition = `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`; + + const salesPerson = ticket.client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + + const message = $t('Added sale to ticket', { + ticketId: id, + ticketUrl: `${origin}/#!/ticket/${id}/sale`, + addition: addition + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + return sale; }; }; diff --git a/modules/ticket/back/methods/ticket/componentUpdate.js b/modules/ticket/back/methods/ticket/componentUpdate.js index b66179eb8..9ebd51bf4 100644 --- a/modules/ticket/back/methods/ticket/componentUpdate.js +++ b/modules/ticket/back/methods/ticket/componentUpdate.js @@ -230,7 +230,7 @@ module.exports = Self => { ticketUrl: `${origin}/#!/ticket/${args.id}/sale`, changes: changesMade }); - await models.Chat.sendCheckingPresence(ctx, salesPersonId, message, myOptions); + await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); } if (tx) await tx.commit(); diff --git a/modules/ticket/back/methods/ticket/getSales.js b/modules/ticket/back/methods/ticket/getSales.js index 7bd2eabbe..155f23362 100644 --- a/modules/ticket/back/methods/ticket/getSales.js +++ b/modules/ticket/back/methods/ticket/getSales.js @@ -60,8 +60,12 @@ module.exports = Self => { const [salesAvailable] = await Self.rawSql(query, [id], myOptions); const itemAvailable = new Map(); - for (let sale of salesAvailable) - itemAvailable.set(sale.itemFk, sale.available); + for (let sale of salesAvailable) { + itemAvailable.set(sale.itemFk, { + visible: sale.visible, + available: sale.available + }); + } // Get claimed sales const saleIds = sales.map(sale => sale.id); @@ -84,7 +88,9 @@ module.exports = Self => { for (let sale of sales) { const problems = saleProblems.get(sale.id); - sale.available = itemAvailable.get(sale.itemFk); + const itemStock = itemAvailable.get(sale.itemFk); + sale.available = itemStock.available; + sale.visible = itemStock.visible; sale.claim = claimedSales.get(sale.id); if (problems) { sale.isAvailable = problems.isAvailable; diff --git a/modules/ticket/back/methods/ticket/specs/addSale.spec.js b/modules/ticket/back/methods/ticket/specs/addSale.spec.js index c2650bf4b..9f6da7ed2 100644 --- a/modules/ticket/back/methods/ticket/specs/addSale.spec.js +++ b/modules/ticket/back/methods/ticket/specs/addSale.spec.js @@ -12,7 +12,13 @@ describe('ticket addSale()', () => { }); it('should create a new sale for the ticket with id 13', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const itemId = 4; const quantity = 10; newSale = await app.models.Ticket.addSale(ctx, ticketId, itemId, quantity); @@ -21,7 +27,13 @@ describe('ticket addSale()', () => { }); it('should not be able to add a sale if the item quantity is not available', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const itemId = 11; const quantity = 10; @@ -36,7 +48,13 @@ describe('ticket addSale()', () => { }); it('should not be able to add a sale if the ticket is not editable', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const notEditableTicketId = 1; const itemId = 4; const quantity = 10; diff --git a/modules/ticket/back/methods/ticket/specs/updateDiscount.spec.js b/modules/ticket/back/methods/ticket/specs/updateDiscount.spec.js index 2e71842b4..406a3f3ee 100644 --- a/modules/ticket/back/methods/ticket/specs/updateDiscount.spec.js +++ b/modules/ticket/back/methods/ticket/specs/updateDiscount.spec.js @@ -35,7 +35,13 @@ describe('sale updateDiscount()', () => { }); it('should throw an error if no sales were selected', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; let error; const ticketId = 11; const sales = []; @@ -51,7 +57,13 @@ describe('sale updateDiscount()', () => { }); it('should throw an error if no sales belong to different tickets', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; let error; const ticketId = 11; const sales = [1, 14]; @@ -67,7 +79,13 @@ describe('sale updateDiscount()', () => { }); it('should throw an error if the ticket is invoiced already', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; let error; const ticketId = 1; const sales = [1]; @@ -83,7 +101,13 @@ describe('sale updateDiscount()', () => { }); it('should update the discount if the salesPerson has mana', async() => { - let ctx = {req: {accessToken: {userId: 18}}}; + const ctx = { + req: { + accessToken: {userId: 18}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const ticketId = 11; const sales = [originalSaleId]; const newDiscount = 100; @@ -105,7 +129,13 @@ describe('sale updateDiscount()', () => { }); it('should update the discount and add company discount component if the worker does not have mana', async() => { - let ctx = {req: {accessToken: {userId: 9}}}; + const ctx = { + req: { + accessToken: {userId: 9}, + headers: {origin: 'localhost:5000'}, + __: () => {} + } + }; const ticketId = 11; const sales = [originalSaleId]; const newDiscount = 100; diff --git a/modules/ticket/back/methods/ticket/updateDiscount.js b/modules/ticket/back/methods/ticket/updateDiscount.js index 1bebaae8d..c9424395d 100644 --- a/modules/ticket/back/methods/ticket/updateDiscount.js +++ b/modules/ticket/back/methods/ticket/updateDiscount.js @@ -17,7 +17,8 @@ module.exports = Self => { description: 'The sales id', type: ['number'], required: true, - }, { + }, + { arg: 'newDiscount', description: 'The new discount', type: 'number', @@ -35,6 +36,7 @@ module.exports = Self => { }); Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => { + const $t = ctx.req.__; // $translate const models = Self.app.models; const tx = await Self.beginTransaction({}); @@ -59,7 +61,7 @@ module.exports = Self => { } }; - let sales = await models.Sale.find(filter, options); + const sales = await models.Sale.find(filter, options); if (sales.length === 0) throw new UserError('Please select at least one sale'); @@ -94,8 +96,11 @@ module.exports = Self => { where: {code: componentCode}}, options); const componentId = discountComponent.id; - let promises = []; + const promises = []; + let changesMade = ''; + for (let sale of sales) { + const oldDiscount = sale.discount; const value = ((-sale.price * newDiscount) / 100); const newComponent = models.SaleComponent.upsert({ saleFk: sale.id, @@ -105,6 +110,7 @@ module.exports = Self => { const updatedSale = sale.updateAttribute('discount', newDiscount, options); promises.push(newComponent, updatedSale); + changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${oldDiscount}% ➔ *${newDiscount}%*`; } await Promise.all(promises); @@ -112,6 +118,32 @@ module.exports = Self => { const query = `call vn.manaSpellersRequery(?)`; await Self.rawSql(query, [userId], options); + const ticket = await models.Ticket.findById(id, { + include: { + relation: 'client', + scope: { + include: { + relation: 'salesPersonUser', + scope: { + fields: ['id', 'name'] + } + } + } + } + }, options); + + const salesPerson = ticket.client().salesPersonUser(); + if (salesPerson) { + const origin = ctx.req.headers.origin; + + const message = $t('Changed sale discount', { + ticketId: id, + ticketUrl: `${origin}/#!/ticket/${id}/sale`, + changes: changesMade + }); + await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message); + } + await tx.commit(); } catch (error) { await tx.rollback(); diff --git a/modules/ticket/front/component/index.js b/modules/ticket/front/component/index.js index 307f8af15..3f262f457 100644 --- a/modules/ticket/front/component/index.js +++ b/modules/ticket/front/component/index.js @@ -82,7 +82,10 @@ class Controller extends Section { if (!this.ticket) return; this.$http.get(`Tickets/${this.ticket.id}/getVolume`) - .then(res => this.ticketVolume = res.data[0].volume); + .then(res => { + if (res.data.length) + this.ticketVolume = res.data[0].volume; + }); } } diff --git a/modules/ticket/front/sale/index.html b/modules/ticket/front/sale/index.html index dec0627c2..cd3f8426d 100644 --- a/modules/ticket/front/sale/index.html +++ b/modules/ticket/front/sale/index.html @@ -59,6 +59,7 @@ + Visible Available Id Quantity @@ -112,6 +113,13 @@ zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}" on-error-src/> + + + {{::sale.visible}} + + this.edit.mana = res.data); + .then(res => { + this.edit.mana = res.data; + this.$.$applyAsync(() => { + this.$.editDiscount.relocate(); + this.$.editPricePopover.relocate(); + }); + }); } /** diff --git a/modules/ticket/front/sale/index.spec.js b/modules/ticket/front/sale/index.spec.js index e5368468a..169b41c5f 100644 --- a/modules/ticket/front/sale/index.spec.js +++ b/modules/ticket/front/sale/index.spec.js @@ -42,6 +42,8 @@ describe('Ticket', () => { $scope.sms = {open: () => {}}; $scope.ticket = ticket; $scope.model = crudModel; + $scope.editDiscount = {relocate: () => {}}; + $scope.editPricePopover = {relocate: () => {}}; $httpBackend = _$httpBackend_; Object.defineProperties($state.params, { id: { diff --git a/modules/ticket/front/summary/index.html b/modules/ticket/front/summary/index.html index 8dd43eac4..87bc1c97f 100644 --- a/modules/ticket/front/summary/index.html +++ b/modules/ticket/front/summary/index.html @@ -120,6 +120,7 @@ Item + Visible Available Quantity Description @@ -169,6 +170,13 @@ {{sale.itemFk | zeroFill:6}} + + + {{::sale.visible}} + + { for (const ticket of tickets) { try { - await db.rawSql(`CALL vn.ticket_close(?)`, [ticket.id]); + await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]); const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice; if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue; diff --git a/print/templates/email/client-debt-statement/assets/css/import.js b/print/templates/email/client-debt-statement/assets/css/import.js new file mode 100644 index 000000000..b44d6bd37 --- /dev/null +++ b/print/templates/email/client-debt-statement/assets/css/import.js @@ -0,0 +1,8 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/email.css`]) + .mergeStyles(); diff --git a/print/templates/email/client-debt-statement/attachments.json b/print/templates/email/client-debt-statement/attachments.json new file mode 100644 index 000000000..9cc4911e6 --- /dev/null +++ b/print/templates/email/client-debt-statement/attachments.json @@ -0,0 +1,6 @@ +[ + { + "filename": "client-debt-statement.pdf", + "component": "client-debt-statement" + } +] \ No newline at end of file diff --git a/print/templates/email/client-debt-statement/client-debt-statement.html b/print/templates/email/client-debt-statement/client-debt-statement.html new file mode 100644 index 000000000..e63eba7f7 --- /dev/null +++ b/print/templates/email/client-debt-statement/client-debt-statement.html @@ -0,0 +1,55 @@ + + + + + + {{ $t('subject') }} + + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+

{{ $t('title') }}

+

{{$t('description.instructions')}}

+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/print/templates/email/client-debt-statement/client-debt-statement.js b/print/templates/email/client-debt-statement/client-debt-statement.js new file mode 100755 index 000000000..c32e68943 --- /dev/null +++ b/print/templates/email/client-debt-statement/client-debt-statement.js @@ -0,0 +1,25 @@ +const Component = require(`${appPath}/core/component`); +const emailHeader = new Component('email-header'); +const emailFooter = new Component('email-footer'); +const attachment = new Component('attachment'); +const attachments = require('./attachments.json'); + +module.exports = { + name: 'client-debt-statement', + components: { + 'email-header': emailHeader.build(), + 'email-footer': emailFooter.build(), + 'attachment': attachment.build() + }, + data() { + return {attachments}; + }, + props: { + recipientId: { + required: true + }, + from: { + required: true + } + } +}; diff --git a/print/templates/email/client-debt-statement/locale/es.yml b/print/templates/email/client-debt-statement/locale/es.yml new file mode 100644 index 000000000..754e8e92c --- /dev/null +++ b/print/templates/email/client-debt-statement/locale/es.yml @@ -0,0 +1,4 @@ +subject: Extracto de tu balance +title: Extracto de tu balance +description: + instructions: Adjuntamos el extracto de tu balance. \ No newline at end of file diff --git a/print/templates/email/credit-request/assets/css/import.js b/print/templates/email/credit-request/assets/css/import.js new file mode 100644 index 000000000..b44d6bd37 --- /dev/null +++ b/print/templates/email/credit-request/assets/css/import.js @@ -0,0 +1,8 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/email.css`]) + .mergeStyles(); diff --git a/print/templates/email/credit-request/attachments.json b/print/templates/email/credit-request/attachments.json new file mode 100644 index 000000000..12b10df4c --- /dev/null +++ b/print/templates/email/credit-request/attachments.json @@ -0,0 +1,6 @@ +[ + { + "filename": "credit-request.pdf", + "component": "credit-request" + } +] \ No newline at end of file diff --git a/print/templates/email/credit-request/credit-request.html b/print/templates/email/credit-request/credit-request.html new file mode 100644 index 000000000..e63eba7f7 --- /dev/null +++ b/print/templates/email/credit-request/credit-request.html @@ -0,0 +1,55 @@ + + + + + + {{ $t('subject') }} + + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+

{{ $t('title') }}

+

{{$t('description.instructions')}}

+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/print/templates/email/credit-request/credit-request.js b/print/templates/email/credit-request/credit-request.js new file mode 100755 index 000000000..69463f43a --- /dev/null +++ b/print/templates/email/credit-request/credit-request.js @@ -0,0 +1,17 @@ +const Component = require(`${appPath}/core/component`); +const emailHeader = new Component('email-header'); +const emailFooter = new Component('email-footer'); +const attachment = new Component('attachment'); +const attachments = require('./attachments.json'); + +module.exports = { + name: 'credit-request', + components: { + 'email-header': emailHeader.build(), + 'email-footer': emailFooter.build(), + 'attachment': attachment.build() + }, + data() { + return {attachments}; + }, +}; diff --git a/print/templates/email/credit-request/locale/es.yml b/print/templates/email/credit-request/locale/es.yml new file mode 100644 index 000000000..aa6e63dcb --- /dev/null +++ b/print/templates/email/credit-request/locale/es.yml @@ -0,0 +1,4 @@ +subject: Solicitud de crédito +title: Solicitud de crédito +description: + instructions: Adjuntamos el formulario para solicitud de crédito. \ No newline at end of file diff --git a/print/templates/email/delivery-note-link/locale/pt.yml b/print/templates/email/delivery-note-link/locale/pt.yml index cff3ea52b..1aab4b6d8 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: Vossa nota de entrega +subject: Sua nota de entrega +title: Sua 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/pt.yml b/print/templates/email/delivery-note/locale/pt.yml index 818a4de4c..0418fe7a5 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: Vossa nota de entrega +subject: Sua nota de entrega +title: Sua 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. diff --git a/print/templates/email/invoice/assets/css/import.js b/print/templates/email/invoice/assets/css/import.js new file mode 100644 index 000000000..b44d6bd37 --- /dev/null +++ b/print/templates/email/invoice/assets/css/import.js @@ -0,0 +1,8 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/email.css`]) + .mergeStyles(); diff --git a/print/templates/email/invoice/attachments.json b/print/templates/email/invoice/attachments.json new file mode 100644 index 000000000..1abbea18a --- /dev/null +++ b/print/templates/email/invoice/attachments.json @@ -0,0 +1,6 @@ +[ + { + "filename": "invoice.pdf", + "component": "invoice" + } +] \ No newline at end of file diff --git a/print/templates/email/invoice/invoice.html b/print/templates/email/invoice/invoice.html new file mode 100644 index 000000000..66bad472a --- /dev/null +++ b/print/templates/email/invoice/invoice.html @@ -0,0 +1,49 @@ + + + + + + {{ $t('subject') }} + + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+ +
+
+

{{ $t('title') }}

+

{{$t('dear')}},

+

+

+

+

+
+
+ +
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/print/templates/email/invoice/invoice.js b/print/templates/email/invoice/invoice.js new file mode 100755 index 000000000..b8d3b8282 --- /dev/null +++ b/print/templates/email/invoice/invoice.js @@ -0,0 +1,25 @@ +const Component = require(`${appPath}/core/component`); +const emailHeader = new Component('email-header'); +const emailFooter = new Component('email-footer'); + +module.exports = { + name: 'invoice', + async serverPrefetch() { + this.invoice = await this.fetchInvoice(this.invoiceId); + }, + methods: { + fetchInvoice(invoiceId) { + return this.findOneFromDef('invoice', [invoiceId]); + }, + }, + components: { + 'email-header': emailHeader.build(), + 'email-footer': emailFooter.build() + }, + props: { + invoiceId: { + type: String, + required: true + } + } +}; diff --git a/print/templates/email/invoice/locale/en.yml b/print/templates/email/invoice/locale/en.yml new file mode 100644 index 000000000..bf1287cae --- /dev/null +++ b/print/templates/email/invoice/locale/en.yml @@ -0,0 +1,9 @@ +subject: Your invoice +title: Your invoice +dear: Dear client +description: The invoice {0} from the order {1} is now available.
+ You can download it by clicking on the attachment of this email. +poll: If you wish, you can answer our satisfaction survey to + help us provide better service. Your opinion is very important for us! +help: Any questions that arise, do not hesitate to consult it, we are here to assist you! +conclusion: Thanks for your attention! diff --git a/print/templates/email/invoice/locale/es.yml b/print/templates/email/invoice/locale/es.yml new file mode 100644 index 000000000..0147d15ac --- /dev/null +++ b/print/templates/email/invoice/locale/es.yml @@ -0,0 +1,10 @@ +subject: Tu factura +title: Tu factura +dear: Estimado cliente +description: Ya está disponible la factura {0} correspondiente al pedido {1}.
+ Puedes descargarla haciendo clic en el adjunto de este correo. +poll: Si lo deseas, puedes responder a nuestra encuesta de satisfacción para + ayudarnos a prestar un mejor servicio. ¡Tu opinión es muy importante para nosotros! +help: Cualquier duda que te surja, no dudes en consultarla, ¡estamos para + atenderte! +conclusion: ¡Gracias por tu atención! diff --git a/print/templates/email/invoice/locale/fr.yml b/print/templates/email/invoice/locale/fr.yml new file mode 100644 index 000000000..9efbdf5a0 --- /dev/null +++ b/print/templates/email/invoice/locale/fr.yml @@ -0,0 +1,9 @@ +subject: Votre facture +title: Votre facture +dear: Cher client, +description: Le facture {0} correspondant à la commande {1} est maintenant disponible.
+ Vous pouvez le télécharger en cliquant sur la pièce jointe dans cet email. +poll: Si vous le souhaitez, vous pouvez répondre à notre questionaire de satisfaction + pour nous aider à améliorer notre service. Votre avis est très important pour nous! +help: N'hésitez pas nous envoyer toute doute ou question, nous sommes là pour vous aider! +conclusion: Merci pour votre attention! diff --git a/print/templates/email/invoice/locale/pt.yml b/print/templates/email/invoice/locale/pt.yml new file mode 100644 index 000000000..4d779cd3d --- /dev/null +++ b/print/templates/email/invoice/locale/pt.yml @@ -0,0 +1,9 @@ +subject: Sua fatura +title: Sua fatura +dear: Estimado cliente +description: Já está disponível sua fatura {0} correspondente a encomenda {1}.
+ Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail. +poll: Si o deseja, podes responder nosso questionário de satiscação para ajudar-nos a prestar-vos um melhor serviço. Tua opinião é muito importante para nós! +help: Cualquer dúvida que surja, no hesites em consultar-la, Estamos aqui para + atender-te! +conclusion: Obrigado por tua atenção! \ No newline at end of file diff --git a/print/templates/email/invoice/sql/invoice.sql b/print/templates/email/invoice/sql/invoice.sql new file mode 100644 index 000000000..195621a36 --- /dev/null +++ b/print/templates/email/invoice/sql/invoice.sql @@ -0,0 +1,7 @@ +SELECT + io.ref, + t.id AS ticketFk +FROM invoiceOut io + JOIN ticket t ON t.refFk = io.ref + JOIN client c ON c.id = io.clientFk +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/assets/css/import.js b/print/templates/reports/client-debt-statement/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/client-debt-statement/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/client-debt-statement/assets/css/style.css b/print/templates/reports/client-debt-statement/assets/css/style.css new file mode 100644 index 000000000..e621f3e23 --- /dev/null +++ b/print/templates/reports/client-debt-statement/assets/css/style.css @@ -0,0 +1,3 @@ +table.column-oriented { + margin-top: 50px !important +} \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/client-debt-statement.html b/print/templates/reports/client-debt-statement/client-debt-statement.html new file mode 100644 index 000000000..88bf15bdb --- /dev/null +++ b/print/templates/reports/client-debt-statement/client-debt-statement.html @@ -0,0 +1,95 @@ + + + + + + + + + +
+ + + +
+
+
+
+
+

{{$t('title')}}

+ + + + + + + + + + + +
{{$t('clientId')}}{{client.id}}
{{$t('date')}}{{dated}}
+
+
+
+
+
{{$t('clientData')}}
+
+

{{client.socialName}}

+
+ {{client.street}} +
+
+ {{client.postcode}}, {{client.city}} ({{client.province}}) +
+
+ {{client.country}} +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('date')}}{{$t('concept')}}{{$t('invoiced')}}{{$t('payed')}}{{$t('balance')}}
{{sale.issued | date('%d-%m-%Y')}}{{sale.ref}}{{sale.debtOut}}{{sale.debtIn}}{{getBalance(sale)}}
+ Total + {{getTotalDebtOut() | currency('EUR', $i18n.locale)}} + {{getTotalDebtIn() | currency('EUR', $i18n.locale)}}{{totalBalance | currency('EUR', $i18n.locale)}}
+
+
+ + + +
+ + \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/client-debt-statement.js b/print/templates/reports/client-debt-statement/client-debt-statement.js new file mode 100755 index 000000000..09b99590b --- /dev/null +++ b/print/templates/reports/client-debt-statement/client-debt-statement.js @@ -0,0 +1,78 @@ +const Component = require(`${appPath}/core/component`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); + +module.exports = { + name: 'client-debt-statement', + async serverPrefetch() { + this.client = await this.fetchClient(this.recipientId); + this.sales = await this.fetchSales(this.recipientId, this.from); + + if (!this.client) + throw new Error('Something went wrong'); + }, + computed: { + dated: function() { + const filters = this.$options.filters; + + return filters.date(new Date(), '%d-%m-%Y'); + } + }, + data() { + return {totalBalance: 0.00}; + }, + methods: { + fetchClient(clientId) { + return this.findOneFromDef('client', [clientId]); + }, + fetchSales(clientId, from) { + return this.rawSqlFromDef('sales', [ + from, + clientId, + from, + clientId, + from, + clientId, + from, + clientId, + from, + clientId + ]); + }, + getBalance(sale) { + if (sale.debtOut) + this.totalBalance += parseFloat(sale.debtOut); + + if (sale.debtIn) + this.totalBalance -= parseFloat(sale.debtIn); + + return parseFloat(this.totalBalance.toFixed(2)); + }, + getTotalDebtOut() { + let debtOut = 0.00; + for (let sale of this.sales) + debtOut += sale.debtOut ? parseFloat(sale.debtOut) : 0; + + return debtOut.toFixed(2); + }, + getTotalDebtIn() { + let debtIn = 0.00; + for (let sale of this.sales) + debtIn += sale.debtIn ? parseFloat(sale.debtIn) : 0; + + return debtIn.toFixed(2); + }, + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build() + }, + props: { + recipientId: { + required: true + }, + from: { + required: true + } + } +}; diff --git a/print/templates/reports/client-debt-statement/locale/es.yml b/print/templates/reports/client-debt-statement/locale/es.yml new file mode 100644 index 000000000..ccdce7b5b --- /dev/null +++ b/print/templates/reports/client-debt-statement/locale/es.yml @@ -0,0 +1,9 @@ +title: Extracto +clientId: Cliente +clientData: Datos del cliente +date: Fecha +concept: Concepto +invoiced: Facturado +payed: Pagado +balance: Saldo +client: Cliente {0} \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/locale/fr.yml b/print/templates/reports/client-debt-statement/locale/fr.yml new file mode 100644 index 000000000..12534f9ff --- /dev/null +++ b/print/templates/reports/client-debt-statement/locale/fr.yml @@ -0,0 +1,9 @@ +title: Relevé de compte +clientId: Client +clientData: Données client +date: Date +concept: Objet +invoiced: Facturé +payed: Payé +balance: Solde +client: Client {0} \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/sql/client.sql b/print/templates/reports/client-debt-statement/sql/client.sql new file mode 100644 index 000000000..d675cf168 --- /dev/null +++ b/print/templates/reports/client-debt-statement/sql/client.sql @@ -0,0 +1,13 @@ +SELECT + c.id, + c.socialName, + c.street, + c.postcode, + c.city, + c.fi, + p.name AS province, + ct.country +FROM client c + JOIN country ct ON ct.id = c.countryFk + LEFT JOIN province p ON p.id = c.provinceFk +WHERE c.id = ? \ No newline at end of file diff --git a/print/templates/reports/client-debt-statement/sql/sales.sql b/print/templates/reports/client-debt-statement/sql/sales.sql new file mode 100644 index 000000000..7a9124da5 --- /dev/null +++ b/print/templates/reports/client-debt-statement/sql/sales.sql @@ -0,0 +1,53 @@ +SELECT + issued, + CAST(debtOut AS DECIMAL(10,2)) debtOut, + CAST(debtIn AS DECIMAL(10,2)) debtIn, + ref, + companyFk, + priority +FROM ( + SELECT + ? AS issued, + SUM(amountUnpaid) AS debtOut, + NULL AS debtIn, + 'Saldo Anterior' AS ref, + companyFk, + 0 as priority + FROM ( + SELECT SUM(amount) AS amountUnpaid, companyFk, 0 + FROM invoiceOut io + WHERE io.clientFk = ? + AND io.issued < ? + GROUP BY io.companyFk + UNION ALL + SELECT SUM(-1 * amountPaid), companyFk, 0 + FROM receipt + WHERE clientFk = ? + AND payed < ? + GROUP BY companyFk) AS transactions + GROUP BY companyFk + UNION ALL + SELECT + issued, + amount as debtOut, + NULL AS debtIn, + ref, + companyFk, + 1 + FROM invoiceOut + WHERE clientFk = ? + AND issued >= ? + UNION ALL + SELECT + r.payed, + NULL as debtOut, + r.amountPaid, + r.invoiceFk, + r.companyFk, + 0 + FROM receipt r + WHERE r.clientFk = ? + AND r.payed >= ?) t + INNER JOIN `client` c ON c.id = ? +HAVING debtOut <> 0 OR debtIn <> 0 +ORDER BY issued, priority DESC, debtIn; \ No newline at end of file diff --git a/print/templates/reports/credit-request/assets/css/import.js b/print/templates/reports/credit-request/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/credit-request/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/credit-request/assets/css/style.css b/print/templates/reports/credit-request/assets/css/style.css new file mode 100644 index 000000000..90bbfd103 --- /dev/null +++ b/print/templates/reports/credit-request/assets/css/style.css @@ -0,0 +1,52 @@ +.wide { + width: 900px !important +} + +.content { + position: absolute; + margin-top: -200px; + height: 400px; + top: 50% +} + +p { + font-size: 1.2em; + margin: 0 +} + +td > span { + width: 100%; + margin-bottom: 15px +} + +.green-background { + color: white; + background-color: #8dba25; + height: 2em; + line-height: 2em; + padding-left: 0.5em; +} + +.info-panel td, .info-panel th { + padding: 1em 1em; +} + +.info-panel { + margin-bottom: 5em; +} + +table { + width: 100%; +} + +th { + width: 30%; +} + +td { + width: 20%; +} + +.field { + float: none +} \ No newline at end of file diff --git a/print/templates/reports/credit-request/credit-request.html b/print/templates/reports/credit-request/credit-request.html new file mode 100644 index 000000000..975115eef --- /dev/null +++ b/print/templates/reports/credit-request/credit-request.html @@ -0,0 +1,190 @@ + + + + + + + + + +
+ + + + +
+
+

{{$t('fields.title')}}

+
+
+ + + + + + + + + +
{{$t('fields.date')}}: +
+ +
+
+ + +
+
+
+
+

{{$t('fields.companyInfo')}}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('fields.companyName')}}: +
+ +
+
{{$t('fields.businessType')}}: +
+ +
+
{{$t('fields.antiquity')}}: +
+ +
+
{{$t('fields.surface')}}: +
+ +
+
{{$t('fields.numberOfEmployees')}}: +
+ +
+
{{$t('fields.owner')}}: +
+ +
+
{{$t('fields.phone')}}: +
+ +
+
{{$t('fields.payer')}}: +
+ +
+
{{$t('fields.phone')}}: +
+ +
+
+
+
+
+
+ + +
+
+
+
+

{{$t('fields.economicInfo')}}

+
+
+ + + + + + + + + + + + + + + +
{{$t('fields.previousSalesVolume')}}: +
+ +
+
{{$t('fields.forecastedSalesVolume')}}: +
+ +
+
{{$t('fields.forecastedPurchases')}}: +
+ +
+
+
+
+
+
+ + +
+
+
+ + + + + + + + + +
{{$t('fields.personFilling')}}: +
+ +
+
{{$t('fields.phone')}}: +
+ +
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/print/templates/reports/credit-request/credit-request.js b/print/templates/reports/credit-request/credit-request.js new file mode 100755 index 000000000..d04106493 --- /dev/null +++ b/print/templates/reports/credit-request/credit-request.js @@ -0,0 +1,20 @@ +const Component = require(`${appPath}/core/component`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); + +const rptCreditRequest = { + name: 'credit-request', + computed: { + dated: function() { + const filters = this.$options.filters; + + return filters.date(new Date(), '%d-%m-%Y'); + } + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build() + } +}; + +module.exports = rptCreditRequest; diff --git a/print/templates/reports/credit-request/locale/es.yml b/print/templates/reports/credit-request/locale/es.yml new file mode 100644 index 000000000..e4e9739a5 --- /dev/null +++ b/print/templates/reports/credit-request/locale/es.yml @@ -0,0 +1,17 @@ +fields: + title: Solicitud de crédito + date: Fecha + companyName: Nombre de la empresa + businessType: Tipo de negocio + antiquity: Antigüedad + surface: Superficie (m²) + numberOfEmployees: Número de empleados + owner: Contacto propietario o Administrador + phone: Teléfono + payer: Contacto responsable de pagos + previousSalesVolume: Previsión ventas ejercicio anterior + forecastedSalesVolume: Previsión ventas para el presente año + forecastedPurchases: Previsión de compras a Verdnatura + personFilling: Persona que rellena el formulario + companyInfo: Información general sobre la empresa + economicInfo: Información económica \ No newline at end of file diff --git a/print/templates/reports/receipt/receipt.html b/print/templates/reports/receipt/receipt.html index 3371e6871..5dc1846f7 100644 --- a/print/templates/reports/receipt/receipt.html +++ b/print/templates/reports/receipt/receipt.html @@ -21,7 +21,7 @@

{{$t('payed', [ - 'Silla', + 'Algemesí', receipt.payed.getDate(), $t('months')[receipt.payed.getMonth()], receipt.payed.getFullYear()])