4548-invoiceIn-pdf #1113

Merged
joan merged 22 commits from 4548-invoiceIn-pdf into dev 2022-11-03 07:48:43 +00:00
29 changed files with 729 additions and 9 deletions

View File

@ -0,0 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES
('InvoiceIn', 'invoiceInPdf', 'READ', 'ALLOW', 'ROLE', 'administrative'),
('InvoiceIn', 'invoiceInEmail', 'WRITE', 'ALLOW', 'ROLE', 'administrative'),
alexm marked this conversation as resolved
Review

perque uno te read y l'altre WRITE?

perque uno te read y l'altre WRITE?

View File

@ -0,0 +1,53 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInEmail', {
description: 'Sends the invoice in email with an attached PDF',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoice id',
http: {source: 'path'}
},
{
arg: 'recipient',
type: 'string',
description: 'The recipient email',
required: true,
},
{
arg: 'recipientId',
type: 'number',
description: 'The recipient id to send to the recipient preferred language',
required: false
}
],
returns: {
type: ['object'],
root: true
},
http: {
path: '/:id/invoice-in-email',
verb: 'POST'
}
});
Self.invoiceInEmail = async ctx => {
const args = Object.assign({}, ctx.args);
const params = {
recipient: args.recipient,
lang: ctx.req.getLocale()
};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const email = new Email('invoiceIn', params);
return email.send();
};
};

View File

@ -0,0 +1,50 @@
const {Report} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('invoiceInPdf', {
description: 'Returns the invoiceIn pdf',
alexm marked this conversation as resolved Outdated
Outdated
Review

Invoice in pdf

Invoice in pdf
accessType: 'READ',
accepts: [
{
arg: 'id',
type: 'number',
required: true,
description: 'The invoiceIn id',
alexm marked this conversation as resolved Outdated
Outdated
Review

Invoice id?

Invoice id?
http: {source: 'path'}
}
],
returns: [
{
arg: 'body',
type: 'file',
root: true
}, {
arg: 'Content-Type',
type: 'String',
http: {target: 'header'}
}, {
arg: 'Content-Disposition',
type: 'String',
http: {target: 'header'}
}
],
http: {
path: '/:id/invoice-in-pdf',
verb: 'GET'
}
});
Self.invoiceInPdf = async(ctx, id) => {
const args = Object.assign({}, ctx.args);
const params = {lang: ctx.req.getLocale()};
delete args.ctx;
for (const param in args)
params[param] = args[param];
const report = new Report('invoiceIn', params);
const stream = await report.toPdfStream();
return [stream, 'application/pdf', `filename="doc-${id}.pdf"`];
};
};

View File

@ -4,4 +4,6 @@ module.exports = Self => {
require('../methods/invoice-in/clone')(Self);
require('../methods/invoice-in/toBook')(Self);
require('../methods/invoice-in/getTotals')(Self);
require('../methods/invoice-in/invoiceInPdf')(Self);
require('../methods/invoice-in/invoiceInEmail')(Self);
};

View File

@ -94,6 +94,11 @@
"model": "Supplier",
"foreignKey": "supplierFk"
},
"supplierContact": {
"type": "hasMany",
"model": "SupplierContact",
"foreignKey": "supplierFk"
},
"currency": {
"type": "belongsTo",
"model": "Currency",

View File

@ -8,6 +8,14 @@ class Controller extends ModuleCard {
{
relation: 'supplier'
},
{
relation: 'supplierContact',
scope: {
where: {
email: {neq: null}
}
}
},
{
relation: 'invoiceInDueDay'
},

View File

@ -1,5 +1,5 @@
<vn-descriptor-content
module="invoiceIn"
<vn-descriptor-content
module="invoiceIn"
description="$ctrl.invoiceIn.supplierRef"
summary="$ctrl.$.summary">
<slot-menu>
@ -10,7 +10,6 @@
translate>
To book
</vn-item>
<vn-item
ng-click="deleteConfirmation.show()"
vn-acl="administrative"
@ -26,6 +25,16 @@
translate>
Clone Invoice
</vn-item>
<vn-item
ng-click="$ctrl.showPdfInvoice()"
translate>
Show agricultural invoice as PDF
</vn-item>
<vn-item
ng-click="sendPdfConfirmation.show({email: $ctrl.entity.supplierContact[0].email})"
translate>
Send agricultural invoice as PDF
</vn-item>
</slot-menu>
<slot-body>
<div class="attributes">
@ -37,7 +46,7 @@
</vn-label-value>
<vn-label-value label="Supplier">
<span ng-click="supplierDescriptor.show($event, $ctrl.invoiceIn.supplier.id)" class="link">
{{$ctrl.invoiceIn.supplier.nickname}}
{{$ctrl.invoiceIn.supplier.nickname}}
</span>
</vn-label-value>
</div>
@ -57,9 +66,9 @@
icon="icon-invoice-in">
</vn-quick-link>
</div>
</div>
</slot-body>
</vn-descriptor-content>
<vn-confirm
@ -75,11 +84,29 @@
<vn-supplier-descriptor-popover
vn-id="supplierDescriptor">
</vn-supplier-descriptor-popover>
<vn-confirm
<vn-confirm
vn-id="confirm-toBookAnyway"
message="Are you sure you want to book this invoice?"
on-accept="$ctrl.onAcceptToBook()">
</vn-confirm>
<vn-popup vn-id="summary">
<vn-invoice-in-summary invoice-in="$ctrl.invoiceIn"></vn-invoice-in-summary>
</vn-popup>
</vn-popup>
<!-- Send PDF invoice confirmation popup -->
<vn-dialog
vn-id="sendPdfConfirmation"
on-accept="$ctrl.sendPdfInvoice($data)"
message="Send PDF invoice">
<tpl-body>
<span translate>Are you sure you want to send it?</span>
<vn-textfield vn-one
label="Email"
ng-model="sendPdfConfirmation.data.email">
</vn-textfield>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Confirm</button>
</tpl-buttons>
</vn-dialog>

View File

@ -96,6 +96,20 @@ class Controller extends Descriptor {
.then(() => this.$state.reload())
.then(() => this.vnApp.showSuccess(this.$t('InvoiceIn booked')));
}
showPdfInvoice() {
this.vnReport.show(`InvoiceIns/${this.id}/invoice-in-pdf`);
}
sendPdfInvoice($data) {
if (!$data.email)
return this.vnApp.showError(this.$t(`The email can't be empty`));
return this.vnEmail.send(`InvoiceIns/${this.entity.id}/invoice-in-email`, {
recipient: $data.email,
recipientId: this.entity.supplier.id
});
}
}
ngModule.vnComponent('vnInvoiceInDescriptor', {

View File

@ -19,3 +19,5 @@ To book: Contabilizar
Total amount: Total importe
Total net: Total neto
Total stems: Total tallos
Show agricultural invoice as PDF: Ver factura agrícola como PDF
alexm marked this conversation as resolved Outdated

i minuscula, y per a l'anglés posa que es agricola.

i minuscula, y per a l'anglés posa que es agricola.
Send agricultural invoice as PDF: Enviar factura agrícola como PDF

View File

@ -7,4 +7,4 @@ copyLink: 'Como alternativa, podes copiar o siguinte link no teu navegador:'
poll: Si o deseja, podes responder nosso questionário de satiscação para ajudar-nos a prestar-vos um melhor serviço. Tua opinião é muito importante para nós!
help: Cualquer dúvida que surja, no hesites em consultar-la, <strong>Estamos aqui para
atender-te!</strong>
conclusion: Obrigado por tua atenção!
conclusion: Obrigado por tua atenção!

View File

@ -0,0 +1,11 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/email.css`])
.mergeStyles();

View File

@ -0,0 +1,6 @@
[
{
"filename": "invoiceIn.pdf",
"component": "invoiceIn"
}
]

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description')"></p>
<p v-html="$t('conclusion')"></p>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,11 @@
const Component = require(`vn-print/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'invoiceIn',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
}
};

View File

@ -0,0 +1,5 @@
subject: Your agricultural invoice
title: Your agricultural invoice
dear: Dear supplier
description: Attached you can find agricultural receipt generated from your last deliveries. Please return a signed and stamped copy to our administration department.
conclusion: Thanks for your attention!

View File

@ -0,0 +1,5 @@
subject: Tu factura agrícola
title: Tu factura agrícola
dear: Estimado proveedor
description: Adjunto puede encontrar recibo agrícola generado de sus últimas entregas. Por favor, devuelva una copia firmada y sellada a nuestro de departamento de administración.
conclusion: ¡Gracias por tu atención!

View File

@ -0,0 +1,5 @@
subject: Votre facture agricole
title: Votre facture agricole
dear: Cher Fournisseur
description: Vous trouverez en pièce jointe le reçu agricole généré à partir de vos dernières livraisons. Veuillez retourner une copie signée et tamponnée à notre service administratif.
conclusion: Merci pour votre attention!

View File

@ -0,0 +1,5 @@
subject: A sua fatura agrícola
title: A sua fatura agrícola
dear: Caro Fornecedor
description: Em anexo encontra-se o recibo agrícola gerado a partir das suas últimas entregas. Por favor, devolva uma cópia assinada e carimbada ao nosso departamento de administração.
conclusion: Obrigado pela atenção.

View File

@ -0,0 +1,12 @@
const Stylesheet = require(`vn-print/core/stylesheet`);
const path = require('path');
const vnPrintPath = path.resolve('print');
module.exports = new Stylesheet([
`${vnPrintPath}/common/css/spacing.css`,
`${vnPrintPath}/common/css/misc.css`,
`${vnPrintPath}/common/css/layout.css`,
`${vnPrintPath}/common/css/report.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,42 @@
h2 {
font-weight: 100;
color: #555
}
.table-title {
margin-bottom: 15px;
font-size: .8rem
}
.table-title h2 {
margin: 0 15px 0 0
}
.ticket-info {
font-size: 22px
}
#nickname h2 {
max-width: 400px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
#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,214 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<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('supplierId')}}</td>
<th>{{invoice.supplierId}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('invoiceId')}}</td>
<th>{{invoice.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{invoice.created | 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">{{invoice.name}}</h3>
<div>
{{invoice.postalAddress}}
</div>
<div>
{{invoice.postcodeCity}}
</div>
<div v-if="invoice.nif">
{{$t('fiscalId')}}: {{invoice.nif}}
</div>
<div v-if="invoice.phone">
{{$t('phone')}}: {{invoice.phone}}
</div>
</div>
</div>
</div>
</div>
<div class="vn-mt-lg" v-for="entry in entries">
<div class="table-title clearfix">
<div class="pull-left">
<h2>{{$t('invoiceId')}}</strong>
</div>
<div class="pull-left vn-mr-md">
<div class="field rectangle">
<span>{{entry.id}}</span>
</div>
</div>
<div class="pull-left">
<h2>{{$t('date')}}</h2>
</div>
<div class="pull-left">
<div class="field rectangle">
<span>{{entry.landed | date}}</span>
</div>
</div>
<span id="nickname" class="pull-right">
<div class="pull-left">
<h2>{{$t('reference')}}</h2>
</div>
<div class="pull-left">
<div class="field rectangle">
<span>{{entry.ref}}</span>
</div>
</div>
</span>
</div>
<table class="column-oriented">
<thead>
<tr>
<th width="50%">{{$t('item')}}</th>
<th class="number">{{$t('quantity')}}</th>
<th class="number">{{$t('buyingValue')}}</th>
<th class="number">{{$t('amount')}}</th>
</tr>
</thead>
<tbody v-for="buy in entry.buys" class="no-page-break">
<tr>
<td width="50%">{{buy.name}}</td>
<td class="number">{{buy.quantity}}</td>
<td class="number">{{buy.buyingValue}}</td>
<td class="number">{{buyImport(buy) | currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="description font light-gray">
<td colspan="4">
<span v-if="buy.value5">
<strong>{{buy.tag5}}</strong> {{buy.value5}}
</span>
<span v-if="buy.value6">
<strong>{{buy.tag6}}</strong> {{buy.value6}}
</span>
<span v-if="buy.value7">
<strong>{{buy.tag7}}</strong> {{buy.value7}}
</span>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="font bold">
<span class="pull-right">{{$t('subtotal')}}</span>
</td>
<td class="number">{{entrySubtotal(entry) | 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.taxableBase | currency('EUR', $i18n.locale)}}
</td>
<td>{{tax.rate | 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, 'taxableBase') | 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 -->
<!-- Observations block -->
<div class="columns vn-mt-xl">
<div class="size50 pull-left no-page-break" >
<div class="panel" >
<div class="header">{{$t('observations')}}</div>
<div class="body">
<div>{{$t('payMethod')}}</div>
<div>{{invoice.payMethod}}</div>
</div>
</div>
</div>
</div>
<!-- End of observations block -->
</div>
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:company-code="invoice.companyCode"
v-bind:left-text="$t('invoiceId')"
v-bind:center-text="invoice.name"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,94 @@
const Component = require(`vn-print/core/component`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
module.exports = {
name: 'invoiceIn',
async serverPrefetch() {
this.invoice = await this.fetchInvoice(this.id);
this.taxes = await this.fetchTaxes(this.id);
if (!this.invoice)
throw new Error('Something went wrong');
const entries = await this.fetchEntry(this.id);
const buys = await this.fetchBuy(this.id);
const map = new Map();
for (let entry of entries) {
entry.buys = [];
map.set(entry.id, entry);
}
for (let buy of buys) {
const entry = map.get(buy.entryFk);
if (entry) entry.buys.push(buy);
}
this.entries = entries;
},
computed: {
},
methods: {
fetchInvoice(id) {
return this.findOneFromDef('invoice', [id]);
},
fetchEntry(id) {
return this.rawSqlFromDef('entry', [id]);
},
fetchBuy(id) {
return this.rawSqlFromDef('buy', [id]);
},
async fetchTaxes(id) {
const taxes = await this.rawSqlFromDef(`taxes`, [id]);
return this.taxVat(taxes);
},
buyImport(buy) {
return buy.quantity * buy.buyingValue;
},
entrySubtotal(entry) {
let subTotal = 0.00;
for (let buy of entry.buys)
subTotal += this.buyImport(buy);
return subTotal;
},
sumTotal(rows, prop) {
let total = 0.00;
for (let row of rows)
total += parseFloat(row[prop]);
return total;
},
taxVat(taxes) {
for (tax of taxes) {
let vat = 0;
if (tax.rate && tax.taxableBase)
vat = (tax.rate / 100) * tax.taxableBase;
tax.vat = vat;
}
return taxes;
},
taxTotal() {
const base = this.sumTotal(this.taxes, 'taxableBase');
const vat = this.sumTotal(this.taxes, 'vat');
return base + vat;
}
},
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build(),
},
props: {
id: {
type: Number,
alexm marked this conversation as resolved Outdated
Outdated
Review

type: Number

type: Number
description: 'The invoice id'
}
}
};

View File

@ -0,0 +1,25 @@
reportName: invoice
title: Agricultural invoice
invoiceId: Agricultural invoice
supplierId: Proveedor
invoiceData: Invoice data
reference: Reference
fiscalId: FI / NIF
phone: Phone
date: Date
item: Item
quantity: Qty.
concept: Concept
buyingValue: PSP/u
discount: Disc.
vat: VAT
amount: Amount
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
total: Total
subtotal: Subtotal
taxBreakdown: Tax breakdown
observations: Observations
payMethod: Pay method

View File

@ -0,0 +1,25 @@
reportName: factura
title: Factura Agrícola
invoiceId: Factura Agrícola
supplierId: Proveedor
invoiceData: Datos de facturación
reference: Referencia
fiscalId: CIF / NIF
phone: Tlf
date: Fecha
item: Artículo
quantity: Cant.
concept: Concepto
buyingValue: PVP/u
discount: Dto.
vat: IVA
amount: Importe
type: Tipo
taxBase: Base imp.
tax: Tasa
fee: Cuota
total: Total
subtotal: Subtotal
taxBreakdown: Desglose impositivo
observations: Observaciones
payMethod: Método de pago

View File

@ -0,0 +1,18 @@
SELECT
b.id,
e.id entryFk,
it.name,
b.quantity,
b.buyingValue,
(b.quantity * b.buyingValue) total,
it.tag5,
it.value5,
it.tag6,
it.value6,
it.tag7,
it.value7
FROM entry e
JOIN invoiceIn i ON i.id = e.invoiceInFk
JOIN buy b ON b.entryFk = e.id
JOIN item it ON it.id = b.itemFk
WHERE i.id = ?

View File

@ -0,0 +1,8 @@
SELECT
e.id,
t.landed,
e.ref
FROM entry e
JOIN invoiceIn i ON i.id = e.invoiceInFk
JOIN travel t ON t.id = e.travelFk
WHERE i.id = ?

View File

@ -0,0 +1,14 @@
SELECT
i.id,
s.id supplierId,
i.created,
s.name,
s.street AS postalAddress,
s.nif,
s.phone,
p.name payMethod
FROM invoiceIn i
JOIN supplier s ON s.id = i.supplierFk
JOIN company c ON c.id = i.companyFk
JOIN payMethod p ON p.id = s.payMethodFk
WHERE i.id = ?

View File

@ -0,0 +1,8 @@
SELECT
ti.iva as name,
ti.PorcentajeIva as rate,
iit.taxableBase
FROM invoiceIn ii
JOIN invoiceInTax iit ON ii.id = iit.invoiceInFk
JOIN sage.TiposIva ti ON ti.CodigoIva = iit.taxTypeSageFk
WHERE ii.id = ?;