diff --git a/db/changes/10370-pickles/00-item_getBalance.sql b/db/changes/10370-pickles/00-item_getBalance.sql
new file mode 100644
index 0000000000..91e5c36815
--- /dev/null
+++ b/db/changes/10370-pickles/00-item_getBalance.sql
@@ -0,0 +1,144 @@
+DROP PROCEDURE IF EXISTS vn.item_getBalance;
+
+DELIMITER $$
+$$
+CREATE
+ definer = root@`%` procedure vn.item_getBalance(IN vItemId int, IN vWarehouse int)
+BEGIN
+ DECLARE vDateInventory DATETIME;
+ DECLARE vCurdate DATE DEFAULT CURDATE();
+ DECLARE vDayEnd DATETIME DEFAULT util.dayEnd(vCurdate);
+
+ SELECT inventoried INTO vDateInventory FROM config;
+ SET @a = 0;
+ SET @currentLineFk = 0;
+ SET @shipped = '';
+
+ SELECT DATE(@shipped:= shipped) shipped,
+ alertLevel,
+ stateName,
+ origin,
+ reference,
+ clientFk,
+ name,
+ `in` AS invalue,
+ `out`,
+ @a := @a + IFNULL(`in`,0) - IFNULL(`out`,0) as balance,
+ @currentLineFk := IF (@shipped < CURDATE()
+ OR (@shipped = CURDATE() AND (isPicked OR alertLevel >= 2)),
+ lineFk,@currentLineFk) lastPreparedLineFk,
+ isTicket,
+ lineFk,
+ isPicked,
+ clientType,
+ claimFk
+ FROM
+ ( SELECT tr.landed AS shipped,
+ b.quantity AS `in`,
+ NULL AS `out`,
+ al.id AS alertLevel,
+ st.name AS stateName,
+ s.name AS name,
+ e.ref AS reference,
+ e.id AS origin,
+ s.id AS clientFk,
+ IF(al.id = 3, TRUE, FALSE) isPicked,
+ FALSE AS isTicket,
+ b.id lineFk,
+ NULL `order`,
+ NULL AS clientType,
+ NULL AS claimFk
+ FROM buy b
+ JOIN entry e ON e.id = b.entryFk
+ JOIN travel tr ON tr.id = e.travelFk
+ JOIN supplier s ON s.id = e.supplierFk
+ JOIN alertLevel al ON al.id =
+ CASE
+ WHEN tr.shipped < CURDATE() THEN 3
+ WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
+ ELSE 0
+ END
+ JOIN state st ON st.code = al.code
+ WHERE tr.landed >= vDateInventory
+ AND vWarehouse = tr.warehouseInFk
+ AND b.itemFk = vItemId
+ AND e.isInventory = FALSE
+ AND e.isRaid = FALSE
+ UNION ALL
+
+ SELECT tr.shipped,
+ NULL,
+ b.quantity,
+ al.id,
+ st.name,
+ s.name,
+ e.ref,
+ e.id,
+ s.id,
+ IF(al.id = 3, TRUE, FALSE),
+ FALSE,
+ b.id,
+ NULL,
+ NULL,
+ NULL
+ FROM buy b
+ JOIN entry e ON e.id = b.entryFk
+ JOIN travel tr ON tr.id = e.travelFk
+ JOIN warehouse w ON w.id = tr.warehouseOutFk
+ JOIN supplier s ON s.id = e.supplierFk
+ JOIN alertLevel al ON al.id =
+ CASE
+ WHEN tr.shipped < CURDATE() THEN 3
+ WHEN tr.shipped = CURDATE() AND tr.isReceived = TRUE THEN 3
+ ELSE 0
+ END
+ JOIN state st ON st.code = al.code
+ WHERE tr.shipped >= vDateInventory
+ AND vWarehouse =tr.warehouseOutFk
+ AND s.id <> 4
+ AND b.itemFk = vItemId
+ AND e.isInventory = FALSE
+ AND w.isFeedStock = FALSE
+ AND e.isRaid = FALSE
+ UNION ALL
+
+ SELECT DATE(t.shipped),
+ NULL,
+ s.quantity,
+ al.id,
+ st.name,
+ t.nickname,
+ t.refFk,
+ t.id,
+ t.clientFk,
+ stk.id,
+ TRUE,
+ s.id,
+ st.`order`,
+ ct.code,
+ cl.id
+ FROM sale s
+ JOIN ticket t ON t.id = s.ticketFk
+ LEFT JOIN ticketState ts ON ts.ticket = t.id
+ LEFT JOIN state st ON st.code = ts.code
+ JOIN client c ON c.id = t.clientFk
+ JOIN clientType ct ON ct.id = c.clientTypeFk
+ JOIN alertLevel al ON al.id =
+ CASE
+ WHEN t.shipped < curdate() THEN 3
+ WHEN t.shipped > util.dayEnd(curdate()) THEN 0
+ ELSE IFNULL(ts.alertLevel, 0)
+ END
+ LEFT JOIN state stPrep ON stPrep.`code` = 'PREPARED'
+ LEFT JOIN saleTracking stk ON stk.saleFk = s.id AND stk.stateFk = stPrep.id
+ LEFT JOIN claim cl ON cl.ticketFk = t.id
+ WHERE t.shipped >= vDateInventory
+ AND s.itemFk = vItemId
+ AND vWarehouse =t.warehouseFk
+ ORDER BY shipped, alertLevel DESC, isTicket, `order` DESC, isPicked DESC, `in` DESC, `out` DESC
+ ) AS itemDiary;
+
+END;
+$$
+DELIMITER ;
+
diff --git a/e2e/helpers/puppeteer.js b/e2e/helpers/puppeteer.js
index 97ec492609..70546b0803 100644
--- a/e2e/helpers/puppeteer.js
+++ b/e2e/helpers/puppeteer.js
@@ -47,6 +47,13 @@ export async function getBrowser() {
});
page = extendPage(page);
page.setDefaultTimeout(5000);
+ await page.addStyleTag({
+ content: `* {
+ transition: none!important;
+ animation: none!important;
+ }`
+ });
+
await page.goto(defaultURL, {waitUntil: 'load'});
return {page, close: browser.close.bind(browser)};
}
diff --git a/e2e/paths/09-invoice-out/03_manualInvoice.spec.js b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js
index d32ab3f3cf..71ece00300 100644
--- a/e2e/paths/09-invoice-out/03_manualInvoice.spec.js
+++ b/e2e/paths/09-invoice-out/03_manualInvoice.spec.js
@@ -18,6 +18,7 @@ describe('InvoiceOut manual invoice path', () => {
it('should open the manual invoice form', async() => {
await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createInvoice);
+ await page.waitForTimeout(1000); // initialization of functionality takes about 1000ms to work
await page.waitToClick(selectors.invoiceOutIndex.createManualInvoice);
await page.waitForSelector(selectors.invoiceOutIndex.manualInvoiceForm);
});
diff --git a/front/core/components/menu/menu.js b/front/core/components/menu/menu.js
index d0c6490040..e0d968e2f8 100755
--- a/front/core/components/menu/menu.js
+++ b/front/core/components/menu/menu.js
@@ -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());
}
}
diff --git a/front/core/components/menu/style.scss b/front/core/components/menu/style.scss
index 92f4372432..6125c0e5bc 100644
--- a/front/core/components/menu/style.scss
+++ b/front/core/components/menu/style.scss
@@ -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
+ }
}
diff --git a/front/core/components/popover/index.js b/front/core/components/popover/index.js
index d78179ab9b..446c0697b9 100644
--- a/front/core/components/popover/index.js
+++ b/front/core/components/popover/index.js
@@ -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`;
diff --git a/front/core/services/email.js b/front/core/services/email.js
index 4385eed5f3..633b13a263 100644
--- a/front/core/services/email.js
+++ b/front/core/services/email.js
@@ -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'];
diff --git a/front/core/services/report.js b/front/core/services/report.js
index 32ccb52a37..c58a0ee0e2 100644
--- a/front/core/services/report.js
+++ b/front/core/services/report.js
@@ -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'];
diff --git a/front/salix/components/layout/index.html b/front/salix/components/layout/index.html
index cb0fb93b4d..cd13c565e0 100644
--- a/front/salix/components/layout/index.html
+++ b/front/salix/components/layout/index.html
@@ -3,7 +3,7 @@
icon="menu"
class="show-menu"
ng-if="$ctrl.leftMenu"
- ng-click="$ctrl.leftMenu.show()">
+ ng-click="$ctrl.leftMenu.toggle()">
diff --git a/front/salix/components/layout/style.scss b/front/salix/components/layout/style.scss
index d12f3a5cdc..aa35f8d14e 100644
--- a/front/salix/components/layout/style.scss
+++ b/front/salix/components/layout/style.scss
@@ -48,6 +48,10 @@ vn-layout {
.show-menu {
display: none;
}
+ & > .show-menu {
+ margin-right: 5px;
+ display: block
+ }
.vn-button {
color: inherit;
font-size: 1.05rem;
@@ -71,6 +75,10 @@ vn-layout {
& > .main-view {
padding-left: $menu-width;
}
+
+ &.shown > .main-view {
+ padding-left: 0;
+ }
}
&.right-menu {
& > vn-topbar > .end {
@@ -85,6 +93,8 @@ vn-layout {
}
& > .main-view {
padding-top: $topbar-height;
+
+ transition: padding-left 200ms ease-out;
}
ui-view {
& > * {
@@ -134,7 +144,8 @@ vn-layout {
& > vn-topbar {
left: 0;
}
- & > .main-view {
+ & > .main-view,
+ &.shown > .main-view {
padding-left: 0;
}
}
diff --git a/front/salix/components/side-menu/side-menu.js b/front/salix/components/side-menu/side-menu.js
index 0e683b4bb7..3af3ce9df7 100644
--- a/front/salix/components/side-menu/side-menu.js
+++ b/front/salix/components/side-menu/side-menu.js
@@ -52,12 +52,18 @@ export default class SideMenu extends Component {
this.hide();
}
+ toggle() {
+ if (this.shown) this.hide();
+ else this.show();
+ }
+
show() {
if (this.shown) return;
this.shown = true;
this.handler = e => this.onEscape(e);
this.$window.addEventListener('keydown', this.handler);
this.stateHandler = this.$transitions.onStart({}, t => this.onTransition(t));
+ this.layout.element.classList.add('shown');
}
hide() {
@@ -65,6 +71,7 @@ export default class SideMenu extends Component {
this.$window.removeEventListener('keydown', this.handler);
this.stateHandler();
this.shown = false;
+ this.layout.element.classList.remove('shown');
}
}
diff --git a/front/salix/components/side-menu/style.scss b/front/salix/components/side-menu/style.scss
index b9722d519c..50066309ac 100644
--- a/front/salix/components/side-menu/style.scss
+++ b/front/salix/components/side-menu/style.scss
@@ -14,16 +14,20 @@ vn-side-menu > .menu {
box-shadow: 0 1px 3px $color-shadow;
overflow: auto;
top: $topbar-height;
+ transition: transform 200ms ease-out;
&.left {
- left: 0;
+ left: 0
}
&.right {
right: 0;
}
+ &.shown {
+ transform: translateZ(0) translateX(-$menu-width);
+ }
+
@media screen and (max-width: $mobile-width) {
- transition: transform 200ms ease-out;
z-index: 15;
top: 0;
diff --git a/front/salix/components/upload-photo/index.html b/front/salix/components/upload-photo/index.html
index 3dd6cdb278..9b64a380fa 100644
--- a/front/salix/components/upload-photo/index.html
+++ b/front/salix/components/upload-photo/index.html
@@ -61,7 +61,7 @@
this.editor.bind({url: e.target.result});
reader.readAsDataURL(value);
- } else if (this.uploadMethod == 'URL')
- this.editor.bind({url: value});
+ } else if (this.uploadMethod == 'URL') {
+ const img = new Image();
+ img.crossOrigin = 'Anonymous';
+ img.src = value;
+ img.onload = () => this.editor.bind({url: value});
+ img.onerror = () => {
+ this.vnApp.showError(
+ this.$t(`This photo provider doesn't allow remote downloads`)
+ );
+ };
+ }
}
}
diff --git a/front/salix/components/upload-photo/locale/es.yml b/front/salix/components/upload-photo/locale/es.yml
index bcc3801d88..398e5172e7 100644
--- a/front/salix/components/upload-photo/locale/es.yml
+++ b/front/salix/components/upload-photo/locale/es.yml
@@ -3,6 +3,8 @@ Select an image: Selecciona una imagen
File name: Nombre del fichero
Rotate left: Girar a la izquierda
Rotate right: Girar a la derecha
-Panoramic: Panorámico
+Panoramic: Panorámica
+Orientation: Orientación
Select from computer: Seleccionar desde ordenador
-Import from external URL: Importar desde URL externa
\ No newline at end of file
+Import from external URL: Importar desde URL externa
+This photo provider doesn't allow remote downloads: Este proveedor de fotos no permite descargas remotas
\ No newline at end of file
diff --git a/modules/invoiceOut/front/descriptor/index.html b/modules/invoiceOut/front/descriptor/index.html
index 4d9a4ffd88..e96e994470 100644
--- a/modules/invoiceOut/front/descriptor/index.html
+++ b/modules/invoiceOut/front/descriptor/index.html
@@ -2,18 +2,49 @@
module="invoiceOut"
description="$ctrl.invoiceOut.ref">
-
- Show invoice PDF
-
-
+
+
+ Show as PDF
+
+
+ Show as CSV
+
+
+
+
+
- Send invoice PDF
+ Send invoice...
+
+
+
+
+ Send PDF
+
+
+ Send CSV
+
+
+
-
-
+
+
Are you sure you want to send it?
+ ng-model="sendPdfConfirmation.data.email">
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to send it?
+
diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js
index 0600ffa5b9..129fe16d1a 100644
--- a/modules/invoiceOut/front/descriptor/index.js
+++ b/modules/invoiceOut/front/descriptor/index.js
@@ -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', {
diff --git a/modules/invoiceOut/front/descriptor/index.spec.js b/modules/invoiceOut/front/descriptor/index.spec.js
index 0a5494b9ab..12430d44db 100644
--- a/modules/invoiceOut/front/descriptor/index.spec.js
+++ b/modules/invoiceOut/front/descriptor/index.spec.js
@@ -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();
+ });
+ });
});
diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml
index 3430fe4e70..2f377ebdf8 100644
--- a/modules/invoiceOut/front/descriptor/locale/es.yml
+++ b/modules/invoiceOut/front/descriptor/locale/es.yml
@@ -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
diff --git a/modules/item/front/diary/index.html b/modules/item/front/diary/index.html
index 9a983a0eb4..2f5bce1973 100644
--- a/modules/item/front/diary/index.html
+++ b/modules/item/front/diary/index.html
@@ -29,6 +29,7 @@
+
Date
Id
State
@@ -48,6 +49,14 @@
vn-repeat-last
on-last="$ctrl.scrollToLine(sale.lastPreparedLineFk)"
ng-attr-id="vnItemDiary-{{::sale.lineFk}}">
+
+
+
+
+
+
diff --git a/modules/item/front/last-entries/index.html b/modules/item/front/last-entries/index.html
index 8200856543..af7bbd751b 100644
--- a/modules/item/front/last-entries/index.html
+++ b/modules/item/front/last-entries/index.html
@@ -7,17 +7,26 @@
order="landed DESC, buyFk DESC"
limit="20">
-
-
-
+
+
+
+
+
+
+
+
+
+ class="vn-mb-xl vn-w-xl vn-pa-md">
diff --git a/modules/item/front/last-entries/index.js b/modules/item/front/last-entries/index.js
index d6a2a9913d..a92ec48584 100644
--- a/modules/item/front/last-entries/index.js
+++ b/modules/item/front/last-entries/index.js
@@ -1,17 +1,16 @@
import ngModule from '../module';
import Section from 'salix/components/section';
-import './style.scss';
class Controller extends Section {
constructor($element, $) {
super($element, $);
const from = new Date();
- from.setDate(from.getDate() - 75);
+ from.setDate(from.getDate());
from.setHours(0, 0, 0, 0);
const to = new Date();
- to.setDate(to.getDate() + 60);
+ to.setDate(to.getDate() + 10);
to.setHours(23, 59, 59, 59);
this.filter = {
@@ -22,19 +21,19 @@ class Controller extends Section {
}
}
};
- this._date = from;
+ this._dateFrom = from;
+ this._dateTo = to;
}
- set date(value) {
- this._date = value;
+ set dateFrom(value) {
+ this._dateFrom = value;
if (!value) return;
const from = new Date(value);
from.setHours(0, 0, 0, 0);
- const to = new Date();
- to.setDate(to.getDate() + 60);
+ const to = new Date(this._dateTo);
to.setHours(23, 59, 59, 59);
this.filter.where.shipped = {
@@ -43,8 +42,29 @@ class Controller extends Section {
this.$.model.refresh();
}
- get date() {
- return this._date;
+ set dateTo(value) {
+ this._dateTo = value;
+
+ if (!value) return;
+
+ const from = new Date(this._dateFrom);
+ from.setHours(0, 0, 0, 0);
+
+ const to = new Date(value);
+ to.setHours(23, 59, 59, 59);
+
+ this.filter.where.shipped = {
+ between: [from, to]
+ };
+ this.$.model.refresh();
+ }
+
+ get dateFrom() {
+ return this._dateFrom;
+ }
+
+ get dateTo() {
+ return this._dateTo;
}
exprBuilder(param, value) {
diff --git a/modules/item/front/last-entries/style.scss b/modules/item/front/last-entries/style.scss
deleted file mode 100644
index 6188daabca..0000000000
--- a/modules/item/front/last-entries/style.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-@import "variables";
-
-vn-item-last-entries {
- .round {
- background-color: $color-spacer;
- border-radius: 25px;
- float: right;
- width: 25px;
- color: $color-font-dark;
- text-align: center;
- font-weight: bold;
- }
- vn-horizontal {
- justify-content: center;
- }
- vn-date-picker {
- flex: none !important;
- }
- @media screen and (max-width: 1440px) {
- .expendable {
- display: none;
- }
- }
-}
diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html
index 555246f9f2..ae5642cf32 100644
--- a/modules/ticket/front/descriptor-menu/index.html
+++ b/modules/ticket/front/descriptor-menu/index.html
@@ -2,6 +2,7 @@
icon="more_vert"
vn-popover="menu">
+
Add turn
-
- Show Delivery Note
+ Show Delivery Note...
+
+
+
+ Show as PDF
+
+
+ Show as CSV
+
+
+
-
- Send Delivery Note
+ Send Delivery Note...
+
+
+
+
+ Send PDF
+
+
+ Send CSV
+
+
+
@@ -133,13 +163,39 @@
-
-
-
+
+
+
+ Are you sure you want to send it?
+
+
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to send it?
+
+
+
+
+
+
+
+
diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js
index 96475b7b94..142f44989c 100644
--- a/modules/ticket/front/descriptor-menu/index.js
+++ b/modules/ticket/front/descriptor-menu/index.js
@@ -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())
diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js
index eabb772355..e9486bcd07 100644
--- a/modules/ticket/front/descriptor-menu/index.spec.js
+++ b/modules/ticket/front/descriptor-menu/index.spec.js
@@ -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();
diff --git a/modules/ticket/front/descriptor-menu/locale/es.yml b/modules/ticket/front/descriptor-menu/locale/es.yml
new file mode 100644
index 0000000000..1f4ee710cb
--- /dev/null
+++ b/modules/ticket/front/descriptor-menu/locale/es.yml
@@ -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
\ No newline at end of file
diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml
index c72b2e9233..f56f29f92a 100644
--- a/modules/ticket/front/descriptor/locale/es.yml
+++ b/modules/ticket/front/descriptor/locale/es.yml
@@ -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
diff --git a/print/core/email.js b/print/core/email.js
index f201be9a81..620c1e083d 100644
--- a/print/core/email.js
+++ b/print/core/email.js
@@ -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);
}
}
diff --git a/print/core/router.js b/print/core/router.js
index feaea214b9..c0f20dd9af 100644
--- a/print/core/router.js
+++ b/print/core/router.js
@@ -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 = [];
diff --git a/print/methods/csv.js b/print/methods/csv.js
new file mode 100644
index 0000000000..4f4cdf2aff
--- /dev/null
+++ b/print/methods/csv.js
@@ -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);
+ }
+};
diff --git a/print/methods/csv/delivery-note/index.js b/print/methods/csv/delivery-note/index.js
new file mode 100644
index 0000000000..9ef0e33fa1
--- /dev/null
+++ b/print/methods/csv/delivery-note/index.js
@@ -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;
+};
diff --git a/print/methods/csv/delivery-note/sql/sales.sql b/print/methods/csv/delivery-note/sql/sales.sql
new file mode 100644
index 0000000000..e5b419571b
--- /dev/null
+++ b/print/methods/csv/delivery-note/sql/sales.sql
@@ -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
\ No newline at end of file
diff --git a/print/methods/csv/delivery-note/sql/ticket.sql b/print/methods/csv/delivery-note/sql/ticket.sql
new file mode 100644
index 0000000000..b80c7c42c9
--- /dev/null
+++ b/print/methods/csv/delivery-note/sql/ticket.sql
@@ -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 = ?
\ No newline at end of file
diff --git a/print/methods/csv/invoice/index.js b/print/methods/csv/invoice/index.js
new file mode 100644
index 0000000000..8f325be021
--- /dev/null
+++ b/print/methods/csv/invoice/index.js
@@ -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;
+};
diff --git a/print/methods/csv/invoice/sql/invoice.sql b/print/methods/csv/invoice/sql/invoice.sql
new file mode 100644
index 0000000000..853aaddc0b
--- /dev/null
+++ b/print/methods/csv/invoice/sql/invoice.sql
@@ -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 = ?
\ No newline at end of file
diff --git a/print/methods/csv/invoice/sql/sales.sql b/print/methods/csv/invoice/sql/sales.sql
new file mode 100644
index 0000000000..34b5af1f7c
--- /dev/null
+++ b/print/methods/csv/invoice/sql/sales.sql
@@ -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
\ No newline at end of file
diff --git a/print/templates/reports/delivery-note/delivery-note.js b/print/templates/reports/delivery-note/delivery-note.js
index 549759651a..9b3328d053 100755
--- a/print/templates/reports/delivery-note/delivery-note.js
+++ b/print/templates/reports/delivery-note/delivery-note.js
@@ -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');
diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js
index 3e87343069..b56a5533c5 100755
--- a/print/templates/reports/invoice/invoice.js
+++ b/print/templates/reports/invoice/invoice.js
@@ -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);