-
-
-
- {{ fyo.schemaMap[schemaName]?.label ?? schemaName }}
-
-
- setExportFieldValue(ef, value)"
- />
-
+
-
diff --git a/src/components/Toast.vue b/src/components/Toast.vue
index 83fa6613..fad5e8fc 100644
--- a/src/components/Toast.vue
+++ b/src/components/Toast.vue
@@ -9,7 +9,7 @@
items-center
mb-3
w-96
- z-10
+ z-30
bg-white
rounded-lg
"
diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue
index 8770ad19..9e2a5fc2 100644
--- a/src/pages/ListView/List.vue
+++ b/src/pages/ListView/List.vue
@@ -101,7 +101,7 @@ import ListCell from './ListCell';
export default defineComponent({
name: 'List',
props: { listConfig: Object, filters: Object, schemaName: String },
- emits: ['makeNewDoc'],
+ emits: ['makeNewDoc', 'updatedData'],
components: {
Row,
ListCell,
@@ -205,6 +205,7 @@ export default defineComponent({
orderBy,
})
).map((d) => ({ ...d, schema: fyo.schemaMap[this.schemaName] }));
+ this.$emit('updatedData', filters);
},
},
});
diff --git a/src/pages/ListView/ListView.vue b/src/pages/ListView/ListView.vue
index dac0e4a4..7f9d3496 100644
--- a/src/pages/ListView/ListView.vue
+++ b/src/pages/ListView/ListView.vue
@@ -25,10 +25,15 @@
:listConfig="listConfig"
:filters="filters"
class="flex-1 flex h-full"
+ @updatedData="updatedData"
@makeNewDoc="makeNewDoc"
/>
-
+
@@ -59,7 +64,11 @@ export default {
ExportWizard,
},
data() {
- return { listConfig: undefined, openExportModal: !false };
+ return {
+ listConfig: undefined,
+ openExportModal: false,
+ listFilters: {},
+ };
},
async activated() {
if (typeof this.filters === 'object') {
@@ -73,6 +82,9 @@ export default {
docsPath.value = '';
},
methods: {
+ updatedData(listFilters) {
+ this.listFilters = listFilters;
+ },
async makeNewDoc() {
const doc = await fyo.doc.getNewDoc(this.schemaName, this.filters ?? {});
const path = this.getFormPath(doc.name);
diff --git a/src/utils/export.ts b/src/utils/export.ts
new file mode 100644
index 00000000..46de34d0
--- /dev/null
+++ b/src/utils/export.ts
@@ -0,0 +1,334 @@
+import { Fyo } from 'fyo';
+import { RawValueMap } from 'fyo/core/types';
+import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
+import { generateCSV } from 'utils/csvParser';
+import { GetAllOptions, QueryFilter } from 'utils/db/types';
+import { getMapFromList } from 'utils/index';
+import { ExportField, ExportTableField } from './types';
+
+const excludedFieldTypes = [
+ FieldTypeEnum.AttachImage,
+ FieldTypeEnum.Attachment,
+];
+
+interface CsvHeader {
+ label: string;
+ schemaName: string;
+ fieldname: string;
+ parentFieldname?: string;
+}
+
+export function getExportFields(
+ fields: Field[],
+ exclude: string[] = []
+): ExportField[] {
+ return fields
+ .filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname))
+ .map((field) => {
+ const { fieldname, label } = field;
+ const fieldtype = field.fieldtype as FieldTypeEnum;
+ return {
+ fieldname,
+ fieldtype,
+ label,
+ export: !excludedFieldTypes.includes(fieldtype),
+ };
+ });
+}
+
+export function getExportTableFields(
+ fields: Field[],
+ fyo: Fyo
+): ExportTableField[] {
+ return fields
+ .filter((f) => f.fieldtype === FieldTypeEnum.Table)
+ .map((f) => {
+ const target = (f as TargetField).target;
+ const tableFields = fyo.schemaMap[target]?.fields ?? [];
+ const exportTableFields = getExportFields(tableFields, ['name']);
+
+ return {
+ fieldname: f.fieldname,
+ label: f.label,
+ target,
+ fields: exportTableFields,
+ };
+ })
+ .filter((f) => !!f.fields.length);
+}
+
+export async function getJsonExportData(
+ schemaName: string,
+ fields: ExportField[],
+ tableFields: ExportTableField[],
+ limit: number | null,
+ filters: QueryFilter,
+ fyo: Fyo
+): Promise
{
+ const data = await getExportData(
+ schemaName,
+ fields,
+ tableFields,
+ limit,
+ filters,
+ fyo
+ );
+ convertParentDataToJsonExport(data.parentData, data.childTableData);
+ return JSON.stringify(data.parentData);
+}
+
+export async function getCsvExportData(
+ schemaName: string,
+ fields: ExportField[],
+ tableFields: ExportTableField[],
+ limit: number | null,
+ filters: QueryFilter,
+ fyo: Fyo
+): Promise {
+ const { childTableData, parentData } = await getExportData(
+ schemaName,
+ fields,
+ tableFields,
+ limit,
+ filters,
+ fyo
+ );
+ /**
+ * parentNameMap: Record>
+ */
+ const parentNameMap = getParentNameMap(childTableData);
+ const headers = getCsvHeaders(schemaName, fields, tableFields);
+
+ const rows: RawValue[][] = [];
+ for (const parentRow of parentData) {
+ const parentName = parentRow.name as string;
+ if (!parentName) {
+ continue;
+ }
+
+ const baseRowData = headers.parent.map(
+ (f) => (parentRow[f.fieldname] as RawValue) ?? ''
+ );
+
+ const tableFieldRowMap = parentNameMap[parentName];
+ if (!tableFieldRowMap || !Object.keys(tableFieldRowMap ?? {}).length) {
+ rows.push([baseRowData, headers.child.map((_) => '')].flat());
+ continue;
+ }
+
+ for (const tableFieldName in tableFieldRowMap) {
+ const tableRows = tableFieldRowMap[tableFieldName] ?? [];
+
+ for (const tableRow of tableRows) {
+ const tableRowData = headers.child.map((f) => {
+ if (f.parentFieldname !== tableFieldName) {
+ return '';
+ }
+
+ return (tableRow[f.fieldname] as RawValue) ?? '';
+ });
+
+ rows.push([baseRowData, tableRowData].flat());
+ }
+ }
+ }
+
+ const flatHeaders = [headers.parent, headers.child].flat();
+ const labels = flatHeaders.map((f) => f.label);
+ const keys = flatHeaders.map((f) => `${f.schemaName}.${f.fieldname}`);
+
+ rows.unshift(keys);
+ rows.unshift(labels);
+
+ return generateCSV(rows);
+}
+
+function getCsvHeaders(
+ schemaName: string,
+ fields: ExportField[],
+ tableFields: ExportTableField[]
+) {
+ const headers = {
+ parent: [] as CsvHeader[],
+ child: [] as CsvHeader[],
+ };
+ for (const { label, fieldname, fieldtype, export: shouldExport } of fields) {
+ if (!shouldExport || fieldtype === FieldTypeEnum.Table) {
+ continue;
+ }
+
+ headers.parent.push({ schemaName, label, fieldname });
+ }
+
+ for (const tf of tableFields) {
+ if (!fields.find((f) => f.fieldname === tf.fieldname)?.export) {
+ continue;
+ }
+
+ for (const field of tf.fields) {
+ if (!field.export) {
+ continue;
+ }
+
+ headers.child.push({
+ schemaName: tf.target,
+ label: field.label,
+ fieldname: field.fieldname,
+ parentFieldname: tf.fieldname,
+ });
+ }
+ }
+
+ return headers;
+}
+
+function getParentNameMap(childTableData: Record) {
+ const parentNameMap: Record> = {};
+ for (const key in childTableData) {
+ for (const row of childTableData[key]) {
+ const parent = row.parent as string;
+ if (!parent) {
+ continue;
+ }
+
+ parentNameMap[parent] ??= {};
+ parentNameMap[parent][key] ??= [];
+ parentNameMap[parent][key].push(row);
+ }
+ }
+ return parentNameMap;
+}
+
+async function getExportData(
+ schemaName: string,
+ fields: ExportField[],
+ tableFields: ExportTableField[],
+ limit: number | null,
+ filters: QueryFilter,
+ fyo: Fyo
+) {
+ const parentData = await getParentData(
+ schemaName,
+ filters,
+ fields,
+ limit,
+ fyo
+ );
+ const parentNames = parentData.map((f) => f.name as string).filter(Boolean);
+ const childTableData = await getAllChildTableData(
+ tableFields,
+ fields,
+ parentNames,
+ fyo
+ );
+ return { parentData, childTableData };
+}
+
+function convertParentDataToJsonExport(
+ parentData: RawValueMap[],
+ childTableData: Record
+) {
+ /**
+ * Map from List does not create copies. Map is a
+ * map of references, hence parentData is altered.
+ */
+
+ const nameMap = getMapFromList(parentData, 'name');
+ for (const fieldname in childTableData) {
+ const data = childTableData[fieldname];
+
+ for (const row of data) {
+ const parent = row.parent as string | undefined;
+ if (!parent || !nameMap?.[parent]) {
+ continue;
+ }
+
+ nameMap[parent][fieldname] ??= [];
+
+ delete row.parent;
+ delete row.name;
+
+ (nameMap[parent][fieldname] as RawValueMap[]).push(row);
+ }
+ }
+}
+
+async function getParentData(
+ schemaName: string,
+ filters: QueryFilter,
+ fields: ExportField[],
+ limit: number | null,
+ fyo: Fyo
+) {
+ const orderBy = !!fields.find((f) => f.fieldname === 'date')
+ ? 'date'
+ : 'created';
+ const options: GetAllOptions = { filters, orderBy, order: 'desc' };
+ if (limit) {
+ options.limit = limit;
+ }
+
+ options.fields = fields
+ .filter((f) => f.export && f.fieldtype !== FieldTypeEnum.Table)
+ .map((f) => f.fieldname);
+ if (!options.fields.includes('name')) {
+ options.fields.unshift('name');
+ }
+ const data = await fyo.db.getAllRaw(schemaName, options);
+ convertRawPesaToFloat(data, fields);
+ return data;
+}
+
+async function getAllChildTableData(
+ tableFields: ExportTableField[],
+ parentFields: ExportField[],
+ parentNames: string[],
+ fyo: Fyo
+) {
+ const childTables: Record = {};
+
+ // Getting Child Row data
+ for (const tf of tableFields) {
+ const f = parentFields.find((f) => f.fieldname === tf.fieldname);
+ if (!f?.export) {
+ continue;
+ }
+
+ childTables[tf.fieldname] = await getChildTableData(tf, parentNames, fyo);
+ }
+
+ return childTables;
+}
+
+async function getChildTableData(
+ exportTableField: ExportTableField,
+ parentNames: string[],
+ fyo: Fyo
+) {
+ const exportTableFields = exportTableField.fields
+ .filter((f) => f.export && f.fieldtype !== FieldTypeEnum.Table)
+ .map((f) => f.fieldname);
+ if (!exportTableFields.includes('parent')) {
+ exportTableFields.unshift('parent');
+ }
+
+ const data = await fyo.db.getAllRaw(exportTableField.target, {
+ orderBy: 'idx',
+ fields: exportTableFields,
+ filters: { parent: ['in', parentNames] },
+ });
+ convertRawPesaToFloat(data, exportTableField.fields);
+ return data;
+}
+
+function convertRawPesaToFloat(data: RawValueMap[], fields: Field[]) {
+ const currencyFields = fields.filter(
+ (f) => f.fieldtype === FieldTypeEnum.Currency
+ );
+
+ for (const row of data) {
+ for (const { fieldname } of currencyFields) {
+ row[fieldname] = parseFloat((row[fieldname] ?? '0') as string);
+ }
+ }
+}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index d9809bb1..fb19c4de 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -1,3 +1,5 @@
+import { FieldTypeEnum } from "schemas/types";
+
export interface MessageDialogButton {
label: string;
action: () => Promise | unknown;
@@ -47,3 +49,20 @@ export interface SidebarItem {
schemaName?: string;
hidden?: () => boolean;
}
+
+
+export interface ExportField {
+ fieldname: string;
+ fieldtype: FieldTypeEnum;
+ label: string;
+ export: boolean;
+}
+
+export interface ExportTableField {
+ fieldname: string;
+ label: string;
+ target: string;
+ fields: ExportField[];
+}
+
+export type ExportFormat = 'csv' | 'json';
\ No newline at end of file
diff --git a/utils/index.ts b/utils/index.ts
index c6f55b4d..9d2dfe6b 100644
--- a/utils/index.ts
+++ b/utils/index.ts
@@ -37,6 +37,10 @@ export function getMapFromList(
list: T[],
name: K
): Record {
+ /**
+ * Do not convert function to use copies of T
+ * instead of references.
+ */
const acc: Record = {};
for (const t of list) {
const key = t[name];