Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 4846-claim.search-panel

This commit is contained in:
Vicent Llopis 2022-11-25 12:24:00 +01:00
commit 7f3265be55
53 changed files with 20747 additions and 256 deletions

View File

@ -32,7 +32,7 @@ module.exports = Self => {
let message = $t(`There's a new urgent ticket:`); let message = $t(`There's a new urgent ticket:`);
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id='; const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
tickets.forEach(ticket => { tickets.forEach(ticket => {
message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} (@${ticket.username})](${ostUri + ticket.id})`; message += `\r\n[ID: *${ticket.number}* - ${ticket.subject} @${ticket.username}](${ostUri + ticket.id})`;
}); });
const department = await models.Department.findOne({ const department = await models.Department.findOne({
@ -42,7 +42,5 @@ module.exports = Self => {
if (channelName) if (channelName)
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`); return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
return;
}; };
}; };

View File

@ -4,4 +4,21 @@ module.exports = Self => {
require('../methods/chat/sendCheckingPresence')(Self); require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(Self); require('../methods/chat/notifyIssues')(Self);
require('../methods/chat/sendQueued')(Self); require('../methods/chat/sendQueued')(Self);
Self.observe('before save', async function(ctx) {
if (!ctx.isNewInstance) return;
let {message} = ctx.instance;
if (!message) return;
const parts = message.match(/(?<=\[).*(?=])/g);
const replacedParts = parts.map(part => {
return part.replace(/[*()]/g, '');
});
for (const [index, part] of parts.entries())
message = message.replace(part, replacedParts[index]);
ctx.instance.message = message;
});
}; };

View File

@ -0,0 +1,2 @@
INSERT INTO `salix`.`ACL` (model,property,accessType,permission,principalType,principalId)
VALUES ('NotificationQueue','*','*','ALLOW','ROLE','employee');

View File

@ -0,0 +1,8 @@
ALTER TABLE
`vn`.`client`
ADD
COLUMN `hasElectronicInvoice` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Registro de facturas mediante FACe'
AFTER
`hasInvoiceSimplified`;
-- sería más correcto hasElectronicInvoice pero ya existe un campo hasInvoiceSimplified

View File

@ -0,0 +1 @@
insert into `util`.`notification` (`id`, `name`,`description`) values (2, 'invoiceElectronic', 'A electronic invoice has been generated');

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('Receipt', 'balanceCompensationEmail', 'WRITE', 'ALLOW', 'ROLE', 'employee'),
('Receipt', 'balanceCompensationPdf', 'READ', 'ALLOW', 'ROLE', 'employee');

View File

@ -0,0 +1,6 @@
UPDATE
`vn`.`client`
SET
hasElectronicInvoice = TRUE
WHERE
businessTypeFk = 'officialOrganism';

View File

@ -0,0 +1 @@
Delete this file

View File

@ -1859,7 +1859,8 @@ INSERT INTO `vn`.`receipt`(`id`, `invoiceFk`, `amountPaid`, `payed`, `workerFk`,
(1, 'Cobro web', 100.50, util.VN_CURDATE(), 9, 1, 1101, util.VN_CURDATE(), 442, 1), (1, 'Cobro web', 100.50, util.VN_CURDATE(), 9, 1, 1101, util.VN_CURDATE(), 442, 1),
(2, 'Cobro web', 200.50, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 9, 1, 1101, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 442, 1), (2, 'Cobro web', 200.50, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 9, 1, 1101, DATE_ADD(util.VN_CURDATE(), INTERVAL -5 DAY), 442, 1),
(3, 'Cobro en efectivo', 300.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 9, 1, 1102, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 442, 0), (3, 'Cobro en efectivo', 300.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 9, 1, 1102, DATE_ADD(util.VN_CURDATE(), INTERVAL -10 DAY), 442, 0),
(4, 'Cobro en efectivo', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 1, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0); (4, 'Cobro en efectivo', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 1, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0),
(5, 'Compensación', 400.00, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 9, 3, 1103, DATE_ADD(util.VN_CURDATE(), INTERVAL -15 DAY), 442, 0);
INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`) INSERT INTO `vn`.`workerTeam`(`id`, `team`, `workerFk`)
VALUES VALUES
@ -2731,3 +2732,7 @@ UPDATE `account`.`user`
INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`) INSERT INTO `vn`.`osTicketConfig` (`id`, `host`, `user`, `password`, `oldStatus`, `newStatusId`, `day`, `comment`, `hostDb`, `userDb`, `passwordDb`, `portDb`, `responseType`, `fromEmailId`, `replyTo`)
VALUES VALUES
(0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all'); (0, 'http://localhost:56596/scp', 'ostadmin', 'Admin1', 'open', 3, 60, 'Este CAU se ha cerrado automáticamente. Si el problema persiste responda a este mensaje.', 'localhost', 'osticket', 'osticket', 40003, 'reply', 1, 'all');
INSERT INTO `vn`.`ticketLog` (`id`, `originFk`, `userFk`, `action`, `changedModel`, `oldInstance`, `newInstance`, `changedModelId`)
VALUES
(1, 1, 9, 'insert', 'Ticket', '{}', '{"clientFk":1, "nickname": "Bat cave"}', 1);

View File

@ -19,6 +19,7 @@
-- Current Database: `account` -- Current Database: `account`
-- --
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `account` /*!40100 DEFAULT CHARACTER SET utf8mb3 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/ `account` /*!40100 DEFAULT CHARACTER SET utf8mb3 */;
USE `account`; USE `account`;

View File

@ -324,7 +324,8 @@ export default {
anyBalanceLine: 'vn-client-balance-index vn-tbody > vn-tr', anyBalanceLine: 'vn-client-balance-index vn-tbody > vn-tr',
firstLineBalance: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(8)', firstLineBalance: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td:nth-child(8)',
firstLineReference: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable', firstLineReference: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable',
firstLineReferenceInput: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable > div > field > vn-textfield' firstLineReferenceInput: 'vn-client-balance-index vn-tbody > vn-tr:nth-child(1) > vn-td-editable > div > field > vn-textfield',
compensationButton: 'vn-client-balance-index vn-icon-button[vn-dialog="send_compensation"]'
}, },
webPayment: { webPayment: {
confirmFirstPaymentButton: 'vn-client-web-payment vn-tr:nth-child(1) vn-icon-button[icon="done_all"]', confirmFirstPaymentButton: 'vn-client-web-payment vn-tr:nth-child(1) vn-icon-button[icon="done_all"]',
@ -1038,6 +1039,17 @@ export default {
booked: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.booked"]', booked: 'vn-invoice-in-basic-data vn-date-picker[ng-model="$ctrl.invoiceIn.booked"]',
currency: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.currencyFk"]', currency: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.currencyFk"]',
company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]', company: 'vn-invoice-in-basic-data vn-autocomplete[ng-model="$ctrl.invoiceIn.companyFk"]',
dms: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"]',
download: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.prepend > prepend > vn-icon-button',
edit: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.append > append > vn-icon-button[icon="edit"]',
create: 'vn-invoice-in-basic-data vn-textfield[ng-model="$ctrl.invoiceIn.dmsFk"] > div.container > div.append > append > vn-icon-button[icon="add_circle"]',
reference: 'vn-textfield[ng-model="$ctrl.dms.reference"]',
companyId: 'vn-autocomplete[ng-model="$ctrl.dms.companyId"]',
warehouseId: 'vn-autocomplete[ng-model="$ctrl.dms.warehouseId"]',
dmsTypeId: 'vn-autocomplete[ng-model="$ctrl.dms.dmsTypeId"]',
description: 'vn-textarea[ng-model="$ctrl.dms.description"]',
inputFile: 'vn-input-file[ng-model="$ctrl.dms.files"]',
confirm: 'button[response="accept"]',
save: 'vn-invoice-in-basic-data button[type=submit]' save: 'vn-invoice-in-basic-data button[type=submit]'
}, },
invoiceInTax: { invoiceInTax: {

View File

@ -0,0 +1,27 @@
import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer';
describe('Client Send balance compensation', () => {
let browser;
let page;
beforeAll(async() => {
browser = await getBrowser();
page = browser.page;
await page.loginAndModule('employee', 'client');
await page.accessToSearchResult('Clark Kent');
await page.accessToSection('client.card.balance.index');
});
afterAll(async() => {
await browser.close();
});
it(`should click on send compensation button`, async() => {
await page.autocompleteSearch(selectors.clientBalance.company, 'VNL');
await page.waitToClick(selectors.clientBalance.compensationButton);
await page.waitToClick(selectors.clientBalance.saveButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Notification sent!');
});
});

View File

@ -4,6 +4,7 @@ import getBrowser from '../../helpers/puppeteer';
describe('InvoiceIn basic data path', () => { describe('InvoiceIn basic data path', () => {
let browser; let browser;
let page; let page;
let newDms;
beforeAll(async() => { beforeAll(async() => {
browser = await getBrowser(); browser = await getBrowser();
@ -24,6 +25,8 @@ describe('InvoiceIn basic data path', () => {
await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura'); await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura');
await page.clearInput(selectors.invoiceInBasicData.supplierRef); await page.clearInput(selectors.invoiceInBasicData.supplierRef);
await page.write(selectors.invoiceInBasicData.supplierRef, '9999'); await page.write(selectors.invoiceInBasicData.supplierRef, '9999');
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '2');
await page.pickDate(selectors.invoiceInBasicData.bookEntried, now); await page.pickDate(selectors.invoiceInBasicData.bookEntried, now);
await page.pickDate(selectors.invoiceInBasicData.booked, now); await page.pickDate(selectors.invoiceInBasicData.booked, now);
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'USD'); await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'USD');
@ -61,4 +64,141 @@ describe('InvoiceIn basic data path', () => {
expect(result).toEqual('ORN'); expect(result).toEqual('ORN');
}); });
it(`should confirm the invoiceIn dms was edited`, async() => {
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
expect(result).toEqual('2');
});
it(`should create a new invoiceIn dms and save the changes`, async() => {
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.waitToClick(selectors.invoiceInBasicData.create);
await page.clearInput(selectors.invoiceInBasicData.reference);
await page.write(selectors.invoiceInBasicData.reference, 'New Dms');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
let message = await page.waitForSnackbar();
expect(message.text).toContain('The company can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.companyId);
await page.autocompleteSearch(selectors.invoiceInBasicData.companyId, 'VNL');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The warehouse can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.warehouseId);
await page.autocompleteSearch(selectors.invoiceInBasicData.warehouseId, 'Warehouse One');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The DMS Type can\'t be empty');
await page.clearInput(selectors.invoiceInBasicData.dmsTypeId);
await page.autocompleteSearch(selectors.invoiceInBasicData.dmsTypeId, 'Ticket');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The description can\'t be empty');
await page.waitToClick(selectors.invoiceInBasicData.description);
await page.write(selectors.invoiceInBasicData.description, 'Dms without edition.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('The files can\'t be empty');
let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/assets/thermograph.jpeg`;
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.waitToClick(selectors.invoiceInBasicData.inputFile)
]);
await fileChooser.accept([filePath]);
await page.waitToClick(selectors.invoiceInBasicData.confirm);
message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
newDms = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
});
it(`should confirm the invoiceIn was edited with the new dms`, async() => {
await page.reloadSection('invoiceIn.card.basicData');
const result = await page
.waitToGetProperty(selectors.invoiceInBasicData.dms, 'value');
expect(result).toEqual(newDms);
});
it(`should edit the invoiceIn`, async() => {
await page.waitToClick(selectors.invoiceInBasicData.edit);
await page.clearInput(selectors.invoiceInBasicData.reference);
await page.write(selectors.invoiceInBasicData.reference, 'Dms Edited');
await page.clearInput(selectors.invoiceInBasicData.companyId);
await page.autocompleteSearch(selectors.invoiceInBasicData.companyId, 'CCs');
await page.clearInput(selectors.invoiceInBasicData.warehouseId);
await page.autocompleteSearch(selectors.invoiceInBasicData.warehouseId, 'Algemesi');
await page.clearInput(selectors.invoiceInBasicData.dmsTypeId);
await page.autocompleteSearch(selectors.invoiceInBasicData.dmsTypeId, 'Basura');
await page.waitToClick(selectors.invoiceInBasicData.description);
await page.write(selectors.invoiceInBasicData.description, ' Nevermind, now is edited.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
let message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
it(`should confirm the new dms has been edited`, async() => {
await page.reloadSection('invoiceIn.card.basicData');
await page.waitToClick(selectors.invoiceInBasicData.edit);
const reference = await page
.waitToGetProperty(selectors.invoiceInBasicData.reference, 'value');
const companyId = await page
.waitToGetProperty(selectors.invoiceInBasicData.companyId, 'value');
const warehouseId = await page
.waitToGetProperty(selectors.invoiceInBasicData.warehouseId, 'value');
const dmsTypeId = await page
.waitToGetProperty(selectors.invoiceInBasicData.dmsTypeId, 'value');
const description = await page
.waitToGetProperty(selectors.invoiceInBasicData.description, 'value');
expect(reference).toEqual('Dms Edited');
expect(companyId).toEqual('CCs');
expect(warehouseId).toEqual('Algemesi');
expect(dmsTypeId).toEqual('Basura');
expect(description).toEqual('Dms without edition. Nevermind, now is edited.');
await page.waitToClick(selectors.invoiceInBasicData.confirm);
});
it(`should disable edit and download if dms doesn't exists, and set back the original dms`, async() => {
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '9999');
await page.waitForSelector(`${selectors.invoiceInBasicData.download}.disabled`);
await page.waitForSelector(`${selectors.invoiceInBasicData.edit}.disabled`);
await page.clearInput(selectors.invoiceInBasicData.dms);
await page.write(selectors.invoiceInBasicData.dms, '1');
await page.waitToClick(selectors.invoiceInBasicData.save);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Data saved!');
});
}); });

View File

@ -26,6 +26,7 @@ export default class Searchbar extends Component {
this.autoState = true; this.autoState = true;
this.separateIndex = true; this.separateIndex = true;
this.entityState = 'card.summary'; this.entityState = 'card.summary';
this.isIndex = false;
this.deregisterCallback = this.$transitions.onSuccess( this.deregisterCallback = this.$transitions.onSuccess(
{}, transition => this.onStateChange(transition)); {}, transition => this.onStateChange(transition));
@ -102,6 +103,9 @@ export default class Searchbar extends Component {
filter = {}; filter = {};
} }
let stateParts = this.$state.current.name.split('.');
this.isIndex = stateParts[1] == 'index';
this.doSearch(filter, 'state'); this.doSearch(filter, 'state');
} }
@ -198,7 +202,7 @@ export default class Searchbar extends Component {
} }
doSearch(filter, source) { doSearch(filter, source) {
if (filter === this.filter && source != 'state') return; if (filter === this.filter && !this.isIndex) return;
let promise = this.onSearch({$params: filter}); let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve(); promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data)); promise.then(data => this.onFilter(filter, source, data));

View File

@ -1,7 +1,7 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 2, "lockfileVersion": 1,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
@ -189,10 +189,14 @@
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw==" "integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw=="
}, },
"angular-animate": { "angular-animate": {
"version": "1.8.2" "version": "1.8.2",
"resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.8.2.tgz",
"integrity": "sha512-Jbr9+grNMs9Kj57xuBU3Ju3NOPAjS1+g2UAwwDv7su1lt0/PLDy+9zEwDiu8C8xJceoTbmBNKiWGPJGBdCQLlA=="
}, },
"angular-moment": { "angular-moment": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": { "requires": {
"moment": ">=2.8.0 <3.0.0" "moment": ">=2.8.0 <3.0.0"
} }
@ -215,18 +219,26 @@
}, },
"argparse": { "argparse": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
}, },
"croppie": { "croppie": {
"version": "2.6.5" "version": "2.6.5",
"resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.5.tgz",
"integrity": "sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ=="
}, },
"esprima": { "esprima": {
"version": "4.0.1" "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
}, },
"js-yaml": { "js-yaml": {
"version": "3.14.1", "version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -234,6 +246,8 @@
}, },
"mg-crud": { "mg-crud": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha512-mAR6t0aQHKnT0QHKHpLOi0kNPZfO36iMpIoiLjFHxuio6mIJyuveBJ4VNlNXJRxLh32/FLADEb41/sYo7QUKFw==",
"requires": { "requires": {
"angular": "^1.6.1" "angular": "^1.6.1"
} }
@ -244,19 +258,27 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
}, },
"oclazyload": { "oclazyload": {
"version": "0.6.3" "version": "0.6.3",
"resolved": "https://registry.npmjs.org/oclazyload/-/oclazyload-0.6.3.tgz",
"integrity": "sha512-HpOSYUgjtt6sTB/C6+FWsExR+9HCnXKsUA96RWkDXfv11C8Cc9X2DlR0WIZwFIiG6FQU0pwB5dhoYyut8bFAOQ=="
}, },
"require-yaml": { "require-yaml": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha512-M6eVEgLPRbeOhgSCnOTtdrOOEQzbXRchg24Xa13c39dMuraFKdI9emUo97Rih0YEFzSICmSKg8w4RQp+rd9pOQ==",
"requires": { "requires": {
"js-yaml": "" "js-yaml": "^4.1.0"
}, },
"dependencies": { "dependencies": {
"argparse": { "argparse": {
"version": "2.0.1" "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
}, },
"js-yaml": { "js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": { "requires": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
} }
@ -264,10 +286,14 @@
} }
}, },
"sprintf-js": { "sprintf-js": {
"version": "1.0.3" "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
}, },
"validator": { "validator": {
"version": "6.3.0" "version": "6.3.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-6.3.0.tgz",
"integrity": "sha512-BylxTwhqwjQI5MDJF7amCy/L0ejJO+74DvCsLV52Lq3+3bhVcVMKqNqOiNcQJm2G48u9EAcw4xFERAmFbwXM9Q=="
} }
} }
} }

View File

@ -33,16 +33,18 @@ export default class Controller extends Section {
set logs(value) { set logs(value) {
this._logs = value; this._logs = value;
if (!value) return;
if (this.logs) { const validations = window.validations;
this.logs.forEach(log => { value.forEach(log => {
log.oldProperties = this.getInstance(log.oldInstance); const locale = validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
log.newProperties = this.getInstance(log.newInstance);
log.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale);
}); });
} }
}
getInstance(instance) { getInstance(instance, locale) {
const properties = []; const properties = [];
let validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/; let validDate = /^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z)?$/;
@ -51,7 +53,8 @@ export default class Controller extends Section {
if (validDate.test(instance[property])) if (validDate.test(instance[property]))
instance[property] = new Date(instance[property]).toLocaleString('es-ES'); instance[property] = new Date(instance[property]).toLocaleString('es-ES');
properties.push({key: property, value: instance[property]}); const key = locale[property] || property;
properties.push({key, value: instance[property]});
}); });
return properties; return properties;
} }

View File

@ -1,3 +1,5 @@
const path = require('path');
const fs = require('fs');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('modelInfo', { Self.remoteMethod('modelInfo', {
@ -19,6 +21,42 @@ module.exports = Self => {
} }
}); });
const modelsLocale = new Map();
const modulesDir = path.resolve(`${__dirname}/../../../../modules`);
const modules = fs.readdirSync(modulesDir);
for (const mod of modules) {
const modelsDir = path.join(modulesDir, mod, `back/locale`);
if (!fs.existsSync(modelsDir)) continue;
const models = fs.readdirSync(modelsDir);
for (const model of models) {
const localeDir = path.join(modelsDir, model);
const localeFiles = fs.readdirSync(localeDir);
let modelName = model.charAt(0).toUpperCase() + model.substring(1);
modelName = modelName.replace(/-\w/g, match => {
return match.charAt(1).toUpperCase();
});
const modelLocale = new Map();
modelsLocale.set(modelName, modelLocale);
for (const localeFile of localeFiles) {
const localePath = path.join(localeDir, localeFile);
const match = localeFile.match(/^([a-z]+)\.yml$/);
if (!match) {
console.warn(`Skipping wrong model locale file: ${localeFile}`);
continue;
}
const translations = require(localePath);
modelLocale.set(match[1], translations);
}
}
}
Self.modelInfo = async function(ctx) { Self.modelInfo = async function(ctx) {
let json = {}; let json = {};
let models = Self.app.models; let models = Self.app.models;
@ -49,9 +87,14 @@ module.exports = Self => {
jsonValidations[fieldName] = jsonField; jsonValidations[fieldName] = jsonField;
} }
const modelLocale = modelsLocale.get(modelName);
const lang = ctx.req.getLocale();
const locale = modelLocale && modelLocale.get(lang);
json[modelName] = { json[modelName] = {
properties: model.definition.rawProperties, properties: model.definition.rawProperties,
validations: jsonValidations validations: jsonValidations,
locale
}; };
} }

View File

@ -138,5 +138,8 @@
"You don't have grant privilege": "You don't have grant privilege", "You don't have grant privilege": "You don't have grant privilege",
"You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user", "You don't own the role and you can't assign it to another user": "You don't own the role and you can't assign it to another user",
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) merged with [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production" "Sale(s) blocked, please contact production": "Sale(s) blocked, please contact production",
"Receipt's bank was not found": "Receipt's bank was not found",
"This receipt was not compensated": "This receipt was not compensated",
"Client's email was not found": "Client's email was not found"
} }

View File

@ -244,5 +244,8 @@
"Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) fusionado con [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})", "Ticket merged": "Ticket [{{id}}]({{{fullPath}}}) ({{{originDated}}}) fusionado con [{{tfId}}]({{{fullPathFuture}}}) ({{{futureDated}}})",
"Already has this status": "Ya tiene este estado", "Already has this status": "Ya tiene este estado",
"There aren't records for this week": "No existen registros para esta semana", "There aren't records for this week": "No existen registros para esta semana",
"Empty data source": "Origen de datos vacio" "Empty data source": "Origen de datos vacio",
"Receipt's bank was not found": "No se encontró el banco del recibo",
"This receipt was not compensated": "Este recibo no ha sido compensado",
"Client's email was not found": "No se encontró el email del cliente"
} }

View File

@ -99,6 +99,10 @@ module.exports = Self => {
{ {
arg: 'hasIncoterms', arg: 'hasIncoterms',
type: 'boolean' type: 'boolean'
},
{
arg: 'hasElectronicInvoice',
type: 'boolean'
} }
], ],
returns: { returns: {
@ -122,19 +126,15 @@ module.exports = Self => {
if (typeof options == 'object') if (typeof options == 'object')
Object.assign(myOptions, options); Object.assign(myOptions, options);
if (!myOptions.transaction) { if (!myOptions.transaction) {
tx = await Self.beginTransaction({}); tx = await Self.beginTransaction({});
myOptions.transaction = tx; myOptions.transaction = tx;
} }
try { try {
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions); const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
const client = await models.Client.findById(clientId, null, myOptions); const client = await models.Client.findById(clientId, null, myOptions);
if (!isSalesAssistant && client.isTaxDataChecked) if (!isSalesAssistant && client.isTaxDataChecked)
throw new UserError(`Not enough privileges to edit a client with verified data`); throw new UserError(`Not enough privileges to edit a client with verified data`);
// Sage data validation // Sage data validation
const taxDataChecked = args.isTaxDataChecked; const taxDataChecked = args.isTaxDataChecked;
const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk; const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk;
@ -143,7 +143,6 @@ module.exports = Self => {
if (taxDataChecked && !hasSageData) if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`); throw new UserError(`You need to fill sage information before you check verified data`);
if (args.despiteOfClient) { if (args.despiteOfClient) {
const logRecord = { const logRecord = {
originFk: clientId, originFk: clientId,
@ -158,7 +157,6 @@ module.exports = Self => {
await models.ClientLog.create(logRecord, myOptions); await models.ClientLog.create(logRecord, myOptions);
} }
// Remove unwanted properties // Remove unwanted properties
delete args.ctx; delete args.ctx;
delete args.id; delete args.id;

View File

@ -1,4 +1,5 @@
const {Email} = require('vn-print'); const {Email} = require('vn-print');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('balanceCompensationEmail', { Self.remoteMethodCtx('balanceCompensationEmail', {
@ -10,7 +11,7 @@ module.exports = Self => {
type: 'Number', type: 'Number',
required: true, required: true,
description: 'The receipt id', description: 'The receipt id',
http: { source: 'path' } http: {source: 'path'}
} }
], ],
returns: { returns: {
@ -23,18 +24,29 @@ module.exports = Self => {
} }
}); });
Self.balanceCompensationEmail = async (ctx, id) => { Self.balanceCompensationEmail = async(ctx, id) => {
const models = Self.app.models; const models = Self.app.models;
const receipt = await models.Receipt.findById(id, {fields: ['clientFk']}); const receipt = await models.Receipt.findById(id, {fields: ['clientFk', 'bankFk']});
const client = await models.Client.findById(receipt.clientFk, {fields:['email']});
const bank = await models.Bank.findById(receipt.bankFk);
if (!bank)
throw new UserError(`Receipt's bank was not found`);
const accountingType = await models.AccountingType.findById(bank.accountingTypeFk);
if (!(accountingType && accountingType.code == 'compensation'))
throw new UserError(`This receipt was not compensated`);
const client = await models.Client.findById(receipt.clientFk, {fields: ['email']});
if (!client.email)
throw new UserError(`Client's email was not found`);
else {
const email = new Email('balance-compensation', { const email = new Email('balance-compensation', {
lang: ctx.req.getLocale(), lang: ctx.req.getLocale(),
recipient: client.email+',administracion@verdnatura.es', recipient: client.email + ',administracion@verdnatura.es',
id id
}); });
return email.send(); return email.send();
}
}; };
}; };

View File

@ -56,11 +56,14 @@ module.exports = Self => {
u.name userName, u.name userName,
r.clientFk, r.clientFk,
FALSE hasPdf, FALSE hasPdf,
FALSE isInvoice FALSE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation
FROM vn.receipt r FROM vn.receipt r
LEFT JOIN vn.worker w ON w.id = r.workerFk LEFT JOIN vn.worker w ON w.id = r.workerFk
LEFT JOIN account.user u ON u.id = w.userFk LEFT JOIN account.user u ON u.id = w.userFk
JOIN vn.company c ON c.id = r.companyFk JOIN vn.company c ON c.id = r.companyFk
JOIN vn.accounting a ON a.id = r.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk
WHERE r.clientFk = ? AND r.companyFk = ? WHERE r.clientFk = ? AND r.companyFk = ?
UNION ALL UNION ALL
SELECT SELECT
@ -77,9 +80,12 @@ module.exports = Self => {
NULL, NULL,
i.clientFk, i.clientFk,
i.hasPdf, i.hasPdf,
TRUE isInvoice TRUE isInvoice,
CASE WHEN at2.code LIKE 'compensation' THEN True ELSE False END as isCompensation
FROM vn.invoiceOut i FROM vn.invoiceOut i
JOIN vn.company c ON c.id = i.companyFk JOIN vn.company c ON c.id = i.companyFk
JOIN vn.accounting a ON a.id = i.bankFk
JOIN vn.accountingType at2 ON at2.id = a.accountingTypeFk
WHERE i.clientFk = ? AND i.companyFk = ? WHERE i.clientFk = ? AND i.companyFk = ?
ORDER BY payed DESC, created DESC ORDER BY payed DESC, created DESC
) t ORDER BY payed DESC, created DESC`, ) t ORDER BY payed DESC, created DESC`,

View File

@ -142,7 +142,11 @@
}, },
"salesPersonFk": { "salesPersonFk": {
"type": "number" "type": "number"
},
"hasElectronicInvoice": {
"type": "boolean"
} }
}, },
"relations": { "relations": {
"account": { "account": {

View File

@ -121,7 +121,7 @@
</vn-icon-button> </vn-icon-button>
</a> </a>
</vn-td> </vn-td>
<vn-td center shrink ng-if="!balance.isInvoice"> <vn-td center shrink ng-if="balance.isCompensation">
<vn-icon-button <vn-icon-button
vn-dialog="send_compensation" vn-dialog="send_compensation"
icon="outgoing_mail" icon="outgoing_mail"

View File

@ -151,5 +151,19 @@ describe('Client', () => {
$httpBackend.flush(); $httpBackend.flush();
}); });
}); });
describe('sendEmail()', () => {
it('should send an email', () => {
jest.spyOn(controller.vnEmail, 'send');
const $data = {id: 1103};
controller.sendEmail($data);
const expectedPath = `Receipts/${$data.id}/balance-compensation-email`;
expect(controller.vnEmail.send).toHaveBeenCalledWith(expectedPath);
});
});
}); });
}); });

View File

@ -1 +1,3 @@
BILL: N/INV {{ref}} BILL: N/INV {{ref}}
Notify compensation: Do you want to report compensation to the client by mail?
Send compensation: Send compensation

View File

@ -1,112 +1,52 @@
<mg-ajax path="Clients/{{patch.params.id}}/updateFiscalData" options="vnPatch"></mg-ajax> <mg-ajax path="Clients/{{patch.params.id}}/updateFiscalData" options="vnPatch"></mg-ajax>
<vn-watcher <vn-watcher vn-id="watcher" data="$ctrl.client" id-field="id" form="form" save="patch">
vn-id="watcher"
data="$ctrl.client"
id-field="id"
form="form"
save="patch">
</vn-watcher> </vn-watcher>
<vn-crud-model <vn-crud-model auto-load="true" url="Provinces/location" data="provincesLocation" order="name">
auto-load="true"
url="Provinces/location"
data="provincesLocation"
order="name">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model auto-load="true" url="Countries" data="countries" order="country">
auto-load="true"
url="Countries"
data="countries"
order="country">
</vn-crud-model> </vn-crud-model>
<vn-crud-model <vn-crud-model auto-load="true" url="SageTaxTypes" data="sageTaxTypes" order="vat">
auto-load="true"
url="SageTaxTypes"
data="sageTaxTypes"
order="vat">
</vn-crud-model> </vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md"> <form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield vn-two vn-focus label="Social name" ng-model="$ctrl.client.socialName" rule
vn-two info="Only letters, numbers and spaces can be used" required="true">
vn-focus
label="Social name"
ng-model="$ctrl.client.socialName"
rule
info="Only letters, numbers and spaces can be used"
required="true">
</vn-textfield> </vn-textfield>
<vn-textfield <vn-textfield vn-one label="Tax number" ng-model="$ctrl.client.fi" rule>
vn-one
label="Tax number"
ng-model="$ctrl.client.fi"
rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield vn-two label="Street" ng-model="$ctrl.client.street" rule>
vn-two
label="Street"
ng-model="$ctrl.client.street"
rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-one <vn-autocomplete vn-one ng-model="$ctrl.client.sageTaxTypeFk" data="sageTaxTypes" show-field="vat"
ng-model="$ctrl.client.sageTaxTypeFk" value-field="id" label="Sage tax type" vn-acl="salesAssistant" rule>
data="sageTaxTypes"
show-field="vat"
value-field="id"
label="Sage tax type"
vn-acl="salesAssistant"
rule>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-one <vn-autocomplete vn-one ng-model="$ctrl.client.sageTransactionTypeFk" url="SageTransactionTypes"
ng-model="$ctrl.client.sageTransactionTypeFk" show-field="transaction" value-field="id" label="Sage transaction type"
url="SageTransactionTypes"
show-field="transaction"
value-field="id"
label="Sage transaction type"
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}" search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
vn-acl="salesAssistant" vn-acl="salesAssistant" order="transaction" rule>
order="transaction"
rule>
<tpl-item>{{id}}: {{transaction}}</tpl-item> <tpl-item>{{id}}: {{transaction}}</tpl-item>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-datalist vn-one <vn-datalist vn-one label="Postcode" ng-model="$ctrl.client.postcode" selection="$ctrl.postcode"
label="Postcode" url="Postcodes/location" fields="['code','townFk']" order="code, townFk" value-field="code"
ng-model="$ctrl.client.postcode" show-field="code" rule>
selection="$ctrl.postcode"
url="Postcodes/location"
fields="['code','townFk']"
order="code, townFk"
value-field="code"
show-field="code"
rule>
<tpl-item> <tpl-item>
{{code}} - {{town.name}} ({{town.province.name}}, {{code}} - {{town.name}} ({{town.province.name}},
{{town.province.country.country}}) {{town.province.country.country}})
</tpl-item> </tpl-item>
<append> <append>
<vn-icon-button <vn-icon-button icon="add_circle" vn-tooltip="New postcode" ng-click="postcode.open()"
icon="add_circle" vn-acl="deliveryBoss" vn-acl-action="remove">
vn-tooltip="New postcode"
ng-click="postcode.open()"
vn-acl="deliveryBoss"
vn-acl-action="remove">
</vn-icon-button> </vn-icon-button>
</append> </append>
</vn-datalist> </vn-datalist>
<vn-datalist vn-id="town" vn-one <vn-datalist vn-id="town" vn-one label="City" ng-model="$ctrl.client.city" selection="$ctrl.town"
label="City" url="Towns/location" fields="['id', 'name', 'provinceFk']" show-field="name" value-field="name">
ng-model="$ctrl.client.city"
selection="$ctrl.town"
url="Towns/location"
fields="['id', 'name', 'provinceFk']"
show-field="name"
value-field="name">
<tpl-item> <tpl-item>
{{name}}, {{province.name}} {{name}}, {{province.name}}
({{province.country.country}}) ({{province.country.country}})
@ -114,112 +54,64 @@
</vn-datalist> </vn-datalist>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete vn-id="province" vn-one <vn-autocomplete vn-id="province" vn-one label="Province" ng-model="$ctrl.client.provinceFk"
label="Province" selection="$ctrl.province" data="provincesLocation" fields="['id', 'name', 'countryFk']"
ng-model="$ctrl.client.provinceFk" show-field="name" value-field="id" rule>
selection="$ctrl.province"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
value-field="id"
rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item> <tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete vn-id="country" vn-one <vn-autocomplete vn-id="country" vn-one ng-model="$ctrl.client.countryFk" data="countries"
ng-model="$ctrl.client.countryFk" show-field="country" value-field="id" label="Country" rule>
data="countries"
show-field="country"
value-field="id"
label="Country"
rule>
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check vn-one label="Active" ng-model="$ctrl.client.isActive">
vn-one
label="Active"
ng-model="$ctrl.client.isActive">
</vn-check> </vn-check>
<vn-check <vn-check vn-one label="Frozen" ng-model="$ctrl.client.isFreezed">
vn-one
label="Frozen"
ng-model="$ctrl.client.isFreezed">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check vn-one label="Has to invoice" ng-model="$ctrl.client.hasToInvoice">
vn-one
label="Has to invoice"
ng-model="$ctrl.client.hasToInvoice">
</vn-check> </vn-check>
<vn-check <vn-check vn-one label="Vies" ng-model="$ctrl.client.isVies">
vn-one
label="Vies"
ng-model="$ctrl.client.isVies">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check vn-one label="Notify by email" ng-model="$ctrl.client.isToBeMailed">
vn-one
label="Notify by email"
ng-model="$ctrl.client.isToBeMailed">
</vn-check> </vn-check>
<vn-check <vn-check vn-one label="Invoice by address" ng-model="$ctrl.client.hasToInvoiceByAddress">
vn-one
label="Invoice by address"
ng-model="$ctrl.client.hasToInvoiceByAddress">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check vn-one label="Is equalizated" ng-model="$ctrl.client.isEqualizated"
vn-one
label="Is equalizated"
ng-model="$ctrl.client.isEqualizated"
info="In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not." info="In order to invoice, this field is not consulted, but the consignee's ET. When modifying this field if the invoice by address option is not checked, the change will be automatically propagated to all addresses, otherwise the user will be asked if he wants to propagate it or not."
on-change="$ctrl.onChangeEqualizated(value)"> on-change="$ctrl.onChangeEqualizated(value)">
</vn-check> </vn-check>
<vn-check <vn-check vn-one label="Verified data" ng-model="$ctrl.client.isTaxDataChecked" vn-acl="salesAssistant">
vn-one
label="Verified data"
ng-model="$ctrl.client.isTaxDataChecked"
vn-acl="salesAssistant">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-check <vn-check vn-one label="Incoterms authorization" ng-model="$ctrl.client.hasIncoterms"
vn-one vn-acl="administrative">
label="Incoterms authorization" </vn-check>
ng-model="$ctrl.client.hasIncoterms" <vn-check vn-one label="Electronic invoice" ng-model="$ctrl.client.hasElectronicInvoice"
vn-acl="administrative"> vn-acl="administrative">
</vn-check> </vn-check>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>
<vn-submit <vn-submit disabled="!watcher.dataChanged()" label="Save">
disabled="!watcher.dataChanged()"
label="Save">
</vn-submit> </vn-submit>
<vn-button <vn-button class="cancel" label="Undo changes" disabled="!watcher.dataChanged()"
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()"> ng-click="watcher.loadOriginalData()">
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>
<vn-confirm <vn-confirm vn-id="propagate-isEqualizated" question="You changed the equalization tax"
vn-id="propagate-isEqualizated" message="Do you want to spread the change?" on-accept="$ctrl.onAcceptEt()">
question="You changed the equalization tax"
message="Do you want to spread the change?"
on-accept="$ctrl.onAcceptEt()">
</vn-confirm> </vn-confirm>
<vn-confirm <vn-confirm vn-id="confirm-duplicatedClient" message="Found a client with this data"
vn-id="confirm-duplicatedClient"
message="Found a client with this data"
on-accept="$ctrl.onAcceptDuplication()"> on-accept="$ctrl.onAcceptDuplication()">
</vn-confirm> </vn-confirm>
<!-- New postcode dialog --> <!-- New postcode dialog -->
<vn-geo-postcode <vn-geo-postcode vn-id="postcode" on-response="$ctrl.onResponse($response)">
vn-id="postcode"
on-response="$ctrl.onResponse($response)">
</vn-geo-postcode> </vn-geo-postcode>

View File

@ -11,3 +11,4 @@ Sage transaction type: Tipo de transacción Sage
Previous client: Cliente anterior Previous client: Cliente anterior
In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente In case of a company succession, specify the grantor company: En el caso de que haya habido una sucesión de empresa, indicar la empresa cedente
Incoterms authorization: Autorización incoterms Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica

View File

@ -5,6 +5,24 @@
form="form" form="form"
save="patch"> save="patch">
</vn-watcher> </vn-watcher>
<vn-crud-model
auto-load="true"
url="Companies"
data="companies"
order="code">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Warehouses"
data="warehouses"
order="name">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="DmsTypes"
data="dmsTypes"
order="name">
</vn-crud-model>
<form name="form" ng-submit="watcher.submit()" class="vn-w-md"> <form name="form" ng-submit="watcher.submit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal> <vn-horizontal>
@ -57,6 +75,36 @@
{{id}} - {{name}} {{id}} - {{name}}
</tpl-item> </tpl-item>
</vn-datalist> </vn-datalist>
<vn-textfield
label="Document"
ng-model="$ctrl.invoiceIn.dmsFk"
ng-change="$ctrl.checkFileExists($ctrl.invoiceIn.dmsFk)"
rule>
<prepend>
<vn-icon-button
disabled="$ctrl.editDownloadDisabled"
ng-if="$ctrl.invoiceIn.dmsFk"
title="{{'Download file' | translate}}"
icon="cloud_download"
ng-click="$ctrl.downloadFile($ctrl.invoiceIn.dmsFk)">
</vn-icon-button>
</prepend>
<append>
<vn-icon-button
disabled="$ctrl.editDownloadDisabled"
ng-if="$ctrl.invoiceIn.dmsFk"
ng-click="$ctrl.openEditDialog($ctrl.invoiceIn.dmsFk)"
icon="edit"
title="{{'Edit document' | translate}}">
</vn-icon-button>
<vn-icon-button
ng-if="!$ctrl.invoiceIn.dmsFk"
ng-click="$ctrl.openCreateDialog()"
icon="add_circle"
title="{{'Create document' | translate}}">
</vn-icon-button>
</append>
</vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-date-picker <vn-date-picker
@ -105,3 +153,163 @@
</vn-button> </vn-button>
</vn-button-bar> </vn-button-bar>
</form> </form>
<!-- Create edit dms dialog -->
<vn-dialog
vn-id="dmsEditDialog"
message="Edit document"
on-accept="$ctrl.onEdit()">
<tpl-body>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete vn-one required="true"
label="Company"
ng-model="$ctrl.dms.companyId"
url="Companies"
show-field="code"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one required="true"
label="Warehouse"
ng-model="$ctrl.dms.warehouseId"
url="Warehouses"
show-field="name"
value-field="id">
</vn-autocomplete>
<vn-autocomplete vn-one required="true"
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
url="DmsTypes"
show-field="name"
value-field="id">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
required="true"
label="Description"
ng-model="$ctrl.dms.description"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file
vn-one
label="File"
ng-model="$ctrl.dms.files"
on-change="$ctrl.onFileChange($files)"
accept="{{$ctrl.allowedContentTypes}}"
required="false"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check disabled="true"
label="Generate identifier for original file"
ng-model="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Save</button>
</tpl-buttons>
</vn-dialog>
<!-- Create new dms dialog -->
<vn-dialog
vn-id="dmsCreateDialog"
message="Create document"
on-accept="$ctrl.onCreate()">
<tpl-body>
<vn-horizontal>
<vn-textfield
vn-one
vn-focus
label="Reference"
ng-model="$ctrl.dms.reference"
rule>
</vn-textfield>
<vn-autocomplete
vn-one
label="Company"
ng-model="$ctrl.dms.companyId"
data="companies"
show-field="code"
value-field="id"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete
vn-one
label="Warehouse"
ng-model="$ctrl.dms.warehouseId"
data="warehouses"
show-field="name"
value-field="id"
required="true">
</vn-autocomplete>
<vn-autocomplete
vn-one
label="Type"
ng-model="$ctrl.dms.dmsTypeId"
data="dmsTypes"
show-field="name"
value-field="id"
required="true">
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textarea
vn-one
label="Description"
ng-model="$ctrl.dms.description"
required="true"
rule>
</vn-textarea>
</vn-horizontal>
<vn-horizontal>
<vn-input-file
vn-one
label="File"
ng-model="$ctrl.dms.files"
on-change="$ctrl.onFileChange($files)"
accept="{{$ctrl.allowedContentTypes}}"
required="true"
multiple="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
<vn-vertical>
<vn-check
label="Generate identifier for original file"
ng-model="$ctrl.dms.hasFile">
</vn-check>
</vn-vertical>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Create</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,9 +1,181 @@
import ngModule from '../module'; import ngModule from '../module';
import Section from 'salix/components/section'; import Section from 'salix/components/section';
import UserError from 'core/lib/user-error';
class Controller extends Section {
constructor($element, $, vnFile) {
super($element, $, vnFile);
this.dms = {
files: [],
hasFile: false,
hasFileAttached: false
};
this.vnFile = vnFile;
this.getAllowedContentTypes();
this._editDownloadDisabled = false;
}
get contentTypesInfo() {
return this.$t('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
get editDownloadDisabled() {
return this._editDownloadDisabled;
}
async checkFileExists(dmsId) {
if (!dmsId) return;
let filter = {
fields: ['id']
};
await this.$http.get(`Dms/${dmsId}`, {filter})
.then(() => this._editDownloadDisabled = false)
.catch(() => this._editDownloadDisabled = true);
}
async getFile(dmsId) {
const path = `Dms/${dmsId}`;
await this.$http.get(path).then(res => {
const dms = res.data && res.data;
this.dms = {
dmsId: dms.id,
reference: dms.reference,
warehouseId: dms.warehouseFk,
companyId: dms.companyFk,
dmsTypeId: dms.dmsTypeFk,
description: dms.description,
hasFile: dms.hasFile,
hasFileAttached: false,
files: []
};
});
}
getAllowedContentTypes() {
this.$http.get('DmsContainers/allowedContentTypes').then(res => {
if (res.data.length > 0) {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
}
});
}
openEditDialog(dmsId) {
this.getFile(dmsId).then(() => this.$.dmsEditDialog.show());
}
openCreateDialog() {
this.dms = {
reference: null,
warehouseId: null,
companyId: null,
dmsTypeId: null,
description: null,
hasFile: true,
hasFileAttached: true,
files: null
};
this.$.dmsCreateDialog.show();
}
downloadFile(dmsId) {
this.vnFile.download(`api/dms/${dmsId}/downloadFile`);
}
onFileChange(files) {
let hasFileAttached = false;
if (files.length > 0)
hasFileAttached = true;
this.$.$applyAsync(() => {
this.dms.hasFileAttached = hasFileAttached;
});
}
onEdit() {
if (!this.dms.companyId)
throw new UserError(`The company can't be empty`);
if (!this.dms.warehouseId)
throw new UserError(`The warehouse can't be empty`);
if (!this.dms.dmsTypeId)
throw new UserError(`The DMS Type can't be empty`);
if (!this.dms.description)
throw new UserError(`The description can't be empty`);
const query = `dms/${this.dms.dmsId}/updateFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$t('Data saved!'));
if (res.data.length > 0) this.invoiceIn.dmsFk = res.data[0].id;
}
});
}
onCreate() {
if (!this.dms.companyId)
throw new UserError(`The company can't be empty`);
if (!this.dms.warehouseId)
throw new UserError(`The warehouse can't be empty`);
if (!this.dms.dmsTypeId)
throw new UserError(`The DMS Type can't be empty`);
if (!this.dms.description)
throw new UserError(`The description can't be empty`);
if (!this.dms.files)
throw new UserError(`The files can't be empty`);
const query = `Dms/uploadFile`;
const options = {
method: 'POST',
url: query,
params: this.dms,
headers: {
'Content-Type': undefined
},
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.dms.files
};
this.$http(options).then(res => {
if (res) {
this.vnApp.showSuccess(this.$t('Data saved!'));
if (res.data.length > 0) this.invoiceIn.dmsFk = res.data[0].id;
}
});
}
}
Controller.$inject = ['$element', '$scope', 'vnFile'];
ngModule.vnComponent('vnInvoiceInBasicData', { ngModule.vnComponent('vnInvoiceInBasicData', {
template: require('./index.html'), template: require('./index.html'),
controller: Section, controller: Controller,
bindings: { bindings: {
invoiceIn: '<' invoiceIn: '<'
} }

View File

@ -0,0 +1,102 @@
import './index.js';
import watcher from 'core/mocks/watcher';
describe('InvoiceIn', () => {
describe('Component vnInvoiceInBasicData', () => {
let controller;
let $scope;
let $httpBackend;
let $httpParamSerializer;
beforeEach(ngModule('invoiceIn'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_, _$httpParamSerializer_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
const $element = angular.element('<vn-invoice-in-basic-data></vn-invoice-in-basic-data>');
controller = $componentController('vnInvoiceInBasicData', {$element, $scope});
controller.$.watcher = watcher;
$httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond({});
}));
describe('onFileChange()', () => {
it('should set dms hasFileAttached property to true if has any files', () => {
const files = [{id: 1, name: 'MyFile'}];
controller.onFileChange(files);
$scope.$apply();
expect(controller.dms.hasFileAttached).toBeTruthy();
});
});
describe('checkFileExists()', () => {
it(`should return false if a file exists`, () => {
const fileIdExists = 1;
controller.checkFileExists(fileIdExists);
expect(controller.editDownloadDisabled).toBe(false);
});
});
describe('onEdit()', () => {
it(`should perform a POST query to edit the dms properties`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const dms = {
dmsId: 1,
reference: 'Ref1',
warehouseId: 1,
companyId: 442,
dmsTypeId: 20,
description: 'This is a description',
files: []
};
controller.dms = dms;
const serializedParams = $httpParamSerializer(controller.dms);
const query = `dms/${controller.dms.dmsId}/updateFile?${serializedParams}`;
$httpBackend.expectPOST(query).respond({});
controller.onEdit();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('onCreate()', () => {
it(`should perform a POST query to create a new dms`, () => {
jest.spyOn(controller.vnApp, 'showSuccess');
const dms = {
reference: 'Ref1',
warehouseId: 1,
companyId: 442,
dmsTypeId: 20,
description: 'This is a description',
files: [{
lastModified: 1668673957761,
lastModifiedDate: new Date(),
name: 'file-example.png',
size: 19653,
type: 'image/png',
webkitRelativePath: ''
}]
};
controller.dms = dms;
const serializedParams = $httpParamSerializer(controller.dms);
const query = `Dms/uploadFile?${serializedParams}`;
$httpBackend.expectPOST(query).respond({});
controller.onCreate();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1 @@
ContentTypesInfo: Allowed file types {{allowedContentTypes}}

View File

@ -0,0 +1,15 @@
Upload file: Subir fichero
Edit file: Editar fichero
Upload: Subir
Document: Documento
ContentTypesInfo: "Tipos de archivo permitidos: {{allowedContentTypes}}"
Generate identifier for original file: Generar identificador para archivo original
File management: Gestión documental
Hard copy: Copia
This file will be deleted: Este fichero va a ser borrado
Are you sure?: Estas seguro?
File deleted: Fichero eliminado
Remove file: Eliminar fichero
Download file: Descargar fichero
Edit document: Editar documento
Create document: Crear documento

View File

@ -21,3 +21,4 @@ Total net: Total neto
Total stems: Total tallos Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF Show agricultural invoice as PDF: Ver factura agrícola como PDF
Send agricultural invoice as PDF: Enviar factura agrícola como PDF Send agricultural invoice as PDF: Enviar factura agrícola como PDF
New InvoiceIn: Nueva Factura

View File

@ -64,7 +64,7 @@ describe('InvoiceOut filter()', () => {
const invoiceOut = await models.InvoiceOut.findById(1, null, options); const invoiceOut = await models.InvoiceOut.findById(1, null, options);
await invoiceOut.updateAttribute('hasPdf', true, options); await invoiceOut.updateAttribute('hasPdf', true, options);
const result = await models.InvoiceOut.filter(ctx, {}, options); const result = await models.InvoiceOut.filter(ctx, {id: invoiceOut.id}, options);
expect(result.length).toEqual(1); expect(result.length).toEqual(1);

View File

@ -0,0 +1,11 @@
concept: concept
quantity: quantity
price: price
discount: discount
reserved: reserved
isPicked: is picked
created: created
originalQuantity: original quantity
itemFk: item
ticketFk: ticket
saleFk: sale

View File

@ -0,0 +1,11 @@
concept: concepto
quantity: cantidad
price: precio
discount: descuento
reserved: reservado
isPicked: esta seleccionado
created: creado
originalQuantity: cantidad original
itemFk: artículo
ticketFk: ticket
saleFk: línea

View File

@ -0,0 +1,22 @@
shipped: shipped
landed: landed
nickname: nickname
location: location
solution: solution
packages: packages
updated: updated
isDeleted: is deleted
priority: priority
zoneFk: zone
zonePrice: zone price
zoneBonus: zone bonus
totalWithVat: total with vat
totalWithoutVat: total without vat
clientFk: client
warehouseFk: warehouse
refFk: reference
addressFk: address
routeFk: route
companyFk: company
agencyModeFk: agency
ticketFk: ticket

View File

@ -0,0 +1,22 @@
shipped: fecha salida
landed: fecha entrega
nickname: alias
location: ubicación
solution: solución
packages: embalajes
updated: fecha última actualización
isDeleted: esta eliminado
priority: prioridad
zoneFk: zona
zonePrice: precio zona
zoneBonus: bonus zona
totalWithVat: total con IVA
totalWithoutVat: total sin IVA
clientFk: cliente
warehouseFk: almacén
refFk: referencia
addressFk: dirección
routeFk: ruta
companyFk: empresa
agencyModeFk: agencia
ticketFk: ticket

View File

@ -88,6 +88,11 @@ module.exports = Self => {
type: 'boolean', type: 'boolean',
description: `Whether to show only tickets with route` description: `Whether to show only tickets with route`
}, },
{
arg: 'hasInvoice',
type: 'boolean',
description: `Whether to show only tickets with invoice`
},
{ {
arg: 'pending', arg: 'pending',
type: 'boolean', type: 'boolean',
@ -199,6 +204,10 @@ module.exports = Self => {
if (value == true) if (value == true)
return {'t.routeFk': {neq: null}}; return {'t.routeFk': {neq: null}};
return {'t.routeFk': null}; return {'t.routeFk': null};
case 'hasInvoice':
if (value == true)
return {'t.refFk': {neq: null}};
return {'t.refFk': null};
case 'pending': case 'pending':
return {'st.isNotValidated': value}; return {'st.isNotValidated': value};
case 'id': case 'id':

View File

@ -39,7 +39,7 @@ describe('ticket filter()', () => {
const filter = {}; const filter = {};
const result = await models.Ticket.filter(ctx, filter, options); const result = await models.Ticket.filter(ctx, filter, options);
expect(result.length).toEqual(6); expect(result.length).toBeGreaterThan(3);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {
@ -278,4 +278,42 @@ describe('ticket filter()', () => {
throw e; throw e;
} }
}); });
it('should return the tickets with the invoice on false', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}, args: {hasInvoice: true}};
const filter = {};
const result = await models.Ticket.filter(ctx, filter, options);
expect(result.length).toEqual(6);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
it('should return the tickets with the invoice on null', async() => {
const tx = await models.Ticket.beginTransaction({});
try {
const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}, args: {hasInvoice: null}};
const filter = {};
const result = await models.Ticket.filter(ctx, filter, options);
expect(result.length).toBeGreaterThanOrEqual(27);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
}); });

View File

@ -35,7 +35,8 @@ class Controller extends ModuleCard {
'credit', 'credit',
'email', 'email',
'phone', 'phone',
'mobile' 'mobile',
'hasElectronicInvoice',
], ],
include: { include: {
relation: 'salesPersonUser', relation: 'salesPersonUser',

View File

@ -65,7 +65,8 @@ class Controller extends Section {
'credit', 'credit',
'email', 'email',
'phone', 'phone',
'mobile' 'mobile',
'hasElectronicInvoice',
], ],
include: { include: {
relation: 'salesPersonUser', relation: 'salesPersonUser',
@ -243,6 +244,23 @@ class Controller extends Section {
makeInvoice() { makeInvoice() {
const params = {ticketsIds: [this.id]}; const params = {ticketsIds: [this.id]};
/*
This should call the notification sistem to insert a new notification
in te queue, yet to check how to handle user permissions,
as of 08-11-2022 every employee can insert a new notification in the queue
*/
const client = this.ticket.client;
if (client.hasElectronicInvoice) {
this.$http.post(`NotificationQueues`, {
notificationFk: 'invoiceElectronic',
authorFk: client.id,
}).then(a => {
this.vnApp.showSuccess(this.$t('Invoice sent'));
});
}
return this.$http.post(`Tickets/makeInvoice`, params) return this.$http.post(`Tickets/makeInvoice`, params)
.then(() => this.reload()) .then(() => this.reload())
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));

View File

@ -9,5 +9,6 @@ Send CSV Delivery Note: Enviar albarán en CSV
Send PDF Delivery Note: Enviar albarán en PDF Send PDF Delivery Note: Enviar albarán en PDF
Show Proforma: Ver proforma Show Proforma: Ver proforma
Refund all: Abonar todo Refund all: Abonar todo
Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}" The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente Transfer client: Transferir cliente

View File

@ -39,7 +39,8 @@ class Controller extends Descriptor {
'name', 'name',
'isActive', 'isActive',
'isFreezed', 'isFreezed',
'isTaxDataChecked' 'isTaxDataChecked',
'hasElectronicInvoice',
], ],
include: { include: {
relation: 'salesPersonUser', relation: 'salesPersonUser',

View File

@ -141,6 +141,11 @@
ng-model="filter.hasRoute" ng-model="filter.hasRoute"
triple-state="true"> triple-state="true">
</vn-check> </vn-check>
<vn-check
vn-one
label="Has invoice"
ng-model="filter.hasInvoice"
triple-state="true">
</vn-horizontal> </vn-horizontal>
<vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg"> <vn-horizontal class="vn-px-lg vn-pb-lg vn-mt-lg">
<vn-submit label="Search"></vn-submit> <vn-submit label="Search"></vn-submit>

View File

@ -13,6 +13,7 @@ Grouped States: Estado agrupado
Days onward: Días adelante Days onward: Días adelante
With problems: Con problemas With problems: Con problemas
Has route: Con ruta Has route: Con ruta
Has invoice: Con factura
Pending: Pendiente Pending: Pendiente
FREE: Libre FREE: Libre
DELIVERED: Servido DELIVERED: Servido

View File

@ -201,7 +201,7 @@
<vn-dialog <vn-dialog
vn-id="excludeDialog" vn-id="excludeDialog"
on-response="$ctrl.onExcludeResponse($response)" on-response="$ctrl.onExcludeResponse($response)"
message="{{$ctrl.isNew ? 'Exclusion' : 'Edit exclusion'}}" message="{{$ctrl.isNew ? 'Add exclusion' : 'Edit exclusion'}}"
on-open="$ctrl.onSearch($params)" on-open="$ctrl.onSearch($params)"
on-close="$ctrl.resetExclusions()"> on-close="$ctrl.resetExclusions()">
<tpl-body> <tpl-body>

View File

@ -8,3 +8,5 @@ All: Todo
Specific locations: Localizaciones concretas Specific locations: Localizaciones concretas
Locations where it is not distributed: Localizaciones en las que no se reparte Locations where it is not distributed: Localizaciones en las que no se reparte
You must select a location: Debes seleccionar una localización You must select a location: Debes seleccionar una localización
Add exclusion: Añadir exclusión
Edit exclusion: Editar exclusión

19651
package-lock.json generated

File diff suppressed because it is too large Load Diff