Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2843-claim_pickup_order
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
jorgebl 2021-03-22 10:58:13 +01:00
commit fb5af479e3
66 changed files with 1405 additions and 237 deletions

View File

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

View File

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

View File

@ -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() => {

View File

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

View File

@ -134,47 +134,49 @@ module.exports = function(Self) {
if (value instanceof Object)
continue;
if (value === undefined || value === null) continue;
if (value === undefined) continue;
for (let relationName in relations) {
const relation = relations[relationName];
if (relation.keyFrom == key && key != 'id') {
const model = relation.modelTo;
const modelName = relation.modelTo.modelName;
const properties = model && model.definition.properties;
const settings = model && model.definition.settings;
if (value) {
for (let relationName in relations) {
const relation = relations[relationName];
if (relation.keyFrom == key && key != 'id') {
const model = relation.modelTo;
const modelName = relation.modelTo.modelName;
const properties = model && model.definition.properties;
const settings = model && model.definition.settings;
const recordSet = await appModels[modelName].findById(value, null, options);
const recordSet = await appModels[modelName].findById(value, null, options);
const hasShowField = settings.log && settings.log.showField;
let showField = hasShowField && recordSet
&& recordSet[settings.log.showField];
const hasShowField = settings.log && settings.log.showField;
let showField = hasShowField && recordSet
&& recordSet[settings.log.showField];
if (!showField) {
const showFieldNames = [
'name',
'description',
'code',
'nickname'
];
for (field of showFieldNames) {
const propField = properties && properties[field];
const recordField = recordSet && recordSet[field];
if (!showField) {
const showFieldNames = [
'name',
'description',
'code',
'nickname'
];
for (field of showFieldNames) {
const propField = properties && properties[field];
const recordField = recordSet && recordSet[field];
if (propField && recordField) {
showField = field;
break;
if (propField && recordField) {
showField = field;
break;
}
}
}
}
if (showField && recordSet && recordSet[showField]) {
value = recordSet[showField];
if (showField && recordSet && recordSet[showField]) {
value = recordSet[showField];
break;
}
value = recordSet && recordSet.id || value;
break;
}
value = recordSet && recordSet.id || value;
break;
}
}
result[key] = value;

View File

@ -57,7 +57,7 @@
"The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one",
"Can't create stowaway for this ticket": "Can't create stowaway for this ticket",
"Swift / BIC can't be empty": "Swift / BIC can't be empty",
"MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} ({{itemId}}) for the ticket id [{{ticketId}}]({{{url}}})",
"Bought units from buy request": "Bought {{quantity}} units of {{concept}} [{{itemId}}]({{{urlItem}}}) for the ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} ({{clientId}})]({{{url}}}) to *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "I have changed the pay method for client [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "I sent *{{quantity}}* units of [{{concept}} ({{itemId}})]({{{itemUrl}}}) to *\"{{nickname}}\"* coming from ticket id [{{ticketId}}]({{{ticketUrl}}})",

View File

@ -121,7 +121,7 @@
"Swift / BIC can't be empty": "Swift / BIC no puede estar vacío",
"Customs agent is required for a non UEE member": "El agente de aduanas es requerido para los clientes extracomunitarios",
"Incoterms is required for a non UEE member": "El incoterms es requerido para los clientes extracomunitarios",
"MESSAGE_BOUGHT_UNITS": "Se ha comprado {{quantity}} unidades de {{concept}} ({{itemId}}) para el ticket id [{{ticketId}}]({{{url}}})",
"Bought units from buy request": "Se ha comprado {{quantity}} unidades de {{concept}} [{{itemId}}]({{{urlItem}}}) para el ticket id [{{ticketId}}]({{{url}}})",
"MESSAGE_INSURANCE_CHANGE": "He cambiado el crédito asegurado del cliente [{{clientName}} ({{clientId}})]({{{url}}}) a *{{credit}} €*",
"MESSAGE_CHANGED_PAYMETHOD": "He cambiado la forma de pago del cliente [{{clientName}} ({{clientId}})]({{{url}}})",
"Sent units from ticket": "Envio *{{quantity}}* unidades de [{{concept}} ({{itemId}})]({{{itemUrl}}}) a *\"{{nickname}}\"* provenientes del ticket id [{{ticketId}}]({{{ticketUrl}}})",
@ -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>",

View File

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

View File

@ -3,7 +3,7 @@
* @param {Object} instance - The model or context instance
* @param {Object} changes - Object containing changes
*/
exports.translateValues = async(instance, changes) => {
exports.translateValues = async(instance, changes, options = {}) => {
const models = instance.app.models;
function getRelation(instance, property) {
const relations = instance.definition.settings.relations;
@ -38,12 +38,20 @@ exports.translateValues = async(instance, changes) => {
const properties = Object.assign({}, changes);
for (let property in properties) {
const firstChar = property.substring(0, 1);
const isPrivate = firstChar == '$';
if (isPrivate) {
delete properties[property];
continue;
}
const relation = getRelation(instance, property);
const value = properties[property];
let finalValue = value;
const hasValue = value != null && value != undefined;
if (relation) {
let fieldsToShow = ['alias', 'name', 'code', 'description'];
let finalValue = value;
if (relation && hasValue) {
let fieldsToShow = ['nickname', 'name', 'code', 'description'];
const modelName = relation.model;
const model = models[modelName];
const log = model.definition.settings.log;
@ -53,7 +61,7 @@ exports.translateValues = async(instance, changes) => {
const row = await model.findById(value, {
fields: fieldsToShow
});
}, options);
const newValue = getValue(row);
if (newValue) finalValue = newValue;
}
@ -76,7 +84,12 @@ exports.translateValues = async(instance, changes) => {
exports.getChanges = (original, changes) => {
const oldChanges = {};
const newChanges = {};
for (let property in changes) {
const firstChar = property.substring(0, 1);
const isPrivate = firstChar == '$';
if (isPrivate) return;
if (changes[property] != original[property]) {
newChanges[property] = changes[property];

View File

@ -19,11 +19,10 @@ module.exports = Self => {
}
});
Self.importToNewRefundTicket = async(ctx, id) => {
Self.importToNewRefundTicket = async(ctx, id, options) => {
const models = Self.app.models;
const token = ctx.req.accessToken;
const userId = token.userId;
const tx = await Self.beginTransaction({});
const filter = {
where: {id: id},
include: [
@ -63,29 +62,39 @@ module.exports = Self => {
]
};
let tx;
let myOptions = {};
if (typeof options == 'object')
Object.assign(myOptions, options);
if (!myOptions.transaction) {
tx = await Self.beginTransaction({});
myOptions.transaction = tx;
}
try {
let options = {transaction: tx};
const worker = await models.Worker.findOne({
where: {userFk: userId}
}, options);
}, myOptions);
const obsevationType = await models.ObservationType.findOne({
where: {description: 'comercial'}
}, options);
}, myOptions);
const agencyMode = await models.AgencyMode.findOne({
where: {code: 'refund'}
}, options);
}, myOptions);
const state = await models.State.findOne({
where: {code: 'DELIVERED'}
}, options);
}, myOptions);
const zone = await models.Zone.findOne({
where: {agencyModeFk: agencyMode.id}
}, options);
}, myOptions);
const claim = await models.Claim.findOne(filter, options);
const claim = await models.Claim.findOne(filter, myOptions);
const today = new Date();
const newRefundTicket = await models.Ticket.create({
@ -98,33 +107,33 @@ module.exports = Self => {
addressFk: claim.ticket().addressFk,
agencyModeFk: agencyMode.id,
zoneFk: zone.id
}, options);
}, myOptions);
await saveObservation({
description: `Reclama ticket: ${claim.ticketFk}`,
ticketFk: newRefundTicket.id,
observationTypeFk: obsevationType.id
}, options);
}, myOptions);
await models.TicketTracking.create({
ticketFk: newRefundTicket.id,
stateFk: state.id,
workerFk: worker.id
}, options);
}, myOptions);
const salesToRefund = await models.ClaimBeginning.find(salesFilter, options);
const createdSales = await addSalesToTicket(salesToRefund, newRefundTicket.id, options);
await insertIntoClaimEnd(createdSales, id, worker.id, options);
const salesToRefund = await models.ClaimBeginning.find(salesFilter, myOptions);
const createdSales = await addSalesToTicket(salesToRefund, newRefundTicket.id, myOptions);
await insertIntoClaimEnd(createdSales, id, worker.id, myOptions);
await Self.rawSql('CALL vn.ticketCalculateClon(?, ?)', [
newRefundTicket.id, claim.ticketFk
], options);
], myOptions);
await tx.commit();
if (tx) await tx.commit();
return newRefundTicket;
} catch (e) {
await tx.rollback();
if (tx) await tx.rollback();
throw e;
}
};

View File

@ -1,42 +1,43 @@
const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
const models = app.models;
describe('claimBeginning', () => {
const claimManagerId = 72;
let ticket;
let refundTicketSales;
let salesInsertedInClaimEnd;
const activeCtx = {
accessToken: {userId: claimManagerId},
};
const ctx = {req: activeCtx};
afterAll(async done => {
try {
await app.models.Ticket.destroyById(ticket.id);
await app.models.Ticket.rawSql(`DELETE FROM vn.orderTicket WHERE ticketFk ='${ticket.id}';`);
} catch (error) {
console.error(error);
}
done();
});
describe('importToNewRefundTicket()', () => {
it('should create a new ticket with negative sales and insert the negative sales into claimEnd', async() => {
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
active: activeCtx
});
let claimId = 1;
ticket = await app.models.ClaimBeginning.importToNewRefundTicket(ctx, claimId);
refundTicketSales = await app.models.Sale.find({where: {ticketFk: ticket.id}});
salesInsertedInClaimEnd = await app.models.ClaimEnd.find({where: {claimFk: claimId}});
const tx = await models.Entry.beginTransaction({});
try {
const options = {transaction: tx};
expect(refundTicketSales.length).toEqual(1);
expect(refundTicketSales[0].quantity).toEqual(-5);
expect(salesInsertedInClaimEnd[0].saleFk).toEqual(refundTicketSales[0].id);
const ticket = await models.ClaimBeginning.importToNewRefundTicket(ctx, claimId, options);
const refundTicketSales = await models.Sale.find({
where: {ticketFk: ticket.id}
}, options);
const salesInsertedInClaimEnd = await models.ClaimEnd.find({
where: {claimFk: claimId}
}, options);
expect(refundTicketSales.length).toEqual(1);
expect(refundTicketSales[0].quantity).toEqual(-5);
expect(salesInsertedInClaimEnd[0].saleFk).toEqual(refundTicketSales[0].id);
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});
});

View File

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

View File

@ -2,7 +2,6 @@ const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context');
describe('entry import()', () => {
let newEntry;
const buyerId = 35;
const companyId = 442;
const travelId = 1;
@ -52,29 +51,32 @@ describe('entry import()', () => {
}
};
const tx = await app.models.Entry.beginTransaction({});
const options = {transaction: tx};
try {
const options = {transaction: tx};
const newEntry = await app.models.Entry.create({
dated: new Date(),
supplierFk: supplierId,
travelFk: travelId,
companyFk: companyId,
observation: 'The entry',
ref: 'Entry ref'
}, options);
newEntry = await app.models.Entry.create({
dated: new Date(),
supplierFk: supplierId,
travelFk: travelId,
companyFk: companyId,
observation: 'The entry',
ref: 'Entry ref'
}, options);
await app.models.Entry.importBuys(ctx, newEntry.id, options);
await app.models.Entry.importBuys(ctx, newEntry.id, options);
const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options);
const entryBuys = await app.models.Buy.find({
where: {entryFk: newEntry.id}
}, options);
const updatedEntry = await app.models.Entry.findById(newEntry.id, null, options);
const entryBuys = await app.models.Buy.find({
where: {entryFk: newEntry.id}
}, options);
expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef);
expect(entryBuys.length).toEqual(2);
expect(updatedEntry.observation).toEqual(expectedObservation);
expect(updatedEntry.ref).toEqual(expectedRef);
expect(entryBuys.length).toEqual(2);
// Restores
await tx.rollback();
await tx.rollback();
} catch (e) {
await tx.rollback();
throw e;
}
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ module.exports = Self => {
require('../methods/invoiceOut/filter')(Self);
require('../methods/invoiceOut/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);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -74,12 +74,13 @@ module.exports = Self => {
const origin = ctx.req.headers.origin;
const requesterId = request.requesterFk;
const message = $t('MESSAGE_BOUGHT_UNITS', {
const message = $t('Bought units from buy request', {
quantity: sale.quantity,
concept: sale.concept,
itemId: sale.itemFk,
ticketId: sale.ticketFk,
url: `${origin}/#!/ticket/${sale.ticketFk}/summary`
url: `${origin}/#!/ticket/${sale.ticketFk}/summary`,
urlItem: `${origin}/#!/item/${sale.itemFk}/summary`
});
await models.Chat.sendCheckingPresence(ctx, requesterId, message);

View File

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

View File

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

View File

@ -2,7 +2,8 @@
"name": "Ticket",
"base": "Loggable",
"log": {
"model":"TicketLog"
"model":"TicketLog",
"showField": "id"
},
"options": {
"mysql": {

View File

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

View File

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

View File

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

View File

@ -17,12 +17,12 @@ Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPo
Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales."
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

View File

@ -45,4 +45,8 @@
.no-page-break {
page-break-inside: avoid;
break-inside: avoid
}
.page-break-after {
page-break-after: always;
}

View File

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

View File

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

View File

@ -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);
},
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,29 @@
h2 {
font-weight: 100;
color: #555
}
.table-title {
margin-bottom: 15px;
font-size: 0.8rem
}
.table-title h2 {
margin: 0 15px 0 0
}
.ticket-info {
font-size: 22px
}
#incoterms table {
font-size: 1.2rem
}
#incoterms table th {
width: 10%
}
#incoterms p {
font-size: 1.2rem
}

View File

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

View File

@ -0,0 +1,39 @@
const Component = require(`${appPath}/core/component`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
module.exports = {
name: 'invoice-incoterms',
async serverPrefetch() {
this.invoice = await this.fetchInvoice(this.invoiceId);
this.client = await this.fetchClient(this.invoiceId);
this.incoterms = await this.fetchIncoterms(this.invoiceId);
if (!this.invoice)
throw new Error('Something went wrong');
},
computed: {
},
methods: {
fetchInvoice(invoiceId) {
return this.findOneFromDef('invoice', [invoiceId]);
},
fetchClient(invoiceId) {
return this.findOneFromDef('client', [invoiceId]);
},
fetchIncoterms(invoiceId) {
return this.findOneFromDef('incoterms', {invoiceId});
}
},
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build()
},
props: {
invoiceId: {
type: String,
required: true
}
}
};

View File

@ -0,0 +1,16 @@
title: Factura
invoice: Factura
clientId: Cliente
date: Fecha
invoiceData: Datos de facturación
fiscalId: CIF / NIF
invoiceRef: Factura {0}
incotermsTitle: Información para la exportación
incoterms: Incoterms
productDescription: Descripción de la mercancia
expeditionDescription: INFORMACIÓN DE LA EXPEDICIÓN
packageNumber: Número de bultos
packageGrossWeight: Peso bruto
packageCubing: Cubicaje
customsInfo: A despachar por la agencia de aduanas
productDisclaimer: Mercancía destinada a la exportación, EXENTA de IVA (Ley 37/1992 - Art. 21)

View File

@ -0,0 +1,12 @@
SELECT
c.id,
c.socialName,
c.street AS postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
CONCAT(c.postcode, ' - ', c.city) postcodeCity
FROM vn.invoiceOut io
JOIN vn.client c ON c.id = io.clientFk
JOIN vn.country cty ON cty.id = c.countryFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
AND ios.taxAreaFk = 'CEE'
WHERE io.id = ?

View File

@ -0,0 +1,71 @@
SELECT io.issued,
c.socialName,
c.street postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
io.clientFk,
c.postcode,
c.city,
io.companyFk,
io.ref,
tc.code,
s.concept,
s.quantity,
s.price,
s.discount,
s.ticketFk,
t.shipped,
t.refFk,
a.nickname,
s.itemFk,
s.id saleFk,
pm.name AS pmname,
sa.iban,
c.phone,
MAX(t.packages) packages,
a.incotermsFk,
ic.name incotermsName ,
sub.description weight,
t.observations,
ca.fiscalName customsAgentName,
ca.street customsAgentStreet,
ca.nif customsAgentNif,
ca.phone customsAgentPhone,
ca.email customsAgentEmail,
CAST(sub2.volume AS DECIMAL (10,2)) volume,
sub3.intrastat
FROM vn.invoiceOut io
JOIN vn.supplier su ON su.id = io.companyFk
JOIN vn.client c ON c.id = io.clientFk
LEFT JOIN vn.province p ON p.id = c.provinceFk
JOIN vn.ticket t ON t.refFk = io.ref
LEFT JOIN (SELECT tob.ticketFk,tob.description
FROM vn.ticketObservation tob
LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk
WHERE ot.description = "Peso Aduana"
)sub ON sub.ticketFk = t.id
JOIN vn.address a ON a.id = t.addressFk
LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk
LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk
JOIN vn.sale s ON s.ticketFk = t.id
JOIN (SELECT SUM(volume) volume
FROM vn.invoiceOut io
JOIN vn.ticket t ON t.refFk = io.ref
JOIN vn.saleVolume sv ON sv.ticketFk = t.id
WHERE io.id = :invoiceId
)sub2 ON TRUE
JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE'
JOIN vn.country cty ON cty.id = c.countryFk
JOIN vn.payMethod pm ON pm.id = c .payMethodFk
JOIN vn.company co ON co.id=io.companyFk
JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk
LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat
FROM vn.ticket t
JOIN vn.invoiceOut io ON io.ref = t.refFk
JOIN vn.sale s ON t.id = s.ticketFk
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
WHERE io.id = :invoiceId
)sub3 ON TRUE
WHERE io.id = :invoiceId

View File

@ -0,0 +1,17 @@
SELECT
io.id,
io.issued,
io.clientFk,
io.companyFk,
io.ref,
pm.code AS payMethodCode,
cny.code companyCode,
sa.iban,
ios.footNotes
FROM invoiceOut io
JOIN client c ON c.id = io.clientFk
JOIN payMethod pm ON pm.id = c.payMethodFk
JOIN company cny ON cny.id = io.companyFk
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
WHERE io.id = ?

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,39 @@
h2 {
font-weight: 100;
color: #555
}
.table-title {
margin-bottom: 15px;
font-size: 0.8rem
}
.table-title h2 {
margin: 0 15px 0 0
}
.ticket-info {
font-size: 22px
}
#nickname h2 {
max-width: 400px;
word-wrap: break-word
}
#phytosanitary {
padding-right: 10px
}
#phytosanitary .flag img {
width: 100%
}
#phytosanitary .flag .flag-text {
padding-left: 10px;
box-sizing: border-box;
}
.phytosanitary-info {
margin-top: 10px
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

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

View File

@ -0,0 +1,130 @@
const Component = require(`${appPath}/core/component`);
const Report = require(`${appPath}/core/report`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
const invoiceIncoterms = new Report('invoice-incoterms');
const db = require(`${appPath}/core/database`);
module.exports = {
name: 'invoice',
async serverPrefetch() {
this.invoice = await this.fetchInvoice(this.invoiceId);
this.client = await this.fetchClient(this.invoiceId);
this.taxes = await this.fetchTaxes(this.invoiceId);
this.intrastat = await this.fetchIntrastat(this.invoiceId);
this.rectified = await this.fetchRectified(this.invoiceId);
this.hasIncoterms = await this.fetchHasIncoterms(this.invoiceId);
const tickets = await this.fetchTickets(this.invoiceId);
const sales = await this.fetchSales(this.invoiceId);
const map = new Map();
for (let ticket of tickets)
map.set(ticket.id, ticket);
for (let sale of sales) {
const ticket = map.get(sale.ticketFk);
if (!ticket.sales) ticket.sales = [];
ticket.sales.push(sale);
}
this.tickets = tickets;
if (!this.invoice)
throw new Error('Something went wrong');
},
data() {
return {totalBalance: 0.00};
},
computed: {
ticketsId() {
const tickets = this.tickets.map(ticket => ticket.id);
return tickets.join(', ');
},
botanical() {
let phytosanitary = [];
for (let ticket of this.tickets) {
for (let sale of ticket.sales) {
if (sale.botanical)
phytosanitary.push(sale.botanical);
}
}
return phytosanitary.filter((item, index) =>
phytosanitary.indexOf(item) == index
).join(', ');
},
taxTotal() {
const base = this.sumTotal(this.taxes, 'base');
const vat = this.sumTotal(this.taxes, 'vat');
return base + vat;
}
},
methods: {
fetchInvoice(invoiceId) {
return this.findOneFromDef('invoice', [invoiceId]);
},
fetchClient(invoiceId) {
return this.findOneFromDef('client', [invoiceId]);
},
fetchTickets(invoiceId) {
return this.rawSqlFromDef('tickets', [invoiceId]);
},
async fetchSales(invoiceId) {
const connection = await db.pool.getConnection();
await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);
const sales = this.rawSqlFromDef('sales', connection);
await this.rawSql(`DROP TEMPORARY TABLE tmp.invoiceTickets`, connection);
await connection.release();
return sales;
},
fetchTaxes(invoiceId) {
return this.rawSqlFromDef(`taxes`, [invoiceId]);
},
fetchIntrastat(invoiceId) {
return this.rawSqlFromDef(`intrastat`, [invoiceId]);
},
fetchRectified(invoiceId) {
return this.rawSqlFromDef(`rectified`, [invoiceId]);
},
fetchHasIncoterms(invoiceId) {
return this.findValueFromDef(`hasIncoterms`, [invoiceId]);
},
saleImport(sale) {
const price = sale.quantity * sale.price;
return price * (1 - sale.discount / 100);
},
ticketSubtotal(ticket) {
let subTotal = 0.00;
for (let sale of ticket.sales)
subTotal += this.saleImport(sale);
return subTotal;
},
sumTotal(rows, prop) {
let total = 0.00;
for (let row of rows)
total += parseFloat(row[prop]);
return total;
}
},
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build(),
'invoice-incoterms': invoiceIncoterms.build()
},
props: {
invoiceId: {
type: String,
required: true
}
}
};

View File

@ -0,0 +1,35 @@
title: Factura
invoice: Factura
clientId: Cliente
invoiceData: Datos de facturación
fiscalId: CIF / NIF
invoiceRef: Factura {0}
deliveryNote: Albarán
shipped: F. envío
date: Fecha
reference: Ref.
quantity: Cant.
concept: Concepto
price: PVP/u
discount: Dto.
vat: IVA
amount: Importe
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
total: Total
subtotal: Subtotal
taxBreakdown: Desglose impositivo
notes: Notas
intrastat: Intrastat
code: Código
description: Descripción
stems: Tallos
netKg: KG Neto
rectifiedInvoices: Facturas rectificadas
issued: F. emisión
plantPassport: Pasaporte fitosanitario
observations: Observaciones
wireTransfer: "Forma de pago: Transferencia"
accountNumber: "Número de cuenta: {0}"

View File

@ -0,0 +1,12 @@
SELECT
c.id,
c.socialName,
c.street AS postalAddress,
IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi,
CONCAT(c.postcode, ' - ', c.city) postcodeCity
FROM vn.invoiceOut io
JOIN vn.client c ON c.id = io.clientFk
JOIN vn.country cty ON cty.id = c.countryFk
LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial
AND ios.taxAreaFk = 'CEE'
WHERE io.id = ?

View File

@ -0,0 +1,8 @@
SELECT IF(incotermsFk IS NULL, FALSE, TRUE) AS hasIncoterms
FROM ticket t
JOIN invoiceOut io ON io.ref = t.refFk
JOIN client c ON c.id = t.clientFk
JOIN address a ON a.id = t.addressFk
WHERE io.id = ?
AND IF(c.hasToinvoiceByAddress = FALSE, c.defaultAddressFk, TRUE)
LIMIT 1

View File

@ -0,0 +1,14 @@
SELECT
ir.id AS code,
ir.description AS description,
CAST(SUM(IFNULL(i.stems,1) * s.quantity) AS DECIMAL(10,2)) as stems,
CAST(SUM( weight) AS DECIMAL(10,2)) as netKg,
CAST(SUM((s.quantity * s.price * (100 - s.discount) / 100 )) AS DECIMAL(10,2)) AS subtotal
FROM vn.sale s
LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id
LEFT JOIN vn.ticket t ON t.id = s.ticketFk
LEFT JOIN vn.invoiceOut io ON io.ref = t.refFk
LEFT JOIN vn.item i ON i.id = s.itemFk
JOIN vn.intrastat ir ON ir.id = i.intrastatFk
WHERE io.id = ?
GROUP BY i.intrastatFk;

View File

@ -0,0 +1,16 @@
SELECT
io.issued,
io.clientFk,
io.companyFk,
io.ref,
pm.code AS payMethodCode,
cny.code companyCode,
sa.iban,
ios.footNotes
FROM invoiceOut io
JOIN client c ON c.id = io.clientFk
JOIN payMethod pm ON pm.id = c.payMethodFk
JOIN company cny ON cny.id = io.companyFk
JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk
LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial
WHERE io.id = ?

View File

@ -0,0 +1,20 @@
CREATE TEMPORARY TABLE tmp.invoiceTickets
ENGINE = MEMORY
SELECT
t.id AS ticketFk,
t.clientFk,
t.shipped,
t.nickname,
io.ref,
c.socialName,
sa.iban,
pm.name AS payMethod,
su.countryFk AS supplierCountryFk
FROM vn.invoiceOut io
JOIN vn.supplier su ON su.id = io.companyFk
JOIN vn.ticket t ON t.refFk = io.ref
JOIN vn.client c ON c.id = t.clientFk
JOIN vn.payMethod pm ON pm.id = c.payMethodFk
JOIN vn.company co ON co.id = io.companyFk
JOIN vn.supplierAccount sa ON sa.id = co.supplierAccountFk
WHERE io.id = ?

View File

@ -0,0 +1,14 @@
SELECT CONCAT( 'A ', GROUP_CONCAT(DISTINCT(ib.ediBotanic) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
'B ES17462130', CHAR(13,10), CHAR(13,10),
'C ', GROUP_CONCAT(DISTINCT(t.id) SEPARATOR ', '), CHAR(13,10), CHAR(13,10),
'D ES' ) phytosanitary
FROM vn.ticket t
JOIN vn.sale s ON s.ticketFk = t.id
JOIN vn.item i ON i.id = s.itemFk
JOIN vn.itemType it ON it.id = i.typeFk
JOIN vn.itemCategory ic ON ic.id = it.categoryFk
JOIN vn.itemBotanicalWithGenus ib ON ib.itemfk = i.id
WHERE t.refFk = # AND
ic.`code` = 'plant' AND
ib.ediBotanic IS NOT NULL
ORDER BY ib.ediBotanic

View File

@ -0,0 +1,9 @@
SELECT
io.amount,
io.ref,
io.issued,
ict.description
FROM vn.invoiceCorrection ic
JOIN vn.invoiceOut io ON io.id = ic.correctedFk
JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk
where ic.correctingFk = ?

View File

@ -0,0 +1,59 @@
SELECT
it.ref,
it.socialName,
it.iban,
it.payMethod,
it.clientFk,
it.shipped,
it.nickname,
s.ticketFk,
s.itemFk,
s.concept,
s.quantity,
s.price,
s.discount,
i.tag5,
i.value5,
i.tag6,
i.value6,
i.tag7,
i.value7,
tc.code AS vatType,
ib.ediBotanic botanical
FROM tmp.invoiceTickets it
JOIN vn.sale s ON s.ticketFk = it.ticketFk
JOIN item i ON i.id = s.itemFk
LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id
AND ic.code = 'plant'
AND ib.ediBotanic IS NOT NULL
JOIN vn.itemTaxCountry itc ON itc.countryFk = it.supplierCountryFk
AND itc.itemFk = s.itemFk
JOIN vn.taxClass tc ON tc.id = itc.taxClassFk
UNION ALL
SELECT
it.ref,
it.socialName,
it.iban,
it.payMethod,
it.clientFk,
it.shipped,
it.nickname,
it.ticketFk,
'',
ts.description concept,
ts.quantity,
ts.price,
0 discount,
NULL AS tag5,
NULL AS value5,
NULL AS tag6,
NULL AS value6,
NULL AS tag7,
NULL AS value7,
tc.code AS vatType,
NULL AS botanical
FROM tmp.invoiceTickets it
JOIN vn.ticketService ts ON ts.ticketFk = it.ticketFk
JOIN vn.taxClass tc ON tc.id = ts.taxClassFk

View File

@ -0,0 +1,11 @@
SELECT
iot.vat,
pgc.name,
IF(pe.equFk IS NULL, taxableBase, 0) AS base,
pgc.rate / 100 AS vatPercent
FROM invoiceOutTax iot
JOIN pgc ON pgc.code = iot.pgcFk
LEFT JOIN pgcEqu pe ON pe.equFk = pgc.code
JOIN invoiceOut io ON io.id = iot.invoiceOutFk
WHERE invoiceOutFk = ?
ORDER BY iot.id

View File

@ -0,0 +1,7 @@
SELECT
t.id,
t.shipped,
t.nickname
FROM invoiceOut io
JOIN ticket t ON t.refFk = io.ref
WHERE io.id = ?

View File