Merge branch 'dev' into 3328-item_search-panel
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Carlos Jimenez Ruiz 2021-11-15 09:34:48 +00:00
commit d69ee4a597
35 changed files with 553 additions and 362 deletions

View File

@ -0,0 +1,4 @@
ALTER TABLE vn.payMethod CHANGE ibanRequired ibanRequiredForClients tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod ADD ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL;
ALTER TABLE vn.payMethod CHANGE ibanRequiredForSuppliers ibanRequiredForSuppliers tinyint(3) DEFAULT 0 NULL AFTER ibanRequiredForClients;
UPDATE vn.payMethod SET ibanRequiredForSuppliers = 1 WHERE code = 'wireTransfer';

View File

@ -217,14 +217,14 @@ UPDATE `vn`.`agencyMode` SET `web` = 1, `reportMail` = 'no-reply@gothamcity.com'
UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23; UPDATE `vn`.`agencyMode` SET `code` = 'refund' WHERE `id` = 23;
INSERT INTO `vn`.`payMethod`(`id`,`code`, `name`, `graceDays`, `outstandingDebt`, `ibanRequired`) INSERT INTO `vn`.`payMethod`(`id`,`code`, `name`, `graceDays`, `outstandingDebt`, `ibanRequiredForClients`, `ibanRequiredForSuppliers`)
VALUES VALUES
(1, NULL, 'PayMethod one', 0, 001, 0), (1, NULL, 'PayMethod one', 0, 001, 0, 0),
(2, NULL, 'PayMethod two', 10, 001, 0), (2, NULL, 'PayMethod two', 10, 001, 0, 0),
(3, 'compensation', 'PayMethod three', 0, 001, 0), (3, 'compensation', 'PayMethod three', 0, 001, 0, 0),
(4, NULL, 'PayMethod with IBAN', 0, 001, 1), (4, NULL, 'PayMethod with IBAN', 0, 001, 1, 0),
(5, NULL, 'PayMethod five', 10, 001, 0), (5, NULL, 'PayMethod five', 10, 001, 0, 0),
(8,'wireTransfer', 'WireTransfer', 5, 001, 1); (8,'wireTransfer', 'WireTransfer', 5, 001, 1, 1);
INSERT INTO `vn`.`payDem`(`id`, `payDem`) INSERT INTO `vn`.`payDem`(`id`, `payDem`)
VALUES VALUES

View File

@ -33928,7 +33928,8 @@ CREATE TABLE `payMethod` (
`solution` varchar(1) COLLATE utf8_unicode_ci DEFAULT NULL, `solution` varchar(1) COLLATE utf8_unicode_ci DEFAULT NULL,
`outstandingDebt` tinyint(3) unsigned zerofill NOT NULL DEFAULT '000', `outstandingDebt` tinyint(3) unsigned zerofill NOT NULL DEFAULT '000',
`graceDays` int(11) unsigned NOT NULL DEFAULT '0', `graceDays` int(11) unsigned NOT NULL DEFAULT '0',
`ibanRequired` tinyint(3) DEFAULT '0', `ibanRequiredForClients` tinyint(3) DEFAULT '0',
`ibanRequiredForSuppliers` tinyint(3) DEFAULT '0',
`isNotified` tinyint(3) NOT NULL DEFAULT '1', `isNotified` tinyint(3) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; ) ENGINE=InnoDBDEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

View File

@ -119,6 +119,7 @@ export default {
name: 'vn-client-create vn-textfield[ng-model="$ctrl.client.name"]', name: 'vn-client-create vn-textfield[ng-model="$ctrl.client.name"]',
taxNumber: 'vn-client-create vn-textfield[ng-model="$ctrl.client.fi"]', taxNumber: 'vn-client-create vn-textfield[ng-model="$ctrl.client.fi"]',
socialName: 'vn-client-create vn-textfield[ng-model="$ctrl.client.socialName"]', socialName: 'vn-client-create vn-textfield[ng-model="$ctrl.client.socialName"]',
businessType: 'vn-client-create vn-autocomplete[ng-model="$ctrl.client.businessTypeFk"]',
street: 'vn-client-create vn-textfield[ng-model="$ctrl.client.street"]', street: 'vn-client-create vn-textfield[ng-model="$ctrl.client.street"]',
addPostCode: 'vn-client-create vn-datalist[ng-model="$ctrl.client.postcode"] vn-icon-button[icon="add_circle"]', addPostCode: 'vn-client-create vn-datalist[ng-model="$ctrl.client.postcode"] vn-icon-button[icon="add_circle"]',
addProvince: 'vn-autocomplete[ng-model="$ctrl.location.provinceFk"] vn-icon-button[icon="add_circle"]', addProvince: 'vn-autocomplete[ng-model="$ctrl.location.provinceFk"] vn-icon-button[icon="add_circle"]',

View File

@ -27,16 +27,19 @@ describe('Client create path', () => {
await page.waitForState('client.create'); await page.waitForState('client.create');
}); });
it('should receive an error when clicking the create button having name and Business name fields empty', async() => { it('should receive an error when clicking the create button having name and Business name fields empty',
async() => {
await page.write(selectors.createClientView.taxNumber, '74451390E'); await page.write(selectors.createClientView.taxNumber, '74451390E');
await page.write(selectors.createClientView.userName, 'CaptainMarvel'); await page.write(selectors.createClientView.userName, 'CaptainMarvel');
await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es'); await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es');
await page.autocompleteSearch(selectors.createClientView.salesPerson, 'salesPerson'); await page.autocompleteSearch(selectors.createClientView.salesPerson, 'salesPerson');
await page.autocompleteSearch(selectors.createClientView.businessType, 'florist');
await page.waitToClick(selectors.createClientView.createButton); await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar(); const message = await page.waitForSnackbar();
expect(message.text).toContain('Some fields are invalid'); expect(message.text).toContain('Some fields are invalid');
}); }
);
it(`should create a new province`, async() => { it(`should create a new province`, async() => {
await page.waitToClick(selectors.createClientView.addPostCode); await page.waitToClick(selectors.createClientView.addPostCode);
@ -80,9 +83,18 @@ describe('Client create path', () => {
expect(message.text).toContain('Some fields are invalid'); expect(message.text).toContain('Some fields are invalid');
}); });
it(`should attempt to create a new user with all it's data but wrong postal code`, async() => { it(`should attempt to create a new user with all it's data but wrong business type`, async() => {
await page.clearInput(selectors.createClientView.email); await page.clearInput(selectors.createClientView.email);
await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es'); await page.write(selectors.createClientView.email, 'CarolDanvers@verdnatura.es');
await page.clearInput(selectors.createClientView.businessType);
await page.waitToClick(selectors.createClientView.createButton);
const message = await page.waitForSnackbar();
expect(message.text).toContain('Some fields are invalid');
});
it(`should attempt to create a new user with all it's data but wrong postal code`, async() => {
await page.autocompleteSearch(selectors.createClientView.businessType, 'florist');
await page.clearInput(selectors.createClientView.postcode); await page.clearInput(selectors.createClientView.postcode);
await page.write(selectors.createClientView.postcode, '479999'); await page.write(selectors.createClientView.postcode, '479999');
await page.waitToClick(selectors.createClientView.createButton); await page.waitToClick(selectors.createClientView.createButton);

View File

@ -117,5 +117,6 @@
"INACTIVE_PROVIDER": "Inactive provider", "INACTIVE_PROVIDER": "Inactive provider",
"reference duplicated": "reference duplicated", "reference duplicated": "reference duplicated",
"The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option", "The PDF document does not exists": "The PDF document does not exists. Try regenerating it from 'Regenerate invoice PDF' option",
"This item is not available": "This item is not available" "This item is not available": "This item is not available",
"Deny buy request": "Purchase request for ticket id [{{ticketId}}]({{{url}}}) has been rejected. Reason: {{observation}}"
} }

View File

@ -133,6 +133,7 @@
"reserved": "reservado", "reserved": "reservado",
"Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}", "Changed sale reserved state": "He cambiado el estado reservado de las siguientes lineas al ticket [{{ticketId}}]({{{ticketUrl}}}): {{{changes}}}",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})", "Bought units from buy request": "Se ha comprado {{quantity}} unidades de [{{itemId}} {{concept}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
"Deny buy request":"Se ha rechazado la petición de compra para el ticket id [{{ticketId}}]({{{url}}}). Motivo: {{observation}}",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*", "MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})", "Changed client paymethod": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})", "Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",

View File

@ -50,7 +50,8 @@ module.exports = function(Self) {
city: data.city, city: data.city,
provinceFk: data.provinceFk, provinceFk: data.provinceFk,
countryFk: data.countryFk, countryFk: data.countryFk,
isEqualizated: data.isEqualizated isEqualizated: data.isEqualizated,
businessTypeFk: data.businessTypeFk
}, myOptions); }, myOptions);
const address = await models.Address.create({ const address = await models.Address.create({

View File

@ -8,7 +8,8 @@ describe('Client Create', () => {
name: 'Wade', name: 'Wade',
socialName: 'Deadpool Marvel', socialName: 'Deadpool Marvel',
street: 'Wall Street', street: 'Wall Street',
city: 'New York' city: 'New York',
businessTypeFk: 'florist'
}; };
it(`should not find Deadpool as he's not created yet`, async() => { it(`should not find Deadpool as he's not created yet`, async() => {
@ -45,6 +46,7 @@ describe('Client Create', () => {
expect(client.email).toEqual(newAccount.email); expect(client.email).toEqual(newAccount.email);
expect(client.fi).toEqual(newAccount.fi); expect(client.fi).toEqual(newAccount.fi);
expect(client.socialName).toEqual(newAccount.socialName); expect(client.socialName).toEqual(newAccount.socialName);
expect(client.businessTypeFk).toEqual(newAccount.businessTypeFk);
await tx.rollback(); await tx.rollback();
} catch (e) { } catch (e) {

View File

@ -129,7 +129,7 @@ module.exports = Self => {
function hasIban(err, done) { function hasIban(err, done) {
Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => { Self.app.models.PayMethod.findById(this.payMethodFk, (_, instance) => {
if (instance && instance.ibanRequired && !this.iban) if (instance && instance.ibanRequiredForClients && !this.iban)
err(); err();
done(); done();
}); });

View File

@ -130,6 +130,13 @@
"mysql": { "mysql": {
"columnName": "transactionTypeSageFk" "columnName": "transactionTypeSageFk"
} }
},
"businessTypeFk": {
"type": "string",
"mysql": {
"columnName": "businessTypeFk"
},
"required": true
} }
}, },
"relations": { "relations": {

View File

@ -25,7 +25,10 @@
"outstandingDebt": { "outstandingDebt": {
"type": "Number" "type": "Number"
}, },
"ibanRequired": { "ibanRequiredForClients": {
"type": "boolean"
},
"ibanRequiredForSuppliers": {
"type": "boolean" "type": "boolean"
} }
} }

View File

@ -19,7 +19,7 @@
vn-acl="salesAssistant" vn-acl="salesAssistant"
ng-model="$ctrl.client.payMethodFk" ng-model="$ctrl.client.payMethodFk"
data="paymethods" data="paymethods"
fields="['ibanRequired']" fields="['ibanRequiredForClients']"
initial-data="$ctrl.client.payMethod"> initial-data="$ctrl.client.payMethod">
</vn-autocomplete> </vn-autocomplete>
<vn-input-number <vn-input-number

View File

@ -26,18 +26,28 @@
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal> </vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-autocomplete
vn-two vn-id="businessTypeFk"
label="Business name" ng-model="$ctrl.client.businessTypeFk"
ng-model="$ctrl.client.socialName" url="BusinessTypes"
show-field="description"
value-field="code"
label="Business type"
rule> rule>
</vn-textfield> </vn-autocomplete>
<vn-textfield <vn-textfield
label="Tax number" label="Tax number"
ng-model="$ctrl.client.fi" ng-model="$ctrl.client.fi"
rule> rule>
</vn-textfield> </vn-textfield>
</vn-horizontal> </vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Business name"
ng-model="$ctrl.client.socialName"
rule>
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-textfield <vn-textfield
vn-two vn-two

View File

@ -56,7 +56,7 @@ module.exports = Self => {
{ {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'socialName'] fields: ['id', 'socialName', 'email']
} }
} }
] ]

View File

@ -0,0 +1,139 @@
<vn-icon-button
icon="more_vert"
vn-popover="menu">
</vn-icon-button>
<vn-menu vn-id="menu">
<vn-list>
<vn-item class="dropdown"
vn-click-stop="showInvoiceMenu.show($event, 'left')"
name="showInvoicePdf"
translate>
Show invoice...
<vn-menu vn-id="showInvoiceMenu">
<vn-list>
<a class="vn-item"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
target="_blank"
name="showInvoicePdf"
translate>
Show as PDF
</a>
<vn-item
ng-click="$ctrl.showCsvInvoice()"
translate>
Show as CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item class="dropdown"
vn-click-stop="sendInvoiceMenu.show($event, 'left')"
name="sendInvoice"
translate>
Send invoice...
<vn-menu vn-id="sendInvoiceMenu">
<vn-list>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send PDF
</vn-item>
<vn-item
ng-click="sendCsvConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="invoicing"
vn-acl-action="remove"
name="deleteInvoice"
translate>
Delete Invoice
</vn-item>
<vn-item
ng-click="bookConfirmation.show()"
vn-acl="invoicing"
vn-acl-action="remove"
name="bookInvoice"
translate>
Book invoice
</vn-item>
<vn-item
ng-click="createInvoicePdfConfirmation.show()"
ng-show="$ctrl.hasInvoicing || !$ctrl.invoiceOut.hasPdf"
name="regenerateInvoice"
translate>
{{!$ctrl.invoiceOut.hasPdf ? 'Generate PDF invoice': 'Regenerate PDF invoice'}}
</vn-item>
<vn-item
ng-click="$ctrl.showExportationLetter()"
ng-show="$ctrl.invoiceOut.serial == 'E'"
translate>
Show CIES letter
</vn-item>
</vn-list>
</vn-menu>
<vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteInvoiceOut()"
question="Are you sure you want to delete this invoice?">
</vn-confirm>
<vn-confirm
vn-id="bookConfirmation"
on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?">
</vn-confirm>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<!-- Create invoice PDF confirmation dialog -->
<vn-confirm
vn-id="createInvoicePdfConfirmation"
on-accept="$ctrl.createPdfInvoice()"
question="Are you sure you want to generate/regenerate the PDF invoice?"
message="Generate PDF invoice document">
</vn-confirm>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Send CSV invoice confirmation popup -->
<vn-dialog
vn-id="sendCsvConfirmation"
on-accept="$ctrl.sendCsvInvoice($data)"
message="Send CSV invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendCsvConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,121 @@
import ngModule from '../module';
import Section from 'salix/components/section';
import './style.scss';
class Controller extends Section {
constructor($element, $, vnReport, vnEmail) {
super($element, $);
this.vnReport = vnReport;
this.vnEmail = vnEmail;
}
get invoiceOut() {
return this._invoiceOut;
}
set invoiceOut(value) {
this._invoiceOut = value;
if (value)
this.id = value.id;
}
loadData() {
const filter = {
include: [
{
relation: 'company',
scope: {
fields: ['id', 'code']
}
}, {
relation: 'client',
scope: {
fields: ['id', 'name', 'email']
}
}
]
};
return this.$http.get(`InvoiceOuts/${this.invoiceOut.id}`, {filter})
.then(res => this.invoiceOut = res.data);
}
reload() {
return this.loadData().then(() => {
if (this.parentReload)
this.parentReload();
});
}
cardReload() {
// Prevents error when not defined
}
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createPdfInvoice() {
return this.$http.post(`InvoiceOuts/${this.id}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
showCsvInvoice() {
this.vnReport.showCsv('invoice', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id
});
}
sendPdfInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.send('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
sendCsvInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.sendCsv('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
showExportationLetter() {
this.vnReport.show('exportation', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id
});
}
}
Controller.$inject = ['$element', '$scope', 'vnReport', 'vnEmail'];
ngModule.vnComponent('vnInvoiceOutDescriptorMenu', {
template: require('./index.html'),
controller: Controller,
bindings: {
invoiceOut: '<',
parentReload: '&'
}
});

View File

@ -0,0 +1,96 @@
import './index';
describe('vnInvoiceOutDescriptorMenu', () => {
let controller;
let $httpBackend;
let $httpParamSerializer;
const invoiceOut = {
id: 1,
client: {id: 1101}
};
beforeEach(ngModule('invoiceOut'));
beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnInvoiceOutDescriptorMenu', {$element: null});
}));
describe('createPdfInvoice()', () => {
it('should make a query to the createPdf() endpoint and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createPdfInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('showCsvInvoice()', () => {
it('should make a query to the csv invoice download endpoint and show a message snackbar', () => {
jest.spyOn(window, 'open').mockReturnThis();
controller.invoiceOut = invoiceOut;
const expectedParams = {
invoiceId: invoiceOut.id,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/csv/invoice/download?${serializedParams}`;
controller.showCsvInvoice();
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`email/invoice?${serializedParams}`).respond();
controller.sendPdfInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
describe('sendCsvInvoice()', () => {
it('should make a query to the csv invoice send endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`csv/invoice/send?${serializedParams}`).respond();
controller.sendCsvInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,17 @@
Show invoice...: Ver factura...
Send invoice...: Enviar factura...
Send PDF invoice: Enviar factura en PDF
Send CSV invoice: Enviar factura en CSV
Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura
Book invoice: Asentar factura
Generate PDF invoice: Generar PDF factura
Show CIES letter: Ver carta CIES
InvoiceOut deleted: Factura eliminada
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura?
InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
The email can't be empty: El correo no puede estar vacío

View File

@ -0,0 +1,24 @@
@import "./effects";
@import "variables";
vn-invoice-out-descriptor-menu {
& > vn-icon-button[icon="more_vert"] {
display: flex;
min-width: 45px;
height: 45px;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
& > vn-icon-button[icon="more_vert"] {
@extend %clickable;
color: inherit;
& > vn-icon {
padding: 10px;
}
vn-icon {
font-size: 1.75rem;
}
}
}

View File

@ -1,81 +1,12 @@
<vn-descriptor-content <vn-descriptor-content
module="invoiceOut" module="invoiceOut"
description="$ctrl.invoiceOut.ref"> description="$ctrl.invoiceOut.ref">
<slot-menu> <slot-dot-menu>
<vn-item class="dropdown" <vn-invoice-out-descriptor-menu
vn-click-stop="showInvoiceMenu.show($event, 'left')" invoice-out="$ctrl.invoiceOut"
name="showInvoicePdf" parent-reload="$ctrl.reload()"
translate> />
Show invoice... </slot-dot-menu>
<vn-menu vn-id="showInvoiceMenu">
<vn-list>
<a class="vn-item"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
target="_blank"
name="showInvoicePdf"
translate>
Show as PDF
</a>
<vn-item
ng-click="$ctrl.showCsvInvoice()"
translate>
Show as CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item class="dropdown"
vn-click-stop="sendInvoiceMenu.show($event, 'left')"
name="sendInvoice"
translate>
Send invoice...
<vn-menu vn-id="sendInvoiceMenu">
<vn-list>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send PDF
</vn-item>
<vn-item
ng-click="sendCsvConfirmation.show({email: $ctrl.invoiceOut.client.email})"
translate>
Send CSV
</vn-item>
</vn-list>
</vn-menu>
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="invoicing"
vn-acl-action="remove"
name="deleteInvoice"
translate>
Delete Invoice
</vn-item>
<vn-item
ng-click="bookConfirmation.show()"
vn-acl="invoicing"
vn-acl-action="remove"
name="bookInvoice"
translate>
Book invoice
</vn-item>
<vn-item
ng-click="createInvoicePdfConfirmation.show()"
ng-show="$ctrl.hasInvoicing || !$ctrl.invoiceOut.hasPdf"
name="regenerateInvoice"
translate>
{{!$ctrl.invoiceOut.hasPdf ? 'Generate PDF invoice': 'Regenerate PDF invoice'}}
</vn-item>
<vn-item
ng-click="$ctrl.showExportationLetter()"
ng-show="$ctrl.invoiceOut.serial == 'E'"
translate>
Show CIES letter
</vn-item>
</slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
<vn-label-value <vn-label-value
@ -119,58 +50,3 @@
</div> </div>
</slot-body> </slot-body>
</vn-descriptor-content> </vn-descriptor-content>
<vn-confirm
vn-id="deleteConfirmation"
on-accept="$ctrl.deleteInvoiceOut()"
question="Are you sure you want to delete this invoice?">
</vn-confirm>
<vn-confirm
vn-id="bookConfirmation"
on-accept="$ctrl.bookInvoiceOut()"
question="Are you sure you want to book this invoice?">
</vn-confirm>
<vn-client-descriptor-popover
vn-id="clientDescriptor">
</vn-client-descriptor-popover>
<!-- Create invoice PDF confirmation dialog -->
<vn-confirm
vn-id="createInvoicePdfConfirmation"
on-accept="$ctrl.createPdfInvoice()"
question="Are you sure you want to generate/regenerate the PDF invoice?"
message="Generate PDF invoice document">
</vn-confirm>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>
<!-- Send CSV invoice confirmation popup -->
<vn-dialog
vn-id="sendCsvConfirmation"
on-accept="$ctrl.sendCsvInvoice($data)"
message="Send CSV invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
ng-model="sendCsvConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -41,70 +41,6 @@ class Controller extends Descriptor {
return this.getData(`InvoiceOuts/${this.id}`, {filter}) return this.getData(`InvoiceOuts/${this.id}`, {filter})
.then(res => this.entity = res.data); .then(res => this.entity = res.data);
} }
reload() {
return this.loadData().then(() => {
if (this.cardReload)
this.cardReload();
});
}
cardReload() {
// Prevents error when not defined
}
deleteInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.id}/delete`)
.then(() => this.$state.go('invoiceOut.index'))
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut deleted')));
}
bookInvoiceOut() {
return this.$http.post(`InvoiceOuts/${this.invoiceOut.ref}/book`)
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
createPdfInvoice() {
const invoiceId = this.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload())
.then(() => {
const snackbarMessage = this.$t(
`The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
showCsvInvoice() {
this.vnReport.showCsv('invoice', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id,
});
}
sendPdfInvoice($data) {
return this.vnEmail.send('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
sendCsvInvoice($data) {
return this.vnEmail.sendCsv('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
showExportationLetter() {
this.vnReport.show('exportation', {
recipientId: this.invoiceOut.client.id,
invoiceId: this.id,
});
}
} }
ngModule.vnComponent('vnInvoiceOutDescriptor', { ngModule.vnComponent('vnInvoiceOutDescriptor', {
@ -112,6 +48,5 @@ ngModule.vnComponent('vnInvoiceOutDescriptor', {
controller: Controller, controller: Controller,
bindings: { bindings: {
invoiceOut: '<', invoiceOut: '<',
cardReload: '&'
} }
}); });

View File

@ -3,17 +3,11 @@ import './index';
describe('vnInvoiceOutDescriptor', () => { describe('vnInvoiceOutDescriptor', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
let $httpParamSerializer;
const invoiceOut = {
id: 1,
client: {id: 1101}
};
beforeEach(ngModule('invoiceOut')); beforeEach(ngModule('invoiceOut'));
beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => { beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnInvoiceOutDescriptor', {$element: null}); controller = $componentController('vnInvoiceOutDescriptor', {$element: null});
})); }));
@ -29,81 +23,4 @@ describe('vnInvoiceOutDescriptor', () => {
expect(controller.invoiceOut).toEqual(response); expect(controller.invoiceOut).toEqual(response);
}); });
}); });
describe('createPdfInvoice()', () => {
it('should make a query to the createPdf() endpoint and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
controller.invoiceOut = invoiceOut;
$httpBackend.whenGET(`InvoiceOuts/${invoiceOut.id}`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond();
controller.createPdfInvoice();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
});
});
describe('showCsvInvoice()', () => {
it('should make a query to the csv invoice download endpoint and show a message snackbar', () => {
jest.spyOn(window, 'open').mockReturnThis();
controller.invoiceOut = invoiceOut;
const expectedParams = {
invoiceId: invoiceOut.id,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
const expectedPath = `api/csv/invoice/download?${serializedParams}`;
controller.showCsvInvoice();
expect(window.open).toHaveBeenCalledWith(expectedPath);
});
});
describe('sendPdfInvoice()', () => {
it('should make a query to the email invoice endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`email/invoice?${serializedParams}`).respond();
controller.sendPdfInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
describe('sendCsvInvoice()', () => {
it('should make a query to the csv invoice send endpoint and show a message snackbar', () => {
jest.spyOn(controller.vnApp, 'showMessage');
controller.invoiceOut = invoiceOut;
const $data = {email: 'brucebanner@gothamcity.com'};
const expectedParams = {
invoiceId: invoiceOut.id,
recipient: $data.email,
recipientId: invoiceOut.client.id
};
const serializedParams = $httpParamSerializer(expectedParams);
$httpBackend.expectGET(`csv/invoice/send?${serializedParams}`).respond();
controller.sendCsvInvoice($data);
$httpBackend.flush();
expect(controller.vnApp.showMessage).toHaveBeenCalled();
});
});
}); });

View File

@ -1,20 +1,2 @@
Volume exceded: Volumen excedido
Volume: Volumen
Client card: Ficha del cliente Client card: Ficha del cliente
Invoice ticket list: Listado de tickets de la factura Invoice ticket list: Listado de tickets de la factura
Show invoice...: Ver factura...
Send invoice...: Enviar factura...
Send PDF invoice: Enviar factura en PDF
Send CSV invoice: Enviar factura en CSV
Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura
Book invoice: Asentar factura
Generate PDF invoice: Generar PDF factura
Show CIES letter: Ver carta CIES
InvoiceOut deleted: Factura eliminada
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Are you sure you want to clone this invoice?: Estas seguro de clonar esta factura?
InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
Regenerate PDF invoice: Regenerar PDF factura
The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado

View File

@ -7,5 +7,6 @@ import './summary';
import './card'; import './card';
import './descriptor'; import './descriptor';
import './descriptor-popover'; import './descriptor-popover';
import './descriptor-menu';
import './index/manual'; import './index/manual';
import './index/global-invoicing'; import './index/global-invoicing';

View File

@ -13,6 +13,10 @@
<vn-icon-button icon="launch"></vn-icon-button> <vn-icon-button icon="launch"></vn-icon-button>
</a> </a>
<span>{{$ctrl.summary.invoiceOut.ref}} - {{$ctrl.summary.invoiceOut.client.socialName}}</span> <span>{{$ctrl.summary.invoiceOut.ref}} - {{$ctrl.summary.invoiceOut.client.socialName}}</span>
<vn-invoice-out-descriptor-menu
invoice-out="$ctrl.summary.invoiceOut"
parent-reload="$ctrl.reload()"
/>
</h5> </h5>
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>

View File

@ -50,7 +50,7 @@
on-last="$ctrl.scrollToLine(sale.lastPreparedLineFk)" on-last="$ctrl.scrollToLine(sale.lastPreparedLineFk)"
ng-attr-id="vnItemDiary-{{::sale.lineFk}}"> ng-attr-id="vnItemDiary-{{::sale.lineFk}}">
<vn-td shrink> <vn-td shrink>
<a ui-sref="claim.card.basicData({id: sale.claimFk})"> <a ui-sref="claim.card.summary({id: sale.claimFk})">
<vn-icon icon="icon-claims" <vn-icon icon="icon-claims"
ng-show="sale.claimFk" ng-show="sale.claimFk"
vn-tooltip="{{::$ctrl.$t('Claim')}}: {{::sale.claimFk}}"> vn-tooltip="{{::$ctrl.$t('Claim')}}: {{::sale.claimFk}}">

View File

@ -1,12 +1,13 @@
const app = require('vn-loopback/server/server'); const models = require('vn-loopback/server/server').models;
const LoopBackContext = require('loopback-context');
describe('loopback model Supplier', () => { describe('loopback model Supplier', () => {
let supplierOne; let supplierOne;
let supplierTwo; let supplierTwo;
beforeAll(async() => { beforeAll(async() => {
supplierOne = await app.models.Supplier.findById(1); supplierOne = await models.Supplier.findById(1);
supplierTwo = await app.models.Supplier.findById(442); supplierTwo = await models.Supplier.findById(442);
}); });
afterAll(async() => { afterAll(async() => {
@ -18,9 +19,9 @@ describe('loopback model Supplier', () => {
it('should throw an error when attempting to set an invalid payMethod id in the supplier', async() => { it('should throw an error when attempting to set an invalid payMethod id in the supplier', async() => {
let error; let error;
const expectedError = 'You can not select this payment method without a registered bankery account'; const expectedError = 'You can not select this payment method without a registered bankery account';
const supplier = await app.models.Supplier.findById(1); const supplier = await models.Supplier.findById(1);
await supplier.updateAttribute('payMethodFk', 4) await supplier.updateAttribute('payMethodFk', 8)
.catch(e => { .catch(e => {
error = e; error = e;
@ -31,14 +32,27 @@ describe('loopback model Supplier', () => {
}); });
it('should not throw if the payMethod id is valid', async() => { it('should not throw if the payMethod id is valid', async() => {
const activeCtx = {
accessToken: {userId: 9},
http: {
req: {
headers: {origin: 'http://localhost'}
}
}
};
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let error; let error;
const supplier = await app.models.Supplier.findById(442); const supplier = await models.Supplier.findById(442);
await supplier.updateAttribute('payMethodFk', 4) await supplier.updateAttribute('payMethodFk', 4)
.catch(e => { .catch(e => {
error = e; error = e;
}); });
expect(error).toBeDefined(); expect(error).not.toBeDefined();
}); });
}); });
}); });

View File

@ -9,40 +9,40 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"forceId": false "forceId": false
}, },
"originFk": { "originFk": {
"type": "Number", "type": "number",
"required": true "required": true
}, },
"userFk": { "userFk": {
"type": "Number" "type": "number"
}, },
"action": { "action": {
"type": "String", "type": "string",
"required": true "required": true
}, },
"changedModel": { "changedModel": {
"type": "String" "type": "string"
}, },
"oldInstance": { "oldInstance": {
"type": "Object" "type": "object"
}, },
"newInstance": { "newInstance": {
"type": "Object" "type": "object"
}, },
"creationDate": { "creationDate": {
"type": "Date" "type": "date"
}, },
"changedModelId": { "changedModelId": {
"type": "String" "type": "string"
}, },
"changedModelValue": { "changedModelValue": {
"type": "String" "type": "string"
}, },
"description": { "description": {
"type": "String" "type": "string"
} }
}, },
"relations": { "relations": {

View File

@ -80,7 +80,7 @@ module.exports = Self => {
const supplierAccount = await Self.app.models.SupplierAccount.findOne({where: {supplierFk: this.id}}); const supplierAccount = await Self.app.models.SupplierAccount.findOne({where: {supplierFk: this.id}});
const hasIban = supplierAccount && supplierAccount.iban; const hasIban = supplierAccount && supplierAccount.iban;
if (payMethod && payMethod.ibanRequired && !hasIban) if (payMethod && payMethod.ibanRequiredForSuppliers && !hasIban)
err(); err();
done(); done();

View File

@ -24,7 +24,7 @@
vn-acl="salesAssistant" vn-acl="salesAssistant"
ng-model="$ctrl.supplier.payMethodFk" ng-model="$ctrl.supplier.payMethodFk"
data="paymethods" data="paymethods"
fields="['ibanRequired']" fields="['ibanRequiredForSuppliers']"
initial-data="$ctrl.supplier.payMethod"> initial-data="$ctrl.supplier.payMethod">
</vn-autocomplete> </vn-autocomplete>
<vn-autocomplete <vn-autocomplete

View File

@ -24,6 +24,8 @@ module.exports = Self => {
}); });
Self.deny = async(ctx, options) => { Self.deny = async(ctx, options) => {
const models = Self.app.models;
const $t = ctx.req.__; // $translate
const myOptions = {}; const myOptions = {};
let tx; let tx;
@ -48,6 +50,17 @@ module.exports = Self => {
const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions); const request = await Self.app.models.TicketRequest.findById(ctx.args.id, null, myOptions);
await request.updateAttributes(params, myOptions); await request.updateAttributes(params, myOptions);
const origin = ctx.req.headers.origin;
const requesterId = request.requesterFk;
const message = $t('Deny buy request', {
ticketId: request.ticketFk,
url: `${origin}/#!/ticket/${request.ticketFk}/request/index`,
observation: params.response
});
await models.Chat.sendCheckingPresence(ctx, requesterId, message, myOptions);
if (tx) await tx.commit(); if (tx) await tx.commit();
return request; return request;

View File

@ -1,13 +1,23 @@
const models = require('vn-loopback/server/server').models; const models = require('vn-loopback/server/server').models;
describe('ticket-request deny()', () => { describe('ticket-request deny()', () => {
it('should return the dinied ticket request', async() => { it('should return the denied ticket request', async() => {
const tx = await models.TicketRequest.beginTransaction({}); const tx = await models.TicketRequest.beginTransaction({});
try { try {
const options = {transaction: tx}; const options = {transaction: tx};
const ctx = {req: {accessToken: {userId: 9}}, args: {id: 4, observation: 'my observation'}}; const ctx = {
req: {
accessToken: {userId: 9},
headers: {origin: 'http://localhost'}
},
args: {id: 4, observation: 'my observation'},
};
ctx.req.__ = value => {
return value;
};
const result = await models.TicketRequest.deny(ctx, options); const result = await models.TicketRequest.deny(ctx, options);

View File

@ -25,7 +25,7 @@
<vn-th number>Quantity</vn-th> <vn-th number>Quantity</vn-th>
<vn-th number>Price</vn-th> <vn-th number>Price</vn-th>
<vn-th number>Item id</vn-th> <vn-th number>Item id</vn-th>
<vn-th number>Ok</vn-th> <vn-th number>State</vn-th>
</vn-tr> </vn-tr>
</vn-thead> </vn-thead>
<vn-tbody> <vn-tbody>
@ -78,13 +78,9 @@
{{::request.saleFk | zeroFill:6}} {{::request.saleFk | zeroFill:6}}
</span> </span>
</vn-td> </vn-td>
<vn-td number> <vn-td number
<vn-check vn-one translate>
ng-model="::request.isOk" {{$ctrl.getRequestState(request.isOk)}}
triple-state="true"
title="{{$ctrl.getRequestState(request.isOk)}}"
disabled="true">
</vn-check>
</vn-td> </vn-td>
<vn-td number> <vn-td number>
<vn-icon-button <vn-icon-button

View File

@ -5,3 +5,6 @@ New request: Crear petición
Sale id: Id linea Sale id: Id linea
Requester: Solicitante Requester: Solicitante
New purchase request: Nueva petición de compra New purchase request: Nueva petición de compra
Denied: Denegada
Acepted: Aceptada
New: Nueva