Merge branch 'dev' into 2940-invoiceInTax-section
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2021-08-10 08:23:13 +02:00
commit d49242751e
113 changed files with 2296 additions and 234 deletions

View File

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

View File

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

View File

@ -0,0 +1,7 @@
ALTER TABLE `vn`.`sample` ADD COLUMN
(`datepickerEnabled` TINYINT(1) NOT NULL DEFAULT 0);
ALTER TABLE `vn`.`sample` MODIFY code VARCHAR(25) charset utf8 NOT NULL;
INSERT INTO `vn`.`sample` (code, description, isVisible, hasCompany, hasPreview, datepickerEnabled)
VALUES ('client-debt-statement', 'Extracto del cliente', 1, 0, 1, 1);

View File

@ -0,0 +1,109 @@
drop procedure `vn`.`ticket_close`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`ticket_close`()
BEGIN
/**
* Realiza el cierre de todos los
* tickets de la tabla tmp.ticket_close.
*
* @table tmp.ticket_close(ticketFk) Identificadores de los tickets a cerrar
*/
DECLARE vDone BOOL;
DECLARE vClientFk INT;
DECLARE vCurTicketFk INT;
DECLARE vIsTaxDataChecked BOOL;
DECLARE vCompanyFk INT;
DECLARE vShipped DATE;
DECLARE vNewInvoiceId INT;
DECLARE vHasDailyInvoice BOOL;
DECLARE vWithPackage BOOL;
DECLARE vHasToInvoice BOOL;
DECLARE cur CURSOR FOR
SELECT ticketFk FROM tmp.ticket_close;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN
RESIGNAL;
END;
OPEN cur;
proc: LOOP
SET vDone = FALSE;
FETCH cur INTO vCurTicketFk;
IF vDone THEN
LEAVE proc;
END IF;
-- Fetch ticket data
SELECT
c.id,
c.isTaxDataChecked,
t.companyFk,
t.shipped,
co.hasDailyInvoice,
w.isManaged,
c.hasToInvoice
INTO vClientFk,
vIsTaxDataChecked,
vCompanyFk,
vShipped,
vHasDailyInvoice,
vWithPackage,
vHasToInvoice
FROM ticket t
JOIN `client` c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
JOIN warehouse w ON w.id = t.warehouseFk
WHERE t.id = vCurTicketFk;
INSERT INTO ticketPackaging (ticketFk, packagingFk, quantity)
(SELECT vCurTicketFk, p.id, COUNT(*)
FROM expedition e
JOIN packaging p ON p.itemFk = e.itemFk
WHERE e.ticketFk = vCurTicketFk AND p.isPackageReturnable
AND vWithPackage
GROUP BY p.itemFk);
-- No retornables o no catalogados
INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, isPriceFixed)
(SELECT e.itemFk, vCurTicketFk, i.name, COUNT(*) AS amount, getSpecialPrice(e.itemFk, vClientFk), 1
FROM expedition e
JOIN item i ON i.id = e.itemFk
LEFT JOIN packaging p ON p.itemFk = i.id
WHERE e.ticketFk = vCurTicketFk AND IFNULL(p.isPackageReturnable, 0) = 0
AND getSpecialPrice(e.itemFk, vClientFk) > 0
GROUP BY e.itemFk);
CALL vn.zonePromo_Make();
IF(vHasDailyInvoice) AND vHasToInvoice THEN
-- Facturacion rapida
CALL ticketTrackingAdd(vCurTicketFk, 'DELIVERED', NULL);
-- Facturar si está contabilizado
IF vIsTaxDataChecked THEN
CALL invoiceOut_newFromClient(
vClientFk,
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'M')),
vShipped,
vCompanyFk,
NULL,
NULL,
vNewInvoiceId);
END IF;
ELSE
CALL ticketTrackingAdd(vCurTicketFk, (SELECT vn.getAlert3State(vCurTicketFk)), NULL);
END IF;
END LOOP;
CLOSE cur;
END;;$$
DELIMITER ;

View File

@ -0,0 +1,118 @@
drop procedure `vn`.`ticket_closeAll`;
DELIMITER $$
$$
create definer = root@`%` procedure `vn`.`ticket_closeAll__`()
BEGIN
/**
* Realiza el cierre de todos los
* tickets de la tabla tmp.ticketClosure.
*
* @param vTicketFk Id del ticket
*/
DECLARE vDone BOOL;
DECLARE vClientFk INT;
DECLARE vCurTicketFk INT;
DECLARE vIsTaxDataChecked BOOL;
DECLARE vCompanyFk INT;
DECLARE vShipped DATE;
DECLARE vNewInvoiceId INT;
DECLARE vHasDailyInvoice BOOL;
DECLARE vWithPackage BOOL;
DECLARE vHasToInvoice BOOL;
DECLARE cur CURSOR FOR
SELECT ticketFk FROM tmp.ticketClosure;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET vDone = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN
RESIGNAL;
END;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketClosure2;
CREATE TEMPORARY TABLE tmp.ticketClosure2
SELECT ticketFk FROM tmp.ticketClosure;
INSERT INTO tmp.ticketClosure
SELECT id FROM stowaway s
JOIN tmp.ticketClosure2 tc ON s.shipFk = tc.ticketFk;
OPEN cur;
proc: LOOP
SET vDone = FALSE;
FETCH cur INTO vCurTicketFk;
IF vDone THEN
LEAVE proc;
END IF;
-- ticketClosure start
SELECT
c.id,
c.isTaxDataChecked,
t.companyFk,
t.shipped,
co.hasDailyInvoice,
w.isManaged,
c.hasToInvoice
INTO vClientFk,
vIsTaxDataChecked,
vCompanyFk,
vShipped,
vHasDailyInvoice,
vWithPackage,
vHasToInvoice
FROM ticket t
JOIN `client` c ON c.id = t.clientFk
JOIN province p ON p.id = c.provinceFk
JOIN country co ON co.id = p.countryFk
JOIN warehouse w ON w.id = t.warehouseFk
WHERE t.id = vCurTicketFk;
INSERT INTO ticketPackaging (ticketFk, packagingFk, quantity)
(SELECT vCurTicketFk, p.id, COUNT(*)
FROM expedition e
JOIN packaging p ON p.itemFk = e.itemFk
WHERE e.ticketFk = vCurTicketFk AND p.isPackageReturnable
AND vWithPackage
GROUP BY p.itemFk);
-- No retornables o no catalogados
INSERT INTO sale (itemFk, ticketFk, concept, quantity, price, isPriceFixed)
(SELECT e.itemFk, vCurTicketFk, i.name, COUNT(*) AS amount, getSpecialPrice(e.itemFk, vClientFk), 1
FROM expedition e
JOIN item i ON i.id = e.itemFk
LEFT JOIN packaging p ON p.itemFk = i.id
WHERE e.ticketFk = vCurTicketFk AND IFNULL(p.isPackageReturnable, 0) = 0
AND getSpecialPrice(e.itemFk, vClientFk) > 0
GROUP BY e.itemFk);
CALL vn.zonePromo_Make();
IF(vHasDailyInvoice) AND vHasToInvoice THEN
-- Facturacion rapida
CALL ticketTrackingAdd(vCurTicketFk, 'DELIVERED', NULL);
-- Facturar si está contabilizado
IF vIsTaxDataChecked THEN
CALL invoiceOut_newFromClient(
vClientFk,
(SELECT invoiceSerial(vClientFk, vCompanyFk, 'M')),
vShipped,
vCompanyFk,
NULL,
NULL,
vNewInvoiceId);
END IF;
ELSE
CALL ticketTrackingAdd(vCurTicketFk, (SELECT vn.getAlert3State(vCurTicketFk)), NULL);
END IF;
END LOOP;
CLOSE cur;
DROP TEMPORARY TABLE IF EXISTS tmp.ticketClosure;
END;;$$
DELIMITER ;

View File

@ -0,0 +1,34 @@
drop procedure `vn`.`ticket_closeByTicket`;
DELIMITER $$
$$
create
definer = root@`%` procedure `vn`.`ticket_closeByTicket`(IN vTicketFk int)
BEGIN
/**
* Inserta el ticket en la tabla temporal
* para ser cerrado.
*
* @param vTicketFk Id del ticket
*/
DROP TEMPORARY TABLE IF EXISTS tmp.ticket_close;
CREATE TEMPORARY TABLE tmp.ticket_close ENGINE = MEMORY (
SELECT
t.id AS ticketFk
FROM expedition e
INNER JOIN ticket t ON t.id = e.ticketFk
LEFT JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.id = ts.alertLevel
WHERE
al.code = 'PACKED'
AND t.id = vTicketFk
AND t.refFk IS NULL
GROUP BY e.ticketFk);
CALL ticket_close();
DROP TEMPORARY TABLE tmp.ticket_close;
END;;$$
DELIMITER ;

View File

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

View File

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

View File

@ -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"]',

View File

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

View File

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

View File

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

View File

@ -57,7 +57,14 @@
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one",
"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"
}

View File

@ -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}}})",

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,9 @@
"Buy": {
"dataSource": "vn"
},
"ItemMatchProperties": {
"dataSource": "vn"
},
"EntryLog": {
"dataSource": "vn"
},

View File

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

View File

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

View File

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

View File

@ -3,3 +3,9 @@ vn-entry-buy-import {
width: 250px
}
}
.itemFilter {
vn-table.scrollable {
height: 500px
}
}

View File

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

View File

@ -0,0 +1,120 @@
const loggable = require('vn-loopback/util/log');
module.exports = Self => {
Self.remoteMethodCtx('clone', {
description: 'Clone the invoiceIn and as many invoiceInTax and invoiceInDueDay referencing it',
accessType: 'WRITE',
accepts: {
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
http: {source: 'path'}
},
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/clone',
verb: 'POST'
}
});
Self.clone = async(ctx, id, options) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
let tx;
const myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const sourceInvoiceIn = await Self.findById(id, {
fields: [
'id',
'serial',
'supplierRef',
'supplierFk',
'issued',
'currencyFk',
'companyFk',
'isVatDeductible',
'withholdingSageFk',
'deductibleExpenseFk',
]
}, myOptions);
const sourceInvoiceInTax = await models.InvoiceInTax.find({where: {invoiceInFk: id}}, myOptions);
const sourceInvoiceInDueDay = await models.InvoiceInDueDay.find({where: {invoiceInFk: id}}, myOptions);
const issued = new Date(sourceInvoiceIn.issued);
issued.setMonth(issued.getMonth() + 1);
const clone = await models.InvoiceIn.create({
serial: sourceInvoiceIn.serial,
supplierRef: sourceInvoiceIn.supplierRef,
supplierFk: sourceInvoiceIn.supplierFk,
issued: issued,
currencyFk: sourceInvoiceIn.currencyFk,
companyFk: sourceInvoiceIn.companyFk,
isVatDeductible: sourceInvoiceIn.isVatDeductible,
withholdingSageFk: sourceInvoiceIn.withholdingSageFk,
deductibleExpenseFk: sourceInvoiceIn.deductibleExpenseFk,
}, myOptions);
const oldProperties = await loggable.translateValues(Self, sourceInvoiceIn);
const newProperties = await loggable.translateValues(Self, clone);
await models.InvoiceInLog.create({
originFk: clone.id,
userFk: userId,
action: 'insert',
changedModel: 'InvoiceIn',
changedModelId: clone.id,
oldInstance: oldProperties,
newInstance: newProperties
}, myOptions);
const promises = [];
for (let tax of sourceInvoiceInTax) {
promises.push(models.InvoiceInTax.create({
invoiceInFk: clone.id,
taxableBase: tax.taxableBase,
expenceFk: tax.expenceFk,
foreignValue: tax.foreignValue,
taxTypeSageFk: tax.taxTypeSageFk,
transactionTypeSageFk: tax.transactionTypeSageFk
}, myOptions));
}
for (let dueDay of sourceInvoiceInDueDay) {
const dueDated = dueDay.dueDated;
dueDated.setMonth(dueDated.getMonth() + 1);
promises.push(models.InvoiceInDueDay.create({
invoiceInFk: clone.id,
dueDated: dueDated,
bankFk: dueDay.bankFk,
amount: dueDay.amount,
foreignValue: dueDated.foreignValue,
}, myOptions));
}
await Promise.all(promises);
if (tx) await tx.commit();
return clone;
} catch (e) {
if (tx) await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,36 @@
const models = require('vn-loopback/server/server').models;
describe('invoiceIn clone()', () => {
it('should return the cloned invoiceIn and also clone invoiceInDueDays and invoiceInTaxes if there are any referencing the invoiceIn', async() => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
const tx = await models.InvoiceIn.beginTransaction({});
const options = {transaction: tx};
try {
const clone = await models.InvoiceIn.clone(ctx, 1, options);
expect(clone.supplierRef).toEqual('1234');
const invoiceInTaxes = await models.InvoiceInTax.find({where: {invoiceInFk: clone.id}}, options);
expect(invoiceInTaxes.length).toEqual(1);
const invoiceInDueDays = await models.InvoiceInDueDay.find({where: {invoiceInFk: clone.id}}, options);
expect(invoiceInDueDays.length).toEqual(2);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

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

View File

@ -30,6 +30,7 @@
"created": {
"type": "date"
}
},
"relations": {
"invoiceIn": {

View File

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

View File

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

View File

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

View File

@ -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: [

View File

@ -9,3 +9,5 @@ Sage transaction: Sage transaccion
Foreign value: Divisa
Due day: Vencimiento
Invoice list: Listado de facturas recibidas
InvoiceIn cloned: Factura clonada

View File

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

View File

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

View File

@ -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', {

View File

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

View File

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

View File

@ -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', {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
Ink: Tinta
Origin: Origen
Producer: Productor.
Producer: Productor
With visible: Con visible
Field: Campo
More fields: Más campos

View File

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

View File

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

View File

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

View File

@ -17,7 +17,14 @@ describe('sale deleteSales()', () => {
});
it('should throw an error if the ticket of the given sales is not editable', async() => {
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;

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,8 @@ module.exports = Self => {
}
});
Self.updateQuantity = async(ctx, id, quantity) => {
Self.updateQuantity = async(ctx, id, newQuantity) => {
const $t = ctx.req.__; // $translate
const models = Self.app.models;
const 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;
};
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`])
.mergeStyles();

View File

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

View File

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

View File

@ -0,0 +1,25 @@
const Component = require(`${appPath}/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
const attachment = new Component('attachment');
const attachments = require('./attachments.json');
module.exports = {
name: 'client-debt-statement',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build(),
'attachment': attachment.build()
},
data() {
return {attachments};
},
props: {
recipientId: {
required: true
},
from: {
required: true
}
}
};

View File

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

View File

@ -0,0 +1,8 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`])
.mergeStyles();

View File

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

View File

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

View File

@ -0,0 +1,17 @@
const Component = require(`${appPath}/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
const attachment = new Component('attachment');
const attachments = require('./attachments.json');
module.exports = {
name: 'credit-request',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build(),
'attachment': attachment.build()
},
data() {
return {attachments};
},
};

View File

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

View File

@ -1,5 +1,5 @@
subject: Vossa nota de entrega
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>.

View File

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

View File

@ -0,0 +1,8 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`])
.mergeStyles();

View File

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

View File

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

View File

@ -0,0 +1,25 @@
const Component = require(`${appPath}/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'invoice',
async serverPrefetch() {
this.invoice = await this.fetchInvoice(this.invoiceId);
},
methods: {
fetchInvoice(invoiceId) {
return this.findOneFromDef('invoice', [invoiceId]);
},
},
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
},
props: {
invoiceId: {
type: String,
required: true
}
}
};

View File

@ -0,0 +1,9 @@
subject: Your invoice
title: Your invoice
dear: Dear client
description: The invoice <strong>{0}</strong> from the order <strong>{1}</strong> is now available. <br/>
You can download it by clicking on the attachment of this email.
poll: If you wish, you can answer our satisfaction survey to
help us provide better service. Your opinion is very important for us!
help: Any questions that arise, do not hesitate to consult it, <strong>we are here to assist you!</strong>
conclusion: Thanks for your attention!

View File

@ -0,0 +1,10 @@
subject: Tu factura
title: Tu factura
dear: Estimado cliente
description: Ya está disponible la factura <strong>{0}</strong> correspondiente al pedido <strong>{1}</strong>. <br/>
Puedes descargarla haciendo clic en el adjunto de este correo.
poll: Si lo deseas, puedes responder a nuestra encuesta de satisfacción para
ayudarnos a prestar un mejor servicio. ¡Tu opinión es muy importante para nosotros!
help: Cualquier duda que te surja, no dudes en consultarla, <strong>¡estamos para
atenderte!</strong>
conclusion: ¡Gracias por tu atención!

View File

@ -0,0 +1,9 @@
subject: Votre facture
title: Votre facture
dear: Cher client,
description: Le facture <strong>{0}</strong> correspondant à la commande <strong>{1}</strong> est maintenant disponible.<br/>
Vous pouvez le télécharger en cliquant sur la pièce jointe dans cet email.
poll: Si vous le souhaitez, vous pouvez répondre à notre questionaire de satisfaction
pour nous aider à améliorer notre service. Votre avis est très important pour nous!
help: N'hésitez pas nous envoyer toute doute ou question, <strong>nous sommes là pour vous aider!</strong>
conclusion: Merci pour votre attention!

View File

@ -0,0 +1,9 @@
subject: Sua fatura
title: Sua fatura
dear: Estimado cliente
description: Já está disponível sua fatura <strong>{0}</strong> correspondente a encomenda {1}. <br/>
Podes descarregar-la fazendo um clique no arquivo anexado ao e-mail.
poll: Si o deseja, podes responder nosso questionário de satiscação para ajudar-nos a prestar-vos um melhor serviço. Tua opinião é muito importante para nós!
help: Cualquer dúvida que surja, no hesites em consultar-la, <strong>Estamos aqui para
atender-te!</strong>
conclusion: Obrigado por tua atenção!

View File

@ -0,0 +1,7 @@
SELECT
io.ref,
t.id AS ticketFk
FROM invoiceOut io
JOIN ticket t ON t.refFk = io.ref
JOIN client c ON c.id = io.clientFk
WHERE io.id = ?

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

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