Send invoice and delivery-note with CSV file #745

Merged
carlosjr merged 8 commits from 3094-invoice_csv into dev 2021-10-18 09:09:46 +00:00
25 changed files with 721 additions and 111 deletions

View File

@ -3,8 +3,8 @@ import Popover from '../popover';
import './style.scss'; import './style.scss';
export default class Menu extends Popover { export default class Menu extends Popover {
show(parent) { show(parent, direction) {
super.show(parent); super.show(parent, direction);
this.windowEl.addEventListener('click', () => this.hide()); this.windowEl.addEventListener('click', () => this.hide());
} }
} }

View File

@ -1,7 +1,18 @@
@import "./effects"; @import "./effects";
@import "variables";
.vn-menu { .vn-menu {
vn-item, .vn-item { vn-item, .vn-item {
@extend %clickable; @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
}
} }

View File

@ -23,12 +23,15 @@ export default class Popover extends Popup {
* it is shown in a visible relative position to it. * it is shown in a visible relative position to it.
* *
* @param {HTMLElement|Event} parent Overrides the parent property * @param {HTMLElement|Event} parent Overrides the parent property
* @param {String} direction - Direction [left]
*/ */
show(parent) { show(parent, direction) {
if (parent instanceof Event) if (parent instanceof Event)
parent = event.target; parent = event.target;
if (parent) this.parent = parent; if (parent) this.parent = parent;
if (direction) this.direction = direction;
super.show(); super.show();
this.content = this.popup.querySelector('.content'); this.content = this.popup.querySelector('.content');
this.$timeout(() => this.relocate(), 10); this.$timeout(() => this.relocate(), 10);
@ -89,21 +92,40 @@ export default class Popover extends Popup {
let width = clamp(popoverRect.width, parentRect.width, maxWith); let width = clamp(popoverRect.width, parentRect.width, maxWith);
let height = popoverRect.height; let height = popoverRect.height;
let left = parentRect.left + parentRect.width / 2 - width / 2; let left;
left = clamp(left, margin, maxRight - width); 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; let showTop = top + height > maxBottom;
if (showTop) top = parentRect.top - height - arrowOffset; if (showTop) top = parentRect.top - height - arrowOffset;
top = Math.max(top, margin); top = Math.max(top, margin);
if (showTop) if (this.direction == 'left')
arrowStyle.left = `0`;
else if (showTop)
arrowStyle.bottom = `0`; arrowStyle.bottom = `0`;
else else
arrowStyle.top = `0`; arrowStyle.top = `0`;
let arrowLeft = (parentRect.left - left) + parentRect.width / 2; let arrowLeft;
arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight); 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`; arrowStyle.left = `${arrowLeft}px`;
style.top = `${top}px`; style.top = `${top}px`;

View File

@ -18,6 +18,18 @@ class Email {
return this.$http.get(`email/${template}`, {params}) return this.$http.get(`email/${template}`, {params})
.then(() => this.vnApp.showMessage(this.$t('Notification sent!'))); .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']; Email.$inject = ['$http', '$translate', 'vnApp'];

View File

@ -20,6 +20,21 @@ class Report {
const serializedParams = this.$httpParamSerializer(params); const serializedParams = this.$httpParamSerializer(params);
window.open(`api/report/${report}?${serializedParams}`); 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']; Report.$inject = ['$httpParamSerializer', 'vnToken'];

View File

@ -2,18 +2,49 @@
module="invoiceOut" module="invoiceOut"
description="$ctrl.invoiceOut.ref"> description="$ctrl.invoiceOut.ref">
<slot-menu> <slot-menu>
<a class="vn-item" <vn-item class="dropdown"
href="api/InvoiceOuts/{{$ctrl.id}}/download?access_token={{$ctrl.vnToken.token}}" vn-click-stop="showInvoiceMenu.show($event, 'left')"
target="_blank"
name="showInvoicePdf" name="showInvoicePdf"
translate> translate>
Show invoice PDF Show invoice...
</a>
<vn-item <vn-menu vn-id="showInvoiceMenu">
ng-click="invoiceConfirmation.show({email: $ctrl.invoiceOut.client.email})" <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" name="sendInvoice"
translate> 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>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
@ -104,15 +135,32 @@
message="Generate PDF invoice document"> message="Generate PDF invoice document">
</vn-confirm> </vn-confirm>
<!-- Send invoice confirmation popup --> <!-- Send PDF invoice confirmation popup -->
<vn-dialog class="edit" <vn-dialog
vn-id="invoiceConfirmation" vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendInvoice($data)" on-accept="$ctrl.sendPdfInvoice($data)"
message="Send invoice PDF"> message="Send PDF invoice">
<tpl-body> <tpl-body>
<span translate>Are you sure you want to send it?</span> <span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one <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> </vn-textfield>
</tpl-body> </tpl-body>
<tpl-buttons> <tpl-buttons>

View File

@ -14,29 +14,6 @@ class Controller extends Descriptor {
return this.aclService.hasAny(['invoicing']); 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() { get filter() {
if (this.invoiceOut) if (this.invoiceOut)
return JSON.stringify({refFk: this.invoiceOut.ref}); return JSON.stringify({refFk: this.invoiceOut.ref});
@ -55,7 +32,7 @@ class Controller extends Descriptor {
}, { }, {
relation: 'client', relation: 'client',
scope: { scope: {
fields: ['id', 'name'] fields: ['id', 'name', 'email']
} }
} }
] ]
@ -76,13 +53,51 @@ class Controller extends Descriptor {
// Prevents error when not defined // 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', { return this.vnEmail.send('invoice', {
recipientId: this.invoiceOut.client.id, recipientId: this.invoiceOut.client.id,
recipient: $data.email, recipient: $data.email,
invoiceId: this.id invoiceId: this.id
}); });
} }
sendCsvInvoice($data) {
return this.vnEmail.sendCsv('invoice', {
recipientId: this.invoiceOut.client.id,
recipient: $data.email,
invoiceId: this.id
});
}
} }
ngModule.vnComponent('vnInvoiceOutDescriptor', { ngModule.vnComponent('vnInvoiceOutDescriptor', {

View File

@ -3,30 +3,20 @@ import './index';
describe('vnInvoiceOutDescriptor', () => { describe('vnInvoiceOutDescriptor', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
const invoiceOut = {id: 1}; let $httpParamSerializer;
const invoiceOut = {
id: 1,
client: {id: 1101}
};
beforeEach(ngModule('invoiceOut')); beforeEach(ngModule('invoiceOut'));
beforeEach(inject(($componentController, _$httpBackend_) => { beforeEach(inject(($componentController, _$httpParamSerializer_, _$httpBackend_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
controller = $componentController('vnInvoiceOutDescriptor', {$element: null}); 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()', () => { describe('loadData()', () => {
it(`should perform a get query to store the invoice in data into the controller`, () => { it(`should perform a get query to store the invoice in data into the controller`, () => {
const id = 1; const id = 1;
@ -39,4 +29,81 @@ 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

@ -2,8 +2,10 @@ Volume exceded: Volumen excedido
Volume: Volumen 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 PDF: Ver factura en PDF Show invoice...: Ver factura...
Send invoice PDF: Enviar factura en PDF Send invoice...: Enviar factura...
Send PDF invoice: Enviar factura en PDF
Send CSV invoice: Enviar factura en CSV
Delete Invoice: Eliminar factura Delete Invoice: Eliminar factura
Clone Invoice: Clonar factura Clone Invoice: Clonar factura
InvoiceOut deleted: Factura eliminada InvoiceOut deleted: Factura eliminada

View File

@ -2,6 +2,7 @@
icon="more_vert" icon="more_vert"
vn-popover="menu"> vn-popover="menu">
</vn-icon-button> </vn-icon-button>
<vn-menu vn-id="menu"> <vn-menu vn-id="menu">
<vn-list> <vn-list>
<vn-item <vn-item
@ -12,15 +13,44 @@
translate> translate>
Add turn Add turn
</vn-item> </vn-item>
<vn-item <vn-item class="dropdown"
ng-click="$ctrl.showDeliveryNote()" vn-click-stop="showDeliveryNoteMenu.show($event, 'left')"
translate> 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>
<vn-item <vn-item class="dropdown"
ng-click="confirmDeliveryNote.show()" vn-click-stop="sendDeliveryNoteMenu.show($event, 'left')"
translate> 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>
<vn-item <vn-item
ng-click="deleteConfirmation.show()" ng-click="deleteConfirmation.show()"
@ -79,7 +109,7 @@
Make invoice Make invoice
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="createInvoicePdfConfirmation.show()" ng-click="createPdfConfirmation.show()"
ng-show="$ctrl.isInvoiced && ($ctrl.hasInvoicing || !$ctrl.ticket.invoiceOut.hasPdf)" ng-show="$ctrl.isInvoiced && ($ctrl.hasInvoicing || !$ctrl.ticket.invoiceOut.hasPdf)"
name="regenerateInvoice" name="regenerateInvoice"
translate> translate>
@ -133,13 +163,39 @@
</div> </div>
</vn-popup> </vn-popup>
<!-- Send delivery note confirmation popup --> <!-- Send PDF delivery note confirmation popup -->
<vn-confirm <vn-dialog
vn-id="confirmDeliveryNote" vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendDeliveryNote()" on-accept="$ctrl.sendPdfDeliveryNote($data)"
question="Are you sure you want to send it?" message="Send PDF Delivery Note">
message="Send Delivery Note"> <tpl-body>
</vn-confirm> <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 --> <!-- Delete ticket confirmation popup -->
<vn-confirm <vn-confirm
@ -206,8 +262,8 @@
<!-- Create invoice PDF confirmation dialog --> <!-- Create invoice PDF confirmation dialog -->
<vn-confirm <vn-confirm
vn-id="createInvoicePdfConfirmation" vn-id="createPdfConfirmation"
on-accept="$ctrl.createInvoicePdf()" on-accept="$ctrl.createPdfInvoice()"
question="Are you sure you want to generate/regenerate the PDF invoice?" question="Are you sure you want to generate/regenerate the PDF invoice?"
message="Generate PDF invoice document"> message="Generate PDF invoice document">
</vn-confirm> </vn-confirm>

View File

@ -115,17 +115,32 @@ class Controller extends Section {
}); });
} }
showDeliveryNote() { showPdfDeliveryNote() {
this.vnReport.show('delivery-note', { this.vnReport.show('delivery-note', {
recipientId: this.ticket.client.id, recipientId: this.ticket.client.id,
ticketId: this.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', { return this.vnEmail.send('delivery-note', {
recipientId: this.ticket.client.id, 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 ticketId: this.id
}); });
} }
@ -227,7 +242,7 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
} }
createInvoicePdf() { createPdfInvoice() {
const invoiceId = this.ticket.invoiceOut.id; const invoiceId = this.ticket.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => this.reload()) .then(() => this.reload())

View File

@ -2,6 +2,7 @@ import './index.js';
describe('Ticket Component vnTicketDescriptorMenu', () => { describe('Ticket Component vnTicketDescriptorMenu', () => {
let $httpBackend; let $httpBackend;
let $httpParamSerializer;
let controller; let controller;
let $state; let $state;
@ -25,8 +26,9 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
beforeEach(ngModule('ticket')); beforeEach(ngModule('ticket'));
beforeEach(inject(($componentController, _$httpBackend_, _$state_) => { beforeEach(inject(($componentController, _$httpBackend_, _$httpParamSerializer_, _$state_) => {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
$httpParamSerializer = _$httpParamSerializer_;
$state = _$state_; $state = _$state_;
$state.params.id = 16; $state.params.id = 16;
$state.getCurrentPath = () => [null, {state: {name: 'ticket'}}]; $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', () => { 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 expectedParams = {
const params = { ticketId: ticket.id,
recipientId: ticket.client.id, recipientId: ticket.client.id
ticketId: ticket.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()', () => { it('should make a query and call vnApp.showMessage()', () => {
jest.spyOn(controller.vnEmail, 'send'); jest.spyOn(controller.vnEmail, 'send');
const $data = {email: 'brucebanner@gothamcity.com'};
const params = { const params = {
recipient: ticket.client.email, recipient: $data.email,
recipientId: ticket.client.id, recipientId: ticket.client.id,
ticketId: ticket.id ticketId: ticket.id
}; };
controller.sendDeliveryNote(); controller.sendPdfDeliveryNote($data);
expect(controller.vnEmail.send).toHaveBeenCalledWith('delivery-note', params); 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()', () => { describe('makeInvoice()', () => {
it('should make a query and call $state.reload() method', () => { it('should make a query and call $state.reload() method', () => {
jest.spyOn(controller, 'reload').mockReturnThis(); 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', () => { it('should make a query and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
$httpBackend.whenGET(`Tickets/16`).respond(); $httpBackend.whenGET(`Tickets/16`).respond();
$httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond(); $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
controller.createInvoicePdf(); controller.createPdfInvoice();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled(); expect(controller.vnApp.showSuccess).toHaveBeenCalled();

View File

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

View File

@ -8,8 +8,6 @@ Add stowaway: Añadir polizón
Delete stowaway: Eliminar 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 delete this stowaway?: ¿Seguro que quieres eliminar este polizón?
Are you sure you want to send it?: ¿Seguro que quieres enviarlo? 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 Show pallet report: Ver hoja de pallet
Change shipped hour: Cambiar hora de envío Change shipped hour: Cambiar hora de envío
Shipped hour: Hora de envío Shipped hour: Hora de envío

View File

@ -37,20 +37,30 @@ class Email extends Component {
return userTranslations.subject; 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 instance = this.build();
const rendered = await this.render(); const rendered = await this.render();
const attachments = []; const attachments = [];
const getAttachments = async(componentPath, files) => { const getAttachments = async(componentPath, files) => {
for (file of files) { for (file of files) {
const fileCopy = Object.assign({}, file); const fileCopy = Object.assign({}, file);
const fileName = fileCopy.filename;
if (options.overrideAttachments && !fileName.includes('.png')) continue;
if (fileCopy.cid) { if (fileCopy.cid) {
const templatePath = `${componentPath}/${file.path}`; const templatePath = `${componentPath}/${file.path}`;
const fullFilePath = path.resolve(__dirname, templatePath); const fullFilePath = path.resolve(__dirname, templatePath);
fileCopy.path = path.resolve(__dirname, fullFilePath); fileCopy.path = path.resolve(__dirname, fullFilePath);
} else { } else {
const reportName = fileCopy.filename.replace('.pdf', ''); const reportName = fileName.replace('.pdf', '');
const report = new Report(reportName, this.args); const report = new Report(reportName, this.args);
fileCopy.content = await report.toPdfStream(); fileCopy.content = await report.toPdfStream();
} }
@ -71,9 +81,14 @@ class Email extends Component {
if (this.attachments) if (this.attachments)
await getAttachments(this.path, 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 localeSubject = await this.getSubject();
const replyTo = this.args.replyTo || this.args.auth.email; const replyTo = this.args.replyTo || this.args.auth.email;
const options = { const mailOptions = {
to: this.args.recipient, to: this.args.recipient,
replyTo: replyTo, replyTo: replyTo,
subject: localeSubject, subject: localeSubject,
@ -81,7 +96,7 @@ class Email extends Component {
attachments: attachments attachments: attachments
}; };
return smtp.send(options); return smtp.send(mailOptions);
} }
} }

View File

@ -8,9 +8,10 @@ module.exports = app => {
const methods = []; const methods = [];
// Get all methods // Get all methods
methodsDir.forEach(method => { for (let method of methodsDir) {
methods.push(method.replace('.js', '')); if (method.includes('.js'))
}); methods.push(method.replace('.js', ''));
}
// Auth middleware // Auth middleware
const paths = []; const paths = [];

31
print/methods/csv.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,9 @@ module.exports = {
const hash = md5(this.signature.id.toString()).substring(0, 3); const hash = md5(this.signature.id.toString()).substring(0, 3);
const file = `${config.storage.root}/${hash}/${this.signature.id}.png`; const file = `${config.storage.root}/${hash}/${this.signature.id}.png`;
if (!fs.existsSync(file)) return null;
const src = fs.readFileSync(file); const src = fs.readFileSync(file);
const base64 = Buffer.from(src, 'utf8').toString('base64'); const base64 = Buffer.from(src, 'utf8').toString('base64');

View File

@ -95,7 +95,6 @@ module.exports = {
}, },
ticketSubtotal(ticket) { ticketSubtotal(ticket) {
let subTotal = 0.00; let subTotal = 0.00;
console.log(ticket.sales);
for (let sale of ticket.sales) for (let sale of ticket.sales)
subTotal += this.saleImport(sale); subTotal += this.saleImport(sale);