2
0
mirror of https://github.com/frappe/books.git synced 2024-09-19 19:19:02 +00:00

incr: enable non GST report exports

This commit is contained in:
18alantom 2022-05-18 12:34:33 +05:30
parent 496b2b77aa
commit e20d7fc6d2
9 changed files with 187 additions and 141 deletions

View File

@ -168,6 +168,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
this._setDirty(true);
if (typeof value === 'string') {
value = value.trim();
}
if (Array.isArray(value)) {
for (const row of value) {
this.push(fieldname, row);

View File

@ -1,5 +1,4 @@
import { t } from 'fyo';
import { Action } from 'fyo/model/types';
import { cloneDeep } from 'lodash';
import { DateTime } from 'luxon';
import { AccountRootType } from 'models/baseModels/Account/types';
@ -413,10 +412,6 @@ export abstract class AccountReport extends LedgerReport {
return [columns, dateColumns].flat();
}
getActions(): Action[] {
return [];
}
metaFilters: string[] = ['basedOn'];
}

View File

@ -1,5 +1,4 @@
import { Fyo, t } from 'fyo';
import { Action } from 'fyo/model/types';
import { DateTime } from 'luxon';
import { ModelNameEnum } from 'models/types';
import { LedgerReport } from 'reports/LedgerReport';
@ -408,8 +407,4 @@ export class GeneralLedger extends LedgerReport {
return columns;
}
getActions(): Action[] {
return [];
}
}

View File

@ -1,8 +1,10 @@
import { t } from 'fyo';
import { Action } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import { Report } from 'reports/Report';
import { GroupedMap, LedgerEntry, RawLedgerEntry } from 'reports/types';
import { QueryFilter } from 'utils/db/types';
import getCommonExportActions from './commonExporter';
type GroupByKey = 'account' | 'party' | 'referenceName';
@ -95,4 +97,8 @@ export abstract class LedgerReport extends Report {
}
abstract _getQueryFilters(): Promise<QueryFilter>;
getActions(): Action[] {
return getCommonExportActions(this);
}
}

View File

@ -21,6 +21,16 @@ export abstract class Report extends Observable<RawValue> {
this.reportData = [];
}
get title() {
// @ts-ignore
return this.constructor.title;
}
get reportName() {
// @ts-ignore
return this.constructor.reportName;
}
async initialize() {
/**
* Not in constructor cause possibly async.

View File

@ -1,5 +1,4 @@
import { t } from 'fyo';
import { Action } from 'fyo/model/types';
import { ValueError } from 'fyo/utils/errors';
import { DateTime } from 'luxon';
import {
@ -284,8 +283,4 @@ export class TrialBalance extends AccountReport {
},
] as ColumnField[];
}
getActions(): Action[] {
return [];
}
}

View File

@ -1,12 +1,17 @@
import { DocValue, DocValueMap } from 'fyo/core/types';
import { Fyo, t } from 'fyo';
import { Action } from 'fyo/model/types';
import { Verb } from 'fyo/telemetry/types';
import { Field } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
import { getIsNullOrUndef } from 'utils';
import { generateCSV } from 'utils/csvParser';
import { Report } from './Report';
import { ReportCell } from './types';
type ExportExtention = 'csv' | 'json';
interface JSONExport {
columns: { fieldname: string; label: string }[];
rows: Record<string, DocValue>[];
rows: Record<string, unknown>[];
filters: Record<string, string>;
timestamp: string;
reportName: string;
@ -14,69 +19,44 @@ interface JSONExport {
softwareVersion: string;
}
type GetReportData = () => {
rows: DocValueMap[];
columns: Field[];
filters: Record<string, string>;
};
export default function getCommonExportActions(report: Report): Action[] {
const exportExtention = ['csv', 'json'] as ExportExtention[];
type TemplateObject = { template: string };
function templateToInnerText(innerHTML: string): string {
const temp = document.createElement('template');
temp.innerHTML = innerHTML.trim();
// @ts-ignore
return temp.content.firstChild!.innerText;
return exportExtention.map((ext) => ({
group: t`Export`,
label: ext.toUpperCase(),
type: 'primary',
action: async () => {
await exportReport(ext, report);
},
}));
}
function deObjectify(value: TemplateObject | DocValue) {
if (typeof value !== 'object') return value;
if (value === null) return '';
async function exportReport(extention: ExportExtention, report: Report) {
const { filePath, canceled } = await getSavePath(
report.reportName,
extention
);
const innerHTML = (value as TemplateObject).template;
if (!innerHTML) return '';
return templateToInnerText(innerHTML);
}
function csvFormat(value: TemplateObject | DocValue): string {
if (typeof value === 'string') {
return `"${value}"`;
} else if (value === null) {
return '';
} else if (typeof value === 'object') {
const innerHTML = (value as TemplateObject).template;
if (!innerHTML) return '';
return csvFormat(deObjectify(value as TemplateObject));
if (canceled || !filePath) {
return;
}
return String(value);
switch (extention) {
case 'csv':
await exportCsv(report, filePath);
break;
case 'json':
await exportJson(report, filePath);
break;
default:
return;
}
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
}
export async function exportCsv(
rows: DocValueMap[],
columns: Field[],
filePath: string
) {
const fieldnames = columns.map(({ fieldname }) => fieldname);
const labels = columns.map(({ label }) => csvFormat(label));
const csvRows = [
labels.join(','),
...rows.map((row) =>
fieldnames.map((f) => csvFormat(row[f] as DocValue)).join(',')
),
];
saveExportData(csvRows.join('\n'), filePath);
}
async function exportJson(
rows: DocValueMap[],
columns: Field[],
filePath: string,
filters: Record<string, string>,
reportName: string
) {
async function exportJson(report: Report, filePath: string) {
const exportObject: JSONExport = {
columns: [],
rows: [],
@ -86,76 +66,120 @@ async function exportJson(
softwareName: '',
softwareVersion: '',
};
const fieldnames = columns.map(({ fieldname }) => fieldname);
const columns = report.columns;
const displayPrecision =
(report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2;
/**
* Set columns as list of fieldname, label
*/
exportObject.columns = columns.map(({ fieldname, label }) => ({
fieldname,
label,
}));
exportObject.rows = rows.map((row) =>
fieldnames.reduce((acc, f) => {
const value = row[f];
if (value === undefined) {
acc[f] = '';
} else {
acc[f] = deObjectify(value as DocValue | TemplateObject);
}
/**
* Set rows as fieldname: value map
*/
for (const row of report.reportData) {
if (row.isEmpty) {
continue;
}
return acc;
}, {} as Record<string, DocValue>)
);
const rowObj: Record<string, unknown> = {};
for (const c in row.cells) {
const { label } = columns[c];
const cell = getValueFromCell(row.cells[c], displayPrecision);
rowObj[label] = cell;
}
exportObject.filters = Object.keys(filters)
.filter((name) => filters[name] !== null && filters[name] !== undefined)
.reduce((acc, name) => {
acc[name] = filters[name];
return acc;
}, {} as Record<string, string>);
exportObject.timestamp = new Date().toISOString();
exportObject.reportName = reportName;
exportObject.softwareName = 'Frappe Books';
exportObject.softwareVersion = fyo.store.appVersion;
await saveExportData(JSON.stringify(exportObject), filePath);
}
async function exportReport(
extention: string,
reportName: string,
getReportData: GetReportData
) {
const { rows, columns, filters } = getReportData();
const { filePath, canceled } = await getSavePath(reportName, extention);
if (canceled || !filePath) return;
switch (extention) {
case 'csv':
await exportCsv(rows, columns, filePath);
break;
case 'json':
await exportJson(rows, columns, filePath, filters, reportName);
break;
default:
return;
exportObject.rows.push(rowObj);
}
fyo.telemetry.log(Verb.Exported, reportName, { extention });
/**
* Set filter map
*/
for (const { fieldname } of report.filters) {
const value = report.get(fieldname);
if (getIsNullOrUndef(value)) {
continue;
}
exportObject.filters[fieldname] = String(value);
}
/**
* Metadata
*/
exportObject.timestamp = new Date().toISOString();
exportObject.reportName = report.reportName;
exportObject.softwareName = 'Frappe Books';
exportObject.softwareVersion = report.fyo.store.appVersion;
await saveExportData(JSON.stringify(exportObject), filePath, report.fyo);
}
export default function getCommonExportActions(reportName: string) {
return ['csv', 'json'].map((ext) => ({
group: fyo.t`Export`,
label: ext.toUpperCase(),
type: 'primary',
action: async (getReportData: GetReportData) =>
await exportReport(ext, reportName, getReportData),
}));
export async function exportCsv(report: Report, filePath: string) {
const csvMatrix = convertReportToCSVMatrix(report);
const csvString = generateCSV(csvMatrix);
saveExportData(csvString, filePath, report.fyo);
}
export async function saveExportData(data: string, filePath: string) {
function convertReportToCSVMatrix(report: Report): unknown[][] {
const displayPrecision =
(report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2;
const reportData = report.reportData;
const columns = report.columns!;
const csvdata: unknown[][] = [];
csvdata.push(columns.map((c) => c.label));
for (const row of reportData) {
if (row.isEmpty) {
csvdata.push(Array(row.cells.length).fill(''));
continue;
}
const csvrow: unknown[] = [];
for (const c in row.cells) {
const cell = getValueFromCell(row.cells[c], displayPrecision);
csvrow.push(cell);
}
csvdata.push(csvrow);
}
return csvdata;
}
function getValueFromCell(cell: ReportCell, displayPrecision: number) {
const rawValue = cell.rawValue;
if (rawValue instanceof Date) {
return rawValue.toISOString();
}
if (typeof rawValue === 'number') {
const value = rawValue.toFixed(displayPrecision);
/**
* remove insignificant zeroes
*/
if (value.endsWith('0'.repeat(displayPrecision))) {
return value.slice(0, -displayPrecision - 1);
}
return value;
}
if (getIsNullOrUndef(cell)) {
return '';
}
return rawValue;
}
export async function saveExportData(data: string, filePath: string, fyo: Fyo) {
await saveData(data, filePath);
showExportInFolder(fyo.t`Export Successful`, filePath);
}

View File

@ -1,19 +1,15 @@
<template>
<div class="flex flex-col w-full h-full">
<PageHeader :title="title">
<!--
<DropdownWithActions
v-for="group of actionGroups"
v-for="group of groupedActions"
:key="group.label"
:type="group.type"
:actions="group.actions"
class="text-xs"
>
{{ group.label }}
{{ group.group }}
</DropdownWithActions>
<DropdownWithActions :actions="actions" />
-->
</PageHeader>
<!-- Filters -->
@ -42,6 +38,7 @@ import { computed } from '@vue/reactivity';
import { t } from 'fyo';
import { reports } from 'reports';
import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import PageHeader from 'src/components/PageHeader.vue';
import ListReport from 'src/components/Report/ListReport.vue';
import { fyo } from 'src/initFyo';
@ -49,7 +46,7 @@ import { defineComponent } from 'vue';
export default defineComponent({
props: {
reportName: String,
reportClassName: String,
},
data() {
return {
@ -62,7 +59,7 @@ export default defineComponent({
report: computed(() => this.report),
};
},
components: { PageHeader, FormControl, ListReport },
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
async activated() {
await this.setReportData();
if (fyo.store.isDevelopment) {
@ -71,12 +68,32 @@ export default defineComponent({
},
computed: {
title() {
return reports[this.reportName]?.title ?? t`Report`;
return reports[this.reportClassName]?.title ?? t`Report`;
},
groupedActions() {
const actions = this.report?.getActions() ?? [];
const actionsMap = actions.reduce((acc, ac) => {
if (!ac.group) {
ac.group = 'none';
}
acc[ac.group] ??= {
group: ac.group,
label: ac.label ?? '',
type: ac.type,
actions: [],
};
acc[ac.group].actions.push(ac);
return acc;
}, {});
return Object.values(actionsMap);
},
},
methods: {
async setReportData() {
const Report = reports[this.reportName];
const Report = reports[this.reportClassName];
if (this.report === null) {
this.report = new Report(fyo);

View File

@ -88,7 +88,7 @@ const routes: RouteRecordRaw[] = [
props: true,
},
{
path: '/report/:reportName',
path: '/report/:reportClassName',
name: 'Report',
component: Report,
props: true,