feat(salix): refs #5926 report(1)
gitea/salix/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Javier Segarra 2024-05-21 23:58:25 +02:00
parent 05beff468e
commit a3a46d28be
12 changed files with 582 additions and 5 deletions

View File

@ -1,7 +1,7 @@
const axios = require('axios'); const axios = require('axios');
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const isProduction = require('vn-loopback/server/boot/isProduction');
module.exports = Self => { module.exports = Self => {
Self.docuwareUpload = async({ctx, tabletFk, ids: ticketIds, myOptions, uri, docuwareOptions, dialogId}) => { Self.docuwareUpload = async({ctx, tabletFk, ids: ticketIds, myOptions, uri, docuwareOptions, dialogId}) => {
for (id of ticketIds) { for (id of ticketIds) {
@ -72,7 +72,7 @@ module.exports = Self => {
] ]
}; };
if (process.env.NODE_ENV != 'production') if (!isProduction(false))
throw new UserError('Action not allowed on the test environment'); throw new UserError('Action not allowed on the test environment');
// delete old // delete old

View File

@ -1,7 +1,7 @@
const axios = require('axios'); const axios = require('axios');
const {models} = require('vn-loopback/server/server'); const {models} = require('vn-loopback/server/server');
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const isProduction = require('vn-loopback/server/boot/isProduction');
module.exports = Self => { module.exports = Self => {
Self.docuwareUpload = async({ctx, tabletFk, ids: workerId, myOptions, uri, docuwareOptions, dialogId}) => { Self.docuwareUpload = async({ctx, tabletFk, ids: workerId, myOptions, uri, docuwareOptions, dialogId}) => {
if (!Array.isArray(workerId)) workerId = [workerId]; if (!Array.isArray(workerId)) workerId = [workerId];
@ -73,7 +73,7 @@ module.exports = Self => {
] ]
}; };
if (process.env.NODE_ENV != 'production') if (!isProduction(false))
throw new UserError('Action not allowed on the test environment'); throw new UserError('Action not allowed on the test environment');
// delete old // delete old

View File

@ -52,7 +52,7 @@ class Report extends Component {
options.headerTemplate = '\n'; options.headerTemplate = '\n';
options.footerTemplate = footer; options.footerTemplate = footer;
const buffer = await page.pdf(options); const buffer = await page.content();
resolve(buffer); resolve(buffer);
} catch (err) { } catch (err) {
reject(err); reject(err);

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,49 @@
#signature {
padding-right: 10px
}
#signature img {
-webkit-filter: brightness(0%);
filter: brightness(0%);
margin-bottom: 10px;
max-width: 150px
}
.description strong {
text-transform: uppercase;
}
h2 {
font-weight: 100;
color: #555
}
.ticket-info {
font-size: 22px
}
#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
}
.observations {
text-align: justify;
text-justify: inter-word;
}
.column-oriented {
margin-bottom: 5px;
}

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,50 @@
reportName: sign-pda
deliveryNote: Sign PDA
proforma: Proforma
withoutPrices: Sign PDA
clientId: Client
deliveryAddress: Delivery address
fiscalData: Fiscal data
saleLines: Line items
date: Date
reference: Ref.
quantity: Qty.
concept: Concept
price: PSP/u
discount: Disc.
vat: VAT
amount: Amount
total: Total
subtotal: Subtotal
vatType: VAT Type
digitalSignature: Digital signature
plantPassport: Plant passport
packages: Packages
services:
title: Services
theader:
quantity: Qty.
concept: Concept
price: PSP/u
vat: VAT
amount: Amount
tfoot:
subtotal: Subtotal
warning: Deposit packaging will be invoiced if they have not been returned after 30 days of their delivery.
packagings:
title: Buckets and packaging
theader:
reference: Reference
quantity: Quantity
concept: Concept
taxes:
title: Tax breakdown
theader:
type: Type
taxBase: Tax base
tax: Tax
fee: Fee
tfoot:
subtotal: Subtotal
total: Total
observations: Observations

View File

@ -0,0 +1,2 @@
reportName: pda
signNote: Recepción PDA

View File

@ -0,0 +1,51 @@
reportName: bon-de-livraison
deliveryNote: Bon de livraison
proforma: Proforma
withoutPrices: Bon de livraison
clientId: Client
deliveryAddress: Adresse de livraison
fiscalData: Coordonnées
saleLines: Lignes de la commande
date: Date
reference: Ref.
quantity: Quant.
concept: Concept
price: PRIX/u
discount: Remise
vat: TVA
amount: Montant
total: Total
subtotal: Total partiel
vatType: Type de TVA
digitalSignature: Signature numérique
ticket: BL {0}
plantPassport: Passeport phytosanitaire
packages: Paquets
services:
title: Service
theader:
quantity: Quantité
concept: Concept
price: PRIX/u
vat: TVA
amount: Montant
tfoot:
subtotal: Total partiel
warning: Les emballages de consigne seront facturés s'ils n'ont pas été retournés après 30 jours de leur livraison.
packagings:
title: Bacs et emballages
theader:
reference: Référence
quantity: Quantité
concept: Concept
taxes:
title: Répartition taxes
theader:
type: Type
taxBase: Base imposable
tax: Taxe
fee: Quote
tfoot:
subtotal: Total partiel
total: Total
observations: Observations

View File

@ -0,0 +1,51 @@
reportName: nota-de-entrega
deliveryNote: Nota de Entrega
proforma: Proforma
withoutPrices: Nota de Entrega
clientId: Cliente
deliveryAddress: Morada de Entrega
fiscalData: Dados Fiscais
saleLines: Linhas da encomenda
date: Data
reference: Ref.
quantity: Qtde.
concept: Conceito
price: PVP/u
discount: Dto.
vat: IVA
amount: Importe
total: Total
subtotal: Sub-total
vatType: Tipo de IVA
digitalSignature: Assinatura digital
ticket: Nota de Entrega {0}
plantPassport: Passaporte vegetal
packages: Pacotes
services:
title: Serviços
theader:
quantity: Quantidade
concept: Conceito
price: PVP/u
vat: IVA
amount: Quantia
tfoot:
subtotal: Subtotal
warning: As embalagens em depósito serão facturadas e cobradas se não são devolvidas 30 dias após a entrega.
packagings:
title: Baldes e Embalagens
theader:
reference: Referência
quantity: Quantidade
concept: Conceito
taxes:
title: Repartição de impostos
theader:
type: Cara
taxBase: Tributável
tax: Taxa
fee: Compartilhado
tfoot:
subtotal: Subtotal
total: Total
observations: Observações

View File

@ -0,0 +1,252 @@
<report-body v-bind="$props">
<template v-slot:header>
<report-header v-bind="$props" v-bind:company-code="ticket.companyCode"> </report-header>
</template>
<div class="grid-row">
<div class="grid-block">
<div class="columns">
<div class="size50">
<div class="size75 vn-mt-ml">
<h1 class="title uppercase">{{$t(deliverNoteType)}}</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(deliverNoteType)}}</td>
<th>{{ticket.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{formatDate(ticket.shipped, '%d-%m-%Y')}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('packages')}}</td>
<th>{{ticket.packages}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('deliveryAddress')}}</div>
<div class="body" v-if="address">
<h3 class="uppercase">{{address.nickname}}</h3>
<div>{{address.street}}</div>
<div>{{address.postalCode}}, {{address.city}} ({{address.province}})</div>
</div>
</div>
<div class="panel">
<div class="header">{{$t('fiscalData')}}</div>
<div class="body">
<div>{{client.socialName}}</div>
<div>{{client.street}}</div>
<div>{{client.fi}}</div>
</div>
</div>
</div>
</div>
<h2>{{$t('saleLines')}}</h2>
<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" v-if="showPrices">{{$t('price')}}</th>
<th class="centered" width="5%" v-if="showPrices">{{$t('discount')}}</th>
<th class="centered" v-if="showPrices">{{$t('vat')}}</th>
<th class="number" v-if="showPrices">{{$t('amount')}}</th>
</tr>
</thead>
<tbody v-for="sale in sales" class="no-page-break">
<tr>
<td width="5%">{{sale.itemFk}}</td>
<td class="number">{{sale.quantity}}</td>
<td width="50%">{{sale.concept}}</td>
<td class="number" v-if="showPrices">{{sale.price | currency('EUR', $i18n.locale)}}</td>
<td class="centered" width="5%" v-if="showPrices">{{(sale.discount / 100) | percentage}}</td>
<td class="centered" v-if="showPrices">{{sale.vatType}}</td>
<td class="number" v-if="showPrices">
{{sale.price * sale.quantity * (1 - sale.discount / 100) | 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 v-if="showPrices">
<tr>
<td colspan="6" class="font bold">
<span class="pull-right">{{$t('subtotal')}}</span>
</td>
<td class="number">{{getSubTotal() | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
<div class="columns vn-mb-ml">
<div class="size100 no-page-break" v-if="services.length > 0">
<h2>{{$t('services.title')}}</h2>
<table class="column-oriented">
<thead>
<tr>
<th width="5%"></th>
<th class="number">{{$t('services.theader.quantity')}}</th>
<th width="50%">{{$t('services.theader.concept')}}</th>
<th class="number">{{$t('services.theader.price')}}</th>
<th class="centered" width="5%"></th>
<th class="centered">{{$t('services.theader.vat')}}</th>
<th class="number">{{$t('services.theader.amount')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="service in services">
<td width="5%"></td>
<td class="number">{{service.quantity}}</td>
<td width="50%">{{service.description}}</td>
<td class="number">{{service.price | currency('EUR', $i18n.locale)}}</td>
<td class="centered" width="5%"></td>
<td class="centered">{{service.taxDescription}}</td>
<td class="number">{{service.total | currency('EUR', $i18n.locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="6" class="font bold">
<span class="pull-right">{{$t('services.tfoot.subtotal')}}</span>
</td>
<td class="number">{{serviceTotal | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
<span class="font gray">* {{ $t('services.warning') }}</span>
</div>
</div>
<div class="columns">
<div id="packagings" class="size100 no-page-break" v-if="packagings.length > 0">
<h2>{{$t('packagings.title')}}</h2>
<table class="column-oriented">
<thead>
<tr>
<th>{{$t('packagings.theader.reference')}}</th>
<th class="number">{{$t('packagings.theader.quantity')}}</th>
<th wihth="75%">{{$t('packagings.theader.concept')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="packaging in packagings">
<td>{{packaging.itemFk}}</td>
<td class="number">{{packaging.quantity}}</td>
<td width="85%">{{packaging.name}}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="columns vn-mt-xl" v-if="showPrices">
<div id="taxes" class="size50 pull-right no-page-break" v-if="taxes">
<table class="column-oriented">
<thead>
<tr>
<th colspan="4">{{$t('taxes.title')}}</th>
</tr>
</thead>
<thead class="light">
<tr>
<th width="45%">{{$t('taxes.theader.type')}}</th>
<th width="25%" class="number">{{$t('taxes.theader.taxBase')}}</th>
<th>{{$t('taxes.theader.tax')}}</th>
<th class="number">{{$t('taxes.theader.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.tax | currency('EUR', $i18n.locale)}}</td>
</tr>
</tbody>
<tfoot>
<tr class="font bold">
<td width="45%">{{$t('subtotal')}}</td>
<td width="20%" class="number">{{getTotalBase() | currency('EUR', $i18n.locale)}}</td>
<td></td>
<td class="number">{{getTotalTax()| currency('EUR', $i18n.locale)}}</td>
</tr>
<tr class="font bold">
<td colspan="2">{{$t('total')}}</td>
<td colspan="2" class="number">{{getTotal() | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
</div>
<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>{{getBotanical()}}</span>
</div>
<div>
<strong>B</strong>
<span>ES17462130</span>
</div>
<div>
<strong>C</strong>
<span>{{ticket.id}}</span>
</div>
<div>
<strong>D</strong>
<span>ES</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns">
<div class="size50 pull-left no-page-break">
<div id="signature" class="panel" v-if="signature && signature.id">
<div class="header">{{$t('digitalSignature')}}</div>
<div class="body centered">
<img v-bind:src="dmsPath" />
<div>{{formatDate(signature.created, '%d-%m-%Y')}}</div>
</div>
</div>
</div>
</div>
<div class="columns vn-mb-ml" v-if="hasObservations">
<div class="size100 no-page-break">
<h2>{{$t('observations')}}</h2>
<p class="observations">{{ticket.description}}</p>
</div>
</div>
</div>
</div>
<template v-slot:footer>
<report-footer id="pageFooter" v-bind:company-code="ticket.companyCode" v-bind:left-text="footerType"
v-bind:center-text="client.socialName" v-bind="$props">
</report-footer>
</template>
</report-body>

View File

@ -0,0 +1,110 @@
const config = require(`vn-print/core/config`);
const vnReport = require('../../../core/mixins/vn-report.js');
const md5 = require('md5');
const fs = require('fs-extra');
module.exports = {
name: 'delivery-note',
mixins: [vnReport],
async serverPrefetch() {
this.ticket = await this.findOneFromDef('ticket', [this.id]);
this.checkMainEntity(this.ticket);
this.client = await this.findOneFromDef('client', [this.id]);
this.sales = await this.rawSqlFromDef('sales', [this.id]);
this.address = await this.findOneFromDef(`address`, [this.id]);
this.services = await this.rawSqlFromDef('services', [this.id]);
this.taxes = await this.findOneFromDef('taxes', [this.id]);
this.packagings = await this.rawSqlFromDef('packagings', [this.id]);
this.signature = await this.findOneFromDef('signature', [this.id]);
},
data() {
return {totalBalance: 0.00};
},
computed: {
dmsPath() {
if (!this.signature) return;
const hash = md5(this.signature.id.toString()).substring(0, 3);
const file = `${config.storage.root}/${hash}/${this.signature.id}.png`;
if (!fs.existsSync(file)) return null;
const src = fs.readFileSync(file);
const base64 = Buffer.from(src, 'utf8').toString('base64');
return `data:image/png;base64, ${base64}`;
},
deliverNoteType() {
return this.type ? this.type : 'deliveryNote';
},
serviceTotal() {
let total = 0.00;
this.services.forEach(service => {
total += parseFloat(service.price) * service.quantity;
});
return total;
},
showPrices() {
return this.deliverNoteType != 'withoutPrices';
},
footerType() {
const translatedType = this.$t(this.deliverNoteType);
return `${translatedType} ${this.id}`;
},
hasObservations() {
return this.ticket.description !== null;
}
},
methods: {
getSubTotal() {
let subTotal = 0.00;
this.sales.forEach(sale => {
subTotal += sale.quantity * sale.price * (1 - sale.discount / 100);
});
return subTotal;
},
getTotalBase() {
let totalBase = 0.00;
this.taxes.forEach(tax => {
totalBase += parseFloat(tax.Base);
});
return totalBase;
},
getTotalTax() {
let totalTax = 0.00;
this.taxes.forEach(tax => {
totalTax += parseFloat(tax.tax);
});
return totalTax;
},
getTotal() {
return this.getTotalBase() + this.getTotalTax();
},
getBotanical() {
let phytosanitary = [];
this.sales.forEach(sale => {
if (sale.botanical)
phytosanitary.push(sale.botanical);
});
return phytosanitary.filter((item, index) =>
phytosanitary.indexOf(item) == index
).join(', ');
}
},
props: {
id: {
type: Number,
required: true,
description: 'The ticket id'
},
type: {
type: String,
required: false
}
}
};