Send invoice and delivery-note with CSV file #745
|
@ -3,8 +3,8 @@ import Popover from '../popover';
|
|||
import './style.scss';
|
||||
|
||||
export default class Menu extends Popover {
|
||||
show(parent) {
|
||||
super.show(parent);
|
||||
show(parent, direction) {
|
||||
super.show(parent, direction);
|
||||
this.windowEl.addEventListener('click', () => this.hide());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
@import "./effects";
|
||||
@import "variables";
|
||||
|
||||
.vn-menu {
|
||||
vn-item, .vn-item {
|
||||
@extend %clickable;
|
||||
}
|
||||
|
||||
vn-item.dropdown:after,
|
||||
.vn-item.dropdown:after {
|
||||
font-family: 'Material Icons';
|
||||
content: 'keyboard_arrow_right';
|
||||
position: absolute;
|
||||
color: $color-spacer;
|
||||
font-size: 1.5em;
|
||||
right: 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,15 @@ export default class Popover extends Popup {
|
|||
* it is shown in a visible relative position to it.
|
||||
*
|
||||
* @param {HTMLElement|Event} parent Overrides the parent property
|
||||
* @param {String} direction - Direction [left]
|
||||
*/
|
||||
show(parent) {
|
||||
show(parent, direction) {
|
||||
if (parent instanceof Event)
|
||||
parent = event.target;
|
||||
|
||||
if (parent) this.parent = parent;
|
||||
if (direction) this.direction = direction;
|
||||
|
||||
super.show();
|
||||
this.content = this.popup.querySelector('.content');
|
||||
this.$timeout(() => this.relocate(), 10);
|
||||
|
@ -89,21 +92,40 @@ export default class Popover extends Popup {
|
|||
let width = clamp(popoverRect.width, parentRect.width, maxWith);
|
||||
let height = popoverRect.height;
|
||||
|
||||
let left = parentRect.left + parentRect.width / 2 - width / 2;
|
||||
left = clamp(left, margin, maxRight - width);
|
||||
let left;
|
||||
if (this.direction == 'left') {
|
||||
left = parentRect.left + parentRect.width;
|
||||
left = clamp(left, margin, maxRight - width);
|
||||
} else {
|
||||
left = parentRect.left + parentRect.width / 2 - width / 2;
|
||||
left = clamp(left, margin, maxRight - width);
|
||||
}
|
||||
|
||||
let top = parentRect.top + parentRect.height + arrowOffset;
|
||||
let top;
|
||||
if (this.direction == 'left')
|
||||
top = parentRect.top;
|
||||
else
|
||||
top = parentRect.top + parentRect.height + arrowOffset;
|
||||
let showTop = top + height > maxBottom;
|
||||
if (showTop) top = parentRect.top - height - arrowOffset;
|
||||
top = Math.max(top, margin);
|
||||
|
||||
if (showTop)
|
||||
if (this.direction == 'left')
|
||||
arrowStyle.left = `0`;
|
||||
else if (showTop)
|
||||
arrowStyle.bottom = `0`;
|
||||
else
|
||||
arrowStyle.top = `0`;
|
||||
|
||||
let arrowLeft = (parentRect.left - left) + parentRect.width / 2;
|
||||
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight);
|
||||
let arrowLeft;
|
||||
if (this.direction == 'left') {
|
||||
arrowLeft = 0;
|
||||
let arrowTop = arrowOffset;
|
||||
arrowStyle.top = `${arrowTop}px`;
|
||||
} else {
|
||||
arrowLeft = (parentRect.left - left) + parentRect.width / 2;
|
||||
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight);
|
||||
}
|
||||
arrowStyle.left = `${arrowLeft}px`;
|
||||
|
||||
style.top = `${top}px`;
|
||||
|
|
|
@ -18,6 +18,18 @@ class Email {
|
|||
return this.$http.get(`email/${template}`, {params})
|
||||
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email displaying a notification when it's sent.
|
||||
*
|
||||
* @param {String} template The email report name
|
||||
* @param {Object} params The email parameters
|
||||
* @return {Promise} Promise resolved when it's sent
|
||||
*/
|
||||
sendCsv(template, params) {
|
||||
return this.$http.get(`csv/${template}/send`, {params})
|
||||
.then(() => this.vnApp.showMessage(this.$t('Notification sent!')));
|
||||
}
|
||||
}
|
||||
Email.$inject = ['$http', '$translate', 'vnApp'];
|
||||
|
||||
|
|
|
@ -20,6 +20,21 @@ class Report {
|
|||
const serializedParams = this.$httpParamSerializer(params);
|
||||
window.open(`api/report/${report}?${serializedParams}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a report in another window, automatically adds the authorization
|
||||
* token to params.
|
||||
*
|
||||
* @param {String} report The report name
|
||||
* @param {Object} params The report parameters
|
||||
*/
|
||||
showCsv(report, params) {
|
||||
params = Object.assign({
|
||||
authorization: this.vnToken.token
|
||||
}, params);
|
||||
const serializedParams = this.$httpParamSerializer(params);
|
||||
window.open(`api/csv/${report}/download?${serializedParams}`);
|
||||
}
|
||||
}
|
||||
Report.$inject = ['$httpParamSerializer', 'vnToken'];
|
||||
|
||||
|
|
|
@ -2,18 +2,49 @@
|
|||
module="invoiceOut"
|
||||
description="$ctrl.invoiceOut.ref">
|
||||
<slot-menu>
|
||||
<a class="vn-item"
|
||||
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}"
|
||||
target="_blank"
|
||||
<vn-item class="dropdown"
|
||||
vn-click-stop="showInvoiceMenu.show($event, 'left')"
|
||||
name="showInvoicePdf"
|
||||
translate>
|
||||
Show invoice PDF
|
||||
</a>
|
||||
<vn-item
|
||||
ng-click="invoiceConfirmation.show({email: $ctrl.invoiceOut.client.email})"
|
||||
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 PDF
|
||||
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()"
|
||||
|
@ -104,15 +135,32 @@
|
|||
message="Generate PDF invoice document">
|
||||
</vn-confirm>
|
||||
|
||||
<!-- Send invoice confirmation popup -->
|
||||
<vn-dialog class="edit"
|
||||
vn-id="invoiceConfirmation"
|
||||
on-accept="$ctrl.sendInvoice($data)"
|
||||
message="Send invoice PDF">
|
||||
<!-- 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="invoiceConfirmation.data.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
|
||||
ng-model="sendCsvConfirmation.data.email">
|
||||
</vn-textfield>
|
||||
</tpl-body>
|
||||
<tpl-buttons>
|
||||
|
|
|
@ -14,29 +14,6 @@ class Controller extends Descriptor {
|
|||
return this.aclService.hasAny(['invoicing']);
|
||||
}
|
||||
|
||||
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')));
|
||||
}
|
||||
|
||||
createInvoicePdf() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
get filter() {
|
||||
if (this.invoiceOut)
|
||||
return JSON.stringify({refFk: this.invoiceOut.ref});
|
||||
|
@ -55,7 +32,7 @@ class Controller extends Descriptor {
|
|||
}, {
|
||||
relation: 'client',
|
||||
scope: {
|
||||
fields: ['id', 'name']
|
||||
fields: ['id', 'name', 'email']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -76,13 +53,51 @@ class Controller extends Descriptor {
|
|||
// Prevents error when not defined
|
||||
}
|
||||
|
||||
sendInvoice($data) {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngModule.vnComponent('vnInvoiceOutDescriptor', {
|
||||
|
|
|
@ -3,30 +3,20 @@ import './index';
|
|||
describe('vnInvoiceOutDescriptor', () => {
|
||||
let controller;
|
||||
let $httpBackend;
|
||||
const invoiceOut = {id: 1};
|
||||
let $httpParamSerializer;
|
||||
const invoiceOut = {
|
||||
id: 1,
|
||||
client: {id: 1101}
|
||||
};
|
||||
|
||||
beforeEach(ngModule('invoiceOut'));
|
||||
|
||||
beforeEach(inject(($componentController, _$httpBackend_) => {
|
||||
beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpParamSerializer = _$httpParamSerializer_;
|
||||
controller = $componentController('vnInvoiceOutDescriptor', {$element: null});
|
||||
}));
|
||||
|
||||
describe('createInvoicePdf()', () => {
|
||||
it('should make a query 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.createInvoicePdf();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadData()', () => {
|
||||
it(`should perform a get query to store the invoice in data into the controller`, () => {
|
||||
const id = 1;
|
||||
|
@ -39,4 +29,81 @@ describe('vnInvoiceOutDescriptor', () => {
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,8 +2,10 @@ Volume exceded: Volumen excedido
|
|||
Volume: Volumen
|
||||
Client card: Ficha del cliente
|
||||
Invoice ticket list: Listado de tickets de la factura
|
||||
Show invoice PDF: Ver factura en PDF
|
||||
Send invoice PDF: Enviar factura en PDF
|
||||
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
|
||||
InvoiceOut deleted: Factura eliminada
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
icon="more_vert"
|
||||
vn-popover="menu">
|
||||
</vn-icon-button>
|
||||
|
||||
<vn-menu vn-id="menu">
|
||||
<vn-list>
|
||||
<vn-item
|
||||
|
@ -12,15 +13,44 @@
|
|||
translate>
|
||||
Add turn
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="$ctrl.showDeliveryNote()"
|
||||
<vn-item class="dropdown"
|
||||
vn-click-stop="showDeliveryNoteMenu.show($event, 'left')"
|
||||
translate>
|
||||
Show Delivery Note
|
||||
Show Delivery Note...
|
||||
<vn-menu vn-id="showDeliveryNoteMenu">
|
||||
<vn-list>
|
||||
<vn-item
|
||||
ng-click="$ctrl.showPdfDeliveryNote()"
|
||||
translate>
|
||||
Show as PDF
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="$ctrl.showCsvDeliveryNote()"
|
||||
translate>
|
||||
Show as CSV
|
||||
</vn-item>
|
||||
</vn-list>
|
||||
</vn-menu>
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="confirmDeliveryNote.show()"
|
||||
<vn-item class="dropdown"
|
||||
vn-click-stop="sendDeliveryNoteMenu.show($event, 'left')"
|
||||
translate>
|
||||
Send Delivery Note
|
||||
Send Delivery Note...
|
||||
|
||||
<vn-menu vn-id="sendDeliveryNoteMenu">
|
||||
<vn-list>
|
||||
<vn-item
|
||||
ng-click="sendPdfConfirmation.show({email: $ctrl.ticket.client.email})"
|
||||
translate>
|
||||
Send PDF
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="sendCsvConfirmation.show({email: $ctrl.ticket.client.email})"
|
||||
translate>
|
||||
Send CSV
|
||||
</vn-item>
|
||||
</vn-list>
|
||||
</vn-menu>
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="deleteConfirmation.show()"
|
||||
|
@ -79,7 +109,7 @@
|
|||
Make invoice
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="createInvoicePdfConfirmation.show()"
|
||||
ng-click="createPdfConfirmation.show()"
|
||||
ng-show="$ctrl.isInvoiced && ($ctrl.hasInvoicing || !$ctrl.ticket.invoiceOut.hasPdf)"
|
||||
name="regenerateInvoice"
|
||||
translate>
|
||||
|
@ -133,13 +163,39 @@
|
|||
</div>
|
||||
</vn-popup>
|
||||
|
||||
<!-- Send delivery note confirmation popup -->
|
||||
<vn-confirm
|
||||
vn-id="confirmDeliveryNote"
|
||||
on-accept="$ctrl.sendDeliveryNote()"
|
||||
question="Are you sure you want to send it?"
|
||||
message="Send Delivery Note">
|
||||
</vn-confirm>
|
||||
<!-- Send PDF delivery note confirmation popup -->
|
||||
<vn-dialog
|
||||
vn-id="sendPdfConfirmation"
|
||||
on-accept="$ctrl.sendPdfDeliveryNote($data)"
|
||||
message="Send PDF Delivery Note">
|
||||
<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 delivery note confirmation popup -->
|
||||
<vn-dialog
|
||||
vn-id="sendCsvConfirmation"
|
||||
on-accept="$ctrl.sendCsvDeliveryNote($data)"
|
||||
message="Send CSV Delivery Note">
|
||||
<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>
|
||||
|
||||
<!-- Delete ticket confirmation popup -->
|
||||
<vn-confirm
|
||||
|
@ -206,8 +262,8 @@
|
|||
|
||||
<!-- Create invoice PDF confirmation dialog -->
|
||||
<vn-confirm
|
||||
vn-id="createInvoicePdfConfirmation"
|
||||
on-accept="$ctrl.createInvoicePdf()"
|
||||
vn-id="createPdfConfirmation"
|
||||
on-accept="$ctrl.createPdfInvoice()"
|
||||
question="Are you sure you want to generate/regenerate the PDF invoice?"
|
||||
message="Generate PDF invoice document">
|
||||
</vn-confirm>
|
||||
|
|
|
@ -115,17 +115,32 @@ class Controller extends Section {
|
|||
});
|
||||
}
|
||||
|
||||
showDeliveryNote() {
|
||||
showPdfDeliveryNote() {
|
||||
this.vnReport.show('delivery-note', {
|
||||
recipientId: this.ticket.client.id,
|
||||
ticketId: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
sendDeliveryNote() {
|
||||
showCsvDeliveryNote() {
|
||||
this.vnReport.showCsv('delivery-note', {
|
||||
recipientId: this.ticket.client.id,
|
||||
ticketId: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
sendPdfDeliveryNote($data) {
|
||||
return this.vnEmail.send('delivery-note', {
|
||||
recipientId: this.ticket.client.id,
|
||||
recipient: this.ticket.client.email,
|
||||
recipient: $data.email,
|
||||
ticketId: this.id
|
||||
});
|
||||
}
|
||||
|
||||
sendCsvDeliveryNote($data) {
|
||||
return this.vnEmail.sendCsv('delivery-note', {
|
||||
recipientId: this.ticket.client.id,
|
||||
recipient: $data.email,
|
||||
ticketId: this.id
|
||||
});
|
||||
}
|
||||
|
@ -227,7 +242,7 @@ class Controller extends Section {
|
|||
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
|
||||
}
|
||||
|
||||
createInvoicePdf() {
|
||||
createPdfInvoice() {
|
||||
const invoiceId = this.ticket.invoiceOut.id;
|
||||
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
|
||||
.then(() => this.reload())
|
||||
|
|
|
@ -2,6 +2,7 @@ import './index.js';
|
|||
|
||||
describe('Ticket Component vnTicketDescriptorMenu', () => {
|
||||
let $httpBackend;
|
||||
let $httpParamSerializer;
|
||||
let controller;
|
||||
let $state;
|
||||
|
||||
|
@ -25,8 +26,9 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
|||
|
||||
beforeEach(ngModule('ticket'));
|
||||
|
||||
beforeEach(inject(($componentController, _$httpBackend_, _$state_) => {
|
||||
beforeEach(inject(($componentController, _$httpBackend_, _$httpParamSerializer_, _$state_) => {
|
||||
$httpBackend = _$httpBackend_;
|
||||
$httpParamSerializer = _$httpParamSerializer_;
|
||||
$state = _$state_;
|
||||
$state.params.id = 16;
|
||||
$state.getCurrentPath = () => [null, {state: {name: 'ticket'}}];
|
||||
|
@ -104,36 +106,74 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('showDeliveryNote()', () => {
|
||||
describe('showPdfDeliveryNote()', () => {
|
||||
it('should open a new window showing a delivery note PDF document', () => {
|
||||
jest.spyOn(controller.vnReport, 'show');
|
||||
jest.spyOn(window, 'open').mockReturnThis();
|
||||
|
||||
window.open = jasmine.createSpy('open');
|
||||
const params = {
|
||||
recipientId: ticket.client.id,
|
||||
ticketId: ticket.id
|
||||
const expectedParams = {
|
||||
ticketId: ticket.id,
|
||||
recipientId: ticket.client.id
|
||||
};
|
||||
controller.showDeliveryNote();
|
||||
const serializedParams = $httpParamSerializer(expectedParams);
|
||||
const expectedPath = `api/report/delivery-note?${serializedParams}`;
|
||||
controller.showPdfDeliveryNote();
|
||||
|
||||
expect(controller.vnReport.show).toHaveBeenCalledWith('delivery-note', params);
|
||||
expect(window.open).toHaveBeenCalledWith(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendDeliveryNote()', () => {
|
||||
describe('sendPdfDeliveryNote()', () => {
|
||||
it('should make a query and call vnApp.showMessage()', () => {
|
||||
jest.spyOn(controller.vnEmail, 'send');
|
||||
|
||||
const $data = {email: 'brucebanner@gothamcity.com'};
|
||||
const params = {
|
||||
recipient: ticket.client.email,
|
||||
recipient: $data.email,
|
||||
recipientId: ticket.client.id,
|
||||
ticketId: ticket.id
|
||||
};
|
||||
controller.sendDeliveryNote();
|
||||
controller.sendPdfDeliveryNote($data);
|
||||
|
||||
expect(controller.vnEmail.send).toHaveBeenCalledWith('delivery-note', params);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showCsvDeliveryNote()', () => {
|
||||
it('should make a query to the csv delivery-note download endpoint and show a message snackbar', () => {
|
||||
jest.spyOn(window, 'open').mockReturnThis();
|
||||
|
||||
const expectedParams = {
|
||||
ticketId: ticket.id,
|
||||
recipientId: ticket.client.id
|
||||
};
|
||||
const serializedParams = $httpParamSerializer(expectedParams);
|
||||
const expectedPath = `api/csv/delivery-note/download?${serializedParams}`;
|
||||
controller.showCsvDeliveryNote();
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith(expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendCsvDeliveryNote()', () => {
|
||||
it('should make a query to the csv delivery-note send endpoint and show a message snackbar', () => {
|
||||
jest.spyOn(controller.vnApp, 'showMessage');
|
||||
|
||||
const $data = {email: 'brucebanner@gothamcity.com'};
|
||||
const expectedParams = {
|
||||
ticketId: ticket.id,
|
||||
recipient: $data.email,
|
||||
recipientId: ticket.client.id,
|
||||
};
|
||||
const serializedParams = $httpParamSerializer(expectedParams);
|
||||
|
||||
$httpBackend.expectGET(`csv/delivery-note/send?${serializedParams}`).respond();
|
||||
controller.sendCsvDeliveryNote($data);
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showMessage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeInvoice()', () => {
|
||||
it('should make a query and call $state.reload() method', () => {
|
||||
jest.spyOn(controller, 'reload').mockReturnThis();
|
||||
|
@ -149,13 +189,13 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createInvoicePdf()', () => {
|
||||
describe('createPdfInvoice()', () => {
|
||||
it('should make a query and show a success snackbar', () => {
|
||||
jest.spyOn(controller.vnApp, 'showSuccess');
|
||||
|
||||
$httpBackend.whenGET(`Tickets/16`).respond();
|
||||
$httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
|
||||
controller.createInvoicePdf();
|
||||
controller.createPdfInvoice();
|
||||
$httpBackend.flush();
|
||||
|
||||
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Show Delivery Note...: Ver albarán...
|
||||
Send Delivery Note...: Enviar albarán...
|
||||
Show as PDF: Ver como PDF
|
||||
Show as CSV: Ver como CSV
|
||||
Send PDF: Enviar PDF
|
||||
Send CSV: Enviar CSV
|
||||
Send CSV Delivery Note: Enviar albarán en CSV
|
||||
Send PDF Delivery Note: Enviar albarán en PDF
|
|
@ -8,8 +8,6 @@ Add stowaway: Añadir polizón
|
|||
Delete stowaway: Eliminar polizón
|
||||
Are you sure you want to delete this stowaway?: ¿Seguro que quieres eliminar este polizón?
|
||||
Are you sure you want to send it?: ¿Seguro que quieres enviarlo?
|
||||
Show Delivery Note: Ver albarán
|
||||
Send Delivery Note: Enviar albarán
|
||||
Show pallet report: Ver hoja de pallet
|
||||
Change shipped hour: Cambiar hora de envío
|
||||
Shipped hour: Hora de envío
|
||||
|
|
|
@ -37,20 +37,30 @@ class Email extends Component {
|
|||
return userTranslations.subject;
|
||||
}
|
||||
|
||||
async send() {
|
||||
/**
|
||||
* @param {Object} [options] - Additional options
|
||||
* @param {Boolean} [options.overrideAttachments] - Overrides default PDF attachments
|
||||
* @param {Array} [options.attachments] - Array containing attachment objects
|
||||
* @return {Promise} SMTP Promise
|
||||
*/
|
||||
async send(options = {}) {
|
||||
const instance = this.build();
|
||||
const rendered = await this.render();
|
||||
const attachments = [];
|
||||
const getAttachments = async(componentPath, files) => {
|
||||
for (file of files) {
|
||||
const fileCopy = Object.assign({}, file);
|
||||
const fileName = fileCopy.filename;
|
||||
|
||||
if (options.overrideAttachments && !fileName.includes('.png')) continue;
|
||||
|
||||
if (fileCopy.cid) {
|
||||
const templatePath = `${componentPath}/${file.path}`;
|
||||
const fullFilePath = path.resolve(__dirname, templatePath);
|
||||
|
||||
fileCopy.path = path.resolve(__dirname, fullFilePath);
|
||||
} else {
|
||||
const reportName = fileCopy.filename.replace('.pdf', '');
|
||||
const reportName = fileName.replace('.pdf', '');
|
||||
const report = new Report(reportName, this.args);
|
||||
fileCopy.content = await report.toPdfStream();
|
||||
}
|
||||
|
@ -71,9 +81,14 @@ class Email extends Component {
|
|||
if (this.attachments)
|
||||
await getAttachments(this.path, this.attachments);
|
||||
|
||||
if (options.attachments) {
|
||||
for (let attachment of options.attachments)
|
||||
attachments.push(attachment);
|
||||
}
|
||||
|
||||
const localeSubject = await this.getSubject();
|
||||
const replyTo = this.args.replyTo || this.args.auth.email;
|
||||
const options = {
|
||||
const mailOptions = {
|
||||
to: this.args.recipient,
|
||||
replyTo: replyTo,
|
||||
subject: localeSubject,
|
||||
|
@ -81,7 +96,7 @@ class Email extends Component {
|
|||
attachments: attachments
|
||||
};
|
||||
|
||||
return smtp.send(options);
|
||||
return smtp.send(mailOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,10 @@ module.exports = app => {
|
|||
const methods = [];
|
||||
|
||||
// Get all methods
|
||||
methodsDir.forEach(method => {
|
||||
methods.push(method.replace('.js', ''));
|
||||
});
|
||||
for (let method of methodsDir) {
|
||||
if (method.includes('.js'))
|
||||
methods.push(method.replace('.js', ''));
|
||||
}
|
||||
|
||||
// Auth middleware
|
||||
const paths = [];
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
module.exports = app => {
|
||||
app.use('/api/csv/delivery-note', require('./csv/delivery-note')(app));
|
||||
app.use('/api/csv/invoice', require('./csv/invoice')(app));
|
||||
|
||||
app.toCSV = function toCSV(rows) {
|
||||
const [columns] = rows;
|
||||
let content = Object.keys(columns).join('\t');
|
||||
for (let row of rows) {
|
||||
const values = Object.values(row);
|
||||
const finalValues = values.map(value => {
|
||||
if (value instanceof Date) return formatDate(value);
|
||||
if (value === null) return '';
|
||||
return value;
|
||||
});
|
||||
content += '\n';
|
||||
content += finalValues.join('\t');
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
function formatDate(date) {
|
||||
return new Intl.DateTimeFormat('es', {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
const express = require('express');
|
||||
const router = new express.Router();
|
||||
const path = require('path');
|
||||
const db = require('../../../core/database');
|
||||
const sqlPath = path.join(__dirname, 'sql');
|
||||
|
||||
module.exports = app => {
|
||||
router.get('/preview', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.ticketId)
|
||||
throw new Error('The argument ticketId is required');
|
||||
|
||||
const ticketId = reqArgs.ticketId;
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `ticket_${ticketId}.csv`;
|
||||
|
||||
res.setHeader('Content-type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(content);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/download', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.ticketId)
|
||||
throw new Error('The argument ticketId is required');
|
||||
|
||||
const ticketId = reqArgs.ticketId;
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `ticket_${ticketId}.csv`;
|
||||
|
||||
res.setHeader('Content-type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(content);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const Email = require('../../../core/email');
|
||||
router.get('/send', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.ticketId)
|
||||
throw new Error('The argument ticketId is required');
|
||||
|
||||
const ticketId = reqArgs.ticketId;
|
||||
const ticket = await db.findOneFromDef(`${sqlPath}/ticket`, [ticketId]);
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [ticketId]);
|
||||
|
||||
const args = Object.assign({
|
||||
ticketId: (String(ticket.id)),
|
||||
recipientId: ticket.clientFk,
|
||||
recipient: ticket.recipient,
|
||||
replyTo: ticket.salesPersonEmail
|
||||
}, reqArgs);
|
||||
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `ticket_${ticketId}.csv`;
|
||||
const email = new Email('delivery-note', args);
|
||||
await email.send({
|
||||
overrideAttachments: true,
|
||||
attachments: [{
|
||||
filename: fileName,
|
||||
content: content
|
||||
}]
|
||||
});
|
||||
|
||||
res.status(200).json({message: 'ok'});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
SELECT io.ref Invoice,
|
||||
io.issued InvoiceDate,
|
||||
s.ticketFk Ticket,
|
||||
s.itemFk Item,
|
||||
s.concept Description,
|
||||
i.size,
|
||||
i.subName Producer,
|
||||
s.quantity Quantity,
|
||||
s.price Price,
|
||||
s.discount Discount,
|
||||
s.created Created,
|
||||
tc.code Taxcode,
|
||||
tc.description TaxDescription,
|
||||
i.tag5,
|
||||
i.value5,
|
||||
i.tag6,
|
||||
i.value6,
|
||||
i.tag7,
|
||||
i.value7,
|
||||
i.tag8,
|
||||
i.value8,
|
||||
i.tag9,
|
||||
i.value9,
|
||||
i.tag10,
|
||||
i.value10
|
||||
FROM vn.sale s
|
||||
JOIN vn.ticket t ON t.id = s.ticketFk
|
||||
JOIN vn.item i ON i.id = s.itemFk
|
||||
JOIN vn.supplier s2 ON s2.id = t.companyFk
|
||||
JOIN vn.itemTaxCountry itc ON itc.itemFk = i.id
|
||||
AND itc.countryFk = s2.countryFk
|
||||
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
|
||||
LEFT JOIN vn.invoiceOut io ON io.id = t.refFk
|
||||
WHERE s.ticketFk = ?
|
||||
ORDER BY s.ticketFk, s.created
|
|
@ -0,0 +1,9 @@
|
|||
SELECT
|
||||
t.id,
|
||||
t.clientFk,
|
||||
c.email recipient,
|
||||
eu.email salesPersonEmail
|
||||
FROM ticket t
|
||||
JOIN client c ON c.id = t.clientFk
|
||||
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
|
||||
WHERE t.id = ?
|
|
@ -0,0 +1,82 @@
|
|||
const express = require('express');
|
||||
const router = new express.Router();
|
||||
const path = require('path');
|
||||
const db = require('../../../core/database');
|
||||
const sqlPath = path.join(__dirname, 'sql');
|
||||
|
||||
module.exports = app => {
|
||||
router.get('/preview', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.invoiceId)
|
||||
throw new Error('The argument invoiceId is required');
|
||||
|
||||
const invoiceId = reqArgs.invoiceId;
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `invoice_${invoiceId}.csv`;
|
||||
|
||||
res.setHeader('Content-type', 'application/json; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(content);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/download', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.invoiceId)
|
||||
throw new Error('The argument invoiceId is required');
|
||||
|
||||
const invoiceId = reqArgs.invoiceId;
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `invoice_${invoiceId}.csv`;
|
||||
|
||||
res.setHeader('Content-type', 'text/csv');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
|
||||
res.end(content);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const Email = require('../../../core/email');
|
||||
router.get('/send', async function(req, res, next) {
|
||||
try {
|
||||
const reqArgs = req.args;
|
||||
if (!reqArgs.invoiceId)
|
||||
throw new Error('The argument invoiceId is required');
|
||||
|
||||
const invoiceId = reqArgs.invoiceId;
|
||||
const invoice = await db.findOneFromDef(`${sqlPath}/invoice`, [invoiceId]);
|
||||
const sales = await db.rawSqlFromDef(`${sqlPath}/sales`, [invoiceId]);
|
||||
|
||||
const args = Object.assign({
|
||||
invoiceId: (String(invoice.id)),
|
||||
recipientId: invoice.clientFk,
|
||||
recipient: invoice.recipient,
|
||||
replyTo: invoice.salesPersonEmail
|
||||
}, reqArgs);
|
||||
|
||||
const content = app.toCSV(sales);
|
||||
const fileName = `invoice_${invoiceId}.csv`;
|
||||
const email = new Email('invoice', args);
|
||||
await email.send({
|
||||
overrideAttachments: true,
|
||||
attachments: [{
|
||||
filename: fileName,
|
||||
content: content
|
||||
}]
|
||||
});
|
||||
|
||||
res.status(200).json({message: 'ok'});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
SELECT
|
||||
io.id,
|
||||
io.clientFk,
|
||||
c.email recipient,
|
||||
eu.email salesPersonEmail
|
||||
FROM invoiceOut io
|
||||
JOIN client c ON c.id = io.clientFk
|
||||
LEFT JOIN account.emailUser eu ON eu.userFk = c.salesPersonFk
|
||||
WHERE io.id = ?
|
|
@ -0,0 +1,35 @@
|
|||
SELECT io.ref Invoice,
|
||||
io.issued InvoiceDate,
|
||||
s.ticketFk Ticket,
|
||||
s.itemFk Item,
|
||||
s.concept Description,
|
||||
i.size,
|
||||
i.subName Producer,
|
||||
s.quantity Quantity,
|
||||
s.price Price,
|
||||
s.discount Discount,
|
||||
s.created Created,
|
||||
tc.code Taxcode,
|
||||
tc.description TaxDescription,
|
||||
i.tag5,
|
||||
i.value5,
|
||||
i.tag6,
|
||||
i.value6,
|
||||
i.tag7,
|
||||
i.value7,
|
||||
i.tag8,
|
||||
i.value8,
|
||||
i.tag9,
|
||||
i.value9,
|
||||
i.tag10,
|
||||
i.value10
|
||||
FROM sale s
|
||||
JOIN ticket t ON t.id = s.ticketFk
|
||||
JOIN item i ON i.id = s.itemFk
|
||||
JOIN supplier s2 ON s2.id = t.companyFk
|
||||
JOIN itemTaxCountry itc ON itc.itemFk = i.id
|
||||
AND itc.countryFk = s2.countryFk
|
||||
JOIN taxClass tc ON tc.id = itc.taxClassFk
|
||||
JOIN invoiceOut io ON io.ref = t.refFk
|
||||
WHERE io.id = ?
|
||||
ORDER BY s.ticketFk, s.created
|
|
@ -29,6 +29,9 @@ module.exports = {
|
|||
|
||||
const hash = md5(this.signature.id.toString()).substring(0, 3);
|
||||
const file = `${config.storage.root}/${hash}/${this.signature.id}.png`;
|
||||
|
||||
if (!fs.existsSync(file)) return null;
|
||||
|
||||
const src = fs.readFileSync(file);
|
||||
const base64 = Buffer.from(src, 'utf8').toString('base64');
|
||||
|
||||
|
|
|
@ -95,7 +95,6 @@ module.exports = {
|
|||
},
|
||||
ticketSubtotal(ticket) {
|
||||
let subTotal = 0.00;
|
||||
console.log(ticket.sales);
|
||||
for (let sale of ticket.sales)
|
||||
subTotal += this.saleImport(sale);
|
||||
|
||||
|
|
Loading…
Reference in New Issue