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`);
|
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);
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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() => {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>",
|
||||||
|
|
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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": {
|
"InvoiceOut": {
|
||||||
"dataSource": "vn"
|
"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/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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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">
|
||||||
|
@ -81,4 +89,12 @@
|
||||||
</vn-confirm>
|
</vn-confirm>
|
||||||
<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>
|
|
@ -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});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -8,4 +8,6 @@ InvoiceOut deleted: Factura eliminada
|
||||||
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
|
Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura?
|
||||||
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
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -45,4 +45,8 @@
|
||||||
.no-page-break {
|
.no-page-break {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
break-inside: avoid
|
break-inside: avoid
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-break-after {
|
||||||
|
page-break-after: always;
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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, {
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
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-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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ h2 {
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticket-info {
|
.ticket-info {
|
||||||
font-size: 26px
|
font-size: 22px
|
||||||
}
|
}
|
||||||
|
|
||||||
#phytosanitary {
|
#phytosanitary {
|
||||||
|
|
|
@ -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
|
|
@ -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