Merge branch 'dev' into 4613-Eliminar-cálculo-portfolio
gitea/salix/pipeline/head There was a failure building this commit Details

This commit is contained in:
Alex Moreno 2022-11-28 08:43:55 +00:00
commit e4bbb6e83a
54 changed files with 20750 additions and 257 deletions

View File

@ -32,7 +32,7 @@ module.exports = Self => {
let message = $t(`There's a new urgent ticket:`);
const ostUri = 'https://cau.verdnatura.es/scp/tickets.php?id=';
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({
@ -42,7 +42,5 @@ module.exports = Self => {
if (channelName)
return Self.send(ctx, `#${channelName}`, `@all ➔ ${message}`);
return;
};
};

View File

@ -27,7 +27,7 @@ describe('Chat notifyIssue()', () => {
subject: 'Issue title'}
]);
// eslint-disable-next-line max-len
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: *00001* - Issue title (@batman)](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const expectedMessage = `@all ➔ There's a new urgent ticket:\r\n[ID: 00001 - Issue title @batman](https://cau.verdnatura.es/scp/tickets.php?id=1)`;
const department = await app.models.Department.findById(departmentId);
let orgChatName = department.chatName;

View File

@ -4,4 +4,23 @@ module.exports = Self => {
require('../methods/chat/sendCheckingPresence')(Self);
require('../methods/chat/notifyIssues')(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(/(?<=\[)[a-zA-Z0-9_\-+!@#$%^&*()={};':"\\|,.<>/?\s]*(?=])/g);
if (!parts) return;
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),
(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),
(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`)
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`)
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');
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`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `account` /*!40100 DEFAULT CHARACTER SET utf8mb3 */;
USE `account`;

View File

@ -324,7 +324,8 @@ export default {
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)',
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: {
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"]',
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"]',
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]'
},
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', () => {
let browser;
let page;
let newDms;
beforeAll(async() => {
browser = await getBrowser();
@ -24,6 +25,8 @@ describe('InvoiceIn basic data path', () => {
await page.autocompleteSearch(selectors.invoiceInBasicData.supplier, 'Verdnatura');
await page.clearInput(selectors.invoiceInBasicData.supplierRef);
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.booked, now);
await page.autocompleteSearch(selectors.invoiceInBasicData.currency, 'USD');
@ -61,4 +64,141 @@ describe('InvoiceIn basic data path', () => {
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.separateIndex = true;
this.entityState = 'card.summary';
this.isIndex = false;
this.deregisterCallback = this.$transitions.onSuccess(
{}, transition => this.onStateChange(transition));
@ -102,6 +103,9 @@ export default class Searchbar extends Component {
filter = {};
}
let stateParts = this.$state.current.name.split('.');
this.isIndex = stateParts[1] == 'index';
this.doSearch(filter, 'state');
}
@ -198,7 +202,7 @@ export default class Searchbar extends Component {
}
doSearch(filter, source) {
if (filter === this.filter && source != 'state') return;
if (filter === this.filter && !this.isIndex) return;
let promise = this.onSearch({$params: filter});
promise = promise || this.$q.resolve();
promise.then(data => this.onFilter(filter, source, data));

View File

@ -1,7 +1,7 @@
{
"name": "salix-front",
"version": "1.0.0",
"lockfileVersion": 2,
"lockfileVersion": 1,
"requires": true,
"packages": {
"": {
@ -189,10 +189,14 @@
"integrity": "sha512-5qjkWIQQVsHj4Sb5TcEs4WZWpFeVFHXwxEBHUhrny41D8UrBAd6T/6nPPAsLngJCReIOqi95W3mxdveveutpZw=="
},
"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": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/angular-moment/-/angular-moment-1.3.0.tgz",
"integrity": "sha512-KG8rvO9MoaBLwtGnxTeUveSyNtrL+RNgGl1zqWN36+HDCCVGk2DGWOzqKWB6o+eTTbO3Opn4hupWKIElc8XETA==",
"requires": {
"moment": ">=2.8.0 <3.0.0"
}
@ -215,18 +219,26 @@
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
"sprintf-js": "~1.0.2"
}
},
"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": {
"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": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -234,6 +246,8 @@
},
"mg-crud": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/mg-crud/-/mg-crud-1.1.2.tgz",
"integrity": "sha512-mAR6t0aQHKnT0QHKHpLOi0kNPZfO36iMpIoiLjFHxuio6mIJyuveBJ4VNlNXJRxLh32/FLADEb41/sYo7QUKFw==",
"requires": {
"angular": "^1.6.1"
}
@ -244,19 +258,27 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/require-yaml/-/require-yaml-0.0.1.tgz",
"integrity": "sha512-M6eVEgLPRbeOhgSCnOTtdrOOEQzbXRchg24Xa13c39dMuraFKdI9emUo97Rih0YEFzSICmSKg8w4RQp+rd9pOQ==",
"requires": {
"js-yaml": ""
"js-yaml": "^4.1.0"
},
"dependencies": {
"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": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
@ -264,10 +286,14 @@
}
},
"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": {
"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) {
this._logs = value;
if (!value) return;
if (this.logs) {
this.logs.forEach(log => {
log.oldProperties = this.getInstance(log.oldInstance);
log.newProperties = this.getInstance(log.newInstance);
const validations = window.validations;
value.forEach(log => {
const locale = validations[log.changedModel].locale ? validations[log.changedModel].locale : {};
log.oldProperties = this.getInstance(log.oldInstance, locale);
log.newProperties = this.getInstance(log.newInstance, locale);
});
}
}
getInstance(instance) {
getInstance(instance, locale) {
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)?$/;
@ -51,7 +53,8 @@ export default class Controller extends Section {
if (validDate.test(instance[property]))
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;
}

View File

@ -1,3 +1,5 @@
const path = require('path');
const fs = require('fs');
module.exports = Self => {
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) {
let json = {};
let models = Self.app.models;
@ -49,9 +87,14 @@ module.exports = Self => {
jsonValidations[fieldName] = jsonField;
}
const modelLocale = modelsLocale.get(modelName);
const lang = ctx.req.getLocale();
const locale = modelLocale && modelLocale.get(lang);
json[modelName] = {
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 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}}})",
"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}}})",
"Already has this status": "Ya tiene este estado",
"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',
type: 'boolean'
},
{
arg: 'hasElectronicInvoice',
type: 'boolean'
}
],
returns: {
@ -122,19 +126,15 @@ module.exports = Self => {
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
const isSalesAssistant = await models.Account.hasRole(userId, 'salesAssistant', myOptions);
const client = await models.Client.findById(clientId, null, myOptions);
if (!isSalesAssistant && client.isTaxDataChecked)
throw new UserError(`Not enough privileges to edit a client with verified data`);
// Sage data validation
const taxDataChecked = args.isTaxDataChecked;
const sageTaxChecked = client.sageTaxTypeFk || args.sageTaxTypeFk;
@ -143,7 +143,6 @@ module.exports = Self => {
if (taxDataChecked && !hasSageData)
throw new UserError(`You need to fill sage information before you check verified data`);
if (args.despiteOfClient) {
const logRecord = {
originFk: clientId,
@ -158,7 +157,6 @@ module.exports = Self => {
await models.ClientLog.create(logRecord, myOptions);
}
// Remove unwanted properties
delete args.ctx;
delete args.id;

View File

@ -1,4 +1,5 @@
const {Email} = require('vn-print');
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethodCtx('balanceCompensationEmail', {
@ -10,7 +11,7 @@ module.exports = Self => {
type: 'Number',
required: true,
description: 'The receipt id',
http: { source: 'path' }
http: {source: 'path'}
}
],
returns: {
@ -23,18 +24,29 @@ module.exports = Self => {
}
});
Self.balanceCompensationEmail = async (ctx, id) => {
Self.balanceCompensationEmail = async(ctx, id) => {
const models = Self.app.models;
const receipt = await models.Receipt.findById(id, {fields: ['clientFk']});
const client = await models.Client.findById(receipt.clientFk, {fields:['email']});
const receipt = await models.Receipt.findById(id, {fields: ['clientFk', 'bankFk']});
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', {
lang: ctx.req.getLocale(),
recipient: client.email+',administracion@verdnatura.es',
recipient: client.email + ',administracion@verdnatura.es',
id
});
return email.send();
}
};
};

View File

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

View File

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

View File

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

View File

@ -151,5 +151,19 @@ describe('Client', () => {
$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}}
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>
<vn-watcher
vn-id="watcher"
data="$ctrl.client"
id-field="id"
form="form"
save="patch">
<vn-watcher vn-id="watcher" data="$ctrl.client" id-field="id" form="form" save="patch">
</vn-watcher>
<vn-crud-model
auto-load="true"
url="Provinces/location"
data="provincesLocation"
order="name">
<vn-crud-model auto-load="true" url="Provinces/location" data="provincesLocation" order="name">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="Countries"
data="countries"
order="country">
<vn-crud-model auto-load="true" url="Countries" data="countries" order="country">
</vn-crud-model>
<vn-crud-model
auto-load="true"
url="SageTaxTypes"
data="sageTaxTypes"
order="vat">
<vn-crud-model auto-load="true" url="SageTaxTypes" data="sageTaxTypes" order="vat">
</vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
vn-two
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-two 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-one
label="Tax number"
ng-model="$ctrl.client.fi"
rule>
<vn-textfield vn-one label="Tax number" ng-model="$ctrl.client.fi" rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-two
label="Street"
ng-model="$ctrl.client.street"
rule>
<vn-textfield vn-two label="Street" ng-model="$ctrl.client.street" rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTaxTypeFk"
data="sageTaxTypes"
show-field="vat"
value-field="id"
label="Sage tax type"
vn-acl="salesAssistant"
rule>
<vn-autocomplete vn-one ng-model="$ctrl.client.sageTaxTypeFk" data="sageTaxTypes" show-field="vat"
value-field="id" label="Sage tax type" vn-acl="salesAssistant" rule>
</vn-autocomplete>
<vn-autocomplete vn-one
ng-model="$ctrl.client.sageTransactionTypeFk"
url="SageTransactionTypes"
show-field="transaction"
value-field="id"
label="Sage transaction type"
<vn-autocomplete vn-one ng-model="$ctrl.client.sageTransactionTypeFk" url="SageTransactionTypes"
show-field="transaction" value-field="id" label="Sage transaction type"
search-function="{or: [{id: $search}, {transaction: {like: '%'+ $search +'%'}}]}"
vn-acl="salesAssistant"
order="transaction"
rule>
vn-acl="salesAssistant" order="transaction" rule>
<tpl-item>{{id}}: {{transaction}}</tpl-item>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-datalist vn-one
label="Postcode"
ng-model="$ctrl.client.postcode"
selection="$ctrl.postcode"
url="Postcodes/location"
fields="['code','townFk']"
order="code, townFk"
value-field="code"
show-field="code"
rule>
<vn-datalist vn-one label="Postcode" ng-model="$ctrl.client.postcode" selection="$ctrl.postcode"
url="Postcodes/location" fields="['code','townFk']" order="code, townFk" value-field="code"
show-field="code" rule>
<tpl-item>
{{code}} - {{town.name}} ({{town.province.name}},
{{town.province.country.country}})
</tpl-item>
<append>
<vn-icon-button
icon="add_circle"
vn-tooltip="New postcode"
ng-click="postcode.open()"
vn-acl="deliveryBoss"
vn-acl-action="remove">
<vn-icon-button icon="add_circle" vn-tooltip="New postcode" ng-click="postcode.open()"
vn-acl="deliveryBoss" vn-acl-action="remove">
</vn-icon-button>
</append>
</vn-datalist>
<vn-datalist vn-id="town" vn-one
label="City"
ng-model="$ctrl.client.city"
selection="$ctrl.town"
url="Towns/location"
fields="['id', 'name', 'provinceFk']"
show-field="name"
value-field="name">
<vn-datalist vn-id="town" vn-one label="City" ng-model="$ctrl.client.city" selection="$ctrl.town"
url="Towns/location" fields="['id', 'name', 'provinceFk']" show-field="name" value-field="name">
<tpl-item>
{{name}}, {{province.name}}
({{province.country.country}})
@ -114,112 +54,64 @@
</vn-datalist>
</vn-horizontal>
<vn-horizontal>
<vn-autocomplete vn-id="province" vn-one
label="Province"
ng-model="$ctrl.client.provinceFk"
selection="$ctrl.province"
data="provincesLocation"
fields="['id', 'name', 'countryFk']"
show-field="name"
value-field="id"
rule>
<vn-autocomplete vn-id="province" vn-one label="Province" ng-model="$ctrl.client.provinceFk"
selection="$ctrl.province" data="provincesLocation" fields="['id', 'name', 'countryFk']"
show-field="name" value-field="id" rule>
<tpl-item>{{name}} ({{country.country}})</tpl-item>
</vn-autocomplete>
<vn-autocomplete vn-id="country" vn-one
ng-model="$ctrl.client.countryFk"
data="countries"
show-field="country"
value-field="id"
label="Country"
rule>
<vn-autocomplete vn-id="country" vn-one ng-model="$ctrl.client.countryFk" data="countries"
show-field="country" value-field="id" label="Country" rule>
</vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Active"
ng-model="$ctrl.client.isActive">
<vn-check vn-one label="Active" ng-model="$ctrl.client.isActive">
</vn-check>
<vn-check
vn-one
label="Frozen"
ng-model="$ctrl.client.isFreezed">
<vn-check vn-one label="Frozen" ng-model="$ctrl.client.isFreezed">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Has to invoice"
ng-model="$ctrl.client.hasToInvoice">
<vn-check vn-one label="Has to invoice" ng-model="$ctrl.client.hasToInvoice">
</vn-check>
<vn-check
vn-one
label="Vies"
ng-model="$ctrl.client.isVies">
<vn-check vn-one label="Vies" ng-model="$ctrl.client.isVies">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Notify by email"
ng-model="$ctrl.client.isToBeMailed">
<vn-check vn-one label="Notify by email" ng-model="$ctrl.client.isToBeMailed">
</vn-check>
<vn-check
vn-one
label="Invoice by address"
ng-model="$ctrl.client.hasToInvoiceByAddress">
<vn-check vn-one label="Invoice by address" ng-model="$ctrl.client.hasToInvoiceByAddress">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Is equalizated"
ng-model="$ctrl.client.isEqualizated"
<vn-check 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."
on-change="$ctrl.onChangeEqualizated(value)">
</vn-check>
<vn-check
vn-one
label="Verified data"
ng-model="$ctrl.client.isTaxDataChecked"
vn-acl="salesAssistant">
<vn-check vn-one label="Verified data" ng-model="$ctrl.client.isTaxDataChecked" vn-acl="salesAssistant">
</vn-check>
</vn-horizontal>
<vn-horizontal>
<vn-check
vn-one
label="Incoterms authorization"
ng-model="$ctrl.client.hasIncoterms"
<vn-check vn-one label="Incoterms authorization" ng-model="$ctrl.client.hasIncoterms"
vn-acl="administrative">
</vn-check>
<vn-check vn-one label="Electronic invoice" ng-model="$ctrl.client.hasElectronicInvoice"
vn-acl="administrative">
</vn-check>
</vn-horizontal>
</vn-card>
<vn-button-bar>
<vn-submit
disabled="!watcher.dataChanged()"
label="Save">
<vn-submit disabled="!watcher.dataChanged()" label="Save">
</vn-submit>
<vn-button
class="cancel"
label="Undo changes"
disabled="!watcher.dataChanged()"
<vn-button class="cancel" label="Undo changes" disabled="!watcher.dataChanged()"
ng-click="watcher.loadOriginalData()">
</vn-button>
</vn-button-bar>
</form>
<vn-confirm
vn-id="propagate-isEqualizated"
question="You changed the equalization tax"
message="Do you want to spread the change?"
on-accept="$ctrl.onAcceptEt()">
<vn-confirm vn-id="propagate-isEqualizated" question="You changed the equalization tax"
message="Do you want to spread the change?" on-accept="$ctrl.onAcceptEt()">
</vn-confirm>
<vn-confirm
vn-id="confirm-duplicatedClient"
message="Found a client with this data"
<vn-confirm vn-id="confirm-duplicatedClient" message="Found a client with this data"
on-accept="$ctrl.onAcceptDuplication()">
</vn-confirm>
<!-- New postcode dialog -->
<vn-geo-postcode
vn-id="postcode"
on-response="$ctrl.onResponse($response)">
<vn-geo-postcode vn-id="postcode" on-response="$ctrl.onResponse($response)">
</vn-geo-postcode>

View File

@ -11,3 +11,4 @@ Sage transaction type: Tipo de transacción Sage
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
Incoterms authorization: Autorización incoterms
Electronic invoice: Factura electrónica

View File

@ -5,6 +5,24 @@
form="form"
save="patch">
</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">
<vn-card class="vn-pa-lg">
<vn-horizontal>
@ -57,6 +75,36 @@
{{id}} - {{name}}
</tpl-item>
</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-date-picker
@ -105,3 +153,163 @@
</vn-button>
</vn-button-bar>
</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 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', {
template: require('./index.html'),
controller: Section,
controller: Controller,
bindings: {
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
Show agricultural invoice as PDF: Ver 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);
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);

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

View File

@ -39,7 +39,7 @@ describe('ticket filter()', () => {
const filter = {};
const result = await models.Ticket.filter(ctx, filter, options);
expect(result.length).toEqual(6);
expect(result.length).toBeGreaterThan(3);
await tx.rollback();
} catch (e) {
@ -278,4 +278,42 @@ describe('ticket filter()', () => {
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',
'email',
'phone',
'mobile'
'mobile',
'hasElectronicInvoice',
],
include: {
relation: 'salesPersonUser',

View File

@ -65,7 +65,8 @@ class Controller extends Section {
'credit',
'email',
'phone',
'mobile'
'mobile',
'hasElectronicInvoice',
],
include: {
relation: 'salesPersonUser',
@ -243,6 +244,23 @@ class Controller extends Section {
makeInvoice() {
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)
.then(() => this.reload())
.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
Show Proforma: Ver proforma
Refund all: Abonar todo
Invoice sent: Factura enviada
The following refund ticket have been created: "Se ha creado siguiente ticket de abono: {{ticketId}}"
Transfer client: Transferir cliente

View File

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

View File

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

View File

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

View File

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

View File

@ -8,3 +8,5 @@ All: Todo
Specific locations: Localizaciones concretas
Locations where it is not distributed: Localizaciones en las que no se reparte
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