Merge pull request '2989 - Added client debt statement sample' (#696) from 2989-client_debt_sample into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #696
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2021-07-20 09:15:13 +00:00
commit d8f92e8b12
19 changed files with 418 additions and 25 deletions

View File

@ -0,0 +1,7 @@
ALTER TABLE `vn`.`sample` ADD COLUMN
(`datepickerEnabled` TINYINT(1) NOT NULL DEFAULT 0);
ALTER TABLE `vn`.`sample` MODIFY code VARCHAR(25) charset utf8 NOT NULL;
INSERT INTO `vn`.`sample` (code, description, isVisible, hasCompany, hasPreview, datepickerEnabled)
VALUES ('client-debt-statement', 'Extracto del cliente', 1, 0, 1, 1);

View File

@ -9,23 +9,26 @@
"properties": { "properties": {
"id": { "id": {
"id": true, "id": true,
"type": "Number", "type": "number",
"description": "Identifier" "description": "Identifier"
}, },
"code": { "code": {
"type": "String" "type": "string"
}, },
"description": { "description": {
"type": "String" "type": "string"
}, },
"isVisible": { "isVisible": {
"type": "Boolean" "type": "boolean"
}, },
"hasCompany": { "hasCompany": {
"type": "Boolean" "type": "boolean"
}, },
"hasPreview": { "hasPreview": {
"type": "Boolean" "type": "boolean"
},
"datepickerEnabled": {
"type": "boolean"
} }
}, },
"scopes": { "scopes": {

View File

@ -25,38 +25,48 @@
</vn-crud-model> </vn-crud-model>
<form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md"> <form name="form" ng-submit="$ctrl.onSubmit()" class="vn-w-md">
<vn-card class="vn-pa-lg"> <vn-card class="vn-pa-lg">
<vn-horizontal>
<vn-textfield
label="Recipient"
ng-model="$ctrl.clientSample.recipient"
info="Its only used when sample is sent">
</vn-textfield>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Reply to"
ng-model="$ctrl.clientSample.replyTo"
info="To who should the recipient reply?">
</vn-textfield>
</vn-horizontal>
<vn-horizontal> <vn-horizontal>
<vn-autocomplete <vn-autocomplete
vn-id="sampleType" vn-id="sampleType"
ng-model="$ctrl.clientSample.typeFk" ng-model="$ctrl.clientSample.typeFk"
model="ClientSample.typeFk" model="ClientSample.typeFk"
fields="['code','hasCompany', 'hasPreview']"
data="samplesVisible" data="samplesVisible"
show-field="description" show-field="description"
label="Sample"> label="Sample"
required="true">
</vn-autocomplete> </vn-autocomplete>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
label="Recipient"
ng-model="$ctrl.clientSample.recipient"
info="Its only used when sample is sent"
required="true">
</vn-textfield>
<vn-textfield
label="Reply to"
ng-model="$ctrl.clientSample.replyTo"
info="To who should the recipient reply?"
required="true">
</vn-textfield>
</vn-horizontal>
<vn-horizontal ng-if="sampleType.selection.hasCompany || sampleType.selection.datepickerEnabled">
<vn-autocomplete <vn-autocomplete
ng-model="$ctrl.companyId" ng-model="$ctrl.companyId"
model="ClientSample.companyFk" model="ClientSample.companyFk"
data="companiesData" data="companiesData"
show-field="code" show-field="code"
label="Company" label="Company"
ng-if="sampleType.selection.hasCompany"> ng-if="sampleType.selection.hasCompany"
required="true">
</vn-autocomplete> </vn-autocomplete>
<vn-date-picker
vn-one
label="From"
ng-model="$ctrl.clientSample.from"
ng-if="sampleType.selection.datepickerEnabled"
required="true">
</vn-date-picker>
</vn-horizontal> </vn-horizontal>
</vn-card> </vn-card>
<vn-button-bar> <vn-button-bar>

View File

@ -80,6 +80,12 @@ class Controller extends Section {
if (sampleType.hasCompany) if (sampleType.hasCompany)
params.companyId = this.clientSample.companyFk; params.companyId = this.clientSample.companyFk;
if (sampleType.datepickerEnabled && !this.clientSample.from)
return this.vnApp.showError(this.$t('Choose a date'));
if (sampleType.datepickerEnabled)
params.from = this.clientSample.from;
let query = `email/${sampleType.code}`; let query = `email/${sampleType.code}`;
if (isPreview) if (isPreview)
query = `email/${sampleType.code}/preview`; query = `email/${sampleType.code}/preview`;

View File

@ -1,5 +1,6 @@
Choose a sample: Selecciona una plantilla Choose a sample: Selecciona una plantilla
Choose a company: Selecciona una empresa Choose a company: Selecciona una empresa
Choose a date: Selecciona una fecha
Email cannot be blank: Debes introducir un email Email cannot be blank: Debes introducir un email
Recipient: Destinatario Recipient: Destinatario
Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla Its only used when sample is sent: Se utiliza únicamente cuando se envía la plantilla

View File

@ -89,8 +89,7 @@ const dbHelper = {
const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName); const absolutePath = path.join(__dirname, '../', this.path, 'sql', queryName);
return db.getSqlFromDef(absolutePath); return db.getSqlFromDef(absolutePath);
}, },
}, }
props: ['tplPath']
}; };
Vue.mixin(dbHelper); Vue.mixin(dbHelper);

View File

@ -0,0 +1,8 @@
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/email.css`])
.mergeStyles();

View File

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

View File

@ -0,0 +1,55 @@
<!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('description.instructions')}}</p>
</div>
</div>
<!-- Preview block -->
<div class="grid-row" v-if="isPreview">
<div class="grid-block vn-pa-ml">
<attachment v-for="attachment in attachments"
v-bind:key="attachment.filename"
v-bind:attachment="attachment"
v-bind:args="$props">
</attachment>
</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,25 @@
const Component = require(`${appPath}/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
const attachment = new Component('attachment');
const attachments = require('./attachments.json');
module.exports = {
name: 'client-debt-statement',
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build(),
'attachment': attachment.build()
},
data() {
return {attachments};
},
props: {
recipientId: {
required: true
},
from: {
required: true
}
}
};

View File

@ -0,0 +1,4 @@
subject: Extracto de tu balance
title: Extracto de tu balance
description:
instructions: Adjuntamos el extracto de tu balance.

View File

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

View File

@ -0,0 +1,3 @@
table.column-oriented {
margin-top: 50px !important
}

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Header block -->
<report-header v-bind="$props"></report-header>
<!-- Block -->
<div class="grid-row">
<div class="grid-block">
<div class="columns">
<div class="size50">
<div class="size75">
<h1 class="title uppercase">{{$t('title')}}</h1>
<table class="row-oriented">
<tbody>
<tr>
<td class="font gray uppercase">{{$t('clientId')}}</td>
<th>{{client.id}}</th>
</tr>
<tr>
<td class="font gray uppercase">{{$t('date')}}</td>
<th>{{dated}}</th>
</tr>
</tbody>
</table>
</div>
</div>
<div class="size50">
<div class="panel">
<div class="header">{{$t('clientData')}}</div>
<div class="body">
<h3 class="uppercase">{{client.socialName}}</h3>
<div>
{{client.street}}
</div>
<div>
{{client.postcode}}, {{client.city}} ({{client.province}})
</div>
<div>
{{client.country}}
</div>
</div>
</div>
</div>
</div>
<table class="column-oriented">
<thead>
<tr>
<th>{{$t('date')}}</th>
<th>{{$t('concept')}}</th>
<th class="number">{{$t('invoiced')}}</th>
<th class="number">{{$t('payed')}}</th>
<th class="number">{{$t('balance')}}</th>
</tr>
</thead>
<tbody v-for="sale in sales" :key="sale.id">
<tr>
<td>{{sale.issued | date('%d-%m-%Y')}}</td>
<td>{{sale.ref}}</td>
<td class="number">{{sale.debtOut}}</td>
<td class="number">{{sale.debtIn}}</td>
<td class="number">{{getBalance(sale)}}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td></td>
<td></td>
<td class="number">
<strong class="pull-left">Total</strong>
{{getTotalDebtOut() | currency('EUR', $i18n.locale)}}
</td>
<td class="number">{{getTotalDebtIn() | currency('EUR', $i18n.locale)}}</td>
<td class="number">{{totalBalance | currency('EUR', $i18n.locale)}}</td>
</tr>
</tfoot>
</table>
</div>
</div>
<!-- Footer block -->
<report-footer id="pageFooter"
v-bind:left-text="$t('client', [client.id])"
v-bind:center-text="client.socialName"
v-bind="$props">
</report-footer>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,78 @@
const Component = require(`${appPath}/core/component`);
const reportHeader = new Component('report-header');
const reportFooter = new Component('report-footer');
module.exports = {
name: 'client-debt-statement',
async serverPrefetch() {
this.client = await this.fetchClient(this.recipientId);
this.sales = await this.fetchSales(this.recipientId, this.from);
if (!this.client)
throw new Error('Something went wrong');
},
computed: {
dated: function() {
const filters = this.$options.filters;
return filters.date(new Date(), '%d-%m-%Y');
}
},
data() {
return {totalBalance: 0.00};
},
methods: {
fetchClient(clientId) {
return this.findOneFromDef('client', [clientId]);
},
fetchSales(clientId, from) {
return this.rawSqlFromDef('sales', [
from,
clientId,
from,
clientId,
from,
clientId,
from,
clientId,
from,
clientId
]);
},
getBalance(sale) {
if (sale.debtOut)
this.totalBalance += parseFloat(sale.debtOut);
if (sale.debtIn)
this.totalBalance -= parseFloat(sale.debtIn);
return parseFloat(this.totalBalance.toFixed(2));
},
getTotalDebtOut() {
let debtOut = 0.00;
for (let sale of this.sales)
debtOut += sale.debtOut ? parseFloat(sale.debtOut) : 0;
return debtOut.toFixed(2);
},
getTotalDebtIn() {
let debtIn = 0.00;
for (let sale of this.sales)
debtIn += sale.debtIn ? parseFloat(sale.debtIn) : 0;
return debtIn.toFixed(2);
},
},
components: {
'report-header': reportHeader.build(),
'report-footer': reportFooter.build()
},
props: {
recipientId: {
required: true
},
from: {
required: true
}
}
};

View File

@ -0,0 +1,9 @@
title: Extracto
clientId: Cliente
clientData: Datos del cliente
date: Fecha
concept: Concepto
invoiced: Facturado
payed: Pagado
balance: Saldo
client: Cliente {0}

View File

@ -0,0 +1,9 @@
title: Relevé de compte
clientId: Client
clientData: Données client
date: Date
concept: Objet
invoiced: Facturé
payed: Payé
balance: Solde
client: Client {0}

View File

@ -0,0 +1,13 @@
SELECT
c.id,
c.socialName,
c.street,
c.postcode,
c.city,
c.fi,
p.name AS province,
ct.country
FROM client c
JOIN country ct ON ct.id = c.countryFk
LEFT JOIN province p ON p.id = c.provinceFk
WHERE c.id = ?

View File

@ -0,0 +1,53 @@
SELECT
issued,
CAST(debtOut AS DECIMAL(10,2)) debtOut,
CAST(debtIn AS DECIMAL(10,2)) debtIn,
ref,
companyFk,
priority
FROM (
SELECT
? AS issued,
SUM(amountUnpaid) AS debtOut,
NULL AS debtIn,
'Saldo Anterior' AS ref,
companyFk,
0 as priority
FROM (
SELECT SUM(amount) AS amountUnpaid, companyFk, 0
FROM invoiceOut io
WHERE io.clientFk = ?
AND io.issued < ?
GROUP BY io.companyFk
UNION ALL
SELECT SUM(-1 * amountPaid), companyFk, 0
FROM receipt
WHERE clientFk = ?
AND payed < ?
GROUP BY companyFk) AS transactions
GROUP BY companyFk
UNION ALL
SELECT
issued,
amount as debtOut,
NULL AS debtIn,
ref,
companyFk,
1
FROM invoiceOut
WHERE clientFk = ?
AND issued >= ?
UNION ALL
SELECT
r.payed,
NULL as debtOut,
r.amountPaid,
r.invoiceFk,
r.companyFk,
0
FROM receipt r
WHERE r.clientFk = ?
AND r.payed >= ?) t
INNER JOIN `client` c ON c.id = ?
HAVING debtOut <> 0 OR debtIn <> 0
ORDER BY issued, priority DESC, debtIn;