Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2879-import_buys
This commit is contained in:
commit
886d38ad8a
|
@ -6,6 +6,9 @@
|
|||
"table": "sage.TiposRetencion"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"showField": "withholding"
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "Number",
|
||||
|
|
|
@ -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);
|
|
@ -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 ;
|
|
@ -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 ;
|
||||
|
|
@ -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 ;
|
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO `vn`.`sample` (`code`, `description`, `isVisible`, `hasCompany`, `hasPreview`)
|
||||
VALUES ('credit-request', 'Solicitud de crédito', 1, 1, 1);
|
|
@ -2339,6 +2339,14 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
|
|||
(9, 9, 9),
|
||||
(10, 10, 10);
|
||||
|
||||
INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxCodeFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`, `created`)
|
||||
VALUES
|
||||
(1, 4, 99.99, '2000000000', null, null, null, CURDATE()),
|
||||
(2, 4, 999.99, '2000000000', null, null, null, CURDATE()),
|
||||
(3, 4, 1000.50, '2000000000', null, null, null, CURDATE()),
|
||||
(4, 4, 0.50, '2000000000', null, null, null, CURDATE()),
|
||||
(5, 4, 150.50, '2000000000', null, null, null, CURDATE());
|
||||
|
||||
INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
|
||||
SELECT `id`
|
||||
FROM `vn`.`ticket` t
|
||||
|
|
|
@ -258,7 +258,9 @@ export default {
|
|||
},
|
||||
clientLog: {
|
||||
lastModificationPreviousValue: 'vn-client-log vn-table vn-td.before',
|
||||
lastModificationCurrentValue: 'vn-client-log vn-table vn-td.after'
|
||||
lastModificationCurrentValue: 'vn-client-log vn-table vn-td.after',
|
||||
penultimateModificationPreviousValue: 'vn-client-log vn-table vn-tr:nth-child(2) vn-td.before',
|
||||
penultimateModificationCurrentValue: 'vn-client-log vn-table vn-tr:nth-child(2) vn-td.after'
|
||||
|
||||
},
|
||||
clientBalance: {
|
||||
|
@ -462,8 +464,8 @@ export default {
|
|||
itemDescriptorPopover: '.vn-popover.shown vn-item-descriptor',
|
||||
itemDescriptorPopoverItemDiaryButton: 'vn-item-descriptor a[href="#!/item/2/diary?warehouseFk=5&lineFk=16"]',
|
||||
popoverDiaryButton: '.vn-popover.shown vn-item-descriptor vn-icon[icon="icon-transaction"]',
|
||||
firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(4)',
|
||||
firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(7)',
|
||||
firstSaleQuantity: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(5)',
|
||||
firstSaleDiscount: 'vn-ticket-summary [name="sales"] vn-table > div > vn-tbody > vn-tr > vn-td:nth-child(8)',
|
||||
invoiceOutRef: 'vn-ticket-summary > vn-card > vn-horizontal > vn-one:nth-child(1) > vn-label-value:nth-child(7) > section > span',
|
||||
setOk: 'vn-ticket-summary vn-button[label="SET OK"] > button',
|
||||
descriptorTicketId: 'vn-ticket-descriptor > vn-descriptor-content > div > div.body > div.top > div'
|
||||
|
@ -566,18 +568,18 @@ export default {
|
|||
moreMenuUpdateDiscountInput: 'vn-input-number[ng-model="$ctrl.edit.discount"] input',
|
||||
transferQuantityInput: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable > span > text',
|
||||
transferQuantityCell: '.vn-popover.shown vn-table > div > vn-tbody > vn-tr > vn-td-editable',
|
||||
firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(5) > span',
|
||||
firstSaleId: 'vn-ticket-sale vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(6) > span',
|
||||
firstSaleClaimIcon: 'vn-ticket-sale vn-table vn-tbody > vn-tr:nth-child(1) vn-icon[icon="icon-claims"]',
|
||||
firstSaleDescriptorImage: '.vn-popover.shown vn-item-descriptor img',
|
||||
firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img',
|
||||
firstSaleZoomedImage: 'body > div > div > img',
|
||||
firstSaleQuantity: 'vn-ticket-sale [ng-model="sale.quantity"]',
|
||||
firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(6)',
|
||||
firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(8) > span',
|
||||
firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(7)',
|
||||
firstSalePrice: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span',
|
||||
firstSalePriceInput: '.vn-popover.shown input[ng-model="$ctrl.field"]',
|
||||
firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(9) > span',
|
||||
firstSaleDiscount: 'vn-ticket-sale vn-table vn-tr:nth-child(1) > vn-td:nth-child(10) > span',
|
||||
firstSaleDiscountInput: '.vn-popover.shown [ng-model="$ctrl.field"]',
|
||||
firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(10)',
|
||||
firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(11)',
|
||||
firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)',
|
||||
firstSaleColour: 'vn-ticket-sale vn-tr:nth-child(1) vn-fetched-tags section',
|
||||
firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[ng-model="sale.checked"]',
|
||||
|
@ -585,8 +587,8 @@ export default {
|
|||
secondSaleId: 'vn-ticket-sale:nth-child(2) vn-td-editable:nth-child(4) text > span',
|
||||
secondSaleIdAutocomplete: 'vn-ticket-sale vn-tr:nth-child(2) vn-autocomplete[ng-model="sale.itemFk"]',
|
||||
secondSaleQuantity: 'vn-ticket-sale vn-table vn-tr:nth-child(2) vn-input-number',
|
||||
secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(6)',
|
||||
secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(7)',
|
||||
secondSaleQuantityCell: 'vn-ticket-sale > div > vn-card > vn-table > div > vn-tbody > vn-tr:nth-child(2) > vn-td-editable:nth-child(7)',
|
||||
secondSaleConceptCell: 'vn-ticket-sale vn-tbody > :nth-child(2) > :nth-child(8)',
|
||||
secondSaleConceptInput: 'vn-ticket-sale vn-tbody > :nth-child(2) > vn-td-editable.ng-isolate-scope.selected vn-textfield',
|
||||
totalImport: 'vn-ticket-sale vn-one.taxes > p:nth-child(3) > strong',
|
||||
selectAllSalesCheckbox: 'vn-ticket-sale vn-thead vn-check',
|
||||
|
@ -936,7 +938,8 @@ export default {
|
|||
invoiceInDescriptor: {
|
||||
moreMenu: 'vn-invoice-in-descriptor vn-icon-button[icon=more_vert]',
|
||||
moreMenuDeleteInvoiceIn: '.vn-menu [name="deleteInvoice"]',
|
||||
acceptDeleteButton: '.vn-confirm.shown button[response="accept"]'
|
||||
moreMenuCloneInvoiceIn: '.vn-menu [name="cloneInvoice"]',
|
||||
acceptButton: '.vn-confirm.shown button[response="accept"]'
|
||||
},
|
||||
invoiceInBasicData: {
|
||||
issued: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.issued"]',
|
||||
|
|
|
@ -16,8 +16,15 @@ describe('Client Edit web access path', () => {
|
|||
await browser.close();
|
||||
});
|
||||
|
||||
it(`should uncheck the Enable web access checkbox and update the name`, async() => {
|
||||
it('should uncheck the Enable web access checkbox', async() => {
|
||||
await page.waitToClick(selectors.clientWebAccess.enableWebAccessCheckbox);
|
||||
await page.waitToClick(selectors.clientWebAccess.saveButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it(`should update the name`, async() => {
|
||||
await page.clearInput(selectors.clientWebAccess.userName);
|
||||
await page.write(selectors.clientWebAccess.userName, 'Hulk');
|
||||
await page.waitToClick(selectors.clientWebAccess.saveButton);
|
||||
|
@ -26,9 +33,8 @@ describe('Client Edit web access path', () => {
|
|||
expect(message.text).toContain('Data saved!');
|
||||
});
|
||||
|
||||
it('should confirm web access is now unchecked', async() => {
|
||||
await page.accessToSection('client.card.basicData');
|
||||
await page.accessToSection('client.card.webAccess');
|
||||
it('should reload the section and confirm web access is now unchecked', async() => {
|
||||
await page.reloadSection('client.card.webAccess');
|
||||
const result = await page.checkboxState(selectors.clientWebAccess.enableWebAccessCheckbox);
|
||||
|
||||
expect(result).toBe('unchecked');
|
||||
|
@ -44,13 +50,23 @@ describe('Client Edit web access path', () => {
|
|||
await page.accessToSection('client.card.log');
|
||||
});
|
||||
|
||||
it(`should confirm the log is showing the updated data for the client`, async() => {
|
||||
it(`should confirm the last log is showing the updated client name and no modifications on the active checkbox`, async() => {
|
||||
let lastModificationPreviousValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.lastModificationPreviousValue, 'innerText');
|
||||
let lastModificationCurrentValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.lastModificationCurrentValue, 'innerText');
|
||||
|
||||
expect(lastModificationPreviousValue).toEqual('name BruceBanner active true');
|
||||
expect(lastModificationPreviousValue).toEqual('name BruceBanner active false');
|
||||
expect(lastModificationCurrentValue).toEqual('name Hulk active false');
|
||||
});
|
||||
|
||||
it(`should confirm the penultimate log is showing the updated avtive field and no modifications on the client name`, async() => {
|
||||
let penultimateModificationPreviousValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.penultimateModificationPreviousValue, 'innerText');
|
||||
let penultimateModificationCurrentValue = await page
|
||||
.waitToGetProperty(selectors.clientLog.penultimateModificationCurrentValue, 'innerText');
|
||||
|
||||
expect(penultimateModificationPreviousValue).toEqual('name BruceBanner active true');
|
||||
expect(penultimateModificationCurrentValue).toEqual('name BruceBanner active false');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,16 +10,30 @@ describe('InvoiceIn descriptor path', () => {
|
|||
page = browser.page;
|
||||
await page.loginAndModule('administrative', 'invoiceIn');
|
||||
await page.accessToSearchResult('10');
|
||||
await page.accessToSection('invoiceIn.card.basicData');
|
||||
});
|
||||
|
||||
afterAll(async() => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
it('should delete the invoiceIn using the descriptor more menu', async() => {
|
||||
it('should clone the invoiceIn using the descriptor more menu', async() => {
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuCloneInvoiceIn);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.acceptButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('InvoiceIn cloned');
|
||||
});
|
||||
|
||||
it('should have been redirected to the created invoiceIn summary', async() => {
|
||||
await page.waitForState('invoiceIn.card.summary');
|
||||
});
|
||||
|
||||
it('should delete the cloned invoiceIn using the descriptor more menu', async() => {
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenu);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.moreMenuDeleteInvoiceIn);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.acceptDeleteButton);
|
||||
await page.waitToClick(selectors.invoiceInDescriptor.acceptButton);
|
||||
const message = await page.waitForSnackbar();
|
||||
|
||||
expect(message.text).toContain('InvoiceIn deleted');
|
||||
|
|
|
@ -57,7 +57,14 @@
|
|||
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one",
|
||||
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket",
|
||||
"Swift / BIC can't be empty": "Swift / BIC can't be empty",
|
||||
"Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})",
|
||||
"Deleted sales from ticket": "I have deleted the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}",
|
||||
"Added sale to ticket": "I have added the following line to the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}",
|
||||
"Changed sale discount": "I have changed the following lines discounts from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Created claim": "I have created the claim [{{claimId}}]({{{claimUrl}}}) for the following lines from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Changed sale price": "I have changed the price of [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) from {{oldPrice}}€ ➔ *{{newPrice}}€* of the ticket [{{ticketId}}]({{{ticketUrl}}})",
|
||||
"Changed sale quantity": "I have changed the quantity of [{{itemId}} {{concept}}]({{{itemUrl}}}) from {{oldQuantity}} ➔ *{{newQuantity}}* of the ticket [{{ticketId}}]({{{ticketUrl}}})",
|
||||
"Changed sale reserved state": "I have changed the following lines reserved state from the ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Bought units from buy request": "Bought {{quantity}} units of [{{itemId}} {{concept}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})",
|
||||
"MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*",
|
||||
"Changed client paymethod": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})",
|
||||
"Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})",
|
||||
|
@ -99,5 +106,8 @@
|
|||
"None": "None",
|
||||
"error densidad = 0": "error densidad = 0",
|
||||
"nickname": "nickname",
|
||||
"This document already exists on this ticket": "This document already exists on this ticket"
|
||||
"This document already exists on this ticket": "This document already exists on this ticket",
|
||||
"State": "State",
|
||||
"regular": "regular",
|
||||
"reserved": "reserved"
|
||||
}
|
|
@ -54,7 +54,7 @@
|
|||
"This address doesn't exist": "Este consignatario no existe",
|
||||
"You can't create an order for a inactive client": "You can't create an order for a inactive client",
|
||||
"You can't create an order for a client that doesn't has tax data verified": "You can't create an order for a client that doesn't has tax data verified",
|
||||
"You must delete the claim id %d first": "Antes debes borrar la reclamacion %d",
|
||||
"You must delete the claim id %d first": "Antes debes borrar la reclamación %d",
|
||||
"You don't have enough privileges": "No tienes suficientes permisos",
|
||||
"Cannot check Equalization Tax in this NIF/CIF": "No se puede marcar RE en este NIF/CIF",
|
||||
"You can't make changes on the basic data of an confirmed order or with rows": "No puedes cambiar los datos basicos de una orden con artículos",
|
||||
|
@ -122,7 +122,17 @@
|
|||
"Swift / BIC can't be empty": "Swift / BIC no puede estar vacío",
|
||||
"Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios",
|
||||
"Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios",
|
||||
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
|
||||
"Deleted sales from ticket": "He eliminado las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{deletions}}}",
|
||||
"Added sale to ticket": "He añadido la siguiente linea al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{addition}}}",
|
||||
"Changed sale discount": "He cambiado el descuento de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Created claim": "He creado la reclamación [{{claimId}}]({{{claimUrl}}}) de las siguientes lineas del ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Changed sale price": "He cambiado el precio de [{{itemId}} {{concept}}]({{{itemUrl}}}) ({{quantity}}) de {{oldPrice}}€ ➔ *{{newPrice}}€* del ticket [{{ticketId}}]({{{ticketUrl}}})",
|
||||
"Changed sale quantity": "He cambiado la cantidad de [{{itemId}} {{concept}}]({{{itemUrl}}}) de {{oldQuantity}} ➔ *{{newQuantity}}* del ticket [{{ticketId}}]({{{ticketUrl}}})",
|
||||
"State": "Estado",
|
||||
"regular": "normal",
|
||||
"reserved": "reservado",
|
||||
"Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
|
||||
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
|
||||
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
|
||||
"Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
|
||||
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
|
||||
|
|
|
@ -3,17 +3,20 @@ const UserError = require('vn-loopback/util/user-error');
|
|||
module.exports = Self => {
|
||||
Self.remoteMethodCtx('createFromSales', {
|
||||
description: 'Create a claim',
|
||||
accepts: [{
|
||||
arg: 'ticketId',
|
||||
type: 'Number',
|
||||
required: true,
|
||||
description: 'The origin ticket id'
|
||||
}, {
|
||||
arg: 'sales',
|
||||
type: ['Object'],
|
||||
required: true,
|
||||
description: 'The claimed sales'
|
||||
}],
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ticketId',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The origin ticket id'
|
||||
},
|
||||
{
|
||||
arg: 'sales',
|
||||
type: ['object'],
|
||||
required: true,
|
||||
description: 'The claimed sales'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'object',
|
||||
root: true
|
||||
|
@ -25,6 +28,7 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.createFromSales = async(ctx, ticketId, sales, options) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
let tx;
|
||||
const myOptions = {};
|
||||
|
@ -39,7 +43,20 @@ module.exports = Self => {
|
|||
const userId = ctx.req.accessToken.userId;
|
||||
|
||||
try {
|
||||
const ticket = await models.Ticket.findById(ticketId, null, myOptions);
|
||||
const ticket = await models.Ticket.findById(ticketId, {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, myOptions);
|
||||
|
||||
if (ticket.isDeleted)
|
||||
throw new UserError(`You can't create a claim for a removed ticket`);
|
||||
|
||||
|
@ -49,6 +66,8 @@ module.exports = Self => {
|
|||
ticketCreated: ticket.shipped,
|
||||
workerFk: userId
|
||||
}, myOptions);
|
||||
|
||||
let changesMade = '';
|
||||
const promises = [];
|
||||
|
||||
for (const sale of sales) {
|
||||
|
@ -59,10 +78,25 @@ module.exports = Self => {
|
|||
}, myOptions);
|
||||
|
||||
promises.push(newClaimBeginning);
|
||||
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const salesPerson = ticket.client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
|
||||
const message = $t('Created claim', {
|
||||
claimId: newClaim.id,
|
||||
ticketId: ticketId,
|
||||
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
|
||||
claimUrl: `${origin}/#!/claim/${newClaim.id}/summary`,
|
||||
changes: changesMade
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
if (tx) await tx.commit();
|
||||
|
||||
return newClaim;
|
||||
|
|
|
@ -7,7 +7,13 @@ describe('Claim createFromSales()', () => {
|
|||
instance: 0,
|
||||
quantity: 10
|
||||
}];
|
||||
const ctx = {req: {accessToken: {userId: 1}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 1},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
it('should create a new claim', async() => {
|
||||
const tx = await app.models.Claim.beginTransaction({});
|
||||
|
|
|
@ -9,23 +9,26 @@
|
|||
"properties": {
|
||||
"id": {
|
||||
"id": true,
|
||||
"type": "Number",
|
||||
"type": "number",
|
||||
"description": "Identifier"
|
||||
},
|
||||
"code": {
|
||||
"type": "String"
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "String"
|
||||
"type": "string"
|
||||
},
|
||||
"isVisible": {
|
||||
"type": "Boolean"
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasCompany": {
|
||||
"type": "Boolean"
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasPreview": {
|
||||
"type": "Boolean"
|
||||
"type": "boolean"
|
||||
},
|
||||
"datepickerEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"scopes": {
|
||||
|
|
|
@ -25,38 +25,48 @@
|
|||
</vn-crud-model>
|
||||
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
|
||||
<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-autocomplete
|
||||
vn-id="sampleType"
|
||||
ng-model="$ctrl.clientSample.typeFk"
|
||||
model="ClientSample.typeFk"
|
||||
fields="['code','hasCompany', 'hasPreview']"
|
||||
data="samplesVisible"
|
||||
show-field="description"
|
||||
label="Sample">
|
||||
label="Sample"
|
||||
required="true">
|
||||
</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
|
||||
ng-model="$ctrl.companyId"
|
||||
model="ClientSample.companyFk"
|
||||
data="companiesData"
|
||||
show-field="code"
|
||||
label="Company"
|
||||
ng-if="sampleType.selection.hasCompany">
|
||||
ng-if="sampleType.selection.hasCompany"
|
||||
required="true">
|
||||
</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-card>
|
||||
<vn-button-bar>
|
||||
|
|
|
@ -80,6 +80,12 @@ class Controller extends Section {
|
|||
if (sampleType.hasCompany)
|
||||
params.companyId = this.clientSample.companyFk;
|
||||
|
||||
if (sampleType.datepickerEnabled && !this.clientSample.from)
|
||||
return this.vnApp.showError(this.$t('Choose a date'));
|
||||
|
||||
if (sampleType.datepickerEnabled)
|
||||
params.from = this.clientSample.from;
|
||||
|
||||
let query = `email/${sampleType.code}`;
|
||||
if (isPreview)
|
||||
query = `email/${sampleType.code}/preview`;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
Choose a sample: Selecciona una plantilla
|
||||
Choose a company: Selecciona una empresa
|
||||
Choose a date: Selecciona una fecha
|
||||
Email cannot be blank: Debes introducir un email
|
||||
Recipient: Destinatario
|
||||
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla
|
||||
|
|
|
@ -293,6 +293,8 @@
|
|||
<vn-tr>
|
||||
<vn-th field="id" number>Id</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="shipped" shrink-date>Date</vn-th>
|
||||
<vn-th>State</vn-th>
|
||||
|
@ -313,6 +315,17 @@
|
|||
{{::ticket.nickname}}
|
||||
</span>
|
||||
</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>
|
||||
{{::ticket.packages}}
|
||||
</vn-td>
|
||||
|
@ -371,6 +384,9 @@
|
|||
<vn-client-descriptor-popover
|
||||
vn-id="clientDescriptor">
|
||||
</vn-client-descriptor-popover>
|
||||
<vn-route-descriptor-popover
|
||||
vn-id="routeDescriptor">
|
||||
</vn-route-descriptor-popover>
|
||||
<vn-worker-descriptor-popover
|
||||
vn-id="workerDescriptor">
|
||||
</vn-worker-descriptor-popover>
|
||||
|
|
|
@ -22,7 +22,13 @@ class Controller extends Summary {
|
|||
scope: {
|
||||
fields: ['id']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'agencyMode',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
<vn-card class="vn-pa-lg">
|
||||
<vn-horizontal>
|
||||
<vn-check
|
||||
vn-id="active"
|
||||
disabled="watcher.orgData.name != $ctrl.account.name"
|
||||
vn-one
|
||||
label="Enable web access"
|
||||
ng-model="$ctrl.account.active"
|
||||
|
@ -17,6 +19,8 @@
|
|||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-textfield
|
||||
vn-id="name"
|
||||
disabled="watcher.orgData.active != $ctrl.account.active"
|
||||
vn-focus
|
||||
vn-one
|
||||
label="User"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -2,6 +2,9 @@
|
|||
"InvoiceIn": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"InvoiceInTax": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"InvoiceInDueDay": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"foreignValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
module.exports = Self => {
|
||||
require('../methods/invoice-in/filter')(Self);
|
||||
require('../methods/invoice-in/summary')(Self);
|
||||
require('../methods/invoice-in/clone')(Self);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"properties": {
|
||||
"id": {
|
||||
"id": true,
|
||||
"type": "Number",
|
||||
"type": "number",
|
||||
"description": "Identifier"
|
||||
},
|
||||
"serialNumber": {
|
||||
|
@ -36,6 +36,9 @@
|
|||
"booked": {
|
||||
"type": "date"
|
||||
},
|
||||
"isVatDeductible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"operated": {
|
||||
"type": "date"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
translate>
|
||||
Delete Invoice
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="cloneConfirmation.show()"
|
||||
vn-acl="invoicing"
|
||||
name="cloneInvoice"
|
||||
translate>
|
||||
Clone Invoice
|
||||
</vn-item>
|
||||
</slot-menu>
|
||||
<slot-body>
|
||||
<div class="attributes">
|
||||
|
@ -42,8 +49,16 @@
|
|||
</div>
|
||||
</slot-body>
|
||||
</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?">
|
||||
</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>
|
||||
|
|
|
@ -30,6 +30,12 @@ class Controller extends Descriptor {
|
|||
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn deleted')));
|
||||
}
|
||||
|
||||
cloneInvoiceIn() {
|
||||
return this.$http.post(`InvoiceIns/${this.id}/clone`)
|
||||
.then(res => this.$state.go('invoiceIn.card.summary', {id: res.data.id}))
|
||||
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn cloned')));
|
||||
}
|
||||
|
||||
loadData() {
|
||||
const filter = {
|
||||
include: [
|
||||
|
|
|
@ -2,4 +2,5 @@ InvoiceIn: Facturas recibidas
|
|||
Search invoices in by reference: Buscar facturas recibidas por referencia
|
||||
Entries list: Listado de entradas
|
||||
Invoice list: Listado de facturas recibidas
|
||||
InvoiceIn deleted: Factura eliminada
|
||||
InvoiceIn deleted: Factura eliminada
|
||||
InvoiceIn cloned: Factura clonada
|
|
@ -21,7 +21,7 @@ class Controller extends ModuleCard {
|
|||
}, {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['id', 'socialName', 'name']
|
||||
fields: ['id', 'socialName', 'name', 'email']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
translate>
|
||||
Show invoice PDF
|
||||
</a>
|
||||
<vn-item
|
||||
ng-click="invoiceConfirmation.show()"
|
||||
name="sendInvoice"
|
||||
translate>
|
||||
Send invoice PDF
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="deleteConfirmation.show()"
|
||||
vn-acl="invoicing"
|
||||
|
@ -97,4 +103,12 @@
|
|||
on-accept="$ctrl.createInvoicePdf()"
|
||||
question="Are you sure you want to regenerate the invoice PDF document?"
|
||||
message="You are going to regenerate the invoice PDF document">
|
||||
</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>
|
|
@ -59,6 +59,14 @@ class Controller extends Descriptor {
|
|||
return this.getData(`InvoiceOuts/${this.id}`, {filter})
|
||||
.then(res => this.entity = res.data);
|
||||
}
|
||||
|
||||
sendInvoice() {
|
||||
return this.vnEmail.send('invoice', {
|
||||
recipientId: this.invoiceOut.client.id,
|
||||
recipient: this.invoiceOut.client.email,
|
||||
invoiceId: this.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceOutDescriptor', {
|
||||
|
|
|
@ -3,9 +3,12 @@ Volume: Volumen
|
|||
Client card: Ficha del cliente
|
||||
Invoice ticket list: Listado de tickets de la factura
|
||||
Show invoice PDF: Ver factura en PDF
|
||||
Send invoice PDF: Enviar factura en PDF
|
||||
Delete Invoice: Eliminar factura
|
||||
Clone Invoice: Clonar factura
|
||||
InvoiceOut deleted: Factura eliminada
|
||||
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
|
||||
Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura?
|
||||
Book invoice: Asentar factura
|
||||
InvoiceOut booked: Factura asentada
|
||||
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vn-crud-model
|
||||
vn-id="ticketsModel"
|
||||
limit="10"
|
||||
auto-load="true"
|
||||
data="tickets">
|
||||
</vn-crud-model>
|
||||
<vn-card class="summary">
|
||||
|
|
|
@ -7,7 +7,7 @@ class Controller extends Summary {
|
|||
this._invoiceOut = value;
|
||||
if (value && value.id) {
|
||||
this.getSummary();
|
||||
this.getTickets();
|
||||
this.$.ticketsModel.url = `InvoiceOuts/${this.invoiceOut.id}/getTickets`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,14 +19,6 @@ class Controller extends Summary {
|
|||
return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}/summary`)
|
||||
.then(res => this.summary = res.data);
|
||||
}
|
||||
|
||||
getTickets() {
|
||||
this.$.$applyAsync(() => {
|
||||
const query = `InvoiceOuts/${this.invoiceOut.id}/getTickets`;
|
||||
this.$.ticketsModel.url = query;
|
||||
this.$.ticketsModel.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceOutSummary', {
|
||||
|
|
|
@ -27,17 +27,5 @@ describe('InvoiceOut', () => {
|
|||
expect(controller.summary).toEqual('the data you are looking for');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTickets()', () => {
|
||||
it('should perform a and then call to the ticketModel refresh() method', () => {
|
||||
jest.spyOn(controller.$.ticketsModel, 'refresh');
|
||||
|
||||
controller.getTickets();
|
||||
$scope.$apply();
|
||||
|
||||
expect(controller.$.ticketsModel.url).toEqual('InvoiceOuts/1/getTickets');
|
||||
expect(controller.$.ticketsModel.refresh).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -71,7 +71,7 @@ module.exports = Self => {
|
|||
const models = Self.app.models;
|
||||
const originalTaxes = await models.ItemTaxCountry.find({
|
||||
where: {itemFk: originalId},
|
||||
fields: ['botanical', 'countryFk', 'taxClassFk']
|
||||
fields: ['countryFk', 'taxClassFk']
|
||||
}, options);
|
||||
|
||||
const promises = [];
|
||||
|
@ -99,7 +99,7 @@ module.exports = Self => {
|
|||
const models = Self.app.models;
|
||||
const botanical = await models.ItemBotanical.findOne({
|
||||
where: {itemFk: originalId},
|
||||
fields: ['botanical', 'genusFk', 'specieFk']
|
||||
fields: ['genusFk', 'specieFk']
|
||||
}, options);
|
||||
|
||||
if (botanical) {
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
"base": "Loggable",
|
||||
"log": {
|
||||
"model": "ItemLog",
|
||||
"relation": "item",
|
||||
"showField": "botanical"
|
||||
"relation": "item"
|
||||
},
|
||||
"options": {
|
||||
"mysql": {
|
||||
|
|
|
@ -6,18 +6,18 @@ module.exports = Self => {
|
|||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'sales',
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
required: true,
|
||||
description: 'The sales to remove'
|
||||
},
|
||||
{
|
||||
arg: 'ticketId',
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The ticket id'
|
||||
}],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -27,10 +27,25 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.deleteSales = async(ctx, sales, ticketId) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
|
||||
const canEditSales = await models.Sale.canEdit(ctx, sales);
|
||||
|
||||
const ticket = await models.Ticket.findById(ticketId, {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
|
||||
if (!isTicketEditable)
|
||||
throw new UserError(`The sales of this ticket can't be modified`);
|
||||
|
@ -39,11 +54,26 @@ module.exports = Self => {
|
|||
throw new UserError(`Sale(s) blocked, please contact production`);
|
||||
|
||||
const promises = [];
|
||||
let deletions = '';
|
||||
for (let sale of sales) {
|
||||
const deletedSale = models.Sale.destroyById(sale.id);
|
||||
deletions += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
|
||||
|
||||
promises.push(deletedSale);
|
||||
}
|
||||
|
||||
const salesPerson = ticket.client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
|
||||
const message = $t('Deleted sales from ticket', {
|
||||
ticketId: ticketId,
|
||||
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
|
||||
deletions: deletions
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -5,24 +5,27 @@ module.exports = Self => {
|
|||
Self.remoteMethodCtx('reserve', {
|
||||
description: 'Change the state of a ticket',
|
||||
accessType: 'WRITE',
|
||||
accepts: [{
|
||||
arg: 'ticketId',
|
||||
type: 'Number',
|
||||
required: true,
|
||||
description: 'The ticket id'
|
||||
}, {
|
||||
arg: 'sales',
|
||||
type: ['Object'],
|
||||
required: true,
|
||||
description: 'The sale to reserve'
|
||||
},
|
||||
{
|
||||
arg: 'reserved',
|
||||
type: 'Boolean',
|
||||
required: true
|
||||
}],
|
||||
accepts: [
|
||||
{
|
||||
arg: 'ticketId',
|
||||
type: 'number',
|
||||
required: true,
|
||||
description: 'The ticket id'
|
||||
},
|
||||
{
|
||||
arg: 'sales',
|
||||
type: ['object'],
|
||||
required: true,
|
||||
description: 'The sale to reserve'
|
||||
},
|
||||
{
|
||||
arg: 'reserved',
|
||||
type: 'boolean',
|
||||
required: true
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -32,7 +35,9 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.reserve = async(ctx, ticketId, sales, reserved) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
|
||||
const isTicketEditable = await models.Ticket.isEditable(ctx, ticketId);
|
||||
if (!isTicketEditable)
|
||||
throw new UserError(`The sales of this ticket can't be modified`);
|
||||
|
@ -42,12 +47,50 @@ module.exports = Self => {
|
|||
if (!canEditSale)
|
||||
throw new UserError(`Sale(s) blocked, please contact production`);
|
||||
|
||||
let changesMade = '';
|
||||
const promises = [];
|
||||
|
||||
for (let sale of sales) {
|
||||
const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved});
|
||||
promises.push(reservedSale);
|
||||
if (sale.reserved != reserved) {
|
||||
const oldState = sale.reserved ? 'reserved' : 'regular';
|
||||
const newState = reserved ? 'reserved' : 'regular';
|
||||
|
||||
const reservedSale = models.Sale.update({id: sale.id}, {reserved: reserved});
|
||||
|
||||
promises.push(reservedSale);
|
||||
|
||||
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${$t('State')}: ${$t(oldState)} ➔ *${$t(newState)}*`;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
const result = await Promise.all(promises);
|
||||
|
||||
const ticket = await models.Ticket.findById(ticketId, {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const salesPerson = ticket.client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
|
||||
const message = $t('Changed sale reserved state', {
|
||||
ticketId: ticketId,
|
||||
ticketUrl: `${origin}/#!/ticket/${ticketId}/sale`,
|
||||
changes: changesMade
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,7 +17,14 @@ describe('sale deleteSales()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the ticket of the given sales is not editable', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
let ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
let error;
|
||||
|
||||
const sales = [{id: 1, instance: 0}, {id: 2, instance: 1}];
|
||||
|
@ -33,7 +40,13 @@ describe('sale deleteSales()', () => {
|
|||
});
|
||||
|
||||
it('should delete the sale', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
let ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
const sales = [{id: newSale.id, instance: 0}];
|
||||
const ticketId = 16;
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('sale reserve()', () => {
|
||||
const ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
afterAll(async done => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
let params = {
|
||||
|
|
|
@ -17,7 +17,13 @@ describe('sale updatePrice()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the ticket is not editable', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 18}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 18},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
let immutableSaleId = 1;
|
||||
let price = 5;
|
||||
|
||||
|
@ -31,7 +37,14 @@ describe('sale updatePrice()', () => {
|
|||
});
|
||||
|
||||
it('should return 0 if the price is an empty string', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 18}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 18},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
let price = '';
|
||||
|
||||
await app.models.Sale.updatePrice(ctx, saleId, price);
|
||||
|
@ -46,7 +59,14 @@ describe('sale updatePrice()', () => {
|
|||
});
|
||||
|
||||
it('should now set price as a number in a string', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 18}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 18},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
let price = '8';
|
||||
|
||||
await app.models.Sale.updatePrice(ctx, saleId, price);
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
const app = require('vn-loopback/server/server');
|
||||
|
||||
describe('sale updateQuantity()', () => {
|
||||
const ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
|
||||
it('should throw an error if the quantity is not a number', async() => {
|
||||
let error;
|
||||
|
|
|
@ -19,7 +19,7 @@ module.exports = Self => {
|
|||
}
|
||||
],
|
||||
returns: {
|
||||
type: 'Number',
|
||||
type: 'number',
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -29,29 +29,37 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.updatePrice = async(ctx, id, newPrice) => {
|
||||
let models = Self.app.models;
|
||||
let tx = await Self.beginTransaction({});
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
const tx = await Self.beginTransaction({});
|
||||
|
||||
try {
|
||||
let options = {transaction: tx};
|
||||
const options = {transaction: tx};
|
||||
|
||||
let filter = {
|
||||
const filter = {
|
||||
include: {
|
||||
relation: 'ticket',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['salesPersonFk']
|
||||
fields: ['salesPersonFk'],
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
fields: ['id', 'clientFk']
|
||||
}
|
||||
}
|
||||
};
|
||||
let sale = await models.Sale.findById(id, filter, options);
|
||||
|
||||
let isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk);
|
||||
const sale = await models.Sale.findById(id, filter, options);
|
||||
|
||||
const isEditable = await models.Ticket.isEditable(ctx, sale.ticketFk);
|
||||
if (!isEditable)
|
||||
throw new UserError(`The sales of this ticket can't be modified`);
|
||||
|
||||
|
@ -60,21 +68,19 @@ module.exports = Self => {
|
|||
if (!canEditSale)
|
||||
throw new UserError(`Sale(s) blocked, please contact production`);
|
||||
|
||||
const oldPrice = sale.price;
|
||||
const userId = ctx.req.accessToken.userId;
|
||||
const usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options);
|
||||
const componentCode = usesMana ? 'mana' : 'buyerDiscount';
|
||||
const discount = await models.Component.findOne({where: {code: componentCode}}, options);
|
||||
const componentId = discount.id;
|
||||
const componentValue = newPrice - sale.price;
|
||||
|
||||
let usesMana = await models.WorkerMana.findOne({where: {workerFk: userId}, fields: 'amount'}, options);
|
||||
|
||||
let componentCode = usesMana ? 'mana' : 'buyerDiscount';
|
||||
|
||||
let discount = await models.Component.findOne({where: {code: componentCode}}, options);
|
||||
let componentId = discount.id;
|
||||
let componentValue = newPrice - sale.price;
|
||||
|
||||
let where = {
|
||||
const where = {
|
||||
componentFk: componentId,
|
||||
saleFk: id
|
||||
};
|
||||
let saleComponent = await models.SaleComponent.findOne({where}, options);
|
||||
const saleComponent = await models.SaleComponent.findOne({where}, options);
|
||||
|
||||
if (saleComponent) {
|
||||
await models.SaleComponent.updateAll(where, {
|
||||
|
@ -92,6 +98,22 @@ module.exports = Self => {
|
|||
query = `CALL vn.manaSpellersRequery(?)`;
|
||||
await Self.rawSql(query, [userId], options);
|
||||
|
||||
const salesPerson = sale.ticket().client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
const message = $t('Changed sale price', {
|
||||
ticketId: sale.ticket().id,
|
||||
itemId: sale.itemFk,
|
||||
concept: sale.concept,
|
||||
quantity: sale.quantity,
|
||||
oldPrice: oldPrice,
|
||||
newPrice: newPrice,
|
||||
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
|
||||
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
await tx.commit();
|
||||
|
||||
return sale;
|
||||
|
|
|
@ -26,7 +26,8 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.updateQuantity = async(ctx, id, quantity) => {
|
||||
Self.updateQuantity = async(ctx, id, newQuantity) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
|
||||
const canEditSale = await models.Sale.canEdit(ctx, [id]);
|
||||
|
@ -34,13 +35,51 @@ module.exports = Self => {
|
|||
if (!canEditSale)
|
||||
throw new UserError(`Sale(s) blocked, please contact production`);
|
||||
|
||||
if (isNaN(quantity))
|
||||
if (isNaN(newQuantity))
|
||||
throw new UserError(`The value should be a number`);
|
||||
|
||||
let currentLine = await models.Sale.findOne({where: {id: id}});
|
||||
if (quantity > currentLine.quantity)
|
||||
const filter = {
|
||||
include: {
|
||||
relation: 'ticket',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sale = await models.Sale.findById(id, filter);
|
||||
|
||||
if (newQuantity > sale.quantity)
|
||||
throw new UserError('The new quantity should be smaller than the old one');
|
||||
|
||||
return await currentLine.updateAttributes({quantity: quantity});
|
||||
const oldQuantity = sale.quantity;
|
||||
const result = await sale.updateAttributes({quantity: newQuantity});
|
||||
|
||||
const salesPerson = sale.ticket().client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
const message = $t('Changed sale quantity', {
|
||||
ticketId: sale.ticket().id,
|
||||
itemId: sale.itemFk,
|
||||
concept: sale.concept,
|
||||
oldQuantity: oldQuantity,
|
||||
newQuantity: newQuantity,
|
||||
ticketUrl: `${origin}/#!/ticket/${sale.ticket().id}/sale`,
|
||||
itemUrl: `${origin}/#!/item/${sale.itemFk}/summary`
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -33,6 +33,7 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.addSale = async(ctx, id, itemId, quantity) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
|
||||
const isEditable = await models.Ticket.isEditable(ctx, id);
|
||||
|
@ -40,7 +41,19 @@ module.exports = Self => {
|
|||
throw new UserError(`The sales of this ticket can't be modified`);
|
||||
|
||||
const item = await models.Item.findById(itemId);
|
||||
const ticket = await models.Ticket.findById(id);
|
||||
const ticket = await models.Ticket.findById(id, {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const res = await models.Item.getVisibleAvailable(itemId, ticket.warehouseFk, ticket.shipped);
|
||||
|
||||
|
@ -63,6 +76,20 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
const addition = `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity})`;
|
||||
|
||||
const salesPerson = ticket.client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
|
||||
const message = $t('Added sale to ticket', {
|
||||
ticketId: id,
|
||||
ticketUrl: `${origin}/#!/ticket/${id}/sale`,
|
||||
addition: addition
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
return sale;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -230,7 +230,7 @@ module.exports = Self => {
|
|||
ticketUrl: `${origin}/#!/ticket/${args.id}/sale`,
|
||||
changes: changesMade
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message, myOptions);
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPersonId, message);
|
||||
}
|
||||
|
||||
if (tx) await tx.commit();
|
||||
|
|
|
@ -60,8 +60,12 @@ module.exports = Self => {
|
|||
const [salesAvailable] = await Self.rawSql(query, [id], myOptions);
|
||||
|
||||
const itemAvailable = new Map();
|
||||
for (let sale of salesAvailable)
|
||||
itemAvailable.set(sale.itemFk, sale.available);
|
||||
for (let sale of salesAvailable) {
|
||||
itemAvailable.set(sale.itemFk, {
|
||||
visible: sale.visible,
|
||||
available: sale.available
|
||||
});
|
||||
}
|
||||
|
||||
// Get claimed sales
|
||||
const saleIds = sales.map(sale => sale.id);
|
||||
|
@ -84,7 +88,9 @@ module.exports = Self => {
|
|||
|
||||
for (let sale of sales) {
|
||||
const problems = saleProblems.get(sale.id);
|
||||
sale.available = itemAvailable.get(sale.itemFk);
|
||||
const itemStock = itemAvailable.get(sale.itemFk);
|
||||
sale.available = itemStock.available;
|
||||
sale.visible = itemStock.visible;
|
||||
sale.claim = claimedSales.get(sale.id);
|
||||
if (problems) {
|
||||
sale.isAvailable = problems.isAvailable;
|
||||
|
|
|
@ -12,7 +12,13 @@ describe('ticket addSale()', () => {
|
|||
});
|
||||
|
||||
it('should create a new sale for the ticket with id 13', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const itemId = 4;
|
||||
const quantity = 10;
|
||||
newSale = await app.models.Ticket.addSale(ctx, ticketId, itemId, quantity);
|
||||
|
@ -21,7 +27,13 @@ describe('ticket addSale()', () => {
|
|||
});
|
||||
|
||||
it('should not be able to add a sale if the item quantity is not available', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const itemId = 11;
|
||||
const quantity = 10;
|
||||
|
||||
|
@ -36,7 +48,13 @@ describe('ticket addSale()', () => {
|
|||
});
|
||||
|
||||
it('should not be able to add a sale if the ticket is not editable', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const notEditableTicketId = 1;
|
||||
const itemId = 4;
|
||||
const quantity = 10;
|
||||
|
|
|
@ -35,7 +35,13 @@ describe('sale updateDiscount()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if no sales were selected', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
let error;
|
||||
const ticketId = 11;
|
||||
const sales = [];
|
||||
|
@ -51,7 +57,13 @@ describe('sale updateDiscount()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if no sales belong to different tickets', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
let error;
|
||||
const ticketId = 11;
|
||||
const sales = [1, 14];
|
||||
|
@ -67,7 +79,13 @@ describe('sale updateDiscount()', () => {
|
|||
});
|
||||
|
||||
it('should throw an error if the ticket is invoiced already', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
let error;
|
||||
const ticketId = 1;
|
||||
const sales = [1];
|
||||
|
@ -83,7 +101,13 @@ describe('sale updateDiscount()', () => {
|
|||
});
|
||||
|
||||
it('should update the discount if the salesPerson has mana', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 18}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 18},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const ticketId = 11;
|
||||
const sales = [originalSaleId];
|
||||
const newDiscount = 100;
|
||||
|
@ -105,7 +129,13 @@ describe('sale updateDiscount()', () => {
|
|||
});
|
||||
|
||||
it('should update the discount and add company discount component if the worker does not have mana', async() => {
|
||||
let ctx = {req: {accessToken: {userId: 9}}};
|
||||
const ctx = {
|
||||
req: {
|
||||
accessToken: {userId: 9},
|
||||
headers: {origin: 'localhost:5000'},
|
||||
__: () => {}
|
||||
}
|
||||
};
|
||||
const ticketId = 11;
|
||||
const sales = [originalSaleId];
|
||||
const newDiscount = 100;
|
||||
|
|
|
@ -17,7 +17,8 @@ module.exports = Self => {
|
|||
description: 'The sales id',
|
||||
type: ['number'],
|
||||
required: true,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
arg: 'newDiscount',
|
||||
description: 'The new discount',
|
||||
type: 'number',
|
||||
|
@ -35,6 +36,7 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.updateDiscount = async(ctx, id, salesIds, newDiscount) => {
|
||||
const $t = ctx.req.__; // $translate
|
||||
const models = Self.app.models;
|
||||
const tx = await Self.beginTransaction({});
|
||||
|
||||
|
@ -59,7 +61,7 @@ module.exports = Self => {
|
|||
}
|
||||
};
|
||||
|
||||
let sales = await models.Sale.find(filter, options);
|
||||
const sales = await models.Sale.find(filter, options);
|
||||
|
||||
if (sales.length === 0)
|
||||
throw new UserError('Please select at least one sale');
|
||||
|
@ -94,8 +96,11 @@ module.exports = Self => {
|
|||
where: {code: componentCode}}, options);
|
||||
|
||||
const componentId = discountComponent.id;
|
||||
let promises = [];
|
||||
const promises = [];
|
||||
let changesMade = '';
|
||||
|
||||
for (let sale of sales) {
|
||||
const oldDiscount = sale.discount;
|
||||
const value = ((-sale.price * newDiscount) / 100);
|
||||
const newComponent = models.SaleComponent.upsert({
|
||||
saleFk: sale.id,
|
||||
|
@ -105,6 +110,7 @@ module.exports = Self => {
|
|||
const updatedSale = sale.updateAttribute('discount', newDiscount, options);
|
||||
|
||||
promises.push(newComponent, updatedSale);
|
||||
changesMade += `\r\n-${sale.itemFk}: ${sale.concept} (${sale.quantity}) ${oldDiscount}% ➔ *${newDiscount}%*`;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
@ -112,6 +118,32 @@ module.exports = Self => {
|
|||
const query = `call vn.manaSpellersRequery(?)`;
|
||||
await Self.rawSql(query, [userId], options);
|
||||
|
||||
const ticket = await models.Ticket.findById(id, {
|
||||
include: {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
include: {
|
||||
relation: 'salesPersonUser',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, options);
|
||||
|
||||
const salesPerson = ticket.client().salesPersonUser();
|
||||
if (salesPerson) {
|
||||
const origin = ctx.req.headers.origin;
|
||||
|
||||
const message = $t('Changed sale discount', {
|
||||
ticketId: id,
|
||||
ticketUrl: `${origin}/#!/ticket/${id}/sale`,
|
||||
changes: changesMade
|
||||
});
|
||||
await models.Chat.sendCheckingPresence(ctx, salesPerson.id, message);
|
||||
}
|
||||
|
||||
await tx.commit();
|
||||
} catch (error) {
|
||||
await tx.rollback();
|
||||
|
|
|
@ -82,7 +82,10 @@ class Controller extends Section {
|
|||
if (!this.ticket) return;
|
||||
|
||||
this.$http.get(`Tickets/${this.ticket.id}/getVolume`)
|
||||
.then(res => this.ticketVolume = res.data[0].volume);
|
||||
.then(res => {
|
||||
if (res.data.length)
|
||||
this.ticketVolume = res.data[0].volume;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
</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>Id</vn-th>
|
||||
<vn-th shrink>Quantity</vn-th>
|
||||
|
@ -112,6 +113,13 @@
|
|||
zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
|
||||
on-error-src/>
|
||||
</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-chip
|
||||
class="transparent"
|
||||
|
|
|
@ -47,7 +47,13 @@ class Controller extends Section {
|
|||
|
||||
getMana() {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -42,6 +42,8 @@ describe('Ticket', () => {
|
|||
$scope.sms = {open: () => {}};
|
||||
$scope.ticket = ticket;
|
||||
$scope.model = crudModel;
|
||||
$scope.editDiscount = {relocate: () => {}};
|
||||
$scope.editPricePopover = {relocate: () => {}};
|
||||
$httpBackend = _$httpBackend_;
|
||||
Object.defineProperties($state.params, {
|
||||
id: {
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
<vn-tr>
|
||||
<vn-th shrink></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>Quantity</vn-th>
|
||||
<vn-th>Description</vn-th>
|
||||
|
@ -169,6 +170,13 @@
|
|||
{{sale.itemFk | zeroFill:6}}
|
||||
</span>
|
||||
</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-chip
|
||||
class="transparent"
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"principalId": "employee",
|
||||
"permission": "ALLOW"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -106,6 +106,7 @@ class Controller extends Section {
|
|||
};
|
||||
this.$.model.applyFilter(filter, params).then(() => {
|
||||
this.getWorkedHours(this.started, this.ended);
|
||||
this.getAbsences();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,7 +130,7 @@ class Controller extends Section {
|
|||
onData(data) {
|
||||
const events = {};
|
||||
|
||||
let addEvent = (day, event) => {
|
||||
const addEvent = (day, event) => {
|
||||
events[new Date(day).getTime()] = event;
|
||||
};
|
||||
|
||||
|
|
|
@ -132,6 +132,16 @@
|
|||
width: 8em
|
||||
}
|
||||
|
||||
.field.wide-rectangle {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.field.wide-rectangle span {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
float: left !important
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
buttons:
|
||||
webAcccess: Visit our website
|
||||
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
|
||||
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,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
buttons:
|
||||
webAcccess: Visitez notre site web
|
||||
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
|
||||
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,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
buttons:
|
||||
webAcccess: Visite o nosso site
|
||||
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
|
||||
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
|
||||
|
|
|
@ -89,8 +89,7 @@ const dbHelper = {
|
|||
const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
|
||||
return db.getSqlFromDef(absolutePath);
|
||||
},
|
||||
},
|
||||
props: ['tplPath']
|
||||
}
|
||||
};
|
||||
|
||||
Vue.mixin(dbHelper);
|
||||
|
|
|
@ -27,8 +27,8 @@ module.exports = {
|
|||
await db.rawSql(`
|
||||
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status)
|
||||
VALUES (?, ?, 1, ?, ?, ?)`, [
|
||||
options.replyTo,
|
||||
options.to,
|
||||
options.replyTo,
|
||||
options.subject,
|
||||
options.text || options.html,
|
||||
error && error.message || 'Sent'
|
||||
|
|
|
@ -172,7 +172,7 @@ module.exports = app => {
|
|||
|
||||
for (const ticket of tickets) {
|
||||
try {
|
||||
await db.rawSql(`CALL vn.ticket_close(?)`, [ticket.id]);
|
||||
await db.rawSql(`CALL vn.ticket_closeByTicket(?)`, [ticket.id]);
|
||||
|
||||
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
|
||||
if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue;
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"filename": "client-debt-statement.pdf",
|
||||
"component": "client-debt-statement"
|
||||
}
|
||||
]
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
subject: Extracto de tu balance
|
||||
title: Extracto de tu balance
|
||||
description:
|
||||
instructions: Adjuntamos el extracto de tu balance.
|
|
@ -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();
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"filename": "credit-request.pdf",
|
||||
"component": "credit-request"
|
||||
}
|
||||
]
|
|
@ -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>
|
|
@ -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};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
subject: Solicitud de crédito
|
||||
title: Solicitud de crédito
|
||||
description:
|
||||
instructions: Adjuntamos el formulario para solicitud de crédito.
|
|
@ -1,5 +1,5 @@
|
|||
subject: Vossa nota de entrega
|
||||
title: Vossa nota de entrega
|
||||
subject: Sua nota de entrega
|
||||
title: Sua nota de entrega
|
||||
dear: Estimado cliente
|
||||
description: Já está disponível sua nota de entrega correspondente a encomenda numero <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>.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
subject: Vossa nota de entrega
|
||||
title: Vossa nota de entrega
|
||||
subject: Sua nota de entrega
|
||||
title: Sua nota de entrega
|
||||
dear: Estimado cliente
|
||||
description: Já está disponível sua nota de entrega correspondente a encomenda {0}. <br/>
|
||||
Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail.
|
||||
|
|
|
@ -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();
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"filename": "invoice.pdf",
|
||||
"component": "invoice"
|
||||
}
|
||||
]
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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!
|
|
@ -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!
|
|
@ -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!
|
|
@ -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!
|
|
@ -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 = ?
|
|
@ -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();
|
|
@ -0,0 +1,3 @@
|
|||
table.column-oriented {
|
||||
margin-top: 50px !important
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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}
|
|
@ -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}
|
|
@ -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 = ?
|
|
@ -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;
|
|
@ -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();
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue