mirror of
https://github.com/frappe/books.git
synced 2024-11-09 23:30:56 +00:00
incr: enable non GST report exports
This commit is contained in:
parent
496b2b77aa
commit
e20d7fc6d2
@ -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);
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -88,7 +88,7 @@ const routes: RouteRecordRaw[] = [
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/report/:reportName',
|
||||
path: '/report/:reportClassName',
|
||||
name: 'Report',
|
||||
component: Report,
|
||||
props: true,
|
||||
|
Loading…
Reference in New Issue
Block a user