diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index e5073771..9330d3ce 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -168,6 +168,10 @@ export class Doc extends Observable { } this._setDirty(true); + if (typeof value === 'string') { + value = value.trim(); + } + if (Array.isArray(value)) { for (const row of value) { this.push(fieldname, row); diff --git a/reports/AccountReport.ts b/reports/AccountReport.ts index 1084608d..5b0eae1c 100644 --- a/reports/AccountReport.ts +++ b/reports/AccountReport.ts @@ -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']; } diff --git a/reports/GeneralLedger/GeneralLedger.ts b/reports/GeneralLedger/GeneralLedger.ts index a5dd6d2b..ec169f73 100644 --- a/reports/GeneralLedger/GeneralLedger.ts +++ b/reports/GeneralLedger/GeneralLedger.ts @@ -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 []; - } } diff --git a/reports/LedgerReport.ts b/reports/LedgerReport.ts index 822f2582..95832265 100644 --- a/reports/LedgerReport.ts +++ b/reports/LedgerReport.ts @@ -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; + + getActions(): Action[] { + return getCommonExportActions(this); + } } diff --git a/reports/Report.ts b/reports/Report.ts index 60a3de6d..4f000073 100644 --- a/reports/Report.ts +++ b/reports/Report.ts @@ -21,6 +21,16 @@ export abstract class Report extends Observable { 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. diff --git a/reports/TrialBalance/TrialBalance.ts b/reports/TrialBalance/TrialBalance.ts index 9dc9ab54..03720391 100644 --- a/reports/TrialBalance/TrialBalance.ts +++ b/reports/TrialBalance/TrialBalance.ts @@ -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 []; - } } diff --git a/reports/commonExporter.ts b/reports/commonExporter.ts index 2a29f18f..17de0728 100644 --- a/reports/commonExporter.ts +++ b/reports/commonExporter.ts @@ -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[]; + rows: Record[]; filters: Record; timestamp: string; reportName: string; @@ -14,69 +19,44 @@ interface JSONExport { softwareVersion: string; } -type GetReportData = () => { - rows: DocValueMap[]; - columns: Field[]; - filters: Record; -}; +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, - 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) - ); + const rowObj: Record = {}; + 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); - - 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); } diff --git a/src/pages/Report.vue b/src/pages/Report.vue index c7e1baae..569707de 100644 --- a/src/pages/Report.vue +++ b/src/pages/Report.vue @@ -1,19 +1,15 @@