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
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:
commit
d8f92e8b12
|
@ -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);
|
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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`;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
|
@ -0,0 +1,6 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"filename": "client-debt-statement.pdf",
|
||||||
|
"component": "client-debt-statement"
|
||||||
|
}
|
||||||
|
]
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
subject: Extracto de tu balance
|
||||||
|
title: Extracto de tu balance
|
||||||
|
description:
|
||||||
|
instructions: Adjuntamos el extracto de tu balance.
|
|
@ -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();
|
|
@ -0,0 +1,3 @@
|
||||||
|
table.column-oriented {
|
||||||
|
margin-top: 50px !important
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -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}
|
|
@ -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}
|
|
@ -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 = ?
|
|
@ -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;
|
Loading…
Reference in New Issue