Merge branch 'dev' into 2940-invoiceInTax-section
gitea/salix/pipeline/head This commit looks good
Details
gitea/salix/pipeline/head This commit looks good
Details
This commit is contained in:
commit
d49242751e
|
@ -6,6 +6,9 @@
|
|||
"table": "sage.TiposRetencion"
|
||||
}
|
||||
},
|
||||
"log": {
|
||||
"showField": "withholding"
|
||||
},
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "Number",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
create table `vn`.`itemMatchProperties`
|
||||
(
|
||||
itemFk int not null,
|
||||
name varchar(80) not null,
|
||||
producer varchar(80) not null,
|
||||
size int not null,
|
||||
constraint itemMatchProperties_pk
|
||||
primary key (itemFk, name, producer, size),
|
||||
constraint itemFk___fk
|
||||
foreign key (itemFk) references item (id)
|
||||
on update cascade on delete cascade
|
||||
)
|
||||
comment 'Propiedades para encontrar articulos equivalentes en verdnatura';
|
||||
|
|
@ -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);
|
|
@ -2349,6 +2349,14 @@ INSERT INTO `vn`.`duaInvoiceIn`(`id`, `duaFk`, `invoiceInFk`)
|
|||
(9, 9, 9),
|
||||
(10, 10, 10);
|
||||
|
||||
INSERT INTO `vn`.`invoiceInTax` (`invoiceInFk`, `taxCodeFk`, `taxableBase`, `expenceFk`, `foreignValue`, `taxTypeSageFk`, `transactionTypeSageFk`, `created`)
|
||||
VALUES
|
||||
(1, 4, 99.99, '2000000000', null, null, null, CURDATE()),
|
||||
(2, 4, 999.99, '2000000000', null, null, null, CURDATE()),
|
||||
(3, 4, 1000.50, '2000000000', null, null, null, CURDATE()),
|
||||
(4, 4, 0.50, '2000000000', null, null, null, CURDATE()),
|
||||
(5, 4, 150.50, '2000000000', null, null, null, CURDATE());
|
||||
|
||||
INSERT INTO `vn`.`ticketRecalc`(`ticketFk`)
|
||||
SELECT `id`
|
||||
FROM `vn`.`ticket` t
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -29,9 +29,6 @@ describe('Entry import, create and edit buys path', () => {
|
|||
});
|
||||
|
||||
it('should fill the form, import the designated JSON file and select items for each import and confirm import', async() => {
|
||||
await page.write(selectors.entryBuys.ref, 'a reference');
|
||||
await page.write(selectors.entryBuys.observation, 'an observation');
|
||||
|
||||
let currentDir = process.cwd();
|
||||
let filePath = `${currentDir}/e2e/assets/07_import_buys.json`;
|
||||
|
||||
|
@ -41,6 +38,9 @@ describe('Entry import, create and edit buys path', () => {
|
|||
]);
|
||||
await fileChooser.accept([filePath]);
|
||||
|
||||
await page.waitForTextInField(selectors.entryBuys.ref, '200573095, 200573106, 200573117, 200573506');
|
||||
await page.waitForTextInField(selectors.entryBuys.observation, '729-6340 2846');
|
||||
|
||||
await page.autocompleteSearch(selectors.entryBuys.firstImportedItem, 'Ranged Reinforced weapon pistol 9mm');
|
||||
await page.autocompleteSearch(selectors.entryBuys.secondImportedItem, 'Melee Reinforced weapon heavy shield 1x0.5m');
|
||||
await page.autocompleteSearch(selectors.entryBuys.thirdImportedItem, 'Container medical box 1m');
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -11,11 +11,6 @@ module.exports = Self => {
|
|||
description: 'The entry id',
|
||||
http: {source: 'path'}
|
||||
},
|
||||
{
|
||||
arg: 'options',
|
||||
type: 'object',
|
||||
description: 'Callback options',
|
||||
},
|
||||
{
|
||||
arg: 'ref',
|
||||
type: 'string',
|
||||
|
@ -28,11 +23,11 @@ module.exports = Self => {
|
|||
},
|
||||
{
|
||||
arg: 'buys',
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
description: 'The buys',
|
||||
}],
|
||||
returns: {
|
||||
type: ['Object'],
|
||||
type: ['object'],
|
||||
root: true
|
||||
},
|
||||
http: {
|
||||
|
@ -41,23 +36,27 @@ module.exports = Self => {
|
|||
}
|
||||
});
|
||||
|
||||
Self.importBuys = async(ctx, id, options = {}) => {
|
||||
Self.importBuys = async(ctx, id, options) => {
|
||||
const conn = Self.dataSource.connector;
|
||||
const args = ctx.args;
|
||||
const models = Self.app.models;
|
||||
let tx;
|
||||
const myOptions = {};
|
||||
|
||||
if (!options.transaction) {
|
||||
if (typeof options == 'object')
|
||||
Object.assign(myOptions, options);
|
||||
|
||||
if (!myOptions.transaction) {
|
||||
tx = await Self.beginTransaction({});
|
||||
options.transaction = tx;
|
||||
myOptions.transaction = tx;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await models.Entry.findById(id, null, options);
|
||||
const entry = await models.Entry.findById(id, null, myOptions);
|
||||
await entry.updateAttributes({
|
||||
observation: args.observation,
|
||||
ref: args.ref
|
||||
}, options);
|
||||
}, myOptions);
|
||||
|
||||
const buys = [];
|
||||
for (let buy of args.buys) {
|
||||
|
@ -71,9 +70,16 @@ module.exports = Self => {
|
|||
buyingValue: buy.buyingValue,
|
||||
packageFk: buy.packageFk
|
||||
});
|
||||
|
||||
await models.ItemMatchProperties.upsert({
|
||||
itemFk: buy.itemFk,
|
||||
name: buy.description,
|
||||
producer: buy.companyName,
|
||||
size: buy.size
|
||||
}, myOptions);
|
||||
}
|
||||
|
||||
const createdBuys = await models.Buy.create(buys, options);
|
||||
const createdBuys = await models.Buy.create(buys, myOptions);
|
||||
const buyIds = createdBuys.map(buy => buy.id);
|
||||
|
||||
const stmts = [];
|
||||
|
@ -90,7 +96,7 @@ module.exports = Self => {
|
|||
stmts.push('CALL buy_recalcPrices()');
|
||||
|
||||
const sql = ParameterizedSQL.join(stmts, ';');
|
||||
await conn.executeStmt(sql, options);
|
||||
await conn.executeStmt(sql, myOptions);
|
||||
if (tx) await tx.commit();
|
||||
} catch (e) {
|
||||
if (tx) await tx.rollback();
|
||||
|
|
|
@ -37,7 +37,21 @@ module.exports = Self => {
|
|||
where: {volume: {gte: buy.volume}},
|
||||
order: 'volume ASC'
|
||||
}, myOptions);
|
||||
buy.packageFk = packaging.id;
|
||||
|
||||
if (packaging)
|
||||
buy.packageFk = packaging.id;
|
||||
|
||||
const reference = await models.ItemMatchProperties.findOne({
|
||||
fields: ['itemFk'],
|
||||
where: {
|
||||
name: buy.description,
|
||||
producer: buy.companyName,
|
||||
size: buy.size
|
||||
}
|
||||
}, myOptions);
|
||||
|
||||
if (reference)
|
||||
buy.itemFk = reference.itemFk;
|
||||
}
|
||||
|
||||
return buys;
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
"Buy": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"ItemMatchProperties": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"EntryLog": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "ItemMatchProperties",
|
||||
"base": "VnModel",
|
||||
"options": {
|
||||
"mysql": {
|
||||
"table": "itemMatchProperties"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"itemFk": {
|
||||
"type": "number",
|
||||
"id": true,
|
||||
"description": "Identifier"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"producer": {
|
||||
"type": "string"
|
||||
},
|
||||
"size": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"relations": {
|
||||
"item": {
|
||||
"type": "belongsTo",
|
||||
"model": "Item",
|
||||
"foreignKey": "itemFk"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,20 +9,6 @@
|
|||
class="vn-ma-md">
|
||||
<div class="vn-w-lg">
|
||||
<vn-card class="vn-pa-lg">
|
||||
<vn-horizontal>
|
||||
<vn-textfield vn-focus
|
||||
vn-one
|
||||
label="Reference"
|
||||
ng-model="$ctrl.import.ref">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-textarea
|
||||
vn-one
|
||||
label="Observation"
|
||||
ng-model="$ctrl.import.observation">
|
||||
</vn-textarea>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal>
|
||||
<vn-input-file
|
||||
vn-one
|
||||
|
@ -40,6 +26,20 @@
|
|||
</append>
|
||||
</vn-input-file>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal ng-show="$ctrl.import.ref">
|
||||
<vn-textfield vn-focus
|
||||
vn-one
|
||||
label="Reference"
|
||||
ng-model="$ctrl.import.ref">
|
||||
</vn-textfield>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal ng-show="$ctrl.import.observation">
|
||||
<vn-textarea
|
||||
vn-one
|
||||
label="Observation"
|
||||
ng-model="$ctrl.import.observation">
|
||||
</vn-textarea>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal ng-show="$ctrl.import.buys.length > 0">
|
||||
<table class="vn-table">
|
||||
<thead>
|
||||
|
@ -51,7 +51,6 @@
|
|||
<th translate center>Grouping</th>
|
||||
<th translate center>Buying value</th>
|
||||
<th translate center>Box</th>
|
||||
<th translate center>Volume</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="buy in $ctrl.import.buys">
|
||||
|
@ -70,20 +69,19 @@
|
|||
<tpl-item>
|
||||
{{::id}} - {{::name}}
|
||||
</tpl-item>
|
||||
<append>
|
||||
<vn-icon-button
|
||||
icon="filter_alt"
|
||||
vn-click-stop="$ctrl.showFilterDialog(buy)"
|
||||
vn-tooltip="Filter...">
|
||||
</vn-icon-button>
|
||||
</append>
|
||||
</vn-autocomplete>
|
||||
</td>
|
||||
<td title="{{::buy.description}}" expand>{{::buy.description | dashIfEmpty}}</td>
|
||||
<td center title="{{::buy.size}}">{{::buy.size | dashIfEmpty}}</td>
|
||||
<td center>
|
||||
<vn-chip>
|
||||
<span>{{::buy.packing | dashIfEmpty}}</span>
|
||||
</vn-chip>
|
||||
</td>
|
||||
<td center>
|
||||
<vn-chip>
|
||||
<span>{{::buy.grouping | dashIfEmpty}}</span>
|
||||
</vn-chip>
|
||||
</vn-td>
|
||||
<td center>{{::buy.packing | dashIfEmpty}}</td>
|
||||
<td center>{{::buy.grouping | dashIfEmpty}}</td>
|
||||
<td>{{::buy.buyingValue | currency: 'EUR':2}}</td>
|
||||
<td center title="{{::buy.packageFk | dashIfEmpty}}">
|
||||
<vn-autocomplete
|
||||
|
@ -95,7 +93,6 @@
|
|||
ng-model="buy.packageFk">
|
||||
</vn-autocomplete>
|
||||
</td>
|
||||
<td center title="{{::buy.volume}}">{{::buy.volume | number}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -110,7 +107,95 @@
|
|||
label="Cancel"
|
||||
ui-sref="entry.card.buy.index">
|
||||
</vn-button>
|
||||
</vn-button>
|
||||
</vn-button-bar>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<vn-dialog
|
||||
vn-id="filterDialog"
|
||||
on-accept="$ctrl.addTime()"
|
||||
message="Filter item">
|
||||
<tpl-body class="itemFilter">
|
||||
<vn-horizontal>
|
||||
<vn-textfield
|
||||
label="Name"
|
||||
ng-model="$ctrl.itemFilterParams.name"
|
||||
vn-focus>
|
||||
</vn-textfield>
|
||||
<vn-textfield
|
||||
label="Size"
|
||||
ng-model="$ctrl.itemFilterParams.size">
|
||||
</vn-textfield>
|
||||
<vn-autocomplete
|
||||
label="Producer"
|
||||
ng-model="$ctrl.itemFilterParams.producerFk"
|
||||
url="Producers"
|
||||
show-field="name"
|
||||
value-field="id">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
label="Type"
|
||||
ng-model="$ctrl.itemFilterParams.typeFk"
|
||||
url="ItemTypes"
|
||||
show-field="name"
|
||||
value-field="id">
|
||||
</vn-autocomplete>
|
||||
<vn-autocomplete
|
||||
label="Color"
|
||||
ng-model="$ctrl.itemFilterParams.inkFk"
|
||||
url="Inks"
|
||||
show-field="name"
|
||||
value-field="id">
|
||||
</vn-autocomplete>
|
||||
</vn-horizontal>
|
||||
<vn-horizontal class="vn-mb-md">
|
||||
<vn-button vn-none
|
||||
label="Search"
|
||||
ng-click="$ctrl.filter()">
|
||||
</vn-button>
|
||||
</vn-horizontal>
|
||||
<vn-crud-model
|
||||
vn-id="itemsModel"
|
||||
url="Items"
|
||||
filter="$ctrl.itemFilter"
|
||||
data="items"
|
||||
limit="10">
|
||||
</vn-crud-model>
|
||||
<vn-data-viewer
|
||||
model="itemsModel"
|
||||
class="vn-w-lg">
|
||||
<vn-table class="scrollable">
|
||||
<vn-thead>
|
||||
<vn-tr>
|
||||
<vn-th shrink>ID</vn-th>
|
||||
<vn-th expand>Item</vn-th>
|
||||
<vn-th number>Size</vn-th>
|
||||
<vn-th expand>Producer</vn-th>
|
||||
<vn-th>Color</vn-th>
|
||||
</vn-tr>
|
||||
</vn-thead>
|
||||
<vn-tbody>
|
||||
<a ng-repeat="item in items"
|
||||
class="clickable vn-tr search-result"
|
||||
ng-click="$ctrl.selectItem(item.id)">
|
||||
<vn-td shrink>
|
||||
<span
|
||||
ng-click="itemDescriptor.show($event, item.id)"
|
||||
class="link">
|
||||
{{::item.id}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td expand>{{::item.name}}</vn-td>
|
||||
<vn-td number>{{::item.size}}</vn-td>
|
||||
<vn-td expand>{{::item.producer.name}}</vn-td>
|
||||
<vn-td>{{::item.ink.name}}</vn-td>
|
||||
</a>
|
||||
</vn-tbody>
|
||||
</vn-table>
|
||||
</vn-data-viewer>
|
||||
<vn-item-descriptor-popover
|
||||
vn-id="item-descriptor"
|
||||
warehouse-fk="$ctrl.vnConfig.warehouseFk">
|
||||
</vn-item-descriptor-popover>
|
||||
</tpl-body>
|
||||
</vn-dialog>
|
|
@ -29,6 +29,7 @@ class Controller extends Section {
|
|||
this.$.$applyAsync(() => {
|
||||
this.import.observation = invoice.tx_awb;
|
||||
|
||||
const companyName = invoice.tx_company;
|
||||
const boxes = invoice.boxes;
|
||||
const buys = [];
|
||||
for (let box of boxes) {
|
||||
|
@ -37,11 +38,12 @@ class Controller extends Section {
|
|||
const packing = product.nu_stems_bunch * product.nu_bunches;
|
||||
buys.push({
|
||||
description: product.nm_product,
|
||||
companyName: companyName,
|
||||
size: product.nu_length,
|
||||
packing: packing,
|
||||
grouping: product.nu_stems_bunch,
|
||||
buyingValue: parseFloat(product.mny_rate_stem),
|
||||
volume: boxVolume
|
||||
volume: boxVolume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +88,59 @@ class Controller extends Section {
|
|||
? {id: $search}
|
||||
: {name: {like: '%' + $search + '%'}};
|
||||
}
|
||||
|
||||
showFilterDialog(buy) {
|
||||
this.activeBuy = buy;
|
||||
this.itemFilterParams = {};
|
||||
this.itemFilter = {
|
||||
include: [
|
||||
{
|
||||
relation: 'producer',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
},
|
||||
{
|
||||
relation: 'ink',
|
||||
scope: {
|
||||
fields: ['name']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.$.filterDialog.show();
|
||||
}
|
||||
|
||||
selectItem(id) {
|
||||
this.activeBuy['itemFk'] = id;
|
||||
this.$.filterDialog.hide();
|
||||
}
|
||||
|
||||
filter() {
|
||||
const filter = this.itemFilter;
|
||||
const params = this.itemFilterParams;
|
||||
const where = {};
|
||||
|
||||
for (let key in params) {
|
||||
const value = params[key];
|
||||
if (!value) continue;
|
||||
|
||||
switch (key) {
|
||||
case 'name':
|
||||
where[key] = {like: `%${value}%`};
|
||||
break;
|
||||
case 'producerFk':
|
||||
case 'typeFk':
|
||||
case 'size':
|
||||
case 'ink':
|
||||
where[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
filter.where = where;
|
||||
this.$.itemsModel.applyFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
Controller.$inject = ['$element', '$scope'];
|
||||
|
|
|
@ -3,3 +3,9 @@ vn-entry-buy-import {
|
|||
width: 250px
|
||||
}
|
||||
}
|
||||
|
||||
.itemFilter {
|
||||
vn-table.scrollable {
|
||||
height: 500px
|
||||
}
|
||||
}
|
|
@ -4,3 +4,5 @@ Box: Embalaje
|
|||
Import buys: Importar compras
|
||||
Some of the imported buys doesn't have an item: Algunas de las compras importadas no tienen un artículo
|
||||
JSON files only: Solo ficheros JSON
|
||||
Filter item: Filtrar artículo
|
||||
Filter...: Filtrar...
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -24,6 +24,9 @@
|
|||
"amount": {
|
||||
"type": "number"
|
||||
},
|
||||
"foreignValue": {
|
||||
"type": "number"
|
||||
},
|
||||
"created": {
|
||||
"type": "date"
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"created": {
|
||||
"type": "date"
|
||||
}
|
||||
|
||||
},
|
||||
"relations": {
|
||||
"invoiceIn": {
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -9,3 +9,5 @@ Sage transaction: Sage transaccion
|
|||
Foreign value: Divisa
|
||||
Due day: Vencimiento
|
||||
Invoice list: Listado de facturas recibidas
|
||||
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"
|
||||
|
@ -98,3 +104,11 @@
|
|||
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": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Ink: Tinta
|
||||
Origin: Origen
|
||||
Producer: Productor.
|
||||
Producer: Productor
|
||||
With visible: Con visible
|
||||
Field: Campo
|
||||
More fields: Más campos
|
||||
|
|
|
@ -28,30 +28,57 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.getItemTypeAvailable = async(orderId, itemCategoryId) => {
|
||||
let stmts = [];
|
||||
const stmts = [];
|
||||
let stmt;
|
||||
|
||||
let order = await app.models.Order.findById(orderId);
|
||||
const order = await app.models.Order.findById(orderId);
|
||||
stmt = new ParameterizedSQL('call vn.available_calc(?, ?, ?)', [
|
||||
order.landed,
|
||||
order.addressFk,
|
||||
order.agencyModeFk
|
||||
]);
|
||||
stmts.push(stmt);
|
||||
stmt = new ParameterizedSQL(`
|
||||
SELECT it.id, it.name, ic.name categoryName
|
||||
|
||||
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.item');
|
||||
|
||||
stmt = new ParameterizedSQL(
|
||||
`CREATE TEMPORARY TABLE tmp.item
|
||||
(PRIMARY KEY (itemFk)) ENGINE = MEMORY
|
||||
SELECT DISTINCT
|
||||
i.id AS itemFk,
|
||||
it.id AS typeFk,
|
||||
it.name,
|
||||
ic.name AS categoryName
|
||||
FROM tmp.availableCalc ac
|
||||
JOIN cache.available a ON a.calc_id = ac.calcFk
|
||||
JOIN item i ON i.id = a.item_id
|
||||
JOIN itemType it ON it.id = i.typeFk
|
||||
JOIN itemCategory ic ON ic.id = it.categoryFk
|
||||
WHERE it.categoryFk = ?
|
||||
GROUP BY it.id`, [itemCategoryId]
|
||||
JOIN vn.item i ON i.id = a.item_id
|
||||
JOIN vn.itemType it ON it.id = i.typeFk
|
||||
JOIN vn.itemCategory ic ON ic.id = it.categoryFk
|
||||
WHERE it.categoryFk = ?`, [itemCategoryId]
|
||||
);
|
||||
let categoriesIndex = stmts.push(stmt) - 1;
|
||||
stmts.push(stmt);
|
||||
|
||||
let sql = ParameterizedSQL.join(stmts, ';');
|
||||
let result = await Self.rawStmt(sql);
|
||||
stmt = new ParameterizedSQL(
|
||||
'CALL vn.catalog_calculate(?, ?, ?)', [
|
||||
order.landed,
|
||||
order.addressFk,
|
||||
order.agencyModeFk,
|
||||
]
|
||||
);
|
||||
stmts.push(stmt);
|
||||
|
||||
stmt = new ParameterizedSQL(`
|
||||
SELECT i.typeFk AS id, i.name, i.categoryName
|
||||
FROM tmp.item i
|
||||
JOIN tmp.ticketCalculateItem tci ON tci.itemFk = i.itemFk
|
||||
GROUP BY i.typeFk`
|
||||
);
|
||||
const categoriesIndex = stmts.push(stmt) - 1;
|
||||
|
||||
stmts.push('DROP TEMPORARY TABLE tmp.item');
|
||||
|
||||
const sql = ParameterizedSQL.join(stmts, ';');
|
||||
const result = await Self.rawStmt(sql);
|
||||
|
||||
return result[categoriesIndex];
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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();
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue