@@ -81,4 +89,12 @@
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js
index cb4b131ac..3e859478d 100644
--- a/modules/invoiceOut/front/descriptor/index.js
+++ b/modules/invoiceOut/front/descriptor/index.js
@@ -22,6 +22,16 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked')));
}
+ createInvoicePdf() {
+ const invoiceId = this.invoiceOut.id;
+ return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
+ .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});
diff --git a/modules/invoiceOut/front/descriptor/index.spec.js b/modules/invoiceOut/front/descriptor/index.spec.js
index 987763b0a..c16900a0a 100644
--- a/modules/invoiceOut/front/descriptor/index.spec.js
+++ b/modules/invoiceOut/front/descriptor/index.spec.js
@@ -3,6 +3,7 @@ import './index';
describe('vnInvoiceOutDescriptor', () => {
let controller;
let $httpBackend;
+ const invoiceOut = {id: 1};
beforeEach(ngModule('invoiceOut'));
@@ -11,6 +12,20 @@ describe('vnInvoiceOutDescriptor', () => {
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.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;
diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml
index e85be96bf..dd67660ee 100644
--- a/modules/invoiceOut/front/descriptor/locale/es.yml
+++ b/modules/invoiceOut/front/descriptor/locale/es.yml
@@ -8,4 +8,6 @@ InvoiceOut deleted: Factura eliminada
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
Book invoice: Asentar factura
InvoiceOut booked: Factura asentada
-Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
\ No newline at end of file
+Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura?
+Regenerate invoice PDF: Regenerar PDF factura
+The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
\ No newline at end of file
diff --git a/modules/ticket/back/methods/ticket-request/confirm.js b/modules/ticket/back/methods/ticket-request/confirm.js
index b80971183..ce4bfdccb 100644
--- a/modules/ticket/back/methods/ticket-request/confirm.js
+++ b/modules/ticket/back/methods/ticket-request/confirm.js
@@ -74,12 +74,13 @@ module.exports = Self => {
const origin = ctx.req.headers.origin;
const requesterId = request.requesterFk;
- const message = $t('MESSAGE_BOUGHT_UNITS', {
+ const message = $t('Bought units from buy request', {
quantity: sale.quantity,
concept: sale.concept,
itemId: sale.itemFk,
ticketId: sale.ticketFk,
- url: `${origin}/#!/ticket/${sale.ticketFk}/summary`
+ url: `${origin}/#!/ticket/${sale.ticketFk}/summary`,
+ urlItem: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, requesterId, message);
diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js
index 29099e379..a44c41e16 100644
--- a/modules/ticket/back/methods/ticket/makeInvoice.js
+++ b/modules/ticket/back/methods/ticket/makeInvoice.js
@@ -67,11 +67,7 @@ module.exports = function(Self) {
if (serial != 'R' && invoiceId) {
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
- await models.PrintServerQueue.create({
- reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas
- param1: invoiceId,
- workerFk: userId
- }, options);
+ await models.InvoiceOut.createPdf(ctx, invoiceId, options);
}
await tx.commit();
diff --git a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js
index 79bd59848..e20d9d8a2 100644
--- a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js
+++ b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js
@@ -5,6 +5,7 @@ describe('ticket makeInvoice()', () => {
const userId = 19;
const activeCtx = {
accessToken: {userId: userId},
+ headers: {origin: 'http://localhost:5000'},
};
const ctx = {req: activeCtx};
@@ -43,6 +44,9 @@ describe('ticket makeInvoice()', () => {
});
it('should invoice a ticket, then try again to fail', async() => {
+ const invoiceOutModel = app.models.InvoiceOut;
+ spyOn(invoiceOutModel, 'createPdf');
+
invoice = await app.models.Ticket.makeInvoice(ctx, ticketId);
expect(invoice.invoiceFk).toBeDefined();
diff --git a/modules/ticket/back/models/ticket.json b/modules/ticket/back/models/ticket.json
index 8f91ee689..65127a78c 100644
--- a/modules/ticket/back/models/ticket.json
+++ b/modules/ticket/back/models/ticket.json
@@ -2,7 +2,8 @@
"name": "Ticket",
"base": "Loggable",
"log": {
- "model":"TicketLog"
+ "model":"TicketLog",
+ "showField": "id"
},
"options": {
"mysql": {
diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html
index 80ad71d5f..390d9daf7 100644
--- a/modules/ticket/front/descriptor-menu/index.html
+++ b/modules/ticket/front/descriptor-menu/index.html
@@ -80,13 +80,13 @@
Make invoice
- Regenerate invoice
+ Regenerate invoice PDF
-
+
+ vn-id="createInvoicePdfConfirmation"
+ on-accept="$ctrl.createInvoicePdf()"
+ question="Are you sure you want to regenerate the invoice PDF document?"
+ message="You are going to regenerate the invoice PDF document">
diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js
index d2dea6c0a..09783ec20 100644
--- a/modules/ticket/front/descriptor-menu/index.js
+++ b/modules/ticket/front/descriptor-menu/index.js
@@ -219,12 +219,12 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
}
- regenerateInvoice() {
+ createInvoicePdf() {
const invoiceId = this.ticket.invoiceOut.id;
- return this.$http.post(`InvoiceOuts/${invoiceId}/regenerate`)
+ return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => {
const snackbarMessage = this.$t(
- `Invoice sent for a regeneration, will be available in a few minutes`);
+ `The invoice PDF document has been regenerated`);
this.vnApp.showSuccess(snackbarMessage);
});
}
diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js
index 3cd08fc38..6a3009daf 100644
--- a/modules/ticket/front/descriptor-menu/index.spec.js
+++ b/modules/ticket/front/descriptor-menu/index.spec.js
@@ -148,12 +148,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
});
});
- describe('regenerateInvoice()', () => {
+ describe('createInvoicePdf()', () => {
it('should make a query and show a success snackbar', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
- $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/regenerate`).respond();
- controller.regenerateInvoice();
+ $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
+ controller.createInvoicePdf();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml
index 6524df353..c2b181c97 100644
--- a/modules/ticket/front/descriptor/locale/es.yml
+++ b/modules/ticket/front/descriptor/locale/es.yml
@@ -17,12 +17,12 @@ Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPo
Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales."
Ticket invoiced: Ticket facturado
Make invoice: Crear factura
-Regenerate invoice: Regenerar factura
+Regenerate invoice PDF: Regenerar PDF factura
+The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado
You are going to invoice this ticket: Vas a facturar este ticket
Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket?
-You are going to regenerate the invoice: Vas a regenerar la factura
-Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura?
-Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos
+You are going to regenerate the invoice PDF document: Vas a regenerar el documento PDF de la factura
+Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de la factura?
Shipped hour updated: Hora de envio modificada
Deleted ticket: Ticket eliminado
Recalculate components: Recalcular componentes
diff --git a/print/common/css/misc.css b/print/common/css/misc.css
index 09d7706b3..df8bf571a 100644
--- a/print/common/css/misc.css
+++ b/print/common/css/misc.css
@@ -45,4 +45,8 @@
.no-page-break {
page-break-inside: avoid;
break-inside: avoid
+}
+
+.page-break-after {
+ page-break-after: always;
}
\ No newline at end of file
diff --git a/print/common/css/report.css b/print/common/css/report.css
index 9331481f4..e8161f1fb 100644
--- a/print/common/css/report.css
+++ b/print/common/css/report.css
@@ -9,6 +9,6 @@ body {
.title {
margin-bottom: 20px;
font-weight: 100;
- font-size: 3em;
+ font-size: 2.6rem;
margin-top: 0
}
\ No newline at end of file
diff --git a/print/core/component.js b/print/core/component.js
index 4985cd061..12474566e 100644
--- a/print/core/component.js
+++ b/print/core/component.js
@@ -83,6 +83,11 @@ class Component {
component.template = juice.inlineContent(this.template, this.stylesheet, {
inlinePseudoElements: true
});
+ const tplPath = this.path;
+ if (!component.computed) component.computed = {};
+ component.computed.path = function() {
+ return tplPath;
+ };
return component;
}
@@ -93,7 +98,7 @@ class Component {
const component = this.build();
const i18n = new VueI18n(config.i18n);
- const props = {tplPath: this.path, ...this.args};
+ const props = {...this.args};
this._component = new Vue({
i18n: i18n,
render: h => h(component, {
diff --git a/print/core/database.js b/print/core/database.js
index e879d0e3a..dee7a2933 100644
--- a/print/core/database.js
+++ b/print/core/database.js
@@ -36,13 +36,14 @@ module.exports = {
* Makes a query from a SQL file
* @param {String} queryName - The SQL file name
* @param {Object} params - Parameterized values
+ * @param {Object} connection - Optional pool connection
*
* @return {Object} - Result promise
*/
- rawSqlFromDef(queryName, params) {
+ rawSqlFromDef(queryName, params, connection) {
const query = fs.readFileSync(`${queryName}.sql`, 'utf8');
- return this.rawSql(query, params);
+ return this.rawSql(query, params, connection);
},
/**
diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js
index 4a6f9e460..1de27e6cd 100644
--- a/print/core/mixins/db-helper.js
+++ b/print/core/mixins/db-helper.js
@@ -19,12 +19,13 @@ const dbHelper = {
* Makes a query from a SQL file
* @param {String} queryName - The SQL file name
* @param {Object} params - Parameterized values
+ * @param {Object} connection - Optional pool connection
*
* @return {Object} - Result promise
*/
- rawSqlFromDef(queryName, params) {
- const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName);
- return db.rawSqlFromDef(absolutePath, params);
+ rawSqlFromDef(queryName, params, connection) {
+ const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
+ return db.rawSqlFromDef(absolutePath, params, connection);
},
/**
@@ -66,7 +67,7 @@ const dbHelper = {
*/
findValueFromDef(queryName, params) {
return this.findOneFromDef(queryName, params).then(row => {
- return Object.values(row)[0];
+ if (row) return Object.values(row)[0];
});
},
@@ -77,7 +78,7 @@ const dbHelper = {
* @return {Object} - SQL
*/
getSqlFromDef(queryName) {
- const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName);
+ const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
return db.getSqlFromDef(absolutePath);
},
},
diff --git a/print/methods/report.js b/print/methods/report.js
index eea249a42..750fec4c8 100644
--- a/print/methods/report.js
+++ b/print/methods/report.js
@@ -6,11 +6,16 @@ module.exports = app => {
const reportName = req.params.name;
const fileName = getFileName(reportName, req.args);
const report = new Report(reportName, req.args);
- const stream = await report.toPdfStream();
+ if (req.args.preview) {
+ const template = await report.render();
+ res.send(template);
+ } else {
+ const stream = await report.toPdfStream();
- res.setHeader('Content-type', 'application/pdf');
- res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
- res.end(stream);
+ res.setHeader('Content-type', 'application/pdf');
+ res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
+ res.end(stream);
+ }
} catch (error) {
next(error);
}
diff --git a/print/templates/reports/delivery-note/assets/css/style.css b/print/templates/reports/delivery-note/assets/css/style.css
index cbe894097..f99c385fa 100644
--- a/print/templates/reports/delivery-note/assets/css/style.css
+++ b/print/templates/reports/delivery-note/assets/css/style.css
@@ -19,7 +19,7 @@ h2 {
}
.ticket-info {
- font-size: 26px
+ font-size: 22px
}
#phytosanitary {
diff --git a/print/templates/reports/delivery-note/sql/sales.sql b/print/templates/reports/delivery-note/sql/sales.sql
index d17f6feee..c449030cf 100644
--- a/print/templates/reports/delivery-note/sql/sales.sql
+++ b/print/templates/reports/delivery-note/sql/sales.sql
@@ -37,6 +37,7 @@ FROM vn.sale s
LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk
LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
AND ic.code = 'plant'
+ AND ib.ediBotanic IS NOT NULL
WHERE s.ticketFk = ?
GROUP BY s.id
ORDER BY (it.isPackaging), s.concept, s.itemFk
\ No newline at end of file
diff --git a/print/templates/reports/invoice-incoterms/assets/css/import.js b/print/templates/reports/invoice-incoterms/assets/css/import.js
new file mode 100644
index 000000000..fd8796c2b
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/assets/css/import.js
@@ -0,0 +1,9 @@
+const Stylesheet = require(`${appPath}/core/stylesheet`);
+
+module.exports = new Stylesheet([
+ `${appPath}/common/css/spacing.css`,
+ `${appPath}/common/css/misc.css`,
+ `${appPath}/common/css/layout.css`,
+ `${appPath}/common/css/report.css`,
+ `${__dirname}/style.css`])
+ .mergeStyles();
diff --git a/print/templates/reports/invoice-incoterms/assets/css/style.css b/print/templates/reports/invoice-incoterms/assets/css/style.css
new file mode 100644
index 000000000..d754d050f
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/assets/css/style.css
@@ -0,0 +1,29 @@
+h2 {
+ font-weight: 100;
+ color: #555
+}
+
+.table-title {
+ margin-bottom: 15px;
+ font-size: 0.8rem
+}
+
+.table-title h2 {
+ margin: 0 15px 0 0
+}
+
+.ticket-info {
+ font-size: 22px
+}
+
+#incoterms table {
+ font-size: 1.2rem
+}
+
+#incoterms table th {
+ width: 10%
+}
+
+#incoterms p {
+ font-size: 1.2rem
+}
\ No newline at end of file
diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.html b/print/templates/reports/invoice-incoterms/invoice-incoterms.html
new file mode 100644
index 000000000..2cceccc93
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('title')}}
+
+
+
+ {{$t('clientId')}} |
+ {{client.id}} |
+
+
+ {{$t('invoice')}} |
+ {{invoice.ref}} |
+
+
+ {{$t('date')}} |
+ {{invoice.issued | date('%d-%m-%Y')}} |
+
+
+
+
+
+
+
+
+
+ {{client.socialName}}
+
+ {{client.postalAddress}}
+
+
+ {{client.postcodeCity}}
+
+
+ {{$t('fiscalId')}}: {{client.fi}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('incoterms')}}
+ asd
+ |
+ {{incoterms.incotermsFk}} - {{incoterms.incotermsName}} |
+
+
+
+ {{$t('productDescription')}}
+ |
+ {{incoterms.intrastat}} |
+
+
+ {{$t('expeditionDescription')}} |
+ |
+
+
+ {{$t('packageNumber')}} |
+ {{incoterms.packages}} |
+
+
+ {{$t('packageGrossWeight')}} |
+ {{incoterms.weight}} KG |
+
+
+ {{$t('packageCubing')}} |
+ {{incoterms.volume}} m3 |
+
+
+
+
+
+
+ {{$t('customsInfo')}}
+ {{incoterms.customsAgentName}}
+
+
+ (
+ {{incoterms.customsAgentNif}}
+ {{incoterms.customsAgentStreet}}
+
+ ☎ {{incoterms.customsAgentPhone}}
+
+
+ ✉ {{incoterms.customsAgentEmail}}
+
+ )
+
+
+
+ {{$t('productDisclaimer')}}
+
+
+
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.js b/print/templates/reports/invoice-incoterms/invoice-incoterms.js
new file mode 100755
index 000000000..cfe0a21cb
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.js
@@ -0,0 +1,39 @@
+const Component = require(`${appPath}/core/component`);
+const reportHeader = new Component('report-header');
+const reportFooter = new Component('report-footer');
+
+module.exports = {
+ name: 'invoice-incoterms',
+ async serverPrefetch() {
+ this.invoice = await this.fetchInvoice(this.invoiceId);
+ this.client = await this.fetchClient(this.invoiceId);
+ this.incoterms = await this.fetchIncoterms(this.invoiceId);
+
+ if (!this.invoice)
+ throw new Error('Something went wrong');
+ },
+ computed: {
+
+ },
+ methods: {
+ fetchInvoice(invoiceId) {
+ return this.findOneFromDef('invoice', [invoiceId]);
+ },
+ fetchClient(invoiceId) {
+ return this.findOneFromDef('client', [invoiceId]);
+ },
+ fetchIncoterms(invoiceId) {
+ return this.findOneFromDef('incoterms', {invoiceId});
+ }
+ },
+ components: {
+ 'report-header': reportHeader.build(),
+ 'report-footer': reportFooter.build()
+ },
+ props: {
+ invoiceId: {
+ type: String,
+ required: true
+ }
+ }
+};
diff --git a/print/templates/reports/invoice-incoterms/locale/es.yml b/print/templates/reports/invoice-incoterms/locale/es.yml
new file mode 100644
index 000000000..9828564d7
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/locale/es.yml
@@ -0,0 +1,16 @@
+title: Factura
+invoice: Factura
+clientId: Cliente
+date: Fecha
+invoiceData: Datos de facturación
+fiscalId: CIF / NIF
+invoiceRef: Factura {0}
+incotermsTitle: Información para la exportación
+incoterms: Incoterms
+productDescription: Descripción de la mercancia
+expeditionDescription: INFORMACIÓN DE LA EXPEDICIÓN
+packageNumber: Número de bultos
+packageGrossWeight: Peso bruto
+packageCubing: Cubicaje
+customsInfo: A despachar por la agencia de aduanas
+productDisclaimer: Mercancía destinada a la exportación, EXENTA de IVA (Ley 37/1992 - Art. 21)
\ No newline at end of file
diff --git a/print/templates/reports/invoice-incoterms/sql/client.sql b/print/templates/reports/invoice-incoterms/sql/client.sql
new file mode 100644
index 000000000..dd6035222
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/sql/client.sql
@@ -0,0 +1,12 @@
+SELECT
+ c.id,
+ c.socialName,
+ c.street AS postalAddress,
+ IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
+ CONCAT(c.postcode, ' - ', c.city) postcodeCity
+FROM vn.invoiceOut io
+ JOIN vn.client c ON c.id = io.clientFk
+ JOIN vn.country cty ON cty.id = c.countryFk
+ LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
+ AND ios.taxAreaFk = 'CEE'
+WHERE io.id = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice-incoterms/sql/incoterms.sql b/print/templates/reports/invoice-incoterms/sql/incoterms.sql
new file mode 100644
index 000000000..e3d3f19c1
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/sql/incoterms.sql
@@ -0,0 +1,71 @@
+SELECT io.issued,
+ c.socialName,
+ c.street postalAddress,
+ IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
+ io.clientFk,
+ c.postcode,
+ c.city,
+ io.companyFk,
+ io.ref,
+ tc.code,
+ s.concept,
+ s.quantity,
+ s.price,
+ s.discount,
+ s.ticketFk,
+ t.shipped,
+ t.refFk,
+ a.nickname,
+ s.itemFk,
+ s.id saleFk,
+ pm.name AS pmname,
+ sa.iban,
+ c.phone,
+ MAX(t.packages) packages,
+ a.incotermsFk,
+ ic.name incotermsName ,
+ sub.description weight,
+ t.observations,
+ ca.fiscalName customsAgentName,
+ ca.street customsAgentStreet,
+ ca.nif customsAgentNif,
+ ca.phone customsAgentPhone,
+ ca.email customsAgentEmail,
+ CAST(sub2.volume AS DECIMAL (10,2)) volume,
+ sub3.intrastat
+ FROM vn.invoiceOut io
+ JOIN vn.supplier su ON su.id = io.companyFk
+ JOIN vn.client c ON c.id = io.clientFk
+ LEFT JOIN vn.province p ON p.id = c.provinceFk
+ JOIN vn.ticket t ON t.refFk = io.ref
+ LEFT JOIN (SELECT tob.ticketFk,tob.description
+ FROM vn.ticketObservation tob
+ LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk
+ WHERE ot.description = "Peso Aduana"
+ )sub ON sub.ticketFk = t.id
+ JOIN vn.address a ON a.id = t.addressFk
+ LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk
+ LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk
+ JOIN vn.sale s ON s.ticketFk = t.id
+ JOIN (SELECT SUM(volume) volume
+ FROM vn.invoiceOut io
+ JOIN vn.ticket t ON t.refFk = io.ref
+ JOIN vn.saleVolume sv ON sv.ticketFk = t.id
+ WHERE io.id = :invoiceId
+ )sub2 ON TRUE
+ JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
+ JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
+ LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE'
+ JOIN vn.country cty ON cty.id = c.countryFk
+ JOIN vn.payMethod pm ON pm.id = c .payMethodFk
+ JOIN vn.company co ON co.id=io.companyFk
+ JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk
+ LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat
+ FROM vn.ticket t
+ JOIN vn.invoiceOut io ON io.ref = t.refFk
+ JOIN vn.sale s ON t.id = s.ticketFk
+ JOIN vn.item i ON i.id = s.itemFk
+ JOIN vn.intrastat ir ON ir.id = i.intrastatFk
+ WHERE io.id = :invoiceId
+ )sub3 ON TRUE
+ WHERE io.id = :invoiceId
diff --git a/print/templates/reports/invoice-incoterms/sql/invoice.sql b/print/templates/reports/invoice-incoterms/sql/invoice.sql
new file mode 100644
index 000000000..b9a929183
--- /dev/null
+++ b/print/templates/reports/invoice-incoterms/sql/invoice.sql
@@ -0,0 +1,17 @@
+SELECT
+ io.id,
+ io.issued,
+ io.clientFk,
+ io.companyFk,
+ io.ref,
+ pm.code AS payMethodCode,
+ cny.code companyCode,
+ sa.iban,
+ ios.footNotes
+FROM invoiceOut io
+ JOIN client c ON c.id = io.clientFk
+ JOIN payMethod pm ON pm.id = c.payMethodFk
+ JOIN company cny ON cny.id = io.companyFk
+ JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
+ LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
+WHERE io.id = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice/assets/css/import.js b/print/templates/reports/invoice/assets/css/import.js
new file mode 100644
index 000000000..fd8796c2b
--- /dev/null
+++ b/print/templates/reports/invoice/assets/css/import.js
@@ -0,0 +1,9 @@
+const Stylesheet = require(`${appPath}/core/stylesheet`);
+
+module.exports = new Stylesheet([
+ `${appPath}/common/css/spacing.css`,
+ `${appPath}/common/css/misc.css`,
+ `${appPath}/common/css/layout.css`,
+ `${appPath}/common/css/report.css`,
+ `${__dirname}/style.css`])
+ .mergeStyles();
diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css
new file mode 100644
index 000000000..cd605db9b
--- /dev/null
+++ b/print/templates/reports/invoice/assets/css/style.css
@@ -0,0 +1,39 @@
+h2 {
+ font-weight: 100;
+ color: #555
+}
+
+.table-title {
+ margin-bottom: 15px;
+ font-size: 0.8rem
+}
+
+.table-title h2 {
+ margin: 0 15px 0 0
+}
+
+.ticket-info {
+ font-size: 22px
+}
+
+#nickname h2 {
+ max-width: 400px;
+ word-wrap: break-word
+}
+
+#phytosanitary {
+ padding-right: 10px
+}
+
+#phytosanitary .flag img {
+ width: 100%
+}
+
+#phytosanitary .flag .flag-text {
+ padding-left: 10px;
+ box-sizing: border-box;
+}
+
+.phytosanitary-info {
+ margin-top: 10px
+}
\ No newline at end of file
diff --git a/print/templates/reports/invoice/assets/images/europe.png b/print/templates/reports/invoice/assets/images/europe.png
new file mode 100644
index 000000000..673be92ae
Binary files /dev/null and b/print/templates/reports/invoice/assets/images/europe.png differ
diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html
new file mode 100644
index 000000000..671bb8c7f
--- /dev/null
+++ b/print/templates/reports/invoice/invoice.html
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('title')}}
+
+
+
+ {{$t('clientId')}} |
+ {{client.id}} |
+
+
+ {{$t('invoice')}} |
+ {{invoice.ref}} |
+
+
+ {{$t('date')}} |
+ {{invoice.issued | date('%d-%m-%Y')}} |
+
+
+
+
+
+
+
+
+
+ {{client.socialName}}
+
+ {{client.postalAddress}}
+
+
+ {{client.postcodeCity}}
+
+
+ {{$t('fiscalId')}}: {{client.fi}}
+
+
+
+
+
+
+
+
+ {{$t('rectifiedInvoices')}}
+
+
+
+ {{$t('invoice')}} |
+ {{$t('issued')}} |
+ {{$t('amount')}} |
+ {{$t('description')}} |
+
+
+
+
+ {{row.ref}} |
+ {{row.issued | date}} |
+ {{row.amount | currency('EUR', $i18n.locale)}} |
+ {{row.description}} |
+
+
+
+
+
+
+
+
+
+
+ {{$t('deliveryNote')}}
+
+
+
+ Shipped
+
+
+
+ {{ticket.shipped | date}}
+
+
+
+ {{ticket.nickname}}
+
+
+
+
+
+ {{$t('reference')}} |
+ {{$t('quantity')}} |
+ {{$t('concept')}} |
+ {{$t('price')}} |
+ {{$t('discount')}} |
+ {{$t('vat')}} |
+ {{$t('amount')}} |
+
+
+
+
+ {{sale.itemFk | zerofill('000000')}} |
+ {{sale.quantity}} |
+ {{sale.concept}} |
+ {{sale.price | currency('EUR', $i18n.locale)}} |
+ {{(sale.discount / 100) | percentage}} |
+ {{sale.vatType}} |
+ {{saleImport(sale) | currency('EUR', $i18n.locale)}} |
+
+
+
+
+ {{sale.tag5}} {{sale.value5}}
+
+
+ {{sale.tag6}} {{sale.value6}}
+
+
+ {{sale.tag7}} {{sale.value7}}
+
+ |
+
+
+
+
+
+ {{$t('subtotal')}}
+ |
+ {{ticketSubtotal(ticket) | currency('EUR', $i18n.locale)}} |
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('taxBreakdown')}} |
+
+
+
+
+ {{$t('type')}} |
+
+ {{$t('taxBase')}}
+ |
+ {{$t('tax')}} |
+ {{$t('fee')}} |
+
+
+
+
+ {{tax.name}} |
+
+ {{tax.base | currency('EUR', $i18n.locale)}}
+ |
+ {{tax.vatPercent | percentage}} |
+ {{tax.vat | currency('EUR', $i18n.locale)}} |
+
+
+
+
+ {{$t('subtotal')}} |
+
+ {{sumTotal(taxes, 'base') | currency('EUR', $i18n.locale)}}
+ |
+ |
+ {{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}} |
+
+
+ {{$t('total')}} |
+ {{taxTotal | currency('EUR', $i18n.locale)}} |
+
+
+
+
+
+
+
+ {{invoice.footNotes}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('plantPassport')}}
+
+
+
+
+
+ A
+ {{botanical}}
+
+
+ B
+ ES17462130
+
+
+ C
+ {{ticketsId}}
+
+
+ D
+ ES
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('intrastat')}}
+
+
+
+ {{$t('code')}} |
+ {{$t('description')}} |
+ {{$t('stems')}} |
+ {{$t('netKg')}} |
+ {{$t('amount')}} |
+
+
+
+
+ {{row.code}} |
+ {{row.description}} |
+ {{row.stems | number($i18n.locale)}} |
+ {{row.netKg | number($i18n.locale)}} |
+ {{row.subtotal | currency('EUR', $i18n.locale)}} |
+
+
+
+
+ |
+
+ {{sumTotal(intrastat, 'stems') | number($i18n.locale)}}
+ |
+
+ {{sumTotal(intrastat, 'netKg') | number($i18n.locale)}}
+ |
+
+ {{sumTotal(intrastat, 'subtotal') | currency('EUR', $i18n.locale)}}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('wireTransfer')}}
+ {{$t('accountNumber', [invoice.iban])}}
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js
new file mode 100755
index 000000000..54df45c36
--- /dev/null
+++ b/print/templates/reports/invoice/invoice.js
@@ -0,0 +1,130 @@
+const Component = require(`${appPath}/core/component`);
+const Report = require(`${appPath}/core/report`);
+const reportHeader = new Component('report-header');
+const reportFooter = new Component('report-footer');
+const invoiceIncoterms = new Report('invoice-incoterms');
+const db = require(`${appPath}/core/database`);
+
+module.exports = {
+ name: 'invoice',
+ async serverPrefetch() {
+ this.invoice = await this.fetchInvoice(this.invoiceId);
+ this.client = await this.fetchClient(this.invoiceId);
+ this.taxes = await this.fetchTaxes(this.invoiceId);
+ this.intrastat = await this.fetchIntrastat(this.invoiceId);
+ this.rectified = await this.fetchRectified(this.invoiceId);
+ this.hasIncoterms = await this.fetchHasIncoterms(this.invoiceId);
+
+ const tickets = await this.fetchTickets(this.invoiceId);
+ const sales = await this.fetchSales(this.invoiceId);
+
+ const map = new Map();
+
+ for (let ticket of tickets)
+ map.set(ticket.id, ticket);
+
+ for (let sale of sales) {
+ const ticket = map.get(sale.ticketFk);
+ if (!ticket.sales) ticket.sales = [];
+ ticket.sales.push(sale);
+ }
+
+ this.tickets = tickets;
+
+ if (!this.invoice)
+ throw new Error('Something went wrong');
+ },
+ data() {
+ return {totalBalance: 0.00};
+ },
+ computed: {
+ ticketsId() {
+ const tickets = this.tickets.map(ticket => ticket.id);
+
+ return tickets.join(', ');
+ },
+ botanical() {
+ let phytosanitary = [];
+ for (let ticket of this.tickets) {
+ for (let sale of ticket.sales) {
+ if (sale.botanical)
+ phytosanitary.push(sale.botanical);
+ }
+ }
+
+ return phytosanitary.filter((item, index) =>
+ phytosanitary.indexOf(item) == index
+ ).join(', ');
+ },
+ taxTotal() {
+ const base = this.sumTotal(this.taxes, 'base');
+ const vat = this.sumTotal(this.taxes, 'vat');
+ return base + vat;
+ }
+ },
+ methods: {
+ fetchInvoice(invoiceId) {
+ return this.findOneFromDef('invoice', [invoiceId]);
+ },
+ fetchClient(invoiceId) {
+ return this.findOneFromDef('client', [invoiceId]);
+ },
+ fetchTickets(invoiceId) {
+ return this.rawSqlFromDef('tickets', [invoiceId]);
+ },
+ async fetchSales(invoiceId) {
+ const connection = await db.pool.getConnection();
+ await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
+ await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);
+
+ const sales = this.rawSqlFromDef('sales', connection);
+
+ await this.rawSql(`DROP TEMPORARY TABLE tmp.invoiceTickets`, connection);
+ await connection.release();
+
+ return sales;
+ },
+ fetchTaxes(invoiceId) {
+ return this.rawSqlFromDef(`taxes`, [invoiceId]);
+ },
+ fetchIntrastat(invoiceId) {
+ return this.rawSqlFromDef(`intrastat`, [invoiceId]);
+ },
+ fetchRectified(invoiceId) {
+ return this.rawSqlFromDef(`rectified`, [invoiceId]);
+ },
+ fetchHasIncoterms(invoiceId) {
+ return this.findValueFromDef(`hasIncoterms`, [invoiceId]);
+ },
+ saleImport(sale) {
+ const price = sale.quantity * sale.price;
+
+ return price * (1 - sale.discount / 100);
+ },
+ ticketSubtotal(ticket) {
+ let subTotal = 0.00;
+ for (let sale of ticket.sales)
+ subTotal += this.saleImport(sale);
+
+ return subTotal;
+ },
+ sumTotal(rows, prop) {
+ let total = 0.00;
+ for (let row of rows)
+ total += parseFloat(row[prop]);
+
+ return total;
+ }
+ },
+ components: {
+ 'report-header': reportHeader.build(),
+ 'report-footer': reportFooter.build(),
+ 'invoice-incoterms': invoiceIncoterms.build()
+ },
+ props: {
+ invoiceId: {
+ type: String,
+ required: true
+ }
+ }
+};
diff --git a/print/templates/reports/invoice/locale/es.yml b/print/templates/reports/invoice/locale/es.yml
new file mode 100644
index 000000000..6fdfc8a14
--- /dev/null
+++ b/print/templates/reports/invoice/locale/es.yml
@@ -0,0 +1,35 @@
+title: Factura
+invoice: Factura
+clientId: Cliente
+invoiceData: Datos de facturación
+fiscalId: CIF / NIF
+invoiceRef: Factura {0}
+deliveryNote: Albarán
+shipped: F. envío
+date: Fecha
+reference: Ref.
+quantity: Cant.
+concept: Concepto
+price: PVP/u
+discount: Dto.
+vat: IVA
+amount: Importe
+type: Tipo
+taxBase: Base imp.
+tax: Tasa
+fee: Cuota
+total: Total
+subtotal: Subtotal
+taxBreakdown: Desglose impositivo
+notes: Notas
+intrastat: Intrastat
+code: Código
+description: Descripción
+stems: Tallos
+netKg: KG Neto
+rectifiedInvoices: Facturas rectificadas
+issued: F. emisión
+plantPassport: Pasaporte fitosanitario
+observations: Observaciones
+wireTransfer: "Forma de pago: Transferencia"
+accountNumber: "Número de cuenta: {0}"
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/client.sql b/print/templates/reports/invoice/sql/client.sql
new file mode 100644
index 000000000..dd6035222
--- /dev/null
+++ b/print/templates/reports/invoice/sql/client.sql
@@ -0,0 +1,12 @@
+SELECT
+ c.id,
+ c.socialName,
+ c.street AS postalAddress,
+ IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
+ CONCAT(c.postcode, ' - ', c.city) postcodeCity
+FROM vn.invoiceOut io
+ JOIN vn.client c ON c.id = io.clientFk
+ JOIN vn.country cty ON cty.id = c.countryFk
+ LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
+ AND ios.taxAreaFk = 'CEE'
+WHERE io.id = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/hasIncoterms.sql b/print/templates/reports/invoice/sql/hasIncoterms.sql
new file mode 100644
index 000000000..27f61f57c
--- /dev/null
+++ b/print/templates/reports/invoice/sql/hasIncoterms.sql
@@ -0,0 +1,8 @@
+SELECT IF(incotermsFk IS NULL, FALSE, TRUE) AS hasIncoterms
+ FROM ticket t
+ JOIN invoiceOut io ON io.ref = t.refFk
+ JOIN client c ON c.id = t.clientFk
+ JOIN address a ON a.id = t.addressFk
+ WHERE io.id = ?
+ AND IF(c.hasToinvoiceByAddress = FALSE, c.defaultAddressFk, TRUE)
+ LIMIT 1
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/intrastat.sql b/print/templates/reports/invoice/sql/intrastat.sql
new file mode 100644
index 000000000..e391056ec
--- /dev/null
+++ b/print/templates/reports/invoice/sql/intrastat.sql
@@ -0,0 +1,14 @@
+SELECT
+ ir.id AS code,
+ ir.description AS description,
+ CAST(SUM(IFNULL(i.stems,1) * s.quantity) AS DECIMAL(10,2)) as stems,
+ CAST(SUM( weight) AS DECIMAL(10,2)) as netKg,
+ CAST(SUM((s.quantity * s.price * (100 - s.discount) / 100 )) AS DECIMAL(10,2)) AS subtotal
+ FROM vn.sale s
+ LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id
+ LEFT JOIN vn.ticket t ON t.id = s.ticketFk
+ LEFT JOIN vn.invoiceOut io ON io.ref = t.refFk
+ LEFT JOIN vn.item i ON i.id = s.itemFk
+ JOIN vn.intrastat ir ON ir.id = i.intrastatFk
+ WHERE io.id = ?
+ GROUP BY i.intrastatFk;
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/invoice.sql b/print/templates/reports/invoice/sql/invoice.sql
new file mode 100644
index 000000000..aacbb0016
--- /dev/null
+++ b/print/templates/reports/invoice/sql/invoice.sql
@@ -0,0 +1,16 @@
+SELECT
+ io.issued,
+ io.clientFk,
+ io.companyFk,
+ io.ref,
+ pm.code AS payMethodCode,
+ cny.code companyCode,
+ sa.iban,
+ ios.footNotes
+FROM invoiceOut io
+ JOIN client c ON c.id = io.clientFk
+ JOIN payMethod pm ON pm.id = c.payMethodFk
+ JOIN company cny ON cny.id = io.companyFk
+ JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
+ LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
+WHERE io.id = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/invoiceTickets.sql b/print/templates/reports/invoice/sql/invoiceTickets.sql
new file mode 100644
index 000000000..089911a63
--- /dev/null
+++ b/print/templates/reports/invoice/sql/invoiceTickets.sql
@@ -0,0 +1,20 @@
+CREATE TEMPORARY TABLE tmp.invoiceTickets
+ ENGINE = MEMORY
+ SELECT
+ t.id AS ticketFk,
+ t.clientFk,
+ t.shipped,
+ t.nickname,
+ io.ref,
+ c.socialName,
+ sa.iban,
+ pm.name AS payMethod,
+ su.countryFk AS supplierCountryFk
+ FROM vn.invoiceOut io
+ JOIN vn.supplier su ON su.id = io.companyFk
+ JOIN vn.ticket t ON t.refFk = io.ref
+ JOIN vn.client c ON c.id = t.clientFk
+ JOIN vn.payMethod pm ON pm.id = c.payMethodFk
+ JOIN vn.company co ON co.id = io.companyFk
+ JOIN vn.supplierAccount sa ON sa.id = co.supplierAccountFk
+ WHERE io.id = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/phytosanitary.sql b/print/templates/reports/invoice/sql/phytosanitary.sql
new file mode 100644
index 000000000..1ae178975
--- /dev/null
+++ b/print/templates/reports/invoice/sql/phytosanitary.sql
@@ -0,0 +1,14 @@
+SELECT CONCAT( 'A ', GROUP_CONCAT(DISTINCT(ib.ediBotanic) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
+ 'B ES17462130', CHAR(13,10), CHAR(13,10),
+ 'C ', GROUP_CONCAT(DISTINCT(t.id) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
+ 'D ES' ) phytosanitary
+ FROM vn.ticket t
+ JOIN vn.sale s ON s.ticketFk = t.id
+ JOIN vn.item i ON i.id = s.itemFk
+ JOIN vn.itemType it ON it.id = i.typeFk
+ JOIN vn.itemCategory ic ON ic.id = it.categoryFk
+ JOIN vn.itemBotanicalWithGenus ib ON ib.itemfk = i.id
+ WHERE t.refFk = # AND
+ ic.`code` = 'plant' AND
+ ib.ediBotanic IS NOT NULL
+ ORDER BY ib.ediBotanic
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/rectified.sql b/print/templates/reports/invoice/sql/rectified.sql
new file mode 100644
index 000000000..1255b973c
--- /dev/null
+++ b/print/templates/reports/invoice/sql/rectified.sql
@@ -0,0 +1,9 @@
+SELECT
+ io.amount,
+ io.ref,
+ io.issued,
+ ict.description
+FROM vn.invoiceCorrection ic
+ JOIN vn.invoiceOut io ON io.id = ic.correctedFk
+ JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk
+where ic.correctingFk = ?
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/sales.sql b/print/templates/reports/invoice/sql/sales.sql
new file mode 100644
index 000000000..0665fc0ff
--- /dev/null
+++ b/print/templates/reports/invoice/sql/sales.sql
@@ -0,0 +1,59 @@
+SELECT
+ it.ref,
+ it.socialName,
+ it.iban,
+ it.payMethod,
+ it.clientFk,
+ it.shipped,
+ it.nickname,
+ s.ticketFk,
+ s.itemFk,
+ s.concept,
+ s.quantity,
+ s.price,
+ s.discount,
+ i.tag5,
+ i.value5,
+ i.tag6,
+ i.value6,
+ i.tag7,
+ i.value7,
+ tc.code AS vatType,
+ ib.ediBotanic botanical
+ FROM tmp.invoiceTickets it
+ JOIN vn.sale s ON s.ticketFk = it.ticketFk
+ JOIN item i ON i.id = s.itemFk
+ LEFT JOIN itemType it ON it.id = i.typeFk
+ LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
+ LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
+ AND ic.code = 'plant'
+ AND ib.ediBotanic IS NOT NULL
+ JOIN vn.itemTaxCountry itc ON itc.countryFk = it.supplierCountryFk
+ AND itc.itemFk = s.itemFk
+ JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
+UNION ALL
+SELECT
+ it.ref,
+ it.socialName,
+ it.iban,
+ it.payMethod,
+ it.clientFk,
+ it.shipped,
+ it.nickname,
+ it.ticketFk,
+ '',
+ ts.description concept,
+ ts.quantity,
+ ts.price,
+ 0 discount,
+ NULL AS tag5,
+ NULL AS value5,
+ NULL AS tag6,
+ NULL AS value6,
+ NULL AS tag7,
+ NULL AS value7,
+ tc.code AS vatType,
+ NULL AS botanical
+ FROM tmp.invoiceTickets it
+ JOIN vn.ticketService ts ON ts.ticketFk = it.ticketFk
+ JOIN vn.taxClass tc ON tc.id = ts.taxClassFk
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/taxes.sql b/print/templates/reports/invoice/sql/taxes.sql
new file mode 100644
index 000000000..19b1cc00e
--- /dev/null
+++ b/print/templates/reports/invoice/sql/taxes.sql
@@ -0,0 +1,11 @@
+SELECT
+ iot.vat,
+ pgc.name,
+ IF(pe.equFk IS NULL, taxableBase, 0) AS base,
+ pgc.rate / 100 AS vatPercent
+ FROM invoiceOutTax iot
+ JOIN pgc ON pgc.code = iot.pgcFk
+ LEFT JOIN pgcEqu pe ON pe.equFk = pgc.code
+ JOIN invoiceOut io ON io.id = iot.invoiceOutFk
+ WHERE invoiceOutFk = ?
+ ORDER BY iot.id
\ No newline at end of file
diff --git a/print/templates/reports/invoice/sql/tickets.sql b/print/templates/reports/invoice/sql/tickets.sql
new file mode 100644
index 000000000..feca81ead
--- /dev/null
+++ b/print/templates/reports/invoice/sql/tickets.sql
@@ -0,0 +1,7 @@
+SELECT
+ t.id,
+ t.shipped,
+ t.nickname
+FROM invoiceOut io
+ JOIN ticket t ON t.refFk = io.ref
+WHERE io.id = ?
\ No newline at end of file
diff --git a/storage/pdfs/invoice/.keep b/storage/pdfs/invoice/.keep
new file mode 100644
index 000000000..e69de29bb