2
0
mirror of https://github.com/frappe/books.git synced 2025-01-02 22:50:14 +00:00

feat: report to pdf

This commit is contained in:
18alantom 2023-06-13 13:37:39 +05:30
parent a4729e2a6c
commit d3d5d56f09
10 changed files with 520 additions and 167 deletions

View File

@ -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>,
}; };
} }

View File

@ -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 = {};
} }

View File

@ -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,

View 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>

View File

@ -11,7 +11,12 @@
> >
{{ group.group }} {{ group.group }}
</DropdownWithActions> </DropdownWithActions>
<Button ref="printButton" :icon="true" :title="t`Open Report Print View`"> <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> <feather-icon name="printer" class="w-4 h-4"></feather-icon>
</Button> </Button>
</PageHeader> </PageHeader>
@ -43,6 +48,7 @@
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 getGSTRExportActions from 'reports/GoodsAndServiceTax/gstExporter';
import { Report } from 'reports/Report'; import { Report } from 'reports/Report';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue'; import FormControl from 'src/components/Controls/FormControl.vue';
@ -50,9 +56,10 @@ 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 { docsPathMap, getReport } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs'; import { docsPathRef } from 'src/utils/refs';
import { ActionGroup } from 'src/utils/types'; import { ActionGroup } from 'src/utils/types';
import { routeTo } from 'src/utils/ui';
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -133,12 +140,10 @@ export default defineComponent({
}, },
}, },
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) {

View File

@ -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> {

View File

@ -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() {

View File

@ -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',

View File

@ -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;
}

View File

@ -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,
},
};