mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +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);
|
this._setDirty(true);
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
for (const row of value) {
|
for (const row of value) {
|
||||||
this.push(fieldname, row);
|
this.push(fieldname, row);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { Action } from 'fyo/model/types';
|
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AccountRootType } from 'models/baseModels/Account/types';
|
import { AccountRootType } from 'models/baseModels/Account/types';
|
||||||
@ -413,10 +412,6 @@ export abstract class AccountReport extends LedgerReport {
|
|||||||
return [columns, dateColumns].flat();
|
return [columns, dateColumns].flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
getActions(): Action[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
metaFilters: string[] = ['basedOn'];
|
metaFilters: string[] = ['basedOn'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Fyo, t } from 'fyo';
|
import { Fyo, t } from 'fyo';
|
||||||
import { Action } from 'fyo/model/types';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { LedgerReport } from 'reports/LedgerReport';
|
import { LedgerReport } from 'reports/LedgerReport';
|
||||||
@ -408,8 +407,4 @@ export class GeneralLedger extends LedgerReport {
|
|||||||
|
|
||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
getActions(): Action[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
|
import { Action } from 'fyo/model/types';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Report } from 'reports/Report';
|
import { Report } from 'reports/Report';
|
||||||
import { GroupedMap, LedgerEntry, RawLedgerEntry } from 'reports/types';
|
import { GroupedMap, LedgerEntry, RawLedgerEntry } from 'reports/types';
|
||||||
import { QueryFilter } from 'utils/db/types';
|
import { QueryFilter } from 'utils/db/types';
|
||||||
|
import getCommonExportActions from './commonExporter';
|
||||||
|
|
||||||
type GroupByKey = 'account' | 'party' | 'referenceName';
|
type GroupByKey = 'account' | 'party' | 'referenceName';
|
||||||
|
|
||||||
@ -95,4 +97,8 @@ export abstract class LedgerReport extends Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract _getQueryFilters(): Promise<QueryFilter>;
|
abstract _getQueryFilters(): Promise<QueryFilter>;
|
||||||
|
|
||||||
|
getActions(): Action[] {
|
||||||
|
return getCommonExportActions(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,16 @@ export abstract class Report extends Observable<RawValue> {
|
|||||||
this.reportData = [];
|
this.reportData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.constructor.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reportName() {
|
||||||
|
// @ts-ignore
|
||||||
|
return this.constructor.reportName;
|
||||||
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
/**
|
/**
|
||||||
* Not in constructor cause possibly async.
|
* Not in constructor cause possibly async.
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { Action } from 'fyo/model/types';
|
|
||||||
import { ValueError } from 'fyo/utils/errors';
|
import { ValueError } from 'fyo/utils/errors';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import {
|
import {
|
||||||
@ -284,8 +283,4 @@ export class TrialBalance extends AccountReport {
|
|||||||
},
|
},
|
||||||
] as ColumnField[];
|
] 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 { Verb } from 'fyo/telemetry/types';
|
||||||
import { Field } from 'schemas/types';
|
|
||||||
import { fyo } from 'src/initFyo';
|
|
||||||
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
|
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 {
|
interface JSONExport {
|
||||||
columns: { fieldname: string; label: string }[];
|
columns: { fieldname: string; label: string }[];
|
||||||
rows: Record<string, DocValue>[];
|
rows: Record<string, unknown>[];
|
||||||
filters: Record<string, string>;
|
filters: Record<string, string>;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
reportName: string;
|
reportName: string;
|
||||||
@ -14,69 +19,44 @@ interface JSONExport {
|
|||||||
softwareVersion: string;
|
softwareVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetReportData = () => {
|
export default function getCommonExportActions(report: Report): Action[] {
|
||||||
rows: DocValueMap[];
|
const exportExtention = ['csv', 'json'] as ExportExtention[];
|
||||||
columns: Field[];
|
|
||||||
filters: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TemplateObject = { template: string };
|
return exportExtention.map((ext) => ({
|
||||||
|
group: t`Export`,
|
||||||
function templateToInnerText(innerHTML: string): string {
|
label: ext.toUpperCase(),
|
||||||
const temp = document.createElement('template');
|
type: 'primary',
|
||||||
temp.innerHTML = innerHTML.trim();
|
action: async () => {
|
||||||
// @ts-ignore
|
await exportReport(ext, report);
|
||||||
return temp.content.firstChild!.innerText;
|
},
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deObjectify(value: TemplateObject | DocValue) {
|
async function exportReport(extention: ExportExtention, report: Report) {
|
||||||
if (typeof value !== 'object') return value;
|
const { filePath, canceled } = await getSavePath(
|
||||||
if (value === null) return '';
|
report.reportName,
|
||||||
|
extention
|
||||||
|
);
|
||||||
|
|
||||||
const innerHTML = (value as TemplateObject).template;
|
if (canceled || !filePath) {
|
||||||
if (!innerHTML) return '';
|
return;
|
||||||
return templateToInnerText(innerHTML);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function csvFormat(value: TemplateObject | DocValue): string {
|
switch (extention) {
|
||||||
if (typeof value === 'string') {
|
case 'csv':
|
||||||
return `"${value}"`;
|
await exportCsv(report, filePath);
|
||||||
} else if (value === null) {
|
break;
|
||||||
return '';
|
case 'json':
|
||||||
} else if (typeof value === 'object') {
|
await exportJson(report, filePath);
|
||||||
const innerHTML = (value as TemplateObject).template;
|
break;
|
||||||
|
default:
|
||||||
if (!innerHTML) return '';
|
return;
|
||||||
return csvFormat(deObjectify(value as TemplateObject));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(value);
|
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function exportCsv(
|
async function exportJson(report: Report, filePath: string) {
|
||||||
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
|
|
||||||
) {
|
|
||||||
const exportObject: JSONExport = {
|
const exportObject: JSONExport = {
|
||||||
columns: [],
|
columns: [],
|
||||||
rows: [],
|
rows: [],
|
||||||
@ -86,76 +66,120 @@ async function exportJson(
|
|||||||
softwareName: '',
|
softwareName: '',
|
||||||
softwareVersion: '',
|
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 }) => ({
|
exportObject.columns = columns.map(({ fieldname, label }) => ({
|
||||||
fieldname,
|
fieldname,
|
||||||
label,
|
label,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
exportObject.rows = rows.map((row) =>
|
/**
|
||||||
fieldnames.reduce((acc, f) => {
|
* Set rows as fieldname: value map
|
||||||
const value = row[f];
|
*/
|
||||||
if (value === undefined) {
|
for (const row of report.reportData) {
|
||||||
acc[f] = '';
|
if (row.isEmpty) {
|
||||||
} else {
|
continue;
|
||||||
acc[f] = deObjectify(value as DocValue | TemplateObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
const rowObj: Record<string, unknown> = {};
|
||||||
}, {} as Record<string, DocValue>)
|
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)
|
exportObject.rows.push(rowObj);
|
||||||
.filter((name) => filters[name] !== null && filters[name] !== undefined)
|
}
|
||||||
.reduce((acc, name) => {
|
|
||||||
acc[name] = filters[name];
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, string>);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.timestamp = new Date().toISOString();
|
||||||
exportObject.reportName = reportName;
|
exportObject.reportName = report.reportName;
|
||||||
exportObject.softwareName = 'Frappe Books';
|
exportObject.softwareName = 'Frappe Books';
|
||||||
exportObject.softwareVersion = fyo.store.appVersion;
|
exportObject.softwareVersion = report.fyo.store.appVersion;
|
||||||
|
|
||||||
await saveExportData(JSON.stringify(exportObject), filePath);
|
await saveExportData(JSON.stringify(exportObject), filePath, report.fyo);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportReport(
|
export async function exportCsv(report: Report, filePath: string) {
|
||||||
extention: string,
|
const csvMatrix = convertReportToCSVMatrix(report);
|
||||||
reportName: string,
|
const csvString = generateCSV(csvMatrix);
|
||||||
getReportData: GetReportData
|
saveExportData(csvString, filePath, report.fyo);
|
||||||
) {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fyo.telemetry.log(Verb.Exported, reportName, { extention });
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function getCommonExportActions(reportName: string) {
|
const csvrow: unknown[] = [];
|
||||||
return ['csv', 'json'].map((ext) => ({
|
for (const c in row.cells) {
|
||||||
group: fyo.t`Export`,
|
const cell = getValueFromCell(row.cells[c], displayPrecision);
|
||||||
label: ext.toUpperCase(),
|
csvrow.push(cell);
|
||||||
type: 'primary',
|
|
||||||
action: async (getReportData: GetReportData) =>
|
|
||||||
await exportReport(ext, reportName, getReportData),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveExportData(data: string, filePath: string) {
|
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);
|
await saveData(data, filePath);
|
||||||
showExportInFolder(fyo.t`Export Successful`, filePath);
|
showExportInFolder(fyo.t`Export Successful`, filePath);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col w-full h-full">
|
<div class="flex flex-col w-full h-full">
|
||||||
<PageHeader :title="title">
|
<PageHeader :title="title">
|
||||||
<!--
|
|
||||||
<DropdownWithActions
|
<DropdownWithActions
|
||||||
v-for="group of actionGroups"
|
v-for="group of groupedActions"
|
||||||
:key="group.label"
|
:key="group.label"
|
||||||
:type="group.type"
|
:type="group.type"
|
||||||
:actions="group.actions"
|
:actions="group.actions"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
>
|
>
|
||||||
{{ group.label }}
|
{{ group.group }}
|
||||||
</DropdownWithActions>
|
</DropdownWithActions>
|
||||||
<DropdownWithActions :actions="actions" />
|
|
||||||
|
|
||||||
-->
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
@ -42,6 +38,7 @@ import { computed } from '@vue/reactivity';
|
|||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { reports } from 'reports';
|
import { reports } from 'reports';
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
import FormControl from 'src/components/Controls/FormControl.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';
|
||||||
@ -49,7 +46,7 @@ import { defineComponent } from 'vue';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
reportName: String,
|
reportClassName: String,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -62,7 +59,7 @@ export default defineComponent({
|
|||||||
report: computed(() => this.report),
|
report: computed(() => this.report),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: { PageHeader, FormControl, ListReport },
|
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
|
||||||
async activated() {
|
async activated() {
|
||||||
await this.setReportData();
|
await this.setReportData();
|
||||||
if (fyo.store.isDevelopment) {
|
if (fyo.store.isDevelopment) {
|
||||||
@ -71,12 +68,32 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
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: {
|
methods: {
|
||||||
async setReportData() {
|
async setReportData() {
|
||||||
const Report = reports[this.reportName];
|
const Report = reports[this.reportClassName];
|
||||||
|
|
||||||
if (this.report === null) {
|
if (this.report === null) {
|
||||||
this.report = new Report(fyo);
|
this.report = new Report(fyo);
|
||||||
|
@ -88,7 +88,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/report/:reportName',
|
path: '/report/:reportClassName',
|
||||||
name: 'Report',
|
name: 'Report',
|
||||||
component: Report,
|
component: Report,
|
||||||
props: true,
|
props: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user