Merge pull request '2734-invoice_report' (#557) from 2734-invoice_report into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #557
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2021-03-16 13:35:11 +00:00
commit acae6fb60d
57 changed files with 1275 additions and 136 deletions

View File

@ -48,7 +48,7 @@ module.exports = Self => {
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
if (process.env.NODE_ENV == 'test') if (process.env.NODE_ENV == 'test')
throw new UserError(`You can't upload images on the test environment`); throw new UserError(`Action not allowed on the test environment`);
// Upload file to temporary path // Upload file to temporary path
const tempContainer = await TempContainer.container(args.collection); const tempContainer = await TempContainer.container(args.collection);

View File

@ -1,4 +1,5 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES VALUES
('Genus', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'), ('Genus', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'); ('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
('InvoiceOut', 'createPdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');

View File

@ -1,7 +1,7 @@
import selectors from '../../helpers/selectors'; import selectors from '../../helpers/selectors';
import getBrowser from '../../helpers/puppeteer'; import getBrowser from '../../helpers/puppeteer';
fdescribe('Client Edit billing data path', () => { describe('Client Edit billing data path', () => {
let browser; let browser;
let page; let page;
beforeAll(async() => { beforeAll(async() => {

View File

@ -162,7 +162,7 @@ describe('Ticket descriptor path', () => {
}); });
it(`should regenerate the invoice using the descriptor menu`, async() => { it(`should regenerate the invoice using the descriptor menu`, async() => {
const expectedMessage = 'Invoice sent for a regeneration, will be available in a few minutes'; const expectedMessage = 'The invoice PDF document has been regenerated';
await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenu);
await page.waitForContentLoaded(); await page.waitForContentLoaded();

View File

@ -164,7 +164,7 @@
"Amount cannot be zero": "El importe no puede ser cero", "Amount cannot be zero": "El importe no puede ser cero",
"Company has to be official": "Empresa inválida", "Company has to be official": "Empresa inválida",
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria", "You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas", "Action not allowed on the test environment": "Esta acción no está permitida en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta", "The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera", "Sorts whole route": "Reordena ruta entera",
"New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong> y un precio de <strong>{{price}} €</strong>", "New ticket request has been created with price": "Se ha creado una nueva petición de compra '{{description}}' para el día <strong>{{shipped}}</strong>, con una cantidad de <strong>{{quantity}}</strong> y un precio de <strong>{{price}} €</strong>",

View File

@ -68,5 +68,16 @@
"image/jpeg", "image/jpeg",
"image/jpg" "image/jpg"
] ]
},
"invoiceStorage": {
"name": "invoiceStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/pdfs/invoice",
"maxFileSize": "52428800",
"allowedContentTypes": [
"application/octet-stream",
"application/pdf"
]
} }
} }

View File

@ -199,7 +199,7 @@
<span <span
ng-click="ticketDescriptor.show($event, action.sale.ticket.id)" ng-click="ticketDescriptor.show($event, action.sale.ticket.id)"
class="link"> class="link">
{{::action.sale.ticket.id | zeroFill:6}} {{::action.sale.ticket.id}}
</span> </span>
</vn-td> </vn-td>
<vn-td expand>{{::action.claimBeggining.description}}</vn-td> <vn-td expand>{{::action.claimBeggining.description}}</vn-td>

View File

@ -21,10 +21,20 @@ module.exports = Self => {
}); });
Self.book = async ref => { Self.book = async ref => {
let ticketAddress = await Self.app.models.Ticket.findOne({where: {invoiceOut: ref}}); const models = Self.app.models;
let invoiceCompany = await Self.app.models.InvoiceOut.findOne({where: {ref: ref}}); const ticketAddress = await models.Ticket.findOne({
let [taxArea] = await Self.rawSql(`Select vn.addressTaxArea(?, ?) AS code`, [ticketAddress.address, invoiceCompany.company]); where: {invoiceOut: ref}
});
const invoiceCompany = await models.InvoiceOut.findOne({
where: {ref: ref}
});
let query = 'SELECT vn.addressTaxArea(?, ?) AS code';
const [taxArea] = await Self.rawSql(query, [
ticketAddress.address,
invoiceCompany.company
]);
return Self.rawSql(`CALL vn.invoiceOutAgain(?, ?)`, [ref, taxArea.code]); query = 'CALL vn.invoiceOutAgain(?, ?)';
return Self.rawSql(query, [ref, taxArea.code]);
}; };
}; };

View File

@ -0,0 +1,86 @@
const fs = require('fs-extra');
const got = require('got');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('createPdf', {
description: 'Creates an invoice PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
description: 'The invoice id',
http: {source: 'path'}
}
],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/createPdf`,
verb: 'POST'
}
});
Self.createPdf = async function(ctx, id, options) {
const models = Self.app.models;
const headers = ctx.req.headers;
const origin = headers.origin;
const authorization = headers.authorization;
if (process.env.NODE_ENV == 'test')
throw new UserError(`Action not allowed on the test environment`);
let tx;
let newOptions = {};
if (typeof options == 'object')
Object.assign(newOptions, options);
if (!newOptions.transaction) {
tx = await Self.beginTransaction({});
newOptions.transaction = tx;
}
let fileSrc;
try {
const invoiceOut = await Self.findById(id, null, newOptions);
await invoiceOut.updateAttributes({
hasPdf: true
}, newOptions);
const response = got.stream(`${origin}/api/report/invoice`, {
query: {
authorization: authorization,
invoiceId: id
}
});
const invoiceYear = invoiceOut.created.getFullYear().toString();
const container = await models.InvoiceContainer.container(invoiceYear);
const rootPath = container.client.root;
const fileName = `${invoiceOut.ref}.pdf`;
fileSrc = path.join(rootPath, invoiceYear, fileName);
const writeStream = fs.createWriteStream(fileSrc);
writeStream.on('open', () => {
response.pipe(writeStream);
});
writeStream.on('finish', async function() {
writeStream.end();
});
if (tx) await tx.commit();
return invoiceOut;
} catch (e) {
if (tx) await tx.rollback();
if (fs.existsSync(fileSrc))
await fs.unlink(fileSrc);
throw e;
}
};
};

View File

@ -1,51 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('regenerate', {
description: 'Sends an invoice to a regeneration queue',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceOut id',
http: {source: 'path'}
}],
returns: {
type: 'object',
root: true
},
http: {
path: '/:id/regenerate',
verb: 'POST'
}
});
Self.regenerate = async(ctx, id) => {
const userId = ctx.req.accessToken.userId;
const models = Self.app.models;
const invoiceReportFk = 30; // Should be deprecated
const worker = await models.Worker.findOne({where: {userFk: userId}});
const tx = await Self.beginTransaction({});
try {
let options = {transaction: tx};
// Remove all invoice references from tickets
const invoiceOut = await models.InvoiceOut.findById(id, null, options);
await invoiceOut.updateAttributes({
hasPdf: false
});
// Send to print queue
await Self.rawSql(`
INSERT INTO vn.printServerQueue (reportFk, param1, workerFk)
VALUES (?, ?, ?)`, [invoiceReportFk, id, worker.id], options);
await tx.commit();
return invoiceOut;
} catch (e) {
await tx.rollback();
throw e;
}
};
};

View File

@ -0,0 +1,26 @@
const app = require('vn-loopback/server/server');
const got = require('got');
describe('InvoiceOut createPdf()', () => {
const userId = 1;
const ctx = {
req: {
accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}
};
it('should create a new PDF file and set true the hasPdf property', async() => {
const invoiceId = 1;
const response = {
pipe: () => {},
on: () => {},
};
spyOn(got, 'stream').and.returnValue(response);
let result = await app.models.InvoiceOut.createPdf(ctx, invoiceId);
expect(result.hasPdf).toBe(true);
});
});

View File

@ -1,36 +0,0 @@
const app = require('vn-loopback/server/server');
describe('invoiceOut regenerate()', () => {
const invoiceReportFk = 30;
const invoiceOutId = 1;
it('should check that the invoice has a PDF and is not in print generation queue', async() => {
const invoiceOut = await app.models.InvoiceOut.findById(invoiceOutId);
const [queue] = await app.models.InvoiceOut.rawSql(`
SELECT COUNT(*) AS total
FROM vn.printServerQueue
WHERE reportFk = ?`, [invoiceReportFk]);
expect(invoiceOut.hasPdf).toBeTruthy();
expect(queue.total).toEqual(0);
});
it(`should mark the invoice as doesn't have PDF and add it to a print queue`, async() => {
const ctx = {req: {accessToken: {userId: 5}}};
const invoiceOut = await app.models.InvoiceOut.regenerate(ctx, invoiceOutId);
const [queue] = await app.models.InvoiceOut.rawSql(`
SELECT COUNT(*) AS total
FROM vn.printServerQueue
WHERE reportFk = ?`, [invoiceReportFk]);
expect(invoiceOut.hasPdf).toBeFalsy();
expect(queue.total).toEqual(1);
// restores
const invoiceOutToRestore = await app.models.InvoiceOut.findById(invoiceOutId);
await invoiceOutToRestore.updateAttributes({hasPdf: true});
await app.models.InvoiceOut.rawSql(`
DELETE FROM vn.printServerQueue
WHERE reportFk = ?`, [invoiceReportFk]);
});
});

View File

@ -1,5 +1,8 @@
{ {
"InvoiceOut": { "InvoiceOut": {
"dataSource": "vn" "dataSource": "vn"
},
"InvoiceContainer": {
"dataSource": "invoiceStorage"
} }
} }

View File

@ -0,0 +1,10 @@
{
"name": "InvoiceContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -2,7 +2,7 @@ module.exports = Self => {
require('../methods/invoiceOut/filter')(Self); require('../methods/invoiceOut/filter')(Self);
require('../methods/invoiceOut/summary')(Self); require('../methods/invoiceOut/summary')(Self);
require('../methods/invoiceOut/download')(Self); require('../methods/invoiceOut/download')(Self);
require('../methods/invoiceOut/regenerate')(Self);
require('../methods/invoiceOut/delete')(Self); require('../methods/invoiceOut/delete')(Self);
require('../methods/invoiceOut/book')(Self); require('../methods/invoiceOut/book')(Self);
require('../methods/invoiceOut/createPdf')(Self);
}; };

View File

@ -25,6 +25,14 @@
translate> translate>
Book invoice Book invoice
</vn-item> </vn-item>
<vn-item
ng-click="createInvoicePdfConfirmation.show()"
vn-acl="invoicing"
vn-acl-action="remove"
name="regenerateInvoice"
translate>
Regenerate invoice PDF
</vn-item>
</slot-menu> </slot-menu>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
@ -82,3 +90,11 @@
<vn-client-descriptor-popover <vn-client-descriptor-popover
vn-id="clientDescriptor"> vn-id="clientDescriptor">
</vn-client-descriptor-popover> </vn-client-descriptor-popover>
<!-- Create invoice PDF confirmation dialog -->
<vn-confirm
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">
</vn-confirm>

View File

@ -22,6 +22,16 @@ class Controller extends Descriptor {
.then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked'))); .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() { get filter() {
if (this.invoiceOut) if (this.invoiceOut)
return JSON.stringify({refFk: this.invoiceOut.ref}); return JSON.stringify({refFk: this.invoiceOut.ref});

View File

@ -3,6 +3,7 @@ import './index';
describe('vnInvoiceOutDescriptor', () => { describe('vnInvoiceOutDescriptor', () => {
let controller; let controller;
let $httpBackend; let $httpBackend;
const invoiceOut = {id: 1};
beforeEach(ngModule('invoiceOut')); beforeEach(ngModule('invoiceOut'));
@ -11,6 +12,20 @@ describe('vnInvoiceOutDescriptor', () => {
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.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;

View File

@ -9,3 +9,5 @@ Are you sure you want to delete this invoice?: Estas seguro de eliminar esta fac
Book invoice: Asentar factura Book invoice: Asentar factura
InvoiceOut booked: Factura asentada InvoiceOut booked: Factura asentada
Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? 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

View File

@ -67,11 +67,7 @@ module.exports = function(Self) {
if (serial != 'R' && invoiceId) { if (serial != 'R' && invoiceId) {
await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options); await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options);
await models.PrintServerQueue.create({ await models.InvoiceOut.createPdf(ctx, invoiceId, options);
reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas
param1: invoiceId,
workerFk: userId
}, options);
} }
await tx.commit(); await tx.commit();

View File

@ -5,6 +5,7 @@ describe('ticket makeInvoice()', () => {
const userId = 19; const userId = 19;
const activeCtx = { const activeCtx = {
accessToken: {userId: userId}, accessToken: {userId: userId},
headers: {origin: 'http://localhost:5000'},
}; };
const ctx = {req: activeCtx}; const ctx = {req: activeCtx};
@ -43,6 +44,9 @@ describe('ticket makeInvoice()', () => {
}); });
it('should invoice a ticket, then try again to fail', async() => { 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); invoice = await app.models.Ticket.makeInvoice(ctx, ticketId);
expect(invoice.invoiceFk).toBeDefined(); expect(invoice.invoiceFk).toBeDefined();

View File

@ -80,13 +80,13 @@
Make invoice Make invoice
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="regenerateInvoiceConfirmation.show()" ng-click="createInvoicePdfConfirmation.show()"
ng-show="$ctrl.isInvoiced" ng-show="$ctrl.isInvoiced"
vn-acl="invoicing" vn-acl="invoicing"
vn-acl-action="remove" vn-acl-action="remove"
name="regenerateInvoice" name="regenerateInvoice"
translate> translate>
Regenerate invoice Regenerate invoice PDF
</vn-item> </vn-item>
<vn-item <vn-item
ng-click="recalculateComponentsConfirmation.show()" ng-click="recalculateComponentsConfirmation.show()"
@ -207,12 +207,12 @@
message="Are you sure you want to invoice this ticket?"> message="Are you sure you want to invoice this ticket?">
</vn-confirm> </vn-confirm>
<!-- Regenerate invoice confirmation dialog --> <!-- Create invoice PDF confirmation dialog -->
<vn-confirm <vn-confirm
vn-id="regenerateInvoiceConfirmation" vn-id="createInvoicePdfConfirmation"
on-accept="$ctrl.regenerateInvoice()" on-accept="$ctrl.createInvoicePdf()"
question="You are going to regenerate the invoice" question="Are you sure you want to regenerate the invoice PDF document?"
message="Are you sure you want to regenerate the invoice?"> message="You are going to regenerate the invoice PDF document">
</vn-confirm> </vn-confirm>
<!-- Recalculate components confirmation dialog --> <!-- Recalculate components confirmation dialog -->

View File

@ -219,12 +219,12 @@ class Controller extends Section {
.then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced')));
} }
regenerateInvoice() { createInvoicePdf() {
const invoiceId = this.ticket.invoiceOut.id; const invoiceId = this.ticket.invoiceOut.id;
return this.$http.post(`InvoiceOuts/${invoiceId}/regenerate`) return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`)
.then(() => { .then(() => {
const snackbarMessage = this.$t( 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); this.vnApp.showSuccess(snackbarMessage);
}); });
} }

View File

@ -148,12 +148,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => {
}); });
}); });
describe('regenerateInvoice()', () => { describe('createInvoicePdf()', () => {
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.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/regenerate`).respond(); $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond();
controller.regenerateInvoice(); controller.createInvoicePdf();
$httpBackend.flush(); $httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled(); expect(controller.vnApp.showSuccess).toHaveBeenCalled();

View File

@ -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." 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 Ticket invoiced: Ticket facturado
Make invoice: Crear factura 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 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? 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 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?: ¿Seguro que quieres regenerar la factura? Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de 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
Shipped hour updated: Hora de envio modificada Shipped hour updated: Hora de envio modificada
Deleted ticket: Ticket eliminado Deleted ticket: Ticket eliminado
Recalculate components: Recalcular componentes Recalculate components: Recalcular componentes

View File

@ -46,3 +46,7 @@
page-break-inside: avoid; page-break-inside: avoid;
break-inside: avoid break-inside: avoid
} }
.page-break-after {
page-break-after: always;
}

View File

@ -9,6 +9,6 @@ body {
.title { .title {
margin-bottom: 20px; margin-bottom: 20px;
font-weight: 100; font-weight: 100;
font-size: 3em; font-size: 2.6rem;
margin-top: 0 margin-top: 0
} }

View File

@ -83,6 +83,11 @@ class Component {
component.template = juice.inlineContent(this.template, this.stylesheet, { component.template = juice.inlineContent(this.template, this.stylesheet, {
inlinePseudoElements: true inlinePseudoElements: true
}); });
const tplPath = this.path;
if (!component.computed) component.computed = {};
component.computed.path = function() {
return tplPath;
};
return component; return component;
} }
@ -93,7 +98,7 @@ class Component {
const component = this.build(); const component = this.build();
const i18n = new VueI18n(config.i18n); const i18n = new VueI18n(config.i18n);
const props = {tplPath: this.path, ...this.args}; const props = {...this.args};
this._component = new Vue({ this._component = new Vue({
i18n: i18n, i18n: i18n,
render: h => h(component, { render: h => h(component, {

View File

@ -36,13 +36,14 @@ module.exports = {
* Makes a query from a SQL file * Makes a query from a SQL file
* @param {String} queryName - The SQL file name * @param {String} queryName - The SQL file name
* @param {Object} params - Parameterized values * @param {Object} params - Parameterized values
* @param {Object} connection - Optional pool connection
* *
* @return {Object} - Result promise * @return {Object} - Result promise
*/ */
rawSqlFromDef(queryName, params) { rawSqlFromDef(queryName, params, connection) {
const query = fs.readFileSync(`${queryName}.sql`, 'utf8'); const query = fs.readFileSync(`${queryName}.sql`, 'utf8');
return this.rawSql(query, params); return this.rawSql(query, params, connection);
}, },
/** /**

View File

@ -19,12 +19,13 @@ const dbHelper = {
* Makes a query from a SQL file * Makes a query from a SQL file
* @param {String} queryName - The SQL file name * @param {String} queryName - The SQL file name
* @param {Object} params - Parameterized values * @param {Object} params - Parameterized values
* @param {Object} connection - Optional pool connection
* *
* @return {Object} - Result promise * @return {Object} - Result promise
*/ */
rawSqlFromDef(queryName, params) { rawSqlFromDef(queryName, params, connection) {
const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
return db.rawSqlFromDef(absolutePath, params); return db.rawSqlFromDef(absolutePath, params, connection);
}, },
/** /**
@ -66,7 +67,7 @@ const dbHelper = {
*/ */
findValueFromDef(queryName, params) { findValueFromDef(queryName, params) {
return this.findOneFromDef(queryName, params).then(row => { 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 * @return {Object} - SQL
*/ */
getSqlFromDef(queryName) { getSqlFromDef(queryName) {
const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
return db.getSqlFromDef(absolutePath); return db.getSqlFromDef(absolutePath);
}, },
}, },

View File

@ -6,11 +6,16 @@ module.exports = app => {
const reportName = req.params.name; const reportName = req.params.name;
const fileName = getFileName(reportName, req.args); const fileName = getFileName(reportName, req.args);
const report = new Report(reportName, req.args); const report = new Report(reportName, req.args);
if (req.args.preview) {
const template = await report.render();
res.send(template);
} else {
const stream = await report.toPdfStream(); const stream = await report.toPdfStream();
res.setHeader('Content-type', 'application/pdf'); res.setHeader('Content-type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`); res.setHeader('Content-Disposition', `inline; filename="${fileName}"`);
res.end(stream); res.end(stream);
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@ -19,7 +19,7 @@ h2 {
} }
.ticket-info { .ticket-info {
font-size: 26px font-size: 22px
} }
#phytosanitary { #phytosanitary {

View File

@ -37,6 +37,7 @@ FROM vn.sale s
LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk
LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
AND ic.code = 'plant' AND ic.code = 'plant'
AND ib.ediBotanic IS NOT NULL
WHERE s.ticketFk = ? WHERE s.ticketFk = ?
GROUP BY s.id GROUP BY s.id
ORDER BY (it.isPackaging), s.concept, s.itemFk ORDER BY (it.isPackaging), s.concept, s.itemFk

View File

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

View File

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

View File

@ -0,0 +1,124 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid no-page-break page-break-after">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"
v-bind:company-code="invoice.companyCode">
</report-header>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="columns vn-mb-lg">
<div class="size50">
<div class="size75 vn-mt-ml">
<h1 class="title uppercase">{{$t('title')}}</h1>
<table class="row-oriented ticket-info">
<tbody>
<tr>
<td class="font gray uppercase">{{$t('clientId')}}</td>
<th>{{client.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('invoice')}}</td>
<th>{{invoice.ref}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{invoice.issued | date('%d-%m-%Y')}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('invoiceData')}}</div>
<div class="body">
<h3 class="uppercase">{{client.socialName}}</h3>
<div>
{{client.postalAddress}}
</div>
<div>
{{client.postcodeCity}}
</div>
<div>
{{$t('fiscalId')}}: {{client.fi}}
</div>
</div>
</div>
</div>
</div>
<div id="incoterms" class="panel">
<div class="header">{{$t('incotermsTitle')}}</div>
<div class="body">
<table class="row-oriented">
<tbody>
<tr>
<th>
{{$t('incoterms')}}
<div class="description">asd</div>
</th>
<td>{{incoterms.incotermsFk}} - {{incoterms.incotermsName}}</td>
</tr>
<tr>
<th>
{{$t('productDescription')}}
</th>
<td>{{incoterms.intrastat}}</td>
</tr>
<tr>
<th>{{$t('expeditionDescription')}}</th>
<td></td>
</tr>
<tr>
<th>{{$t('packageNumber')}}</th>
<td>{{incoterms.packages}}</td>
</tr>
<tr>
<th>{{$t('packageGrossWeight')}}</th>
<td>{{incoterms.weight}} KG</td>
</tr>
<tr>
<th>{{$t('packageCubing')}}</th>
<td>{{incoterms.volume}} m3</td>
</tr>
</tbody>
</table>
<p>
<div class="font bold">
<span>{{$t('customsInfo')}}</span>
<span>{{incoterms.customsAgentName}}</span>
</div>
<div class="font bold">
<span>(</span>
<span>{{incoterms.customsAgentNif}}</span>
<span>{{incoterms.customsAgentStreet}}</span>
<span v-if="incoterms.customsAgentPhone">
&#9742; {{incoterms.customsAgentPhone}}
</span>
<span v-if="incoterms.customsAgentEmail">
&#9993; {{incoterms.customsAgentEmail}}
</span>
<span>)</span>
</div>
</p>
<p>
<strong>{{$t('productDisclaimer')}}</strong>
</p>
</div>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,319 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Incoterms block -->
<invoice-incoterms
v-if="hasIncoterms"
v-bind="$props">
</invoice-incoterms>
<!-- Header block -->
<report-header v-bind="$props"
v-bind:company-code="invoice.companyCode">
</report-header>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="columns vn-mb-lg">
<div class="size50">
<div class="size75 vn-mt-ml">
<h1 class="title uppercase">{{$t('title')}}</h1>
<table class="row-oriented ticket-info">
<tbody>
<tr>
<td class="font gray uppercase">{{$t('clientId')}}</td>
<th>{{client.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('invoice')}}</td>
<th>{{invoice.ref}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{invoice.issued | date('%d-%m-%Y')}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('invoiceData')}}</div>
<div class="body">
<h3 class="uppercase">{{client.socialName}}</h3>
<div>
{{client.postalAddress}}
</div>
<div>
{{client.postcodeCity}}
</div>
<div>
{{$t('fiscalId')}}: {{client.fi}}
</div>
</div>
</div>
</div>
</div>
<!-- Rectified invoices block -->
<div class="size100 no-page-break" v-if="rectified.length > 0">
<h2>{{$t('rectifiedInvoices')}}</h2>
<table class="column-oriented">
<thead>
<tr>
<th>{{$t('invoice')}}</th>
<th>{{$t('issued')}}</th>
<th class="number">{{$t('amount')}}</th>
<th width="50%">{{$t('description')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rectified">
<td>{{row.ref}}</td>
<td>{{row.issued | date}}</td>
<td class="number">{{row.amount | currency('EUR', $i18n.locale)}}</td>
<td width="50%">{{row.description}}</td>
</tr>
</tbody>
</table>
</div>
<!-- End of rectified invoices block -->
<!-- Sales block -->
<div class="vn-mt-lg no-page-break" v-for="ticket in tickets">
<div class="table-title clearfix">
<div class="pull-left">
<h2>{{$t('deliveryNote')}}</strong>
</div>
<div class="pull-left vn-mr-md">
<div class="field rectangle">
<span>{{ticket.id}}</span>
</div>
</div>
<div class="pull-left">
<h2>Shipped</h2>
</div>
<div class="pull-left">
<div class="field rectangle">
<span>{{ticket.shipped | date}}</span>
</div>
</div>
<span id="nickname" class="pull-right">
<h2>{{ticket.nickname}}</h2>
</span>
</div>
<table class="column-oriented">
<thead>
<tr>
<th width="5%">{{$t('reference')}}</th>
<th class="number">{{$t('quantity')}}</th>
<th width="50%">{{$t('concept')}}</th>
<th class="number">{{$t('price')}}</th>
<th class="centered" width="5%">{{$t('discount')}}</th>
<th class="centered">{{$t('vat')}}</th>
<th class="number">{{$t('amount')}}</th>
</tr>
</thead>
<tbody v-for="sale in ticket.sales" class="no-page-break">
<tr>
<td width="5%">{{sale.itemFk | zerofill('000000')}}</td>
<td class="number">{{sale.quantity}}</td>
<td width="50%">{{sale.concept}}</td>
<td class="number">{{sale.price | currency('EUR', $i18n.locale)}}</td>
<td class="centered" width="5%">{{(sale.discount / 100) | percentage}}</td>
<td class="centered">{{sale.vatType}}</td>
<td class="number">{{saleImport(sale) | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="description font light-gray">
<td colspan="7">
<span v-if="sale.value5">
<strong>{{sale.tag5}}</strong> {{sale.value5}}
</span>
<span v-if="sale.value6">
<strong>{{sale.tag6}}</strong> {{sale.value6}}
</span>
<span v-if="sale.value7">
<strong>{{sale.tag7}}</strong> {{sale.value7}}
</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="6" class="font bold">
<span class="pull-right">{{$t('subtotal')}}</span>
</td>
<td class="number">{{ticketSubtotal(ticket) | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
</div>
<!-- End of sales block -->
<div class="columns vn-mt-xl">
<!-- Taxes block -->
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
<table class="column-oriented">
<thead>
<tr>
<th colspan="4">{{$t('taxBreakdown')}}</th>
</tr>
</thead>
<thead class="light">
<tr>
<th width="45%">{{$t('type')}}</th>
<th width="25%" class="number">
{{$t('taxBase')}}
</th>
<th>{{$t('tax')}}</th>
<th class="number">{{$t('fee')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="tax in taxes">
<td width="45%">{{tax.name}}</td>
<td width="25%" class="number">
{{tax.base | currency('EUR', $i18n.locale)}}
</td>
<td>{{tax.vatPercent | percentage}}</td>
<td class="number">{{tax.vat | currency('EUR', $i18n.locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr class="font bold">
<td width="45%">{{$t('subtotal')}}</td>
<td width="20%" class="number">
{{sumTotal(taxes, 'base') | currency('EUR', $i18n.locale)}}
</td>
<td></td>
<td class="number">{{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="font bold">
<td colspan="2">{{$t('total')}}</td>
<td colspan="2" class="number">{{taxTotal | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
<div class="panel" v-if="invoice.footNotes">
<div class="header">{{$t('notes')}}</div>
<div class="body">
<span>{{invoice.footNotes}}</span>
</div>
</div>
</div>
<!-- End of taxes block -->
<!-- Phytosanitary block -->
<div id="phytosanitary" class="size50 pull-left no-page-break">
<div class="panel">
<div class="body">
<div class="flag">
<div class="columns">
<div class="size25">
<img v-bind:src="getReportSrc('europe.png')"/>
</div>
<div class="size75 flag-text">
<strong>{{$t('plantPassport')}}</strong><br/>
</div>
</div>
</div>
<div class="phytosanitary-info">
<div>
<strong>A</strong>
<span>{{botanical}}</span>
</div>
<div>
<strong>B</strong>
<span>ES17462130</span>
</div>
<div>
<strong>C</strong>
<span>{{ticketsId}}</span>
</div>
<div>
<strong>D</strong>
<span>ES</span>
</div>
</div>
</div>
</div>
</div>
<!-- End of phytosanitary block -->
</div>
<!-- Intrastat block -->
<div class="size100 no-page-break" v-if="intrastat.length > 0">
<h2>{{$t('intrastat')}}</h2>
<table class="column-oriented">
<thead>
<tr>
<th>{{$t('code')}}</th>
<th width="50%">{{$t('description')}}</th>
<th class="number">{{$t('stems')}}</th>
<th class="number">{{$t('netKg')}}</th>
<th class="number">{{$t('amount')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in intrastat">
<td>{{row.code}}</td>
<td width="50%">{{row.description}}</td>
<td class="number">{{row.stems | number($i18n.locale)}}</td>
<td class="number">{{row.netKg | number($i18n.locale)}}</td>
<td class="number">{{row.subtotal | currency('EUR', $i18n.locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2"></td>
<td class="number">
<strong>{{sumTotal(intrastat, 'stems') | number($i18n.locale)}}</strong>
</td>
<td class="number">
<strong>{{sumTotal(intrastat, 'netKg') | number($i18n.locale)}}</strong>
</td>
<td class="number">
<strong>{{sumTotal(intrastat, 'subtotal') | currency('EUR', $i18n.locale)}}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
<!-- End of intrastat block -->
<!-- Observations block -->
<div class="columns vn-mt-xl" v-if="invoice.payMethodCode == 'wireTransfer'">
<div class="size50 pull-left no-page-break" >
<div class="panel" >
<div class="header">{{$t('observations')}}</div>
<div class="body">
<div>{{$t('wireTransfer')}}</div>
<div>{{$t('accountNumber', [invoice.iban])}}</div>
</div>
</div>
</div>
</div>
<!-- End of observations block -->
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:company-code="invoice.companyCode"
v-bind:left-text="$t('invoiceRef', [invoice.ref])"
v-bind:center-text="client.socialName"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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