diff --git a/back/methods/image/upload.js b/back/methods/image/upload.js index a93ead651..676a4b5fb 100644 --- a/back/methods/image/upload.js +++ b/back/methods/image/upload.js @@ -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); diff --git a/db/changes/10291-invoiceIn/00-ACL.sql b/db/changes/10291-invoiceIn/00-ACL.sql index b4067d1a3..5a1cf6843 100644 --- a/db/changes/10291-invoiceIn/00-ACL.sql +++ b/db/changes/10291-invoiceIn/00-ACL.sql @@ -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'); \ No newline at end of file + ('Specie', '*', 'WRITE', 'ALLOW', 'ROLE', 'logisticBoss'), + ('InvoiceOut', 'createPdf', 'WRITE', 'ALLOW', 'ROLE', 'invoicing'); diff --git a/e2e/paths/02-client/04_edit_billing_data.spec.js b/e2e/paths/02-client/04_edit_billing_data.spec.js index da5e6232e..24ee3c29a 100644 --- a/e2e/paths/02-client/04_edit_billing_data.spec.js +++ b/e2e/paths/02-client/04_edit_billing_data.spec.js @@ -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() => { diff --git a/e2e/paths/05-ticket/12_descriptor.spec.js b/e2e/paths/05-ticket/12_descriptor.spec.js index 471d7a536..d81c1c3ed 100644 --- a/e2e/paths/05-ticket/12_descriptor.spec.js +++ b/e2e/paths/05-ticket/12_descriptor.spec.js @@ -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(); diff --git a/loopback/common/models/loggable.js b/loopback/common/models/loggable.js index 258fff4ff..557aad66a 100644 --- a/loopback/common/models/loggable.js +++ b/loopback/common/models/loggable.js @@ -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; diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 5eb81edd6..65028a8a8 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -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}}})", diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 8d5156842..96858eb4a 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -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 {{shipped}}, con una cantidad de {{quantity}} y un precio de {{price}} €", diff --git a/loopback/server/datasources.json b/loopback/server/datasources.json index 8ce442b8e..343bcedd8 100644 --- a/loopback/server/datasources.json +++ b/loopback/server/datasources.json @@ -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" + ] } } diff --git a/loopback/util/log.js b/loopback/util/log.js index b491b97d0..9832a018a 100644 --- a/loopback/util/log.js +++ b/loopback/util/log.js @@ -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]; diff --git a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js index c2ab2001e..293aae012 100644 --- a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js +++ b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.js @@ -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; } }; diff --git a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js index 8c013c172..b05b2ac15 100644 --- a/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js +++ b/modules/claim/back/methods/claim-beginning/importToNewRefundTicket.spec.js @@ -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; + } }); }); }); diff --git a/modules/claim/front/summary/index.html b/modules/claim/front/summary/index.html index e9ec1e765..0d52c7f47 100644 --- a/modules/claim/front/summary/index.html +++ b/modules/claim/front/summary/index.html @@ -199,7 +199,7 @@ - {{::action.sale.ticket.id | zeroFill:6}} + {{::action.sale.ticket.id}} {{::action.claimBeggining.description}} diff --git a/modules/entry/back/methods/entry/specs/importBuys.spec.js b/modules/entry/back/methods/entry/specs/importBuys.spec.js index d0793a2f6..942ce0a0b 100644 --- a/modules/entry/back/methods/entry/specs/importBuys.spec.js +++ b/modules/entry/back/methods/entry/specs/importBuys.spec.js @@ -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; + } }); }); diff --git a/modules/invoiceOut/back/methods/invoiceOut/book.js b/modules/invoiceOut/back/methods/invoiceOut/book.js index 358de8fd5..af495c1f0 100644 --- a/modules/invoiceOut/back/methods/invoiceOut/book.js +++ b/modules/invoiceOut/back/methods/invoiceOut/book.js @@ -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]); }; }; diff --git a/modules/invoiceOut/back/methods/invoiceOut/createPdf.js b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js new file mode 100644 index 000000000..9bf4e93a3 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/createPdf.js @@ -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; + } + }; +}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js b/modules/invoiceOut/back/methods/invoiceOut/regenerate.js deleted file mode 100644 index fda12c3cb..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/regenerate.js +++ /dev/null @@ -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; - } - }; -}; diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js new file mode 100644 index 000000000..3372411c1 --- /dev/null +++ b/modules/invoiceOut/back/methods/invoiceOut/specs/createPdf.spec.js @@ -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); + }); +}); diff --git a/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js b/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js deleted file mode 100644 index 2d495ea0e..000000000 --- a/modules/invoiceOut/back/methods/invoiceOut/specs/regenerate.spec.js +++ /dev/null @@ -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]); - }); -}); diff --git a/modules/invoiceOut/back/model-config.json b/modules/invoiceOut/back/model-config.json index f3492dbe6..e144ce80e 100644 --- a/modules/invoiceOut/back/model-config.json +++ b/modules/invoiceOut/back/model-config.json @@ -1,5 +1,8 @@ { "InvoiceOut": { "dataSource": "vn" + }, + "InvoiceContainer": { + "dataSource": "invoiceStorage" } } diff --git a/modules/invoiceOut/back/models/invoice-container.json b/modules/invoiceOut/back/models/invoice-container.json new file mode 100644 index 000000000..5b713c0c4 --- /dev/null +++ b/modules/invoiceOut/back/models/invoice-container.json @@ -0,0 +1,10 @@ +{ + "name": "InvoiceContainer", + "base": "Container", + "acls": [{ + "accessType": "READ", + "principalType": "ROLE", + "principalId": "$everyone", + "permission": "ALLOW" + }] +} \ No newline at end of file diff --git a/modules/invoiceOut/back/models/invoiceOut.js b/modules/invoiceOut/back/models/invoiceOut.js index 8046f1dc4..e84a0495e 100644 --- a/modules/invoiceOut/back/models/invoiceOut.js +++ b/modules/invoiceOut/back/models/invoiceOut.js @@ -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); }; diff --git a/modules/invoiceOut/front/descriptor/index.html b/modules/invoiceOut/front/descriptor/index.html index fe22e4dd8..b4c76d808 100644 --- a/modules/invoiceOut/front/descriptor/index.html +++ b/modules/invoiceOut/front/descriptor/index.html @@ -25,6 +25,14 @@ translate> Book invoice + + Regenerate invoice PDF +
@@ -81,4 +89,12 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/modules/invoiceOut/front/descriptor/index.js b/modules/invoiceOut/front/descriptor/index.js index cb4b131ac..3e859478d 100644 --- a/modules/invoiceOut/front/descriptor/index.js +++ b/modules/invoiceOut/front/descriptor/index.js @@ -22,6 +22,16 @@ class Controller extends Descriptor { .then(() => this.vnApp.showSuccess(this.$t('InvoiceOut booked'))); } + createInvoicePdf() { + const invoiceId = this.invoiceOut.id; + return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) + .then(() => { + const snackbarMessage = this.$t( + `The invoice PDF document has been regenerated`); + this.vnApp.showSuccess(snackbarMessage); + }); + } + get filter() { if (this.invoiceOut) return JSON.stringify({refFk: this.invoiceOut.ref}); diff --git a/modules/invoiceOut/front/descriptor/index.spec.js b/modules/invoiceOut/front/descriptor/index.spec.js index 987763b0a..c16900a0a 100644 --- a/modules/invoiceOut/front/descriptor/index.spec.js +++ b/modules/invoiceOut/front/descriptor/index.spec.js @@ -3,6 +3,7 @@ import './index'; describe('vnInvoiceOutDescriptor', () => { let controller; let $httpBackend; + const invoiceOut = {id: 1}; beforeEach(ngModule('invoiceOut')); @@ -11,6 +12,20 @@ describe('vnInvoiceOutDescriptor', () => { controller = $componentController('vnInvoiceOutDescriptor', {$element: null}); })); + describe('createInvoicePdf()', () => { + it('should make a query and show a success snackbar', () => { + jest.spyOn(controller.vnApp, 'showSuccess'); + + controller.invoiceOut = invoiceOut; + + $httpBackend.expectPOST(`InvoiceOuts/${invoiceOut.id}/createPdf`).respond(); + controller.createInvoicePdf(); + $httpBackend.flush(); + + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + describe('loadData()', () => { it(`should perform a get query to store the invoice in data into the controller`, () => { const id = 1; diff --git a/modules/invoiceOut/front/descriptor/locale/es.yml b/modules/invoiceOut/front/descriptor/locale/es.yml index e85be96bf..dd67660ee 100644 --- a/modules/invoiceOut/front/descriptor/locale/es.yml +++ b/modules/invoiceOut/front/descriptor/locale/es.yml @@ -8,4 +8,6 @@ InvoiceOut deleted: Factura eliminada Are you sure you want to delete this invoice?: Estas seguro de eliminar esta factura? Book invoice: Asentar factura InvoiceOut booked: Factura asentada -Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? \ No newline at end of file +Are you sure you want to book this invoice?: Estas seguro de querer asentar esta factura? +Regenerate invoice PDF: Regenerar PDF factura +The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado \ No newline at end of file diff --git a/modules/ticket/back/methods/ticket-request/confirm.js b/modules/ticket/back/methods/ticket-request/confirm.js index b80971183..ce4bfdccb 100644 --- a/modules/ticket/back/methods/ticket-request/confirm.js +++ b/modules/ticket/back/methods/ticket-request/confirm.js @@ -74,12 +74,13 @@ module.exports = Self => { const origin = ctx.req.headers.origin; const requesterId = request.requesterFk; - const message = $t('MESSAGE_BOUGHT_UNITS', { + const message = $t('Bought units from buy request', { quantity: sale.quantity, concept: sale.concept, itemId: sale.itemFk, ticketId: sale.ticketFk, - url: `${origin}/#!/ticket/${sale.ticketFk}/summary` + url: `${origin}/#!/ticket/${sale.ticketFk}/summary`, + urlItem: `${origin}/#!/item/${sale.itemFk}/summary` }); await models.Chat.sendCheckingPresence(ctx, requesterId, message); diff --git a/modules/ticket/back/methods/ticket/makeInvoice.js b/modules/ticket/back/methods/ticket/makeInvoice.js index 29099e379..a44c41e16 100644 --- a/modules/ticket/back/methods/ticket/makeInvoice.js +++ b/modules/ticket/back/methods/ticket/makeInvoice.js @@ -67,11 +67,7 @@ module.exports = function(Self) { if (serial != 'R' && invoiceId) { await Self.rawSql('CALL invoiceOutBooking(?)', [invoiceId], options); - await models.PrintServerQueue.create({ - reportFk: 3, // Tarea #2734 (Nueva): crear informe facturas - param1: invoiceId, - workerFk: userId - }, options); + await models.InvoiceOut.createPdf(ctx, invoiceId, options); } await tx.commit(); diff --git a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js index 79bd59848..e20d9d8a2 100644 --- a/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js +++ b/modules/ticket/back/methods/ticket/specs/makeInvoice.spec.js @@ -5,6 +5,7 @@ describe('ticket makeInvoice()', () => { const userId = 19; const activeCtx = { accessToken: {userId: userId}, + headers: {origin: 'http://localhost:5000'}, }; const ctx = {req: activeCtx}; @@ -43,6 +44,9 @@ describe('ticket makeInvoice()', () => { }); it('should invoice a ticket, then try again to fail', async() => { + const invoiceOutModel = app.models.InvoiceOut; + spyOn(invoiceOutModel, 'createPdf'); + invoice = await app.models.Ticket.makeInvoice(ctx, ticketId); expect(invoice.invoiceFk).toBeDefined(); diff --git a/modules/ticket/back/models/ticket.json b/modules/ticket/back/models/ticket.json index 8f91ee689..65127a78c 100644 --- a/modules/ticket/back/models/ticket.json +++ b/modules/ticket/back/models/ticket.json @@ -2,7 +2,8 @@ "name": "Ticket", "base": "Loggable", "log": { - "model":"TicketLog" + "model":"TicketLog", + "showField": "id" }, "options": { "mysql": { diff --git a/modules/ticket/front/descriptor-menu/index.html b/modules/ticket/front/descriptor-menu/index.html index 80ad71d5f..390d9daf7 100644 --- a/modules/ticket/front/descriptor-menu/index.html +++ b/modules/ticket/front/descriptor-menu/index.html @@ -80,13 +80,13 @@ Make invoice - Regenerate invoice + Regenerate invoice PDF - + + vn-id="createInvoicePdfConfirmation" + on-accept="$ctrl.createInvoicePdf()" + question="Are you sure you want to regenerate the invoice PDF document?" + message="You are going to regenerate the invoice PDF document"> diff --git a/modules/ticket/front/descriptor-menu/index.js b/modules/ticket/front/descriptor-menu/index.js index d2dea6c0a..09783ec20 100644 --- a/modules/ticket/front/descriptor-menu/index.js +++ b/modules/ticket/front/descriptor-menu/index.js @@ -219,12 +219,12 @@ class Controller extends Section { .then(() => this.vnApp.showSuccess(this.$t('Ticket invoiced'))); } - regenerateInvoice() { + createInvoicePdf() { const invoiceId = this.ticket.invoiceOut.id; - return this.$http.post(`InvoiceOuts/${invoiceId}/regenerate`) + return this.$http.post(`InvoiceOuts/${invoiceId}/createPdf`) .then(() => { const snackbarMessage = this.$t( - `Invoice sent for a regeneration, will be available in a few minutes`); + `The invoice PDF document has been regenerated`); this.vnApp.showSuccess(snackbarMessage); }); } diff --git a/modules/ticket/front/descriptor-menu/index.spec.js b/modules/ticket/front/descriptor-menu/index.spec.js index 3cd08fc38..6a3009daf 100644 --- a/modules/ticket/front/descriptor-menu/index.spec.js +++ b/modules/ticket/front/descriptor-menu/index.spec.js @@ -148,12 +148,12 @@ describe('Ticket Component vnTicketDescriptorMenu', () => { }); }); - describe('regenerateInvoice()', () => { + describe('createInvoicePdf()', () => { it('should make a query and show a success snackbar', () => { jest.spyOn(controller.vnApp, 'showSuccess'); - $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/regenerate`).respond(); - controller.regenerateInvoice(); + $httpBackend.expectPOST(`InvoiceOuts/${ticket.invoiceOut.id}/createPdf`).respond(); + controller.createInvoicePdf(); $httpBackend.flush(); expect(controller.vnApp.showSuccess).toHaveBeenCalled(); diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index 6524df353..c2b181c97 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -17,12 +17,12 @@ Make a payment: "Verdnatura le comunica:\rSu pedido está pendiente de pago.\rPo Minimum is needed: "Verdnatura le recuerda:\rEs necesario un importe mínimo de 50€ (Sin IVA) en su pedido {{ticketId}} del día {{created | date: 'dd/MM/yyyy'}} para recibirlo sin portes adicionales." Ticket invoiced: Ticket facturado Make invoice: Crear factura -Regenerate invoice: Regenerar factura +Regenerate invoice PDF: Regenerar PDF factura +The invoice PDF document has been regenerated: El documento PDF de la factura ha sido regenerado You are going to invoice this ticket: Vas a facturar este ticket Are you sure you want to invoice this ticket?: ¿Seguro que quieres facturar este ticket? -You are going to regenerate the invoice: Vas a regenerar la factura -Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura? -Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos +You are going to regenerate the invoice PDF document: Vas a regenerar el documento PDF de la factura +Are you sure you want to regenerate the invoice PDF document?: ¿Seguro que quieres regenerar el documento PDF de la factura? Shipped hour updated: Hora de envio modificada Deleted ticket: Ticket eliminado Recalculate components: Recalcular componentes diff --git a/print/common/css/misc.css b/print/common/css/misc.css index 09d7706b3..df8bf571a 100644 --- a/print/common/css/misc.css +++ b/print/common/css/misc.css @@ -45,4 +45,8 @@ .no-page-break { page-break-inside: avoid; break-inside: avoid +} + +.page-break-after { + page-break-after: always; } \ No newline at end of file diff --git a/print/common/css/report.css b/print/common/css/report.css index 9331481f4..e8161f1fb 100644 --- a/print/common/css/report.css +++ b/print/common/css/report.css @@ -9,6 +9,6 @@ body { .title { margin-bottom: 20px; font-weight: 100; - font-size: 3em; + font-size: 2.6rem; margin-top: 0 } \ No newline at end of file diff --git a/print/core/component.js b/print/core/component.js index 4985cd061..12474566e 100644 --- a/print/core/component.js +++ b/print/core/component.js @@ -83,6 +83,11 @@ class Component { component.template = juice.inlineContent(this.template, this.stylesheet, { inlinePseudoElements: true }); + const tplPath = this.path; + if (!component.computed) component.computed = {}; + component.computed.path = function() { + return tplPath; + }; return component; } @@ -93,7 +98,7 @@ class Component { const component = this.build(); const i18n = new VueI18n(config.i18n); - const props = {tplPath: this.path, ...this.args}; + const props = {...this.args}; this._component = new Vue({ i18n: i18n, render: h => h(component, { diff --git a/print/core/database.js b/print/core/database.js index e879d0e3a..dee7a2933 100644 --- a/print/core/database.js +++ b/print/core/database.js @@ -36,13 +36,14 @@ module.exports = { * Makes a query from a SQL file * @param {String} queryName - The SQL file name * @param {Object} params - Parameterized values + * @param {Object} connection - Optional pool connection * * @return {Object} - Result promise */ - rawSqlFromDef(queryName, params) { + rawSqlFromDef(queryName, params, connection) { const query = fs.readFileSync(`${queryName}.sql`, 'utf8'); - return this.rawSql(query, params); + return this.rawSql(query, params, connection); }, /** diff --git a/print/core/mixins/db-helper.js b/print/core/mixins/db-helper.js index 4a6f9e460..1de27e6cd 100644 --- a/print/core/mixins/db-helper.js +++ b/print/core/mixins/db-helper.js @@ -19,12 +19,13 @@ const dbHelper = { * Makes a query from a SQL file * @param {String} queryName - The SQL file name * @param {Object} params - Parameterized values + * @param {Object} connection - Optional pool connection * * @return {Object} - Result promise */ - rawSqlFromDef(queryName, params) { - const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); - return db.rawSqlFromDef(absolutePath, params); + rawSqlFromDef(queryName, params, connection) { + const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); + return db.rawSqlFromDef(absolutePath, params, connection); }, /** @@ -66,7 +67,7 @@ const dbHelper = { */ findValueFromDef(queryName, params) { return this.findOneFromDef(queryName, params).then(row => { - return Object.values(row)[0]; + if (row) return Object.values(row)[0]; }); }, @@ -77,7 +78,7 @@ const dbHelper = { * @return {Object} - SQL */ getSqlFromDef(queryName) { - const absolutePath = path.join(__dirname, '../', this.tplPath, 'sql', queryName); + const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); return db.getSqlFromDef(absolutePath); }, }, diff --git a/print/methods/report.js b/print/methods/report.js index eea249a42..750fec4c8 100644 --- a/print/methods/report.js +++ b/print/methods/report.js @@ -6,11 +6,16 @@ module.exports = app => { const reportName = req.params.name; const fileName = getFileName(reportName, req.args); const report = new Report(reportName, req.args); - const stream = await report.toPdfStream(); + if (req.args.preview) { + const template = await report.render(); + res.send(template); + } else { + const stream = await report.toPdfStream(); - res.setHeader('Content-type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="${fileName}"`); - res.end(stream); + res.setHeader('Content-type', 'application/pdf'); + res.setHeader('Content-Disposition', `inline; filename="${fileName}"`); + res.end(stream); + } } catch (error) { next(error); } diff --git a/print/templates/reports/delivery-note/assets/css/style.css b/print/templates/reports/delivery-note/assets/css/style.css index cbe894097..f99c385fa 100644 --- a/print/templates/reports/delivery-note/assets/css/style.css +++ b/print/templates/reports/delivery-note/assets/css/style.css @@ -19,7 +19,7 @@ h2 { } .ticket-info { - font-size: 26px + font-size: 22px } #phytosanitary { diff --git a/print/templates/reports/delivery-note/sql/sales.sql b/print/templates/reports/delivery-note/sql/sales.sql index d17f6feee..c449030cf 100644 --- a/print/templates/reports/delivery-note/sql/sales.sql +++ b/print/templates/reports/delivery-note/sql/sales.sql @@ -37,6 +37,7 @@ FROM vn.sale s LEFT JOIN taxClass tcl ON tcl.id = itc.taxClassFk LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id AND ic.code = 'plant' + AND ib.ediBotanic IS NOT NULL WHERE s.ticketFk = ? GROUP BY s.id ORDER BY (it.isPackaging), s.concept, s.itemFk \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/assets/css/import.js b/print/templates/reports/invoice-incoterms/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/invoice-incoterms/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/invoice-incoterms/assets/css/style.css b/print/templates/reports/invoice-incoterms/assets/css/style.css new file mode 100644 index 000000000..d754d050f --- /dev/null +++ b/print/templates/reports/invoice-incoterms/assets/css/style.css @@ -0,0 +1,29 @@ +h2 { + font-weight: 100; + color: #555 +} + +.table-title { + margin-bottom: 15px; + font-size: 0.8rem +} + +.table-title h2 { + margin: 0 15px 0 0 +} + +.ticket-info { + font-size: 22px +} + +#incoterms table { + font-size: 1.2rem +} + +#incoterms table th { + width: 10% +} + +#incoterms p { + font-size: 1.2rem +} \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.html b/print/templates/reports/invoice-incoterms/invoice-incoterms.html new file mode 100644 index 000000000..2cceccc93 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.html @@ -0,0 +1,124 @@ + + + + + + + + + +
+ + + + +
+
+
+
+
+

{{$t('title')}}

+ + + + + + + + + + + + + + + +
{{$t('clientId')}}{{client.id}}
{{$t('invoice')}}{{invoice.ref}}
{{$t('date')}}{{invoice.issued | date('%d-%m-%Y')}}
+
+
+
+
+
{{$t('invoiceData')}}
+
+

{{client.socialName}}

+
+ {{client.postalAddress}} +
+
+ {{client.postcodeCity}} +
+
+ {{$t('fiscalId')}}: {{client.fi}} +
+
+
+
+
+ +
+
{{$t('incotermsTitle')}}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{$t('incoterms')}} +
asd
+
{{incoterms.incotermsFk}} - {{incoterms.incotermsName}}
+ {{$t('productDescription')}} + {{incoterms.intrastat}}
{{$t('expeditionDescription')}}
{{$t('packageNumber')}}{{incoterms.packages}}
{{$t('packageGrossWeight')}}{{incoterms.weight}} KG
{{$t('packageCubing')}}{{incoterms.volume}} m3
+ +

+

+ {{$t('customsInfo')}} + {{incoterms.customsAgentName}} +
+
+ ( + {{incoterms.customsAgentNif}} + {{incoterms.customsAgentStreet}} + + ☎ {{incoterms.customsAgentPhone}} + + + ✉ {{incoterms.customsAgentEmail}} + + ) +
+

+

+ {{$t('productDisclaimer')}} +

+
+
+
+
+
+ + \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/invoice-incoterms.js b/print/templates/reports/invoice-incoterms/invoice-incoterms.js new file mode 100755 index 000000000..cfe0a21cb --- /dev/null +++ b/print/templates/reports/invoice-incoterms/invoice-incoterms.js @@ -0,0 +1,39 @@ +const Component = require(`${appPath}/core/component`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); + +module.exports = { + name: 'invoice-incoterms', + async serverPrefetch() { + this.invoice = await this.fetchInvoice(this.invoiceId); + this.client = await this.fetchClient(this.invoiceId); + this.incoterms = await this.fetchIncoterms(this.invoiceId); + + if (!this.invoice) + throw new Error('Something went wrong'); + }, + computed: { + + }, + methods: { + fetchInvoice(invoiceId) { + return this.findOneFromDef('invoice', [invoiceId]); + }, + fetchClient(invoiceId) { + return this.findOneFromDef('client', [invoiceId]); + }, + fetchIncoterms(invoiceId) { + return this.findOneFromDef('incoterms', {invoiceId}); + } + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build() + }, + props: { + invoiceId: { + type: String, + required: true + } + } +}; diff --git a/print/templates/reports/invoice-incoterms/locale/es.yml b/print/templates/reports/invoice-incoterms/locale/es.yml new file mode 100644 index 000000000..9828564d7 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/locale/es.yml @@ -0,0 +1,16 @@ +title: Factura +invoice: Factura +clientId: Cliente +date: Fecha +invoiceData: Datos de facturación +fiscalId: CIF / NIF +invoiceRef: Factura {0} +incotermsTitle: Información para la exportación +incoterms: Incoterms +productDescription: Descripción de la mercancia +expeditionDescription: INFORMACIÓN DE LA EXPEDICIÓN +packageNumber: Número de bultos +packageGrossWeight: Peso bruto +packageCubing: Cubicaje +customsInfo: A despachar por la agencia de aduanas +productDisclaimer: Mercancía destinada a la exportación, EXENTA de IVA (Ley 37/1992 - Art. 21) \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/sql/client.sql b/print/templates/reports/invoice-incoterms/sql/client.sql new file mode 100644 index 000000000..dd6035222 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/client.sql @@ -0,0 +1,12 @@ +SELECT + c.id, + c.socialName, + c.street AS postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + CONCAT(c.postcode, ' - ', c.city) postcodeCity +FROM vn.invoiceOut io + JOIN vn.client c ON c.id = io.clientFk + JOIN vn.country cty ON cty.id = c.countryFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + AND ios.taxAreaFk = 'CEE' +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice-incoterms/sql/incoterms.sql b/print/templates/reports/invoice-incoterms/sql/incoterms.sql new file mode 100644 index 000000000..e3d3f19c1 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/incoterms.sql @@ -0,0 +1,71 @@ +SELECT io.issued, + c.socialName, + c.street postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + io.clientFk, + c.postcode, + c.city, + io.companyFk, + io.ref, + tc.code, + s.concept, + s.quantity, + s.price, + s.discount, + s.ticketFk, + t.shipped, + t.refFk, + a.nickname, + s.itemFk, + s.id saleFk, + pm.name AS pmname, + sa.iban, + c.phone, + MAX(t.packages) packages, + a.incotermsFk, + ic.name incotermsName , + sub.description weight, + t.observations, + ca.fiscalName customsAgentName, + ca.street customsAgentStreet, + ca.nif customsAgentNif, + ca.phone customsAgentPhone, + ca.email customsAgentEmail, + CAST(sub2.volume AS DECIMAL (10,2)) volume, + sub3.intrastat + FROM vn.invoiceOut io + JOIN vn.supplier su ON su.id = io.companyFk + JOIN vn.client c ON c.id = io.clientFk + LEFT JOIN vn.province p ON p.id = c.provinceFk + JOIN vn.ticket t ON t.refFk = io.ref + LEFT JOIN (SELECT tob.ticketFk,tob.description + FROM vn.ticketObservation tob + LEFT JOIN vn.observationType ot ON ot.id = tob.observationTypeFk + WHERE ot.description = "Peso Aduana" + )sub ON sub.ticketFk = t.id + JOIN vn.address a ON a.id = t.addressFk + LEFT JOIN vn.incoterms ic ON ic.code = a.incotermsFk + LEFT JOIN vn.customsAgent ca ON ca.id = a.customsAgentFk + JOIN vn.sale s ON s.ticketFk = t.id + JOIN (SELECT SUM(volume) volume + FROM vn.invoiceOut io + JOIN vn.ticket t ON t.refFk = io.ref + JOIN vn.saleVolume sv ON sv.ticketFk = t.id + WHERE io.id = :invoiceId + )sub2 ON TRUE + JOIN vn.itemTaxCountry itc ON itc.countryFk = su.countryFk AND itc.itemFk = s.itemFk + JOIN vn.taxClass tc ON tc.id = itc.taxClassFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial AND ios.taxAreaFk = 'CEE' + JOIN vn.country cty ON cty.id = c.countryFk + JOIN vn.payMethod pm ON pm.id = c .payMethodFk + JOIN vn.company co ON co.id=io.companyFk + JOIN vn.supplierAccount sa ON sa.id=co.supplierAccountFk + LEFT JOIN (SELECT GROUP_CONCAT(DISTINCT ir.description ORDER BY ir.description SEPARATOR '. ' ) as intrastat + FROM vn.ticket t + JOIN vn.invoiceOut io ON io.ref = t.refFk + JOIN vn.sale s ON t.id = s.ticketFk + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.intrastat ir ON ir.id = i.intrastatFk + WHERE io.id = :invoiceId + )sub3 ON TRUE + WHERE io.id = :invoiceId diff --git a/print/templates/reports/invoice-incoterms/sql/invoice.sql b/print/templates/reports/invoice-incoterms/sql/invoice.sql new file mode 100644 index 000000000..b9a929183 --- /dev/null +++ b/print/templates/reports/invoice-incoterms/sql/invoice.sql @@ -0,0 +1,17 @@ +SELECT + io.id, + io.issued, + io.clientFk, + io.companyFk, + io.ref, + pm.code AS payMethodCode, + cny.code companyCode, + sa.iban, + ios.footNotes +FROM invoiceOut io + JOIN client c ON c.id = io.clientFk + JOIN payMethod pm ON pm.id = c.payMethodFk + JOIN company cny ON cny.id = io.companyFk + JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk + LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/assets/css/import.js b/print/templates/reports/invoice/assets/css/import.js new file mode 100644 index 000000000..fd8796c2b --- /dev/null +++ b/print/templates/reports/invoice/assets/css/import.js @@ -0,0 +1,9 @@ +const Stylesheet = require(`${appPath}/core/stylesheet`); + +module.exports = new Stylesheet([ + `${appPath}/common/css/spacing.css`, + `${appPath}/common/css/misc.css`, + `${appPath}/common/css/layout.css`, + `${appPath}/common/css/report.css`, + `${__dirname}/style.css`]) + .mergeStyles(); diff --git a/print/templates/reports/invoice/assets/css/style.css b/print/templates/reports/invoice/assets/css/style.css new file mode 100644 index 000000000..cd605db9b --- /dev/null +++ b/print/templates/reports/invoice/assets/css/style.css @@ -0,0 +1,39 @@ +h2 { + font-weight: 100; + color: #555 +} + +.table-title { + margin-bottom: 15px; + font-size: 0.8rem +} + +.table-title h2 { + margin: 0 15px 0 0 +} + +.ticket-info { + font-size: 22px +} + +#nickname h2 { + max-width: 400px; + word-wrap: break-word +} + +#phytosanitary { + padding-right: 10px +} + +#phytosanitary .flag img { + width: 100% +} + +#phytosanitary .flag .flag-text { + padding-left: 10px; + box-sizing: border-box; +} + +.phytosanitary-info { + margin-top: 10px +} \ No newline at end of file diff --git a/print/templates/reports/invoice/assets/images/europe.png b/print/templates/reports/invoice/assets/images/europe.png new file mode 100644 index 000000000..673be92ae Binary files /dev/null and b/print/templates/reports/invoice/assets/images/europe.png differ diff --git a/print/templates/reports/invoice/invoice.html b/print/templates/reports/invoice/invoice.html new file mode 100644 index 000000000..671bb8c7f --- /dev/null +++ b/print/templates/reports/invoice/invoice.html @@ -0,0 +1,319 @@ + + + + + + + + + +
+ + + + + + + + + +
+
+
+
+
+

{{$t('title')}}

+ + + + + + + + + + + + + + + +
{{$t('clientId')}}{{client.id}}
{{$t('invoice')}}{{invoice.ref}}
{{$t('date')}}{{invoice.issued | date('%d-%m-%Y')}}
+
+
+
+
+
{{$t('invoiceData')}}
+
+

{{client.socialName}}

+
+ {{client.postalAddress}} +
+
+ {{client.postcodeCity}} +
+
+ {{$t('fiscalId')}}: {{client.fi}} +
+
+
+
+
+ + +
+

{{$t('rectifiedInvoices')}}

+ + + + + + + + + + + + + + + + + +
{{$t('invoice')}}{{$t('issued')}}{{$t('amount')}}{{$t('description')}}
{{row.ref}}{{row.issued | date}}{{row.amount | currency('EUR', $i18n.locale)}}{{row.description}}
+
+ + + +
+
+
+

{{$t('deliveryNote')}} +

+
+
+ {{ticket.id}} +
+
+
+

Shipped

+
+
+
+ {{ticket.shipped | date}} +
+
+ +

{{ticket.nickname}}

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('reference')}}{{$t('quantity')}}{{$t('concept')}}{{$t('price')}}{{$t('discount')}}{{$t('vat')}}{{$t('amount')}}
{{sale.itemFk | zerofill('000000')}}{{sale.quantity}}{{sale.concept}}{{sale.price | currency('EUR', $i18n.locale)}}{{(sale.discount / 100) | percentage}}{{sale.vatType}}{{saleImport(sale) | currency('EUR', $i18n.locale)}}
+ + {{sale.tag5}} {{sale.value5}} + + + {{sale.tag6}} {{sale.value6}} + + + {{sale.tag7}} {{sale.value7}} + +
+ {{$t('subtotal')}} + {{ticketSubtotal(ticket) | currency('EUR', $i18n.locale)}}
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('taxBreakdown')}}
{{$t('type')}} + {{$t('taxBase')}} + {{$t('tax')}}{{$t('fee')}}
{{tax.name}} + {{tax.base | currency('EUR', $i18n.locale)}} + {{tax.vatPercent | percentage}}{{tax.vat | currency('EUR', $i18n.locale)}}
{{$t('subtotal')}} + {{sumTotal(taxes, 'base') | currency('EUR', $i18n.locale)}} + {{sumTotal(taxes, 'vat') | currency('EUR', $i18n.locale)}}
{{$t('total')}}{{taxTotal | currency('EUR', $i18n.locale)}}
+ +
+
{{$t('notes')}}
+
+ {{invoice.footNotes}} +
+
+
+ + + +
+
+
+
+
+
+ +
+
+ {{$t('plantPassport')}}
+
+
+
+
+
+ A + {{botanical}} +
+
+ B + ES17462130 +
+
+ C + {{ticketsId}} +
+
+ D + ES +
+
+
+
+
+ +
+ + + +
+

{{$t('intrastat')}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{$t('code')}}{{$t('description')}}{{$t('stems')}}{{$t('netKg')}}{{$t('amount')}}
{{row.code}}{{row.description}}{{row.stems | number($i18n.locale)}}{{row.netKg | number($i18n.locale)}}{{row.subtotal | currency('EUR', $i18n.locale)}}
+ {{sumTotal(intrastat, 'stems') | number($i18n.locale)}} + + {{sumTotal(intrastat, 'netKg') | number($i18n.locale)}} + + {{sumTotal(intrastat, 'subtotal') | currency('EUR', $i18n.locale)}} +
+
+ + + +
+
+
+
{{$t('observations')}}
+
+
{{$t('wireTransfer')}}
+
{{$t('accountNumber', [invoice.iban])}}
+
+
+
+
+ + +
+
+ + + +
+ + \ No newline at end of file diff --git a/print/templates/reports/invoice/invoice.js b/print/templates/reports/invoice/invoice.js new file mode 100755 index 000000000..54df45c36 --- /dev/null +++ b/print/templates/reports/invoice/invoice.js @@ -0,0 +1,130 @@ +const Component = require(`${appPath}/core/component`); +const Report = require(`${appPath}/core/report`); +const reportHeader = new Component('report-header'); +const reportFooter = new Component('report-footer'); +const invoiceIncoterms = new Report('invoice-incoterms'); +const db = require(`${appPath}/core/database`); + +module.exports = { + name: 'invoice', + async serverPrefetch() { + this.invoice = await this.fetchInvoice(this.invoiceId); + this.client = await this.fetchClient(this.invoiceId); + this.taxes = await this.fetchTaxes(this.invoiceId); + this.intrastat = await this.fetchIntrastat(this.invoiceId); + this.rectified = await this.fetchRectified(this.invoiceId); + this.hasIncoterms = await this.fetchHasIncoterms(this.invoiceId); + + const tickets = await this.fetchTickets(this.invoiceId); + const sales = await this.fetchSales(this.invoiceId); + + const map = new Map(); + + for (let ticket of tickets) + map.set(ticket.id, ticket); + + for (let sale of sales) { + const ticket = map.get(sale.ticketFk); + if (!ticket.sales) ticket.sales = []; + ticket.sales.push(sale); + } + + this.tickets = tickets; + + if (!this.invoice) + throw new Error('Something went wrong'); + }, + data() { + return {totalBalance: 0.00}; + }, + computed: { + ticketsId() { + const tickets = this.tickets.map(ticket => ticket.id); + + return tickets.join(', '); + }, + botanical() { + let phytosanitary = []; + for (let ticket of this.tickets) { + for (let sale of ticket.sales) { + if (sale.botanical) + phytosanitary.push(sale.botanical); + } + } + + return phytosanitary.filter((item, index) => + phytosanitary.indexOf(item) == index + ).join(', '); + }, + taxTotal() { + const base = this.sumTotal(this.taxes, 'base'); + const vat = this.sumTotal(this.taxes, 'vat'); + return base + vat; + } + }, + methods: { + fetchInvoice(invoiceId) { + return this.findOneFromDef('invoice', [invoiceId]); + }, + fetchClient(invoiceId) { + return this.findOneFromDef('client', [invoiceId]); + }, + fetchTickets(invoiceId) { + return this.rawSqlFromDef('tickets', [invoiceId]); + }, + async fetchSales(invoiceId) { + const connection = await db.pool.getConnection(); + await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection); + await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection); + + const sales = this.rawSqlFromDef('sales', connection); + + await this.rawSql(`DROP TEMPORARY TABLE tmp.invoiceTickets`, connection); + await connection.release(); + + return sales; + }, + fetchTaxes(invoiceId) { + return this.rawSqlFromDef(`taxes`, [invoiceId]); + }, + fetchIntrastat(invoiceId) { + return this.rawSqlFromDef(`intrastat`, [invoiceId]); + }, + fetchRectified(invoiceId) { + return this.rawSqlFromDef(`rectified`, [invoiceId]); + }, + fetchHasIncoterms(invoiceId) { + return this.findValueFromDef(`hasIncoterms`, [invoiceId]); + }, + saleImport(sale) { + const price = sale.quantity * sale.price; + + return price * (1 - sale.discount / 100); + }, + ticketSubtotal(ticket) { + let subTotal = 0.00; + for (let sale of ticket.sales) + subTotal += this.saleImport(sale); + + return subTotal; + }, + sumTotal(rows, prop) { + let total = 0.00; + for (let row of rows) + total += parseFloat(row[prop]); + + return total; + } + }, + components: { + 'report-header': reportHeader.build(), + 'report-footer': reportFooter.build(), + 'invoice-incoterms': invoiceIncoterms.build() + }, + props: { + invoiceId: { + type: String, + required: true + } + } +}; diff --git a/print/templates/reports/invoice/locale/es.yml b/print/templates/reports/invoice/locale/es.yml new file mode 100644 index 000000000..6fdfc8a14 --- /dev/null +++ b/print/templates/reports/invoice/locale/es.yml @@ -0,0 +1,35 @@ +title: Factura +invoice: Factura +clientId: Cliente +invoiceData: Datos de facturación +fiscalId: CIF / NIF +invoiceRef: Factura {0} +deliveryNote: Albarán +shipped: F. envío +date: Fecha +reference: Ref. +quantity: Cant. +concept: Concepto +price: PVP/u +discount: Dto. +vat: IVA +amount: Importe +type: Tipo +taxBase: Base imp. +tax: Tasa +fee: Cuota +total: Total +subtotal: Subtotal +taxBreakdown: Desglose impositivo +notes: Notas +intrastat: Intrastat +code: Código +description: Descripción +stems: Tallos +netKg: KG Neto +rectifiedInvoices: Facturas rectificadas +issued: F. emisión +plantPassport: Pasaporte fitosanitario +observations: Observaciones +wireTransfer: "Forma de pago: Transferencia" +accountNumber: "Número de cuenta: {0}" \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/client.sql b/print/templates/reports/invoice/sql/client.sql new file mode 100644 index 000000000..dd6035222 --- /dev/null +++ b/print/templates/reports/invoice/sql/client.sql @@ -0,0 +1,12 @@ +SELECT + c.id, + c.socialName, + c.street AS postalAddress, + IF (ios.taxAreaFk IS NOT NULL, CONCAT(cty.code, c.fi), c.fi) fi, + CONCAT(c.postcode, ' - ', c.city) postcodeCity +FROM vn.invoiceOut io + JOIN vn.client c ON c.id = io.clientFk + JOIN vn.country cty ON cty.id = c.countryFk + LEFT JOIN vn.invoiceOutSerial ios ON ios.code = io.serial + AND ios.taxAreaFk = 'CEE' +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/hasIncoterms.sql b/print/templates/reports/invoice/sql/hasIncoterms.sql new file mode 100644 index 000000000..27f61f57c --- /dev/null +++ b/print/templates/reports/invoice/sql/hasIncoterms.sql @@ -0,0 +1,8 @@ +SELECT IF(incotermsFk IS NULL, FALSE, TRUE) AS hasIncoterms + FROM ticket t + JOIN invoiceOut io ON io.ref = t.refFk + JOIN client c ON c.id = t.clientFk + JOIN address a ON a.id = t.addressFk + WHERE io.id = ? + AND IF(c.hasToinvoiceByAddress = FALSE, c.defaultAddressFk, TRUE) + LIMIT 1 \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/intrastat.sql b/print/templates/reports/invoice/sql/intrastat.sql new file mode 100644 index 000000000..e391056ec --- /dev/null +++ b/print/templates/reports/invoice/sql/intrastat.sql @@ -0,0 +1,14 @@ +SELECT + ir.id AS code, + ir.description AS description, + CAST(SUM(IFNULL(i.stems,1) * s.quantity) AS DECIMAL(10,2)) as stems, + CAST(SUM( weight) AS DECIMAL(10,2)) as netKg, + CAST(SUM((s.quantity * s.price * (100 - s.discount) / 100 )) AS DECIMAL(10,2)) AS subtotal + FROM vn.sale s + LEFT JOIN vn.saleVolume sv ON sv.saleFk = s.id + LEFT JOIN vn.ticket t ON t.id = s.ticketFk + LEFT JOIN vn.invoiceOut io ON io.ref = t.refFk + LEFT JOIN vn.item i ON i.id = s.itemFk + JOIN vn.intrastat ir ON ir.id = i.intrastatFk + WHERE io.id = ? + GROUP BY i.intrastatFk; \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoice.sql b/print/templates/reports/invoice/sql/invoice.sql new file mode 100644 index 000000000..aacbb0016 --- /dev/null +++ b/print/templates/reports/invoice/sql/invoice.sql @@ -0,0 +1,16 @@ +SELECT + io.issued, + io.clientFk, + io.companyFk, + io.ref, + pm.code AS payMethodCode, + cny.code companyCode, + sa.iban, + ios.footNotes +FROM invoiceOut io + JOIN client c ON c.id = io.clientFk + JOIN payMethod pm ON pm.id = c.payMethodFk + JOIN company cny ON cny.id = io.companyFk + JOIN supplierAccount sa ON sa.id = cny.supplierAccountFk + LEFT JOIN invoiceOutSerial ios ON ios.code = io.serial +WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/invoiceTickets.sql b/print/templates/reports/invoice/sql/invoiceTickets.sql new file mode 100644 index 000000000..089911a63 --- /dev/null +++ b/print/templates/reports/invoice/sql/invoiceTickets.sql @@ -0,0 +1,20 @@ +CREATE TEMPORARY TABLE tmp.invoiceTickets + ENGINE = MEMORY + SELECT + t.id AS ticketFk, + t.clientFk, + t.shipped, + t.nickname, + io.ref, + c.socialName, + sa.iban, + pm.name AS payMethod, + su.countryFk AS supplierCountryFk + FROM vn.invoiceOut io + JOIN vn.supplier su ON su.id = io.companyFk + JOIN vn.ticket t ON t.refFk = io.ref + JOIN vn.client c ON c.id = t.clientFk + JOIN vn.payMethod pm ON pm.id = c.payMethodFk + JOIN vn.company co ON co.id = io.companyFk + JOIN vn.supplierAccount sa ON sa.id = co.supplierAccountFk + WHERE io.id = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/phytosanitary.sql b/print/templates/reports/invoice/sql/phytosanitary.sql new file mode 100644 index 000000000..1ae178975 --- /dev/null +++ b/print/templates/reports/invoice/sql/phytosanitary.sql @@ -0,0 +1,14 @@ +SELECT CONCAT( 'A ', GROUP_CONCAT(DISTINCT(ib.ediBotanic) SEPARATOR ', '), CHAR(13,10), CHAR(13,10), + 'B ES17462130', CHAR(13,10), CHAR(13,10), + 'C ', GROUP_CONCAT(DISTINCT(t.id) SEPARATOR ', '), CHAR(13,10), CHAR(13,10), + 'D ES' ) phytosanitary + FROM vn.ticket t + JOIN vn.sale s ON s.ticketFk = t.id + JOIN vn.item i ON i.id = s.itemFk + JOIN vn.itemType it ON it.id = i.typeFk + JOIN vn.itemCategory ic ON ic.id = it.categoryFk + JOIN vn.itemBotanicalWithGenus ib ON ib.itemfk = i.id + WHERE t.refFk = # AND + ic.`code` = 'plant' AND + ib.ediBotanic IS NOT NULL + ORDER BY ib.ediBotanic \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/rectified.sql b/print/templates/reports/invoice/sql/rectified.sql new file mode 100644 index 000000000..1255b973c --- /dev/null +++ b/print/templates/reports/invoice/sql/rectified.sql @@ -0,0 +1,9 @@ +SELECT + io.amount, + io.ref, + io.issued, + ict.description +FROM vn.invoiceCorrection ic + JOIN vn.invoiceOut io ON io.id = ic.correctedFk + JOIN vn.invoiceCorrectionType ict ON ict.id = ic.invoiceCorrectionTypeFk +where ic.correctingFk = ? \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/sales.sql b/print/templates/reports/invoice/sql/sales.sql new file mode 100644 index 000000000..0665fc0ff --- /dev/null +++ b/print/templates/reports/invoice/sql/sales.sql @@ -0,0 +1,59 @@ +SELECT + it.ref, + it.socialName, + it.iban, + it.payMethod, + it.clientFk, + it.shipped, + it.nickname, + s.ticketFk, + s.itemFk, + s.concept, + s.quantity, + s.price, + s.discount, + i.tag5, + i.value5, + i.tag6, + i.value6, + i.tag7, + i.value7, + tc.code AS vatType, + ib.ediBotanic botanical + FROM tmp.invoiceTickets it + JOIN vn.sale s ON s.ticketFk = it.ticketFk + JOIN item i ON i.id = s.itemFk + LEFT JOIN itemType it ON it.id = i.typeFk + LEFT JOIN itemCategory ic ON ic.id = it.categoryFk + LEFT JOIN itemBotanicalWithGenus ib ON ib.itemFk = i.id + AND ic.code = 'plant' + AND ib.ediBotanic IS NOT NULL + JOIN vn.itemTaxCountry itc ON itc.countryFk = it.supplierCountryFk + AND itc.itemFk = s.itemFk + JOIN vn.taxClass tc ON tc.id = itc.taxClassFk +UNION ALL +SELECT + it.ref, + it.socialName, + it.iban, + it.payMethod, + it.clientFk, + it.shipped, + it.nickname, + it.ticketFk, + '', + ts.description concept, + ts.quantity, + ts.price, + 0 discount, + NULL AS tag5, + NULL AS value5, + NULL AS tag6, + NULL AS value6, + NULL AS tag7, + NULL AS value7, + tc.code AS vatType, + NULL AS botanical + FROM tmp.invoiceTickets it + JOIN vn.ticketService ts ON ts.ticketFk = it.ticketFk + JOIN vn.taxClass tc ON tc.id = ts.taxClassFk \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/taxes.sql b/print/templates/reports/invoice/sql/taxes.sql new file mode 100644 index 000000000..19b1cc00e --- /dev/null +++ b/print/templates/reports/invoice/sql/taxes.sql @@ -0,0 +1,11 @@ +SELECT + iot.vat, + pgc.name, + IF(pe.equFk IS NULL, taxableBase, 0) AS base, + pgc.rate / 100 AS vatPercent + FROM invoiceOutTax iot + JOIN pgc ON pgc.code = iot.pgcFk + LEFT JOIN pgcEqu pe ON pe.equFk = pgc.code + JOIN invoiceOut io ON io.id = iot.invoiceOutFk + WHERE invoiceOutFk = ? + ORDER BY iot.id \ No newline at end of file diff --git a/print/templates/reports/invoice/sql/tickets.sql b/print/templates/reports/invoice/sql/tickets.sql new file mode 100644 index 000000000..feca81ead --- /dev/null +++ b/print/templates/reports/invoice/sql/tickets.sql @@ -0,0 +1,7 @@ +SELECT + t.id, + t.shipped, + t.nickname +FROM invoiceOut io + JOIN ticket t ON t.refFk = io.ref +WHERE io.id = ? \ No newline at end of file diff --git a/storage/pdfs/invoice/.keep b/storage/pdfs/invoice/.keep new file mode 100644 index 000000000..e69de29bb