mirror of
https://github.com/frappe/books.git
synced 2024-11-13 00:46:28 +00:00
Merge pull request #661 from frappe/report-print
feat: print reports to PDF
This commit is contained in:
commit
b1849928d2
@ -19,6 +19,8 @@ import * as errors from './utils/errors';
|
|||||||
import { format } from './utils/format';
|
import { format } from './utils/format';
|
||||||
import { t, T } from './utils/translation';
|
import { t, T } from './utils/translation';
|
||||||
import { ErrorLog } from './utils/types';
|
import { ErrorLog } from './utils/types';
|
||||||
|
import type { reports } from 'reports/index';
|
||||||
|
import type { Report } from 'reports/Report';
|
||||||
|
|
||||||
export class Fyo {
|
export class Fyo {
|
||||||
t = t;
|
t = t;
|
||||||
@ -234,6 +236,7 @@ export class Fyo {
|
|||||||
deviceId: '',
|
deviceId: '',
|
||||||
openCount: -1,
|
openCount: -1,
|
||||||
appFlags: {} as Record<string, boolean>,
|
appFlags: {} as Record<string, boolean>,
|
||||||
|
reports: {} as Record<keyof typeof reports, Report | undefined>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
|
import { Attachment } from 'fyo/core/types';
|
||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { HiddenMap } from 'fyo/model/types';
|
import { HiddenMap } from 'fyo/model/types';
|
||||||
|
|
||||||
export class PrintSettings extends Doc {
|
export class PrintSettings extends Doc {
|
||||||
|
logo?: Attachment;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
address?: string;
|
||||||
|
companyName?: string;
|
||||||
|
color?: string;
|
||||||
|
font?: string;
|
||||||
|
displayLogo?: boolean;
|
||||||
override hidden: HiddenMap = {};
|
override hidden: HiddenMap = {};
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,10 @@ import { BalanceSheet } from './BalanceSheet/BalanceSheet';
|
|||||||
import { GeneralLedger } from './GeneralLedger/GeneralLedger';
|
import { GeneralLedger } from './GeneralLedger/GeneralLedger';
|
||||||
import { GSTR1 } from './GoodsAndServiceTax/GSTR1';
|
import { GSTR1 } from './GoodsAndServiceTax/GSTR1';
|
||||||
import { GSTR2 } from './GoodsAndServiceTax/GSTR2';
|
import { GSTR2 } from './GoodsAndServiceTax/GSTR2';
|
||||||
import { StockLedger } from './inventory/StockLedger';
|
|
||||||
import { StockBalance } from './inventory/StockBalance';
|
|
||||||
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
|
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
|
||||||
import { Report } from './Report';
|
|
||||||
import { TrialBalance } from './TrialBalance/TrialBalance';
|
import { TrialBalance } from './TrialBalance/TrialBalance';
|
||||||
|
import { StockBalance } from './inventory/StockBalance';
|
||||||
|
import { StockLedger } from './inventory/StockLedger';
|
||||||
|
|
||||||
export const reports = {
|
export const reports = {
|
||||||
GeneralLedger,
|
GeneralLedger,
|
||||||
@ -17,4 +16,4 @@ export const reports = {
|
|||||||
GSTR2,
|
GSTR2,
|
||||||
StockLedger,
|
StockLedger,
|
||||||
StockBalance,
|
StockBalance,
|
||||||
} as Record<string, typeof Report>;
|
} as const;
|
||||||
|
@ -1,47 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex">
|
<div class="flex flex-col flex-1 bg-gray-25">
|
||||||
<div class="flex flex-col flex-1 bg-gray-25">
|
<PageHeader :border="true">
|
||||||
<PageHeader :border="true">
|
<template #left>
|
||||||
<template #left>
|
<AutoComplete
|
||||||
<AutoComplete
|
v-if="templateList.length"
|
||||||
v-if="templateList.length"
|
:df="{
|
||||||
:df="{
|
fieldtype: 'AutoComplete',
|
||||||
fieldtype: 'AutoComplete',
|
fieldname: 'templateName',
|
||||||
fieldname: 'templateName',
|
label: t`Template Name`,
|
||||||
label: t`Template Name`,
|
options: templateList.map((n) => ({ label: n, value: n })),
|
||||||
options: templateList.map((n) => ({ label: n, value: n })),
|
}"
|
||||||
}"
|
input-class="text-base py-0 h-8"
|
||||||
input-class="text-base py-0 h-8"
|
class="w-56"
|
||||||
class="w-56"
|
:border="true"
|
||||||
:border="true"
|
:value="templateName ?? ''"
|
||||||
:value="templateName ?? ''"
|
@change="onTemplateNameChange"
|
||||||
@change="onTemplateNameChange"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<DropdownWithActions :actions="actions" :title="t`More`" />
|
|
||||||
<Button class="text-xs" type="primary" @click="savePDF">
|
|
||||||
{{ t`Save as PDF` }}
|
|
||||||
</Button>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<!-- Template Display Area -->
|
|
||||||
<div class="overflow-auto custom-scroll p-4">
|
|
||||||
<!-- Display Hints -->
|
|
||||||
<div v-if="helperMessage" class="text-sm text-gray-700">
|
|
||||||
{{ helperMessage }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Template Container -->
|
|
||||||
<PrintContainer
|
|
||||||
ref="printContainer"
|
|
||||||
v-if="printProps"
|
|
||||||
:template="printProps.template"
|
|
||||||
:values="printProps.values"
|
|
||||||
:scale="scale"
|
|
||||||
:width="templateDoc?.width"
|
|
||||||
:height="templateDoc?.height"
|
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
<DropdownWithActions :actions="actions" :title="t`More`" />
|
||||||
|
<Button class="text-xs" type="primary" @click="savePDF">
|
||||||
|
{{ t`Save as PDF` }}
|
||||||
|
</Button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<!-- Template Display Area -->
|
||||||
|
<div class="overflow-auto custom-scroll p-4">
|
||||||
|
<!-- Display Hints -->
|
||||||
|
<div v-if="helperMessage" class="text-sm text-gray-700">
|
||||||
|
{{ helperMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Template Container -->
|
||||||
|
<PrintContainer
|
||||||
|
ref="printContainer"
|
||||||
|
v-if="printProps"
|
||||||
|
:template="printProps.template"
|
||||||
|
:values="printProps.values"
|
||||||
|
:scale="scale"
|
||||||
|
:width="templateDoc?.width"
|
||||||
|
:height="templateDoc?.height"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -61,6 +59,7 @@ import { PrintValues } from 'src/utils/types';
|
|||||||
import { getFormRoute, openSettings, routeTo } from 'src/utils/ui';
|
import { getFormRoute, openSettings, routeTo } from 'src/utils/ui';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import PrintContainer from '../TemplateBuilder/PrintContainer.vue';
|
import PrintContainer from '../TemplateBuilder/PrintContainer.vue';
|
||||||
|
import { showSidebar } from 'src/utils/refs';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'PrintView',
|
name: 'PrintView',
|
||||||
@ -204,11 +203,22 @@ export default defineComponent({
|
|||||||
this.values = await getPrintTemplatePropValues(this.doc as Doc);
|
this.values = await getPrintTemplatePropValues(this.doc as Doc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setScale() {
|
||||||
|
this.scale = 1;
|
||||||
|
const width = (this.templateDoc?.width ?? 21) * 37.8;
|
||||||
|
let containerWidth = window.innerWidth - 32;
|
||||||
|
if (showSidebar.value) {
|
||||||
|
containerWidth -= 12 * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scale = Math.min(containerWidth / width, 1);
|
||||||
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.doc = null;
|
this.doc = null;
|
||||||
this.values = null;
|
this.values = null;
|
||||||
this.templateList = [];
|
this.templateList = [];
|
||||||
this.templateDoc = null;
|
this.templateDoc = null;
|
||||||
|
this.scale = 1;
|
||||||
},
|
},
|
||||||
async onTemplateNameChange(value: string | null): Promise<void> {
|
async onTemplateNameChange(value: string | null): Promise<void> {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@ -225,6 +235,7 @@ export default defineComponent({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
await handleErrorWithDialog(error);
|
await handleErrorWithDialog(error);
|
||||||
}
|
}
|
||||||
|
this.setScale();
|
||||||
},
|
},
|
||||||
async setTemplateList(): Promise<void> {
|
async setTemplateList(): Promise<void> {
|
||||||
const list = (await this.fyo.db.getAllRaw(ModelNameEnum.PrintTemplate, {
|
const list = (await this.fyo.db.getAllRaw(ModelNameEnum.PrintTemplate, {
|
||||||
|
310
src/pages/PrintView/ReportPrintView.vue
Normal file
310
src/pages/PrintView/ReportPrintView.vue
Normal file
@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full h-full">
|
||||||
|
<PageHeader :title="t`Print ${title}`">
|
||||||
|
<Button class="text-xs" type="primary" @click="savePDF">
|
||||||
|
{{ t`Save as PDF` }}
|
||||||
|
</Button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="outer-container">
|
||||||
|
<!-- Report Print Display Area -->
|
||||||
|
<div
|
||||||
|
class="p-4 bg-gray-25 overflow-auto flex justify-center custom-scroll"
|
||||||
|
>
|
||||||
|
<!-- Report Print Display Container -->
|
||||||
|
<ScaledContainer
|
||||||
|
class="shadow-lg border bg-white"
|
||||||
|
ref="scaledContainer"
|
||||||
|
:scale="scale"
|
||||||
|
:width="size.width"
|
||||||
|
:height="size.height"
|
||||||
|
:show-overflow="true"
|
||||||
|
>
|
||||||
|
<div class="bg-white mx-auto">
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="font-semibold text-xl w-full flex justify-between">
|
||||||
|
<h1>
|
||||||
|
{{ `${fyo.singles.PrintSettings?.companyName}` }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Data -->
|
||||||
|
<div class="grid" :style="rowStyles">
|
||||||
|
<template v-for="(row, r) of matrix" :key="`row-${r}`">
|
||||||
|
<div
|
||||||
|
v-for="(cell, c) of row"
|
||||||
|
:key="`cell-${r}.${c}`"
|
||||||
|
:class="cellClasses(cell.idx, r)"
|
||||||
|
class="text-sm p-2"
|
||||||
|
style="min-height: 2rem"
|
||||||
|
>
|
||||||
|
{{ cell.value }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t p-2">
|
||||||
|
<p class="text-xs text-right w-full">
|
||||||
|
{{ fyo.format(new Date(), 'Datetime') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScaledContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Report Print Settings -->
|
||||||
|
<div class="border-l flex flex-col" v-if="report">
|
||||||
|
<p class="p-4 text-sm text-gray-600">
|
||||||
|
{{
|
||||||
|
[
|
||||||
|
t`Hidden values will be visible on Print on.`,
|
||||||
|
t`Report will use more than one page if required.`,
|
||||||
|
].join(' ')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<!-- Row Selection -->
|
||||||
|
<div class="p-4 border-t">
|
||||||
|
<Int
|
||||||
|
:show-label="true"
|
||||||
|
:border="true"
|
||||||
|
:df="{
|
||||||
|
label: t`Start From Row Index`,
|
||||||
|
fieldtype: 'Int',
|
||||||
|
fieldname: 'numRows',
|
||||||
|
minvalue: 1,
|
||||||
|
maxvalue: report?.reportData.length ?? 1000,
|
||||||
|
}"
|
||||||
|
:value="start"
|
||||||
|
@change="(v) => (start = v)"
|
||||||
|
/>
|
||||||
|
<Int
|
||||||
|
class="mt-4"
|
||||||
|
:show-label="true"
|
||||||
|
:border="true"
|
||||||
|
:df="{
|
||||||
|
label: t`Number of Rows`,
|
||||||
|
fieldtype: 'Int',
|
||||||
|
fieldname: 'numRows',
|
||||||
|
minvalue: 0,
|
||||||
|
maxvalue: report?.reportData.length ?? 1000,
|
||||||
|
}"
|
||||||
|
:value="limit"
|
||||||
|
@change="(v) => (limit = v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Size Selection -->
|
||||||
|
<div class="border-t p-4">
|
||||||
|
<Select
|
||||||
|
:show-label="true"
|
||||||
|
:border="true"
|
||||||
|
:df="printSizeDf"
|
||||||
|
:value="printSize"
|
||||||
|
@change="(v) => (printSize = v)"
|
||||||
|
/>
|
||||||
|
<Check
|
||||||
|
class="mt-4"
|
||||||
|
:show-label="true"
|
||||||
|
:border="true"
|
||||||
|
:df="{
|
||||||
|
label: t`Is Landscape`,
|
||||||
|
fieldname: 'isLandscape',
|
||||||
|
fieldtype: 'Check',
|
||||||
|
}"
|
||||||
|
:value="isLandscape"
|
||||||
|
@change="(v) => (isLandscape = v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pick Columns -->
|
||||||
|
<div class="border-t p-4">
|
||||||
|
<h2 class="text-sm text-gray-600">
|
||||||
|
{{ t`Pick Columns` }}
|
||||||
|
</h2>
|
||||||
|
<div class="border rounded grid grid-cols-2 mt-1">
|
||||||
|
<Check
|
||||||
|
v-for="(col, i) of report?.columns"
|
||||||
|
:show-label="true"
|
||||||
|
:key="col.fieldname"
|
||||||
|
:df="{
|
||||||
|
label: col.label,
|
||||||
|
fieldname: col.fieldname,
|
||||||
|
fieldtype: 'Check',
|
||||||
|
}"
|
||||||
|
:value="columnSelection[i]"
|
||||||
|
@change="(v) => (columnSelection[i] = v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Report } from 'reports/Report';
|
||||||
|
import { reports } from 'reports/index';
|
||||||
|
import Button from 'src/components/Button.vue';
|
||||||
|
import Check from 'src/components/Controls/Check.vue';
|
||||||
|
import Int from 'src/components/Controls/Int.vue';
|
||||||
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
|
import { getReport } from 'src/utils/misc';
|
||||||
|
import { PropType, defineComponent } from 'vue';
|
||||||
|
import ScaledContainer from '../TemplateBuilder/ScaledContainer.vue';
|
||||||
|
import { getPathAndMakePDF } from 'src/utils/printTemplates';
|
||||||
|
import { OptionField } from 'schemas/types';
|
||||||
|
import { paperSizeMap, printSizes } from 'src/utils/ui';
|
||||||
|
import Select from 'src/components/Controls/Select.vue';
|
||||||
|
import { showSidebar } from 'src/utils/refs';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
reportName: {
|
||||||
|
type: String as PropType<keyof typeof reports>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
start: 1,
|
||||||
|
limit: 0,
|
||||||
|
printSize: 'A4' as typeof printSizes[number],
|
||||||
|
isLandscape: false,
|
||||||
|
scale: 0.65,
|
||||||
|
report: null as null | Report,
|
||||||
|
columnSelection: [] as boolean[],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.report = await getReport(this.reportName);
|
||||||
|
this.limit = this.report.reportData.length;
|
||||||
|
this.columnSelection = this.report.columns.map(() => true);
|
||||||
|
this.setScale();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
size() {
|
||||||
|
this.setScale();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title(): string {
|
||||||
|
return reports[this.reportName]?.title ?? this.t`Report`;
|
||||||
|
},
|
||||||
|
printSizeDf(): OptionField {
|
||||||
|
return {
|
||||||
|
label: 'Print Size',
|
||||||
|
fieldname: 'printSize',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options: printSizes
|
||||||
|
.filter((p) => p !== 'Custom')
|
||||||
|
.map((name) => ({ value: name, label: name })),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
matrix(): { value: string; idx: number }[][] {
|
||||||
|
if (!this.report) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = this.report.columns
|
||||||
|
.map((col, idx) => ({ value: col.label, idx }))
|
||||||
|
.filter((_, i) => this.columnSelection[i]);
|
||||||
|
|
||||||
|
const matrix: { value: string; idx: number }[][] = [columns];
|
||||||
|
const start = Math.max(this.start - 1, 1);
|
||||||
|
const end = Math.min(start + this.limit, this.report.reportData.length);
|
||||||
|
for (const i in this.report.reportData.slice(start, end)) {
|
||||||
|
const row = this.report.reportData[Number(i) + start];
|
||||||
|
|
||||||
|
matrix.push([]);
|
||||||
|
for (const j in row.cells) {
|
||||||
|
if (!this.columnSelection[j]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = row.cells[j].value;
|
||||||
|
matrix.at(-1)?.push({ value, idx: Number(j) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
},
|
||||||
|
rowStyles(): Record<string, string> {
|
||||||
|
const style: Record<string, string> = {};
|
||||||
|
const numColumns = this.columnSelection.filter(Boolean).length;
|
||||||
|
style['grid-template-columns'] = `repeat(${numColumns}, minmax(0, auto))`;
|
||||||
|
return style;
|
||||||
|
},
|
||||||
|
size(): { width: number; height: number } {
|
||||||
|
const size = paperSizeMap[this.printSize];
|
||||||
|
const long = size.width > size.height ? size.width : size.height;
|
||||||
|
const short = size.width <= size.height ? size.width : size.height;
|
||||||
|
|
||||||
|
if (this.isLandscape) {
|
||||||
|
return { width: long, height: short };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { width: short, height: long };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setScale() {
|
||||||
|
const width = this.size.width * 37.2;
|
||||||
|
let containerWidth = window.innerWidth - 26 * 16;
|
||||||
|
if (showSidebar.value) {
|
||||||
|
containerWidth -= 12 * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scale = Math.min(containerWidth / width, 1);
|
||||||
|
},
|
||||||
|
async savePDF(): Promise<void> {
|
||||||
|
// @ts-ignore
|
||||||
|
const innerHTML = this.$refs.scaledContainer.$el.children[0].innerHTML;
|
||||||
|
if (typeof innerHTML !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = this.title + ' - ' + this.fyo.format(new Date(), 'Date');
|
||||||
|
await getPathAndMakePDF(
|
||||||
|
name,
|
||||||
|
innerHTML,
|
||||||
|
this.size.width,
|
||||||
|
this.size.height
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cellClasses(cIdx: number, rIdx: number): string[] {
|
||||||
|
const classes: string[] = [];
|
||||||
|
if (!this.report) {
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const col = this.report.columns[cIdx];
|
||||||
|
const isFirst = cIdx === 0;
|
||||||
|
if (col.align) {
|
||||||
|
classes.push(`text-${col.align}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rIdx === 0) {
|
||||||
|
classes.push('font-semibold');
|
||||||
|
}
|
||||||
|
|
||||||
|
classes.push('border-t');
|
||||||
|
if (!isFirst) {
|
||||||
|
classes.push('border-l');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { PageHeader, Button, Check, Int, ScaledContainer, Select },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.outer-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto var(--w-quick-edit);
|
||||||
|
@apply h-full overflow-auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -11,6 +11,14 @@
|
|||||||
>
|
>
|
||||||
{{ group.group }}
|
{{ group.group }}
|
||||||
</DropdownWithActions>
|
</DropdownWithActions>
|
||||||
|
<Button
|
||||||
|
ref="printButton"
|
||||||
|
:icon="true"
|
||||||
|
:title="t`Open Report Print View`"
|
||||||
|
@click="routeTo(`/report-print/${reportClassName}`)"
|
||||||
|
>
|
||||||
|
<feather-icon name="printer" class="w-4 h-4"></feather-icon>
|
||||||
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@ -28,7 +36,7 @@
|
|||||||
:df="field"
|
:df="field"
|
||||||
:value="report.get(field.fieldname)"
|
:value="report.get(field.fieldname)"
|
||||||
:read-only="loading"
|
:read-only="loading"
|
||||||
@change="async (value) => await report.set(field.fieldname, value)"
|
@change="async (value) => await report?.set(field.fieldname, value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -36,22 +44,33 @@
|
|||||||
<ListReport v-if="report" :report="report" class="" />
|
<ListReport v-if="report" :report="report" class="" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { computed } from '@vue/reactivity';
|
import { computed } from '@vue/reactivity';
|
||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { reports } from 'reports';
|
import { reports } from 'reports';
|
||||||
|
import { Report } from 'reports/Report';
|
||||||
|
import Button from 'src/components/Button.vue';
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||||
import PageHeader from 'src/components/PageHeader.vue';
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
import ListReport from 'src/components/Report/ListReport.vue';
|
import ListReport from 'src/components/Report/ListReport.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { shortcutsKey } from 'src/utils/injectionKeys';
|
||||||
|
import { docsPathMap, getReport } from 'src/utils/misc';
|
||||||
import { docsPathRef } from 'src/utils/refs';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
import { defineComponent } from 'vue';
|
import { ActionGroup } from 'src/utils/types';
|
||||||
|
import { routeTo } from 'src/utils/ui';
|
||||||
|
import { PropType, defineComponent, inject } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
return { shortcuts: inject(shortcutsKey) };
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
reportClassName: String,
|
reportClassName: {
|
||||||
|
type: String as PropType<keyof typeof reports>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
defaultFilters: {
|
defaultFilters: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '{}',
|
default: '{}',
|
||||||
@ -60,7 +79,7 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
report: null,
|
report: null as null | Report,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
@ -68,27 +87,40 @@ export default defineComponent({
|
|||||||
report: computed(() => this.report),
|
report: computed(() => this.report),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
FormControl,
|
||||||
|
ListReport,
|
||||||
|
DropdownWithActions,
|
||||||
|
Button,
|
||||||
|
},
|
||||||
async activated() {
|
async activated() {
|
||||||
docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
|
docsPathRef.value =
|
||||||
|
docsPathMap[this.reportClassName] ?? docsPathMap.Reports!;
|
||||||
await this.setReportData();
|
await this.setReportData();
|
||||||
|
|
||||||
const filters = JSON.parse(this.defaultFilters);
|
const filters = JSON.parse(this.defaultFilters);
|
||||||
const filterKeys = Object.keys(filters);
|
const filterKeys = Object.keys(filters);
|
||||||
for (const key of filterKeys) {
|
for (const key of filterKeys) {
|
||||||
await this.report.set(key, filters[key]);
|
await this.report?.set(key, filters[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterKeys.length) {
|
if (filterKeys.length) {
|
||||||
await this.report.updateData();
|
await this.report?.updateData();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fyo.store.isDevelopment) {
|
if (fyo.store.isDevelopment) {
|
||||||
|
// @ts-ignore
|
||||||
window.rep = this;
|
window.rep = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.shortcuts?.pmod.set(this.reportClassName, ['KeyP'], () => {
|
||||||
|
routeTo(`/report-print/${this.reportClassName}`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPathRef.value = '';
|
docsPathRef.value = '';
|
||||||
|
this.shortcuts?.delete(this.reportClassName);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
@ -104,24 +136,22 @@ export default defineComponent({
|
|||||||
acc[ac.group] ??= {
|
acc[ac.group] ??= {
|
||||||
group: ac.group,
|
group: ac.group,
|
||||||
label: ac.label ?? '',
|
label: ac.label ?? '',
|
||||||
e: ac.type,
|
type: ac.type ?? 'secondary',
|
||||||
actions: [],
|
actions: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
acc[ac.group].actions.push(ac);
|
acc[ac.group].actions.push(ac);
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {} as Record<string, ActionGroup>);
|
||||||
|
|
||||||
return Object.values(actionsMap);
|
return Object.values(actionsMap);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
routeTo,
|
||||||
async setReportData() {
|
async setReportData() {
|
||||||
const Report = reports[this.reportClassName];
|
|
||||||
|
|
||||||
if (this.report === null) {
|
if (this.report === null) {
|
||||||
this.report = new Report(fyo);
|
this.report = await getReport(this.reportClassName);
|
||||||
await this.report.initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.report.reportData.length) {
|
if (!this.report.reportData.length) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overflow-hidden" :style="outerContainerStyle">
|
<div class="overflow-hidden" :style="outerContainerStyle">
|
||||||
<div :style="innerContainerStyle">
|
<div
|
||||||
|
:style="innerContainerStyle"
|
||||||
|
:class="showOverflow ? 'overflow-auto no-scrollbar' : ''"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -21,6 +24,7 @@ export default defineComponent({
|
|||||||
height: { type: Number, default: 29.7 },
|
height: { type: Number, default: 29.7 },
|
||||||
width: { type: Number, default: 21 },
|
width: { type: Number, default: 21 },
|
||||||
scale: { type: Number, default: 0.65 },
|
scale: { type: Number, default: 0.65 },
|
||||||
|
showOverflow: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
innerContainerStyle(): Record<string, string> {
|
innerContainerStyle(): Record<string, string> {
|
||||||
|
@ -48,165 +48,10 @@ import Button from 'src/components/Button.vue';
|
|||||||
import Float from 'src/components/Controls/Float.vue';
|
import Float from 'src/components/Controls/Float.vue';
|
||||||
import Select from 'src/components/Controls/Select.vue';
|
import Select from 'src/components/Controls/Select.vue';
|
||||||
import FormHeader from 'src/components/FormHeader.vue';
|
import FormHeader from 'src/components/FormHeader.vue';
|
||||||
|
import { paperSizeMap, printSizes } from 'src/utils/ui';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
const printSizes = [
|
|
||||||
'A0',
|
|
||||||
'A1',
|
|
||||||
'A2',
|
|
||||||
'A3',
|
|
||||||
'A4',
|
|
||||||
'A5',
|
|
||||||
'A6',
|
|
||||||
'A7',
|
|
||||||
'A8',
|
|
||||||
'A9',
|
|
||||||
'B0',
|
|
||||||
'B1',
|
|
||||||
'B2',
|
|
||||||
'B3',
|
|
||||||
'B4',
|
|
||||||
'B5',
|
|
||||||
'B6',
|
|
||||||
'B7',
|
|
||||||
'B8',
|
|
||||||
'B9',
|
|
||||||
'Letter',
|
|
||||||
'Legal',
|
|
||||||
'Executive',
|
|
||||||
'C5E',
|
|
||||||
'Comm10',
|
|
||||||
'DLE',
|
|
||||||
'Folio',
|
|
||||||
'Ledger',
|
|
||||||
'Tabloid',
|
|
||||||
'Custom',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type SizeName = typeof printSizes[number];
|
type SizeName = typeof printSizes[number];
|
||||||
const paperSizeMap: Record<SizeName, { width: number; height: number }> = {
|
|
||||||
A0: {
|
|
||||||
width: 84.1,
|
|
||||||
height: 118.9,
|
|
||||||
},
|
|
||||||
A1: {
|
|
||||||
width: 59.4,
|
|
||||||
height: 84.1,
|
|
||||||
},
|
|
||||||
A2: {
|
|
||||||
width: 42,
|
|
||||||
height: 59.4,
|
|
||||||
},
|
|
||||||
A3: {
|
|
||||||
width: 29.7,
|
|
||||||
height: 42,
|
|
||||||
},
|
|
||||||
A4: {
|
|
||||||
width: 21,
|
|
||||||
height: 29.7,
|
|
||||||
},
|
|
||||||
A5: {
|
|
||||||
width: 14.8,
|
|
||||||
height: 21,
|
|
||||||
},
|
|
||||||
A6: {
|
|
||||||
width: 10.5,
|
|
||||||
height: 14.8,
|
|
||||||
},
|
|
||||||
A7: {
|
|
||||||
width: 7.4,
|
|
||||||
height: 10.5,
|
|
||||||
},
|
|
||||||
A8: {
|
|
||||||
width: 5.2,
|
|
||||||
height: 7.4,
|
|
||||||
},
|
|
||||||
A9: {
|
|
||||||
width: 3.7,
|
|
||||||
height: 5.2,
|
|
||||||
},
|
|
||||||
B0: {
|
|
||||||
width: 100,
|
|
||||||
height: 141.4,
|
|
||||||
},
|
|
||||||
B1: {
|
|
||||||
width: 70.7,
|
|
||||||
height: 100,
|
|
||||||
},
|
|
||||||
B2: {
|
|
||||||
width: 50,
|
|
||||||
height: 70.7,
|
|
||||||
},
|
|
||||||
B3: {
|
|
||||||
width: 35.3,
|
|
||||||
height: 50,
|
|
||||||
},
|
|
||||||
B4: {
|
|
||||||
width: 25,
|
|
||||||
height: 35.3,
|
|
||||||
},
|
|
||||||
B5: {
|
|
||||||
width: 17.6,
|
|
||||||
height: 25,
|
|
||||||
},
|
|
||||||
B6: {
|
|
||||||
width: 12.5,
|
|
||||||
height: 17.6,
|
|
||||||
},
|
|
||||||
B7: {
|
|
||||||
width: 8.8,
|
|
||||||
height: 12.5,
|
|
||||||
},
|
|
||||||
B8: {
|
|
||||||
width: 6.2,
|
|
||||||
height: 8.8,
|
|
||||||
},
|
|
||||||
B9: {
|
|
||||||
width: 4.4,
|
|
||||||
height: 6.2,
|
|
||||||
},
|
|
||||||
Letter: {
|
|
||||||
width: 21.59,
|
|
||||||
height: 27.94,
|
|
||||||
},
|
|
||||||
Legal: {
|
|
||||||
width: 21.59,
|
|
||||||
height: 35.56,
|
|
||||||
},
|
|
||||||
Executive: {
|
|
||||||
width: 19.05,
|
|
||||||
height: 25.4,
|
|
||||||
},
|
|
||||||
C5E: {
|
|
||||||
width: 16.3,
|
|
||||||
height: 22.9,
|
|
||||||
},
|
|
||||||
Comm10: {
|
|
||||||
width: 10.5,
|
|
||||||
height: 24.1,
|
|
||||||
},
|
|
||||||
DLE: {
|
|
||||||
width: 11,
|
|
||||||
height: 22,
|
|
||||||
},
|
|
||||||
Folio: {
|
|
||||||
width: 21,
|
|
||||||
height: 33,
|
|
||||||
},
|
|
||||||
Ledger: {
|
|
||||||
width: 43.2,
|
|
||||||
height: 27.9,
|
|
||||||
},
|
|
||||||
Tabloid: {
|
|
||||||
width: 27.9,
|
|
||||||
height: 43.2,
|
|
||||||
},
|
|
||||||
Custom: {
|
|
||||||
width: -1,
|
|
||||||
height: -1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: { doc: { type: PrintTemplate, required: true } },
|
props: { doc: { type: PrintTemplate, required: true } },
|
||||||
data() {
|
data() {
|
||||||
|
@ -5,6 +5,7 @@ import GetStarted from 'src/pages/GetStarted.vue';
|
|||||||
import ImportWizard from 'src/pages/ImportWizard.vue';
|
import ImportWizard from 'src/pages/ImportWizard.vue';
|
||||||
import ListView from 'src/pages/ListView/ListView.vue';
|
import ListView from 'src/pages/ListView/ListView.vue';
|
||||||
import PrintView from 'src/pages/PrintView/PrintView.vue';
|
import PrintView from 'src/pages/PrintView/PrintView.vue';
|
||||||
|
import ReportPrintView from 'src/pages/PrintView/ReportPrintView.vue';
|
||||||
import QuickEditForm from 'src/pages/QuickEditForm.vue';
|
import QuickEditForm from 'src/pages/QuickEditForm.vue';
|
||||||
import Report from 'src/pages/Report.vue';
|
import Report from 'src/pages/Report.vue';
|
||||||
import Settings from 'src/pages/Settings/Settings.vue';
|
import Settings from 'src/pages/Settings/Settings.vue';
|
||||||
@ -69,6 +70,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: PrintView,
|
component: PrintView,
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/report-print/:reportName',
|
||||||
|
name: 'ReportPrintView',
|
||||||
|
component: ReportPrintView,
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/report/:reportClassName',
|
path: '/report/:reportClassName',
|
||||||
name: 'Report',
|
name: 'Report',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||||
import { Doc } from 'fyo/model/doc';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
|
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
@ -9,6 +8,7 @@ import { Schema } from 'schemas/types';
|
|||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { QueryFilter } from 'utils/db/types';
|
import { QueryFilter } from 'utils/db/types';
|
||||||
import { PeriodKey } from './types';
|
import { PeriodKey } from './types';
|
||||||
|
import { reports } from 'reports/index';
|
||||||
|
|
||||||
export function getDatesAndPeriodList(period: PeriodKey): {
|
export function getDatesAndPeriodList(period: PeriodKey): {
|
||||||
periodList: DateTime[];
|
periodList: DateTime[];
|
||||||
@ -167,3 +167,15 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
|
|||||||
export function getIsMac() {
|
export function getIsMac() {
|
||||||
return navigator.userAgent.indexOf('Mac') !== -1;
|
return navigator.userAgent.indexOf('Mac') !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getReport(name: keyof typeof reports) {
|
||||||
|
const cachedReport = fyo.store.reports[name];
|
||||||
|
if (cachedReport) {
|
||||||
|
return cachedReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = new reports[name](fyo);
|
||||||
|
await report.initialize();
|
||||||
|
fyo.store.reports[name] = report;
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
@ -157,7 +157,8 @@ function getCreateList(fyo: Fyo): SearchItem[] {
|
|||||||
function getReportList(fyo: Fyo): SearchItem[] {
|
function getReportList(fyo: Fyo): SearchItem[] {
|
||||||
const hasGstin = !!fyo.singles?.AccountingSettings?.gstin;
|
const hasGstin = !!fyo.singles?.AccountingSettings?.gstin;
|
||||||
const hasInventory = !!fyo.singles?.AccountingSettings?.enableInventory;
|
const hasInventory = !!fyo.singles?.AccountingSettings?.enableInventory;
|
||||||
return Object.keys(reports)
|
const reportNames = Object.keys(reports) as (keyof typeof reports)[];
|
||||||
|
return reportNames
|
||||||
.filter((r) => {
|
.filter((r) => {
|
||||||
const report = reports[r];
|
const report = reports[r];
|
||||||
if (report.isInventory && !hasInventory) {
|
if (report.isInventory && !hasInventory) {
|
||||||
|
159
src/utils/ui.ts
159
src/utils/ui.ts
@ -711,3 +711,162 @@ function getDocReferenceLabel(doc: Doc) {
|
|||||||
|
|
||||||
return doc.name || label;
|
return doc.name || label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const printSizes = [
|
||||||
|
'A0',
|
||||||
|
'A1',
|
||||||
|
'A2',
|
||||||
|
'A3',
|
||||||
|
'A4',
|
||||||
|
'A5',
|
||||||
|
'A6',
|
||||||
|
'A7',
|
||||||
|
'A8',
|
||||||
|
'A9',
|
||||||
|
'B0',
|
||||||
|
'B1',
|
||||||
|
'B2',
|
||||||
|
'B3',
|
||||||
|
'B4',
|
||||||
|
'B5',
|
||||||
|
'B6',
|
||||||
|
'B7',
|
||||||
|
'B8',
|
||||||
|
'B9',
|
||||||
|
'Letter',
|
||||||
|
'Legal',
|
||||||
|
'Executive',
|
||||||
|
'C5E',
|
||||||
|
'Comm10',
|
||||||
|
'DLE',
|
||||||
|
'Folio',
|
||||||
|
'Ledger',
|
||||||
|
'Tabloid',
|
||||||
|
'Custom',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const paperSizeMap: Record<
|
||||||
|
typeof printSizes[number],
|
||||||
|
{ width: number; height: number }
|
||||||
|
> = {
|
||||||
|
A0: {
|
||||||
|
width: 84.1,
|
||||||
|
height: 118.9,
|
||||||
|
},
|
||||||
|
A1: {
|
||||||
|
width: 59.4,
|
||||||
|
height: 84.1,
|
||||||
|
},
|
||||||
|
A2: {
|
||||||
|
width: 42,
|
||||||
|
height: 59.4,
|
||||||
|
},
|
||||||
|
A3: {
|
||||||
|
width: 29.7,
|
||||||
|
height: 42,
|
||||||
|
},
|
||||||
|
A4: {
|
||||||
|
width: 21,
|
||||||
|
height: 29.7,
|
||||||
|
},
|
||||||
|
A5: {
|
||||||
|
width: 14.8,
|
||||||
|
height: 21,
|
||||||
|
},
|
||||||
|
A6: {
|
||||||
|
width: 10.5,
|
||||||
|
height: 14.8,
|
||||||
|
},
|
||||||
|
A7: {
|
||||||
|
width: 7.4,
|
||||||
|
height: 10.5,
|
||||||
|
},
|
||||||
|
A8: {
|
||||||
|
width: 5.2,
|
||||||
|
height: 7.4,
|
||||||
|
},
|
||||||
|
A9: {
|
||||||
|
width: 3.7,
|
||||||
|
height: 5.2,
|
||||||
|
},
|
||||||
|
B0: {
|
||||||
|
width: 100,
|
||||||
|
height: 141.4,
|
||||||
|
},
|
||||||
|
B1: {
|
||||||
|
width: 70.7,
|
||||||
|
height: 100,
|
||||||
|
},
|
||||||
|
B2: {
|
||||||
|
width: 50,
|
||||||
|
height: 70.7,
|
||||||
|
},
|
||||||
|
B3: {
|
||||||
|
width: 35.3,
|
||||||
|
height: 50,
|
||||||
|
},
|
||||||
|
B4: {
|
||||||
|
width: 25,
|
||||||
|
height: 35.3,
|
||||||
|
},
|
||||||
|
B5: {
|
||||||
|
width: 17.6,
|
||||||
|
height: 25,
|
||||||
|
},
|
||||||
|
B6: {
|
||||||
|
width: 12.5,
|
||||||
|
height: 17.6,
|
||||||
|
},
|
||||||
|
B7: {
|
||||||
|
width: 8.8,
|
||||||
|
height: 12.5,
|
||||||
|
},
|
||||||
|
B8: {
|
||||||
|
width: 6.2,
|
||||||
|
height: 8.8,
|
||||||
|
},
|
||||||
|
B9: {
|
||||||
|
width: 4.4,
|
||||||
|
height: 6.2,
|
||||||
|
},
|
||||||
|
Letter: {
|
||||||
|
width: 21.59,
|
||||||
|
height: 27.94,
|
||||||
|
},
|
||||||
|
Legal: {
|
||||||
|
width: 21.59,
|
||||||
|
height: 35.56,
|
||||||
|
},
|
||||||
|
Executive: {
|
||||||
|
width: 19.05,
|
||||||
|
height: 25.4,
|
||||||
|
},
|
||||||
|
C5E: {
|
||||||
|
width: 16.3,
|
||||||
|
height: 22.9,
|
||||||
|
},
|
||||||
|
Comm10: {
|
||||||
|
width: 10.5,
|
||||||
|
height: 24.1,
|
||||||
|
},
|
||||||
|
DLE: {
|
||||||
|
width: 11,
|
||||||
|
height: 22,
|
||||||
|
},
|
||||||
|
Folio: {
|
||||||
|
width: 21,
|
||||||
|
height: 33,
|
||||||
|
},
|
||||||
|
Ledger: {
|
||||||
|
width: 43.2,
|
||||||
|
height: 27.9,
|
||||||
|
},
|
||||||
|
Tabloid: {
|
||||||
|
width: 27.9,
|
||||||
|
height: 43.2,
|
||||||
|
},
|
||||||
|
Custom: {
|
||||||
|
width: -1,
|
||||||
|
height: -1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user