Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2879-import_buys

This commit is contained in:
Joan Sanchez 2021-08-05 14:33:28 +02:00
commit 886d38ad8a
103 changed files with 2037 additions and 175 deletions

View File

@ -6,6 +6,9 @@
"table": "sage.TiposRetencion" "table": "sage.TiposRetencion"
} }
}, },
"log": {
"showField": "withholding"
},
"properties": { "properties": {
"id": { "id": {
"type": "Number", "type": "Number",

View File

@ -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);

View File

@ -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 ;

View File

@ -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 ;

View File

@ -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 ;

View File

@ -0,0 +1,2 @@
INSERT INTO `vn`.`sample` (`code`, `description`, `isVisible`, `hasCompany`, `hasPreview`)
VALUES ('credit-request', 'Solicitud de crédito', 1, 1, 1);

View File

@ -2339,6 +2339,14 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
(9, 9, 9), (9, 9, 9),
(10, 10, 10); (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`) INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
SELECT `id` SELECT `id`
FROM `vn`.`ticket` t FROM `vn`.`ticket` t

View File

@ -258,7 +258,9 @@ export default {
}, },
clientLog: { clientLog: {
lastModificationPreviousValue: 'vn-client-log vn-table vn-td.before', 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: { clientBalance: {
@ -462,8 +464,8 @@ export default {
itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor', itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor',
itemDescriptorPopoverItemDiaryButton: 'vn-item-descriptor a[href="#!/item/2/diary?warehouseFk=5&lineFk=16"]', 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"]', 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)', 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(7)', 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', 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', setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button',
descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div' 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', 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', 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', 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"]', 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', 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', firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img',
firstSaleZoomedImage: 'body > div > div > img', firstSaleZoomedImage: 'body > div > div > img',
firstSaleQuantity: 'vn-ticket-sale [ng-model="sale.quantity"]', firstSaleQuantity: 'vn-ticket-sale [ng-model="sale.quantity"]',
firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(6)', 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(8) > span', 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"]', 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"]', 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)', 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', 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"]', 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', 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"]', 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', 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)', 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(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', 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', totalImport: 'vn-ticket-sale vn-one.taxes > p:nth-child(3) > strong',
selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check', selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check',
@ -936,7 +938,8 @@ export default {
invoiceInDescriptor: { invoiceInDescriptor: {
moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]', moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]',
moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]', 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: { invoiceInBasicData: {
issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]', issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]',

View File

@ -16,8 +16,15 @@ describe('Client Edit web access path', () => {
await browser.close(); 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.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.clearInput(selectors.clientWebAccess.userName);
await page.write(selectors.clientWebAccess.userName, 'Hulk'); await page.write(selectors.clientWebAccess.userName, 'Hulk');
await page.waitToClick(selectors.clientWebAccess.saveButton); await page.waitToClick(selectors.clientWebAccess.saveButton);
@ -26,9 +33,8 @@ describe('Client Edit web access path', () => {
expect(message.text).toContain('Data saved!'); expect(message.text).toContain('Data saved!');
}); });
it('should confirm web access is now unchecked', async() => { it('should reload the section and confirm web access is now unchecked', async() => {
await page.accessToSection('client.card.basicData'); await page.reloadSection('client.card.webAccess');
await page.accessToSection('client.card.webAccess');
const result = await page.checkboxState(selectors.clientWebAccess.enableWebAccessCheckbox); const result = await page.checkboxState(selectors.clientWebAccess.enableWebAccessCheckbox);
expect(result).toBe('unchecked'); expect(result).toBe('unchecked');
@ -44,13 +50,23 @@ describe('Client Edit web access path', () => {
await page.accessToSection('client.card.log'); 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 let lastModificationPreviousValue = await page
.waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText'); .waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText');
let lastModificationCurrentValue = await page let lastModificationCurrentValue = await page
.waitToGetProperty(selectors.clientLog.lastModificationCurrentValue, 'innerText'); .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'); 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');
});
}); });

View File

@ -10,16 +10,30 @@ describe('InvoiceIn descriptor path', () => {
page = browser.page; page = browser.page;
await page.loginAndModule('administrative', 'invoiceIn'); await page.loginAndModule('administrative', 'invoiceIn');
await page.accessToSearchResult('10'); await page.accessToSearchResult('10');
await page.accessToSection('invoiceIn.card.basicData');
}); });
afterAll(async() => { afterAll(async() => {
await browser.close(); 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.moreMenu);
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn); await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn);
await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton); await page.waitToClick(selectors.invoiceInDescriptor.acceptButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('InvoiceIn deleted'); expect(message.text).toContain('InvoiceIn deleted');

View File

@ -57,7 +57,14 @@
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "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", "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", "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}} €*", "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}}})", "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}}})", "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", "None": "None",
"error densidad = 0": "error densidad = 0", "error densidad = 0": "error densidad = 0",
"nickname": "nickname", "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"
} }

View File

@ -54,7 +54,7 @@
"This address doesn't exist": "Este consignatario no existe", "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 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 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", "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", "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", "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", "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", "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", "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}} €*", "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}}})", "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}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",

View File

@ -3,17 +3,20 @@ const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('createFromSales', { Self.remoteMethodCtx('createFromSales', {
description: 'Create a claim', description: 'Create a claim',
accepts: [{ accepts: [
{
arg: 'ticketId', arg: 'ticketId',
type: 'Number', type: 'number',
required: true, required: true,
description: 'The origin ticket id' description: 'The origin ticket id'
}, { },
{
arg: 'sales', arg: 'sales',
type: ['Object'], type: ['object'],
required: true, required: true,
description: 'The claimed sales' description: 'The claimed sales'
}], }
],
returns: { returns: {
type: 'object', type: 'object',
root: true root: true
@ -25,6 +28,7 @@ module.exports = Self => {
}); });
Self.createFromSales = async(ctx, ticketId, sales, options) => { Self.createFromSales = async(ctx, ticketId, sales, options) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
let tx; let tx;
const myOptions = {}; const myOptions = {};
@ -39,7 +43,20 @@ module.exports = Self => {
const userId = ctx.req.accessToken.userId; const userId = ctx.req.accessToken.userId;
try { 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) if (ticket.isDeleted)
throw new UserError(`You can't create a claim for a removed ticket`); throw new UserError(`You can't create a claim for a removed ticket`);
@ -49,6 +66,8 @@ module.exports = Self => {
ticketCreated: ticket.shipped, ticketCreated: ticket.shipped,
workerFk: userId workerFk: userId
}, myOptions); }, myOptions);
let changesMade = '';
const promises = []; const promises = [];
for (const sale of sales) { for (const sale of sales) {
@ -59,10 +78,25 @@ module.exports = Self => {
}, myOptions); }, myOptions);
promises.push(newClaimBeginning); promises.push(newClaimBeginning);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
} }
await Promise.all(promises); 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(); if (tx) await tx.commit();
return newClaim; return newClaim;

View File

@ -7,7 +7,13 @@ describe('Claim createFromSales()', () => {
instance: 0, instance: 0,
quantity: 10 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() => { it('should create a new claim', async() => {
const tx = await app.models.Claim.beginTransaction({}); const tx = await app.models.Claim.beginTransaction({});

View File

@ -9,23 +9,26 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"code": { "code": {
"type": "String" "type": "string"
}, },
"description": { "description": {
"type": "String" "type": "string"
}, },
"isVisible": { "isVisible": {
"type": "Boolean" "type": "boolean"
}, },
"hasCompany": { "hasCompany": {
"type": "Boolean" "type": "boolean"
}, },
"hasPreview": { "hasPreview": {
"type": "Boolean" "type": "boolean"
},
"datepickerEnabled": {
"type": "boolean"
} }
}, },
"scopes": { "scopes": {

View File

@ -25,38 +25,48 @@
</vn-crud-model> </vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md"> <form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Recipient"
ng-model="$ctrl.clientSample.recipient"
info="Its only used when sample is sent">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Reply to"
ng-model="$ctrl.clientSample.replyTo"
info="To who should the recipient reply?">
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-id="sampleType" vn-id="sampleType"
ng-model="$ctrl.clientSample.typeFk" ng-model="$ctrl.clientSample.typeFk"
model="ClientSample.typeFk" model="ClientSample.typeFk"
fields="['code','hasCompany', 'hasPreview']"
data="samplesVisible" data="samplesVisible"
show-field="description" show-field="description"
label="Sample"> label="Sample"
required="true">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Recipient"
ng-model="$ctrl.clientSample.recipient"
info="Its only used when sample is sent"
required="true">
</vn-textfield>
<vn-textfield
label="Reply to"
ng-model="$ctrl.clientSample.replyTo"
info="To who should the recipient reply?"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal ng-if="sampleType.selection.hasCompany || sampleType.selection.datepickerEnabled">
<vn-autocomplete <vn-autocomplete
ng-model="$ctrl.companyId" ng-model="$ctrl.companyId"
model="ClientSample.companyFk" model="ClientSample.companyFk"
data="companiesData" data="companiesData"
show-field="code" show-field="code"
label="Company" label="Company"
ng-if="sampleType.selection.hasCompany"> ng-if="sampleType.selection.hasCompany"
required="true">
</vn-autocomplete> </vn-autocomplete>
<vn-date-picker
vn-one
label="From"
ng-model="$ctrl.clientSample.from"
ng-if="sampleType.selection.datepickerEnabled"
required="true">
</vn-date-picker>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -80,6 +80,12 @@ class Controller extends Section {
if (sampleType.hasCompany) if (sampleType.hasCompany)
params.companyId = this.clientSample.companyFk; params.companyId = this.clientSample.companyFk;
if (sampleType.datepickerEnabled && !this.clientSample.from)
return this.vnApp.showError(this.$t('Choose a date'));
if (sampleType.datepickerEnabled)
params.from = this.clientSample.from;
let query = `email/${sampleType.code}`; let query = `email/${sampleType.code}`;
if (isPreview) if (isPreview)
query = `email/${sampleType.code}/preview`; query = `email/${sampleType.code}/preview`;

View File

@ -1,5 +1,6 @@
Choose a sample: Selecciona una plantilla Choose a sample: Selecciona una plantilla
Choose a company: Selecciona una empresa Choose a company: Selecciona una empresa
Choose a date: Selecciona una fecha
Email cannot be blank: Debes introducir un email Email cannot be blank: Debes introducir un email
Recipient: Destinatario Recipient: Destinatario
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla

View File

@ -293,6 +293,8 @@
<vn-tr> <vn-tr>
<vn-th field="id" number>Id</vn-th> <vn-th field="id" number>Id</vn-th>
<vn-th field="nickname" expand>Nickname</vn-th> <vn-th field="nickname" expand>Nickname</vn-th>
<vn-th field="agencyModeFk" expand>Agency</vn-th>
<vn-th field="routeFk" expand>Route</vn-th>
<vn-th field="packages" shrink>Packages</vn-th> <vn-th field="packages" shrink>Packages</vn-th>
<vn-th field="shipped" shrink-date>Date</vn-th> <vn-th field="shipped" shrink-date>Date</vn-th>
<vn-th>State</vn-th> <vn-th>State</vn-th>
@ -313,6 +315,17 @@
{{::ticket.nickname}} {{::ticket.nickname}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink>
{{::ticket.agencyMode.name}}
</vn-td>
<vn-td shrink>
<span
title="{{::ticket.route}}"
vn-click-stop="routeDescriptor.show($event, ticket.routeFk)"
class="link">
{{::ticket.routeFk}}
</span>
</vn-td>
<vn-td shrink> <vn-td shrink>
{{::ticket.packages}} {{::ticket.packages}}
</vn-td> </vn-td>
@ -371,6 +384,9 @@
<vn-client-descriptor-popover <vn-client-descriptor-popover
vn-id="clientDescriptor"> vn-id="clientDescriptor">
</vn-client-descriptor-popover> </vn-client-descriptor-popover>
<vn-route-descriptor-popover
vn-id="routeDescriptor">
</vn-route-descriptor-popover>
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="workerDescriptor"> vn-id="workerDescriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>

View File

@ -22,7 +22,13 @@ class Controller extends Summary {
scope: { scope: {
fields: ['id'] fields: ['id']
} }
},
{
relation: 'agencyMode',
scope: {
fields: ['name']
} }
},
] ]
}; };
} }

View File

@ -9,6 +9,8 @@
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check
vn-id="active"
disabled="watcher.orgData.name != $ctrl.account.name"
vn-one vn-one
label="Enable web access" label="Enable web access"
ng-model="$ctrl.account.active" ng-model="$ctrl.account.active"
@ -17,6 +19,8 @@
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield
vn-id="name"
disabled="watcher.orgData.active != $ctrl.account.active"
vn-focus vn-focus
vn-one vn-one
label="User" label="User"

View File

@ -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;
}
};
};

View File

@ -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;
}
});
});

View File

@ -2,6 +2,9 @@
"InvoiceIn": { "InvoiceIn": {
"dataSource": "vn" "dataSource": "vn"
}, },
"InvoiceInTax": {
"dataSource": "vn"
},
"InvoiceInDueDay": { "InvoiceInDueDay": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -24,6 +24,9 @@
"amount": { "amount": {
"type": "number" "type": "number"
}, },
"foreignValue": {
"type": "number"
},
"created": { "created": {
"type": "date" "type": "date"
} }

View File

@ -0,0 +1,42 @@
{
"name": "InvoiceInTax",
"base": "VnModel",
"options": {
"mysql": {
"table": "invoiceInTax"
}
},
"properties": {
"id": {
"id": true,
"type": "number",
"description": "Identifier"
},
"invoiceInFk": {
"type": "number"
},
"taxCodeFk": {
"type": "number"
},
"taxableBase": {
"type": "number"
},
"expenceFk": {
"type": "string"
},
"foreignValue": {
"type": "number"
},
"taxTypeSageFk": {
"type": "number"
},
"transactionTypeSageFk": {
"type": "number"
},
"created": {
"type": "date"
}
}
}

View File

@ -1,4 +1,5 @@
module.exports = Self => { module.exports = Self => {
require('../methods/invoice-in/filter')(Self); require('../methods/invoice-in/filter')(Self);
require('../methods/invoice-in/summary')(Self); require('../methods/invoice-in/summary')(Self);
require('../methods/invoice-in/clone')(Self);
}; };

View File

@ -12,7 +12,7 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"serialNumber": { "serialNumber": {
@ -36,6 +36,9 @@
"booked": { "booked": {
"type": "date" "type": "date"
}, },
"isVatDeductible": {
"type": "boolean"
},
"operated": { "operated": {
"type": "date" "type": "date"
}, },

View File

@ -8,6 +8,13 @@
translate> translate>
Delete Invoice Delete Invoice
</vn-item> </vn-item>
<vn-item
ng-click="cloneConfirmation.show()"
vn-acl="invoicing"
name="cloneInvoice"
translate>
Clone Invoice
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -42,8 +49,16 @@
</div> </div>
</slot-body> </slot-body>
</vn-descriptor-content> </vn-descriptor-content>
<vn-confirm vn-id="deleteConfirmation" on-accept="$ctrl.deleteInvoiceIn()" <vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteInvoiceIn()"
question="Are you sure you want to delete this invoice?"> question="Are you sure you want to delete this invoice?">
</vn-confirm> </vn-confirm>
<vn-supplier-descriptor-popover vn-id="supplierDescriptor"> <vn-confirm
vn-id="cloneConfirmation"
on-accept="$ctrl.cloneInvoiceIn()"
question="Are you sure you want to clone this invoice?">
</vn-confirm>
<vn-supplier-descriptor-popover
vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover> </vn-supplier-descriptor-popover>

View File

@ -30,6 +30,12 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn deleted'))); .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() { loadData() {
const filter = { const filter = {
include: [ include: [

View File

@ -3,3 +3,4 @@ Search invoices in by reference: Buscar facturas recibidas por referencia
Entries list: Listado de entradas Entries list: Listado de entradas
Invoice list: Listado de facturas recibidas Invoice list: Listado de facturas recibidas
InvoiceIn deleted: Factura eliminada InvoiceIn deleted: Factura eliminada
InvoiceIn cloned: Factura clonada

View File

@ -21,7 +21,7 @@ class Controller extends ModuleCard {
}, { }, {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'socialName', 'name'] fields: ['id', 'socialName', 'name', 'email']
} }
} }
] ]

View File

@ -9,6 +9,12 @@
translate> translate>
Show invoice PDF Show invoice PDF
</a> </a>
<vn-item
ng-click="invoiceConfirmation.show()"
name="sendInvoice"
translate>
Send invoice PDF
</vn-item>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
vn-acl="invoicing" vn-acl="invoicing"
@ -98,3 +104,11 @@
question="Are you sure you want to regenerate the invoice PDF document?" question="Are you sure you want to regenerate the invoice PDF document?"
message="You are going to regenerate the invoice PDF document"> message="You are going to regenerate the invoice PDF document">
</vn-confirm> </vn-confirm>
<!-- Send invoice confirmation popup -->
<vn-confirm
vn-id="invoiceConfirmation"
on-accept="$ctrl.sendInvoice()"
question="Are you sure you want to send it?"
message="Send invoice PDF">
</vn-confirm>

View File

@ -59,6 +59,14 @@ class Controller extends Descriptor {
return this.getData(`InvoiceOuts/${this.id}`, {filter}) return this.getData(`InvoiceOuts/${this.id}`, {filter})
.then(res => this.entity = res.data); .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', { ngModule.vnComponent('vnInvoiceOutDescriptor', {

View File

@ -3,9 +3,12 @@ Volume: Volumen
Client card: Ficha del cliente Client card: Ficha del cliente
Invoice ticket list: Listado de tickets de la factura Invoice ticket list: Listado de tickets de la factura
Show invoice PDF: Ver factura en PDF Show invoice PDF: Ver factura en PDF
Send invoice PDF: Enviar factura en PDF
Delete Invoice: Eliminar factura Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura
InvoiceOut deleted: Factura eliminada 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 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 Book invoice: Asentar factura
InvoiceOut booked: Factura asentada InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?

View File

@ -1,6 +1,7 @@
<vn-crud-model <vn-crud-model
vn-id="ticketsModel" vn-id="ticketsModel"
limit="10" limit="10"
auto-load="true"
data="tickets"> data="tickets">
</vn-crud-model> </vn-crud-model>
<vn-card class="summary"> <vn-card class="summary">

View File

@ -7,7 +7,7 @@ class Controller extends Summary {
this._invoiceOut = value; this._invoiceOut = value;
if (value && value.id) { if (value && value.id) {
this.getSummary(); 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`) return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`)
.then(res => this.summary = res.data); .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', { ngModule.vnComponent('vnInvoiceOutSummary', {

View File

@ -27,17 +27,5 @@ describe('InvoiceOut', () => {
expect(controller.summary).toEqual('the data you are looking for'); 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();
});
});
}); });
}); });

View File

@ -71,7 +71,7 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const originalTaxes = await models.ItemTaxCountry.find({ const originalTaxes = await models.ItemTaxCountry.find({
where: {itemFk: originalId}, where: {itemFk: originalId},
fields: ['botanical', 'countryFk', 'taxClassFk'] fields: ['countryFk', 'taxClassFk']
}, options); }, options);
const promises = []; const promises = [];
@ -99,7 +99,7 @@ module.exports = Self => {
const models = Self.app.models; const models = Self.app.models;
const botanical = await models.ItemBotanical.findOne({ const botanical = await models.ItemBotanical.findOne({
where: {itemFk: originalId}, where: {itemFk: originalId},
fields: ['botanical', 'genusFk', 'specieFk'] fields: ['genusFk', 'specieFk']
}, options); }, options);
if (botanical) { if (botanical) {

View File

@ -3,8 +3,7 @@
"base": "Loggable", "base": "Loggable",
"log": { "log": {
"model": "ItemLog", "model": "ItemLog",
"relation": "item", "relation": "item"
"showField": "botanical"
}, },
"options": { "options": {
"mysql": { "mysql": {

View File

@ -6,18 +6,18 @@ module.exports = Self => {
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [{
arg: 'sales', arg: 'sales',
type: ['Object'], type: ['object'],
required: true, required: true,
description: 'The sales to remove' description: 'The sales to remove'
}, },
{ {
arg: 'ticketId', arg: 'ticketId',
type: 'Number', type: 'number',
required: true, required: true,
description: 'The ticket id' description: 'The ticket id'
}], }],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -27,10 +27,25 @@ module.exports = Self => {
}); });
Self.deleteSales = async(ctx, sales, ticketId) => { Self.deleteSales = async(ctx, sales, ticketId) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const canEditSales = await models.Sale.canEdit(ctx, sales); 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); const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
if (!isTicketEditable) if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`); 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`); throw new UserError(`Sale(s) blocked, please contact production`);
const promises = []; const promises = [];
let deletions = '';
for (let sale of sales) { for (let sale of sales) {
const deletedSale = models.Sale.destroyById(sale.id); const deletedSale = models.Sale.destroyById(sale.id);
deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
promises.push(deletedSale); 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); return Promise.all(promises);
}; };
}; };

View File

@ -5,24 +5,27 @@ module.exports = Self => {
Self.remoteMethodCtx('reserve', { Self.remoteMethodCtx('reserve', {
description: 'Change the state of a ticket', description: 'Change the state of a ticket',
accessType: 'WRITE', accessType: 'WRITE',
accepts: [{ accepts: [
{
arg: 'ticketId', arg: 'ticketId',
type: 'Number', type: 'number',
required: true, required: true,
description: 'The ticket id' description: 'The ticket id'
}, { },
{
arg: 'sales', arg: 'sales',
type: ['Object'], type: ['object'],
required: true, required: true,
description: 'The sale to reserve' description: 'The sale to reserve'
}, },
{ {
arg: 'reserved', arg: 'reserved',
type: 'Boolean', type: 'boolean',
required: true required: true
}], }
],
returns: { returns: {
type: ['Object'], type: ['object'],
root: true root: true
}, },
http: { http: {
@ -32,7 +35,9 @@ module.exports = Self => {
}); });
Self.reserve = async(ctx, ticketId, sales, reserved) => { Self.reserve = async(ctx, ticketId, sales, reserved) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId); const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
if (!isTicketEditable) if (!isTicketEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
@ -42,12 +47,50 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
let changesMade = '';
const promises = []; const promises = [];
for (let sale of sales) { for (let sale of sales) {
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}); const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved});
promises.push(reservedSale); 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;
}; };
}; };

View File

@ -17,7 +17,14 @@ describe('sale deleteSales()', () => {
}); });
it('should throw an error if the ticket of the given sales is not editable', async() => { 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; let error;
const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}]; const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}];
@ -33,7 +40,13 @@ describe('sale deleteSales()', () => {
}); });
it('should delete the sale', async() => { 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 sales = [{id: newSale.id, instance: 0}];
const ticketId = 16; const ticketId = 16;

View File

@ -1,7 +1,14 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('sale reserve()', () => { describe('sale reserve()', () => {
const ctx = {req: {accessToken: {userId: 9}}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'localhost:5000'},
__: () => {}
}
};
afterAll(async done => { afterAll(async done => {
let ctx = {req: {accessToken: {userId: 9}}}; let ctx = {req: {accessToken: {userId: 9}}};
let params = { let params = {

View File

@ -17,7 +17,13 @@ describe('sale updatePrice()', () => {
}); });
it('should throw an error if the ticket is not editable', async() => { 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 immutableSaleId = 1;
let price = 5; let price = 5;
@ -31,7 +37,14 @@ describe('sale updatePrice()', () => {
}); });
it('should return 0 if the price is an empty string', async() => { 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 = ''; let price = '';
await app.models.Sale.updatePrice(ctx, saleId, 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() => { 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'; let price = '8';
await app.models.Sale.updatePrice(ctx, saleId, price); await app.models.Sale.updatePrice(ctx, saleId, price);

View File

@ -1,7 +1,13 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
describe('sale updateQuantity()', () => { 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() => { it('should throw an error if the quantity is not a number', async() => {
let error; let error;

View File

@ -19,7 +19,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: 'Number', type: 'number',
root: true root: true
}, },
http: { http: {
@ -29,29 +29,37 @@ module.exports = Self => {
}); });
Self.updatePrice = async(ctx, id, newPrice) => { Self.updatePrice = async(ctx, id, newPrice) => {
let models = Self.app.models; const $t = ctx.req.__; // $translate
let tx = await Self.beginTransaction({}); const models = Self.app.models;
const tx = await Self.beginTransaction({});
try { try {
let options = {transaction: tx}; const options = {transaction: tx};
let filter = { const filter = {
include: { include: {
relation: 'ticket', relation: 'ticket',
scope: { scope: {
include: { include: {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['salesPersonFk'] fields: ['salesPersonFk'],
include: {
relation: 'salesPersonUser',
scope: {
fields: ['id', 'name']
}
}
} }
}, },
fields: ['id', 'clientFk'] 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) if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`); throw new UserError(`The sales of this ticket can't be modified`);
@ -60,21 +68,19 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
const oldPrice = sale.price;
const userId = ctx.req.accessToken.userId; 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); const where = {
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 = {
componentFk: componentId, componentFk: componentId,
saleFk: id saleFk: id
}; };
let saleComponent = await models.SaleComponent.findOne({where}, options); const saleComponent = await models.SaleComponent.findOne({where}, options);
if (saleComponent) { if (saleComponent) {
await models.SaleComponent.updateAll(where, { await models.SaleComponent.updateAll(where, {
@ -92,6 +98,22 @@ module.exports = Self => {
query = `CALL vn.manaSpellersRequery(?)`; query = `CALL vn.manaSpellersRequery(?)`;
await Self.rawSql(query, [userId], options); 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(); await tx.commit();
return sale; return sale;

View File

@ -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 models = Self.app.models;
const canEditSale = await models.Sale.canEdit(ctx, [id]); const canEditSale = await models.Sale.canEdit(ctx, [id]);
@ -34,13 +35,51 @@ module.exports = Self => {
if (!canEditSale) if (!canEditSale)
throw new UserError(`Sale(s) blocked, please contact production`); throw new UserError(`Sale(s) blocked, please contact production`);
if (isNaN(quantity)) if (isNaN(newQuantity))
throw new UserError(`The value should be a number`); throw new UserError(`The value should be a number`);
let currentLine = await models.Sale.findOne({where: {id: id}}); const filter = {
if (quantity > currentLine.quantity) 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'); 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;
}; };
}; };

View File

@ -33,6 +33,7 @@ module.exports = Self => {
}); });
Self.addSale = async(ctx, id, itemId, quantity) => { Self.addSale = async(ctx, id, itemId, quantity) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const isEditable = await models.Ticket.isEditable(ctx, id); 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`); throw new UserError(`The sales of this ticket can't be modified`);
const item = await models.Item.findById(itemId); 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); 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; return sale;
}; };
}; };

View File

@ -230,7 +230,7 @@ module.exports = Self => {
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`, ticketUrl: `${origin}/#!/ticket/${args.id}/sale`,
changes: changesMade changes: changesMade
}); });
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message, myOptions); await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
} }
if (tx) await tx.commit(); if (tx) await tx.commit();

View File

@ -60,8 +60,12 @@ module.exports = Self => {
const [salesAvailable] = await Self.rawSql(query, [id], myOptions); const [salesAvailable] = await Self.rawSql(query, [id], myOptions);
const itemAvailable = new Map(); const itemAvailable = new Map();
for (let sale of salesAvailable) for (let sale of salesAvailable) {
itemAvailable.set(sale.itemFk, sale.available); itemAvailable.set(sale.itemFk, {
visible: sale.visible,
available: sale.available
});
}
// Get claimed sales // Get claimed sales
const saleIds = sales.map(sale => sale.id); const saleIds = sales.map(sale => sale.id);
@ -84,7 +88,9 @@ module.exports = Self => {
for (let sale of sales) { for (let sale of sales) {
const problems = saleProblems.get(sale.id); 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); sale.claim = claimedSales.get(sale.id);
if (problems) { if (problems) {
sale.isAvailable = problems.isAvailable; sale.isAvailable = problems.isAvailable;

View File

@ -12,7 +12,13 @@ describe('ticket addSale()', () => {
}); });
it('should create a new sale for the ticket with id 13', async() => { 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 itemId = 4;
const quantity = 10; const quantity = 10;
newSale = await app.models.Ticket.addSale(ctx, ticketId, itemId, quantity); 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() => { 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 itemId = 11;
const quantity = 10; 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() => { 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 notEditableTicketId = 1;
const itemId = 4; const itemId = 4;
const quantity = 10; const quantity = 10;

View File

@ -35,7 +35,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if no sales were selected', async() => { 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; let error;
const ticketId = 11; const ticketId = 11;
const sales = []; const sales = [];
@ -51,7 +57,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if no sales belong to different tickets', async() => { 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; let error;
const ticketId = 11; const ticketId = 11;
const sales = [1, 14]; const sales = [1, 14];
@ -67,7 +79,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should throw an error if the ticket is invoiced already', async() => { 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; let error;
const ticketId = 1; const ticketId = 1;
const sales = [1]; const sales = [1];
@ -83,7 +101,13 @@ describe('sale updateDiscount()', () => {
}); });
it('should update the discount if the salesPerson has mana', async() => { 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 ticketId = 11;
const sales = [originalSaleId]; const sales = [originalSaleId];
const newDiscount = 100; 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() => { 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 ticketId = 11;
const sales = [originalSaleId]; const sales = [originalSaleId];
const newDiscount = 100; const newDiscount = 100;

View File

@ -17,7 +17,8 @@ module.exports = Self => {
description: 'The sales id', description: 'The sales id',
type: ['number'], type: ['number'],
required: true, required: true,
}, { },
{
arg: 'newDiscount', arg: 'newDiscount',
description: 'The new discount', description: 'The new discount',
type: 'number', type: 'number',
@ -35,6 +36,7 @@ module.exports = Self => {
}); });
Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => { Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({}); 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) if (sales.length === 0)
throw new UserError('Please select at least one sale'); throw new UserError('Please select at least one sale');
@ -94,8 +96,11 @@ module.exports = Self => {
where: {code: componentCode}}, options); where: {code: componentCode}}, options);
const componentId = discountComponent.id; const componentId = discountComponent.id;
let promises = []; const promises = [];
let changesMade = '';
for (let sale of sales) { for (let sale of sales) {
const oldDiscount = sale.discount;
const value = ((-sale.price * newDiscount) / 100); const value = ((-sale.price * newDiscount) / 100);
const newComponent = models.SaleComponent.upsert({ const newComponent = models.SaleComponent.upsert({
saleFk: sale.id, saleFk: sale.id,
@ -105,6 +110,7 @@ module.exports = Self => {
const updatedSale = sale.updateAttribute('discount', newDiscount, options); const updatedSale = sale.updateAttribute('discount', newDiscount, options);
promises.push(newComponent, updatedSale); promises.push(newComponent, updatedSale);
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${oldDiscount}% ➔ *${newDiscount}%*`;
} }
await Promise.all(promises); await Promise.all(promises);
@ -112,6 +118,32 @@ module.exports = Self => {
const query = `call vn.manaSpellersRequery(?)`; const query = `call vn.manaSpellersRequery(?)`;
await Self.rawSql(query, [userId], options); 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(); await tx.commit();
} catch (error) { } catch (error) {
await tx.rollback(); await tx.rollback();

View File

@ -82,7 +82,10 @@ class Controller extends Section {
if (!this.ticket) return; if (!this.ticket) return;
this.$http.get(`Tickets/${this.ticket.id}/getVolume`) 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;
});
} }
} }

View File

@ -59,6 +59,7 @@
</vn-th> </vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th shrink>Visible</vn-th>
<vn-th shrink>Available</vn-th> <vn-th shrink>Available</vn-th>
<vn-th>Id</vn-th> <vn-th>Id</vn-th>
<vn-th shrink>Quantity</vn-th> <vn-th shrink>Quantity</vn-th>
@ -112,6 +113,13 @@
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td shrink>
<vn-chip
class="transparent"
ng-class="{'alert': sale.visible < 0}">
{{::sale.visible}}
</vn-chip>
</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-chip <vn-chip
class="transparent" class="transparent"

View File

@ -47,7 +47,13 @@ class Controller extends Section {
getMana() { getMana() {
this.$http.get(`Tickets/${this.$params.id}/getSalesPersonMana`) this.$http.get(`Tickets/${this.$params.id}/getSalesPersonMana`)
.then(res => this.edit.mana = res.data); .then(res => {
this.edit.mana = res.data;
this.$.$applyAsync(() => {
this.$.editDiscount.relocate();
this.$.editPricePopover.relocate();
});
});
} }
/** /**

View File

@ -42,6 +42,8 @@ describe('Ticket', () => {
$scope.sms = {open: () => {}}; $scope.sms = {open: () => {}};
$scope.ticket = ticket; $scope.ticket = ticket;
$scope.model = crudModel; $scope.model = crudModel;
$scope.editDiscount = {relocate: () => {}};
$scope.editPricePopover = {relocate: () => {}};
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
Object.defineProperties($state.params, { Object.defineProperties($state.params, {
id: { id: {

View File

@ -120,6 +120,7 @@
<vn-tr> <vn-tr>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th number shrink>Item</vn-th> <vn-th number shrink>Item</vn-th>
<vn-th number shrink>Visible</vn-th>
<vn-th number shrink>Available</vn-th> <vn-th number shrink>Available</vn-th>
<vn-th number shrink>Quantity</vn-th> <vn-th number shrink>Quantity</vn-th>
<vn-th>Description</vn-th> <vn-th>Description</vn-th>
@ -169,6 +170,13 @@
{{sale.itemFk | zeroFill:6}} {{sale.itemFk | zeroFill:6}}
</span> </span>
</vn-td> </vn-td>
<vn-td number shrink>
<vn-chip
class="transparent"
ng-class="{'alert': sale.visible < 0}">
{{::sale.visible}}
</vn-chip>
</vn-td>
<vn-td number shrink> <vn-td number shrink>
<vn-chip <vn-chip
class="transparent" class="transparent"

View File

@ -35,7 +35,7 @@
{ {
"accessType": "READ", "accessType": "READ",
"principalType": "ROLE", "principalType": "ROLE",
"principalId": "$everyone", "principalId": "employee",
"permission": "ALLOW" "permission": "ALLOW"
} }
] ]

View File

@ -106,6 +106,7 @@ class Controller extends Section {
}; };
this.$.model.applyFilter(filter, params).then(() => { this.$.model.applyFilter(filter, params).then(() => {
this.getWorkedHours(this.started, this.ended); this.getWorkedHours(this.started, this.ended);
this.getAbsences();
}); });
} }
@ -129,7 +130,7 @@ class Controller extends Section {
onData(data) { onData(data) {
const events = {}; const events = {};
let addEvent = (day, event) => { const addEvent = (day, event) => {
events[new Date(day).getTime()] = event; events[new Date(day).getTime()] = event;
}; };

View File

@ -132,6 +132,16 @@
width: 8em width: 8em
} }
.field.wide-rectangle {
white-space: nowrap
}
.field.wide-rectangle span {
display: inline-block;
height: 2em;
width: 100%;
}
.pull-left { .pull-left {
float: left !important float: left !important
} }

View File

@ -1,7 +1,7 @@
buttons: buttons:
webAcccess: Visit our website webAcccess: Visit our website
info: Help us to improve info: Help us to improve
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es · www.verdnatura.es · clientes@verdnatura.es
disclaimer: '- NOTICE - This message is private and confidential, and should be used disclaimer: '- NOTICE - This message is private and confidential, and should be used
exclusively by the person receiving it. If you have received this message by mistake, exclusively by the person receiving it. If you have received this message by mistake,

View File

@ -1,7 +1,7 @@
buttons: buttons:
webAcccess: Visitez notre site web webAcccess: Visitez notre site web
info: Aidez-nous à améliorer info: Aidez-nous à améliorer
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es · www.verdnatura.es · clientes@verdnatura.es
disclaimer: "- AVIS - Ce message est privé et confidentiel et doit être utilisé disclaimer: "- AVIS - Ce message est privé et confidentiel et doit être utilisé
exclusivement par le destinataire. Si vous avez reçu ce message par erreur, exclusivement par le destinataire. Si vous avez reçu ce message par erreur,

View File

@ -1,7 +1,7 @@
buttons: buttons:
webAcccess: Visite o nosso site webAcccess: Visite o nosso site
info: Ajude-nos a melhorar info: Ajude-nos a melhorar
fiscalAddress: VERDNATURA LEVANTE SL, B97367486 Avda. Espioca, 100, 46460 Silla fiscalAddress: VERDNATURA LEVANTE SL, B97367486 C/ Fenollar, 2. 46680 ALGEMESI
· www.verdnatura.es · clientes@verdnatura.es · www.verdnatura.es · clientes@verdnatura.es
disclaimer: '- AVISO - Esta mensagem é privada e confidencial e deve ser usada exclusivamente disclaimer: '- AVISO - Esta mensagem é privada e confidencial e deve ser usada exclusivamente
pela pessoa que a recebe. Se você recebeu esta mensagem por engano, notifique o remetente e pela pessoa que a recebe. Se você recebeu esta mensagem por engano, notifique o remetente e

View File

@ -89,8 +89,7 @@ const dbHelper = {
const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
return db.getSqlFromDef(absolutePath); return db.getSqlFromDef(absolutePath);
}, },
}, }
props: ['tplPath']
}; };
Vue.mixin(dbHelper); Vue.mixin(dbHelper);

View File

@ -27,8 +27,8 @@ module.exports = {
await db.rawSql(` await db.rawSql(`
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status) INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status)
VALUES (?, ?, 1, ?, ?, ?)`, [ VALUES (?, ?, 1, ?, ?, ?)`, [
options.replyTo,
options.to, options.to,
options.replyTo,
options.subject, options.subject,
options.text || options.html, options.text || options.html,
error && error.message || 'Sent' error && error.message || 'Sent'

View File

@ -172,7 +172,7 @@ module.exports = app => {
for (const ticket of tickets) { for (const ticket of tickets) {
try { 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; const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue; if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue;

View File

@ -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();

View File

@ -0,0 +1,6 @@
[
{
"filename": "client-debt-statement.pdf",
"component": "client-debt-statement"
}
]

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('description.instructions')}}</p>
</div>
</div>
<!-- Preview block -->
<div class="grid-row" v-if="isPreview">
<div class="grid-block vn-pa-ml">
<attachment v-for="attachment in attachments"
v-bind:key="attachment.filename"
v-bind:attachment="attachment"
v-bind:args="$props">
</attachment>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -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
}
}
};

View File

@ -0,0 +1,4 @@
subject: Extracto de tu balance
title: Extracto de tu balance
description:
instructions: Adjuntamos el extracto de tu balance.

View File

@ -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();

View File

@ -0,0 +1,6 @@
[
{
"filename": "credit-request.pdf",
"component": "credit-request"
}
]

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('description.instructions')}}</p>
</div>
</div>
<!-- Preview block -->
<div class="grid-row" v-if="isPreview">
<div class="grid-block vn-pa-ml">
<attachment v-for="attachment in attachments"
v-bind:key="attachment.filename"
v-bind:attachment="attachment"
v-bind:args="$props">
</attachment>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -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};
},
};

View File

@ -0,0 +1,4 @@
subject: Solicitud de crédito
title: Solicitud de crédito
description:
instructions: Adjuntamos el formulario para solicitud de crédito.

View File

@ -1,5 +1,5 @@
subject: Vossa nota de entrega subject: Sua nota de entrega
title: Vossa nota de entrega title: Sua nota de entrega
dear: Estimado cliente dear: Estimado cliente
description: Já está disponível sua nota de entrega correspondente a encomenda numero <strong>{0}</strong>. <br/> description: Já está disponível sua nota de entrega correspondente a encomenda numero <strong>{0}</strong>. <br/>
Para ver-lo faça um clique <a href="https://www.verdnatura.es/#!form=ecomerce/ticket&ticket={0}">neste link</a>. Para ver-lo faça um clique <a href="https://www.verdnatura.es/#!form=ecomerce/ticket&ticket={0}">neste link</a>.

View File

@ -1,5 +1,5 @@
subject: Vossa nota de entrega subject: Sua nota de entrega
title: Vossa nota de entrega title: Sua nota de entrega
dear: Estimado cliente dear: Estimado cliente
description: Já está disponível sua nota de entrega correspondente a encomenda {0}. <br/> description: Já está disponível sua nota de entrega correspondente a encomenda {0}. <br/>
Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail. Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail.

View File

@ -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();

View File

@ -0,0 +1,6 @@
[
{
"filename": "invoice.pdf",
"component": "invoice"
}
]

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description', [invoice.ref, invoice.ticketFk])"></p>
<p v-html="$t('poll')"></p>
<p v-html="$t('help')"></p>
<p v-html="$t('conclusion')"></p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -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
}
}
};

View File

@ -0,0 +1,9 @@
subject: Your invoice
title: Your invoice
dear: Dear client
description: The invoice <strong>{0}</strong> from the order <strong>{1}</strong> is now available. <br/>
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, <strong>we are here to assist you!</strong>
conclusion: Thanks for your attention!

View File

@ -0,0 +1,10 @@
subject: Tu factura
title: Tu factura
dear: Estimado cliente
description: Ya está disponible la factura <strong>{0}</strong> correspondiente al pedido <strong>{1}</strong>. <br/>
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, <strong>¡estamos para
atenderte!</strong>
conclusion: ¡Gracias por tu atención!

View File

@ -0,0 +1,9 @@
subject: Votre facture
title: Votre facture
dear: Cher client,
description: Le facture <strong>{0}</strong> correspondant à la commande <strong>{1}</strong> est maintenant disponible.<br/>
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, <strong>nous sommes là pour vous aider!</strong>
conclusion: Merci pour votre attention!

View File

@ -0,0 +1,9 @@
subject: Sua fatura
title: Sua fatura
dear: Estimado cliente
description: Já está disponível sua fatura <strong>{0}</strong> correspondente a encomenda {1}. <br/>
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, <strong>Estamos aqui para
atender-te!</strong>
conclusion: Obrigado por tua atenção!

View File

@ -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 = ?

View File

@ -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();

View File

@ -0,0 +1,3 @@
table.column-oriented {
margin-top: 50px !important
}

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"></report-header>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="columns">
<div class="size50">
<div class="size75">
<h1 class="title uppercase">{{$t('title')}}</h1>
<table class="row-oriented">
<tbody>
<tr>
<td class="font gray uppercase">{{$t('clientId')}}</td>
<th>{{client.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{dated}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('clientData')}}</div>
<div class="body">
<h3 class="uppercase">{{client.socialName}}</h3>
<div>
{{client.street}}
</div>
<div>
{{client.postcode}}, {{client.city}} ({{client.province}})
</div>
<div>
{{client.country}}
</div>
</div>
</div>
</div>
</div>
<table class="column-oriented">
<thead>
<tr>
<th>{{$t('date')}}</th>
<th>{{$t('concept')}}</th>
<th class="number">{{$t('invoiced')}}</th>
<th class="number">{{$t('payed')}}</th>
<th class="number">{{$t('balance')}}</th>
</tr>
</thead>
<tbody v-for="sale in sales" :key="sale.id">
<tr>
<td>{{sale.issued | date('%d-%m-%Y')}}</td>
<td>{{sale.ref}}</td>
<td class="number">{{sale.debtOut}}</td>
<td class="number">{{sale.debtIn}}</td>
<td class="number">{{getBalance(sale)}}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td></td>
<td></td>
<td class="number">
<strong class="pull-left">Total</strong>
{{getTotalDebtOut() | currency('EUR', $i18n.locale)}}
</td>
<td class="number">{{getTotalDebtIn() | currency('EUR', $i18n.locale)}}</td>
<td class="number">{{totalBalance | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:left-text="$t('client', [client.id])"
v-bind:center-text="client.socialName"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -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
}
}
};

View File

@ -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}

View File

@ -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}

View File

@ -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 = ?

View File

@ -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;

View File

@ -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();

View File

@ -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
}

View File

@ -0,0 +1,190 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"></report-header>
<!-- Block -->
<div class="grid-row info-panel">
<div class="grid-block">
<h1 class="title centered uppercase">{{$t('fields.title')}}</h1>
</div>
</div>
<!-- Block -->
<table>
<tbody>
<tr>
<td style="width: 70%; text-align: right; padding-right: 2em">{{$t('fields.date')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
</tbody>
</table>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="info-panel">
<div class="green-background">
<h3>{{$t('fields.companyInfo')}}</h3>
</div>
<div class="body">
<table style="width: 100%">
<tbody>
<tr>
<td>{{$t('fields.companyName')}}:</td>
<th colspan="3">
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr>
<td>{{$t('fields.businessType')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
<td>{{$t('fields.antiquity')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr>
<td>{{$t('fields.surface')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
<td>{{$t('fields.numberOfEmployees')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr>
<td>{{$t('fields.owner')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
<td>{{$t('fields.phone')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr>
<td>{{$t('fields.payer')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
<td>{{$t('fields.phone')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="info-panel" style="width: 98%">
<div class="green-background">
<h3>{{$t('fields.economicInfo')}}</h3>
</div>
<div class="body">
<table>
<tbody>
<tr class="row-oriented">
<td>{{$t('fields.previousSalesVolume')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr class="row-oriented">
<td>{{$t('fields.forecastedSalesVolume')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
<tr class="row-oriented">
<td>{{$t('fields.forecastedPurchases')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div>
<table style="border-top: 1px solid #DDD" class="info-panel">
<tbody>
<tr class="row-oriented" style="width: 100%">
<td>{{$t('fields.personFilling')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
<td>{{$t('fields.phone')}}:</td>
<th>
<div class="field wide-rectangle">
<span></span>
</div>
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Footer block -->
<report-footer
id="pageFooter"
v-bind:left-text="dated"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More