Merge pull request '2734-invoice_report' (#557) from 2734-invoice_report into dev
gitea/salix/pipeline/head This commit looks good
Details
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:
commit
acae6fb60d
|
@ -48,7 +48,7 @@ module.exports = Self => {
|
|||
throw new UserError(`You don't have enough privileges`);
|
||||
|
||||
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
|
||||
const tempContainer = await TempContainer.container(args.collection);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
|
||||
VALUES
|
||||
('Genus', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
|
||||
('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss');
|
||||
('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'),
|
||||
('InvoiceOut', 'createPdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing');
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import selectors from '../../helpers/selectors';
|
||||
import getBrowser from '../../helpers/puppeteer';
|
||||
|
||||
fdescribe('Client Edit billing data path', () => {
|
||||
describe('Client Edit billing data path', () => {
|
||||
let browser;
|
||||
let page;
|
||||
beforeAll(async() => {
|
||||
|
|
|
@ -162,7 +162,7 @@ describe('Ticket descriptor path', () => {
|
|||
});
|
||||
|
||||
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.waitForContentLoaded();
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
"Amount cannot be zero": "El importe no puede ser cero",
|
||||
"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'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",
|
||||
"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>",
|
||||
|
|
|
@ -68,5 +68,16 @@
|
|||
"image/jpeg",
|
||||
"image/jpg"
|
||||
]
|
||||
},
|
||||
"invoiceStorage": {
|
||||
"name": "invoiceStorage",
|
||||
"connector": "loopback-component-storage",
|
||||
"provider": "filesystem",
|
||||
"root": "./storage/pdfs/invoice",
|
||||
"maxFileSize": "52428800",
|
||||
"allowedContentTypes": [
|
||||
"application/octet-stream",
|
||||
"application/pdf"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@
|
|||
<span
|
||||
ng-click="ticketDescriptor.show($event, action.sale.ticket.id)"
|
||||
class="link">
|
||||
{{::action.sale.ticket.id | zeroFill:6}}
|
||||
{{::action.sale.ticket.id}}
|
||||
</span>
|
||||
</vn-td>
|
||||
<vn-td expand>{{::action.claimBeggining.description}}</vn-td>
|
||||
|
|
|
@ -21,10 +21,20 @@ module.exports = Self => {
|
|||
});
|
||||
|
||||
Self.book = async ref => {
|
||||
let ticketAddress = await Self.app.models.Ticket.findOne({where: {invoiceOut: ref}});
|
||||
let invoiceCompany = await Self.app.models.InvoiceOut.findOne({where: {ref: ref}});
|
||||
let [taxArea] = await Self.rawSql(`Select vn.addressTaxArea(?, ?) AS code`, [ticketAddress.address, invoiceCompany.company]);
|
||||
const models = Self.app.models;
|
||||
const ticketAddress = await models.Ticket.findOne({
|
||||
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]);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"InvoiceOut": {
|
||||
"dataSource": "vn"
|
||||
},
|
||||
"InvoiceContainer": {
|
||||
"dataSource": "invoiceStorage"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "InvoiceContainer",
|
||||
"base": "Container",
|
||||
"acls": [{
|
||||
"accessType": "READ",
|
||||
"principalType": "ROLE",
|
||||
"principalId": "$everyone",
|
||||
"permission": "ALLOW"
|
||||
}]
|
||||
}
|
|
@ -2,7 +2,7 @@ module.exports = Self => {
|
|||
require('../methods/invoiceOut/filter')(Self);
|
||||
require('../methods/invoiceOut/summary')(Self);
|
||||
require('../methods/invoiceOut/download')(Self);
|
||||
require('../methods/invoiceOut/regenerate')(Self);
|
||||
require('../methods/invoiceOut/delete')(Self);
|
||||
require('../methods/invoiceOut/book')(Self);
|
||||
require('../methods/invoiceOut/createPdf')(Self);
|
||||
};
|
||||
|
|
|
@ -25,6 +25,14 @@
|
|||
translate>
|
||||
Book invoice
|
||||
</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-body>
|
||||
<div class="attributes">
|
||||
|
@ -81,4 +89,12 @@
|
|||
</vn-confirm>
|
||||
<vn-client-descriptor-popover
|
||||
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>
|
|
@ -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});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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?
|
||||
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
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -80,13 +80,13 @@
|
|||
Make invoice
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="regenerateInvoiceConfirmation.show()"
|
||||
ng-click="createInvoicePdfConfirmation.show()"
|
||||
ng-show="$ctrl.isInvoiced"
|
||||
vn-acl="invoicing"
|
||||
vn-acl-action="remove"
|
||||
name="regenerateInvoice"
|
||||
translate>
|
||||
Regenerate invoice
|
||||
Regenerate invoice PDF
|
||||
</vn-item>
|
||||
<vn-item
|
||||
ng-click="recalculateComponentsConfirmation.show()"
|
||||
|
@ -207,12 +207,12 @@
|
|||
message="Are you sure you want to invoice this ticket?">
|
||||
</vn-confirm>
|
||||
|
||||
<!-- Regenerate invoice confirmation dialog -->
|
||||
<!-- Create invoice PDF confirmation dialog -->
|
||||
<vn-confirm
|
||||
vn-id="regenerateInvoiceConfirmation"
|
||||
on-accept="$ctrl.regenerateInvoice()"
|
||||
question="You are going to regenerate the invoice"
|
||||
message="Are you sure you want to regenerate the invoice?">
|
||||
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>
|
||||
|
||||
<!-- Recalculate components confirmation dialog -->
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,4 +45,8 @@
|
|||
.no-page-break {
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid
|
||||
}
|
||||
|
||||
.page-break-after {
|
||||
page-break-after: always;
|
||||
}
|
|
@ -9,6 +9,6 @@ body {
|
|||
.title {
|
||||
margin-bottom: 20px;
|
||||
font-weight: 100;
|
||||
font-size: 3em;
|
||||
font-size: 2.6rem;
|
||||
margin-top: 0
|
||||
}
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ h2 {
|
|||
}
|
||||
|
||||
.ticket-info {
|
||||
font-size: 26px
|
||||
font-size: 22px
|
||||
}
|
||||
|
||||
#phytosanitary {
|
||||
|
|
|
@ -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
|
|
@ -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();
|
|
@ -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
|
||||
}
|
|
@ -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">
|
||||
☎ {{incoterms.customsAgentPhone}}
|
||||
</span>
|
||||
<span v-if="incoterms.customsAgentEmail">
|
||||
✉ {{incoterms.customsAgentEmail}}
|
||||
</span>
|
||||
<span>)</span>
|
||||
</div>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{$t('productDisclaimer')}}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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)
|
|
@ -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 = ?
|
|
@ -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
|
|
@ -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 = ?
|
|
@ -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();
|
|
@ -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 |
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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}"
|
|
@ -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 = ?
|
|
@ -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
|
|
@ -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;
|
|
@ -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 = ?
|
|
@ -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 = ?
|
|
@ -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
|
|
@ -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 = ?
|
|
@ -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
|
|
@ -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
|
|
@ -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 = ?
|
Loading…
Reference in New Issue