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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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}} |
+
+
+
+
+
+
+
+
+
+ {{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()])