2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

Merge pull request #484 from 18alantom/exports

feat: Exports
This commit is contained in:
Alan 2022-10-17 10:13:04 -07:00 committed by GitHub
commit 465ca089d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 733 additions and 147 deletions

View File

@ -7,8 +7,8 @@ import {
} from '../../utils/translationHelpers';
import { ValueError } from './errors';
type TranslationArgs = boolean | number | string;
type TranslationLiteral = TemplateStringsArray | TranslationArgs;
export type TranslationArgs = boolean | number | string;
export type TranslationLiteral = TemplateStringsArray | TranslationArgs;
class TranslationString {
args: TranslationLiteral[];

View File

@ -1,7 +1,10 @@
<template>
<div :class="[inputClasses, containerClasses]">
<label class="flex items-center">
<div class="mr-3 text-gray-600 text-sm" v-if="showLabel && !labelRight">
<label
class="flex items-center"
:class="spaceBetween ? 'justify-between' : ''"
>
<div class="mr-3" :class="labelClasses" v-if="showLabel && !labelRight">
{{ df.label }}
</div>
<div
@ -63,7 +66,7 @@
@focus="(e) => $emit('focus', e)"
/>
</div>
<div class="ml-3 text-gray-600 text-sm" v-if="showLabel && labelRight">
<div class="ml-3" :class="labelClasses" v-if="showLabel && labelRight">
{{ df.label }}
</div>
</label>
@ -77,10 +80,15 @@ export default {
extends: Base,
emits: ['focus'],
props: {
spaceBetween: {
default: false,
type: Boolean,
},
labelRight: {
default: true,
type: Boolean,
},
labelClass: String,
},
data() {
return {
@ -90,11 +98,13 @@ export default {
};
},
computed: {
/*
inputClasses() {
return this.getInputClassesFromProp([]);
labelClasses() {
if (this.labelClass) {
return this.labelClass;
}
return 'text-gray-600 text-sm';
},
*/
checked() {
return this.value;
},

View File

@ -1,111 +1,269 @@
<template>
<div id="exportWizard" class="modal-body">
<div class="ml-4 col-6 text-left">
<input
id="select-cbox"
@change="toggleSelect"
class="form-check-input"
type="checkbox"
v-model="selectAllFlag"
>
<label class="form-check-label bold ml-2" for="select-cbox">{{ "Select/Clear All" }}</label>
<div>
<!-- Export Wizard Header -->
<FormHeader :form-title="label" :form-sub-title="t`Export Wizard`" />
<hr />
<!-- Export Config -->
<div class="grid grid-cols-3 p-4 gap-4">
<Check
v-if="configFields.useListFilters && Object.keys(listFilters).length"
:df="configFields.useListFilters"
:space-between="true"
:show-label="true"
:label-right="false"
:value="useListFilters"
:border="true"
@change="(value: boolean) => (useListFilters = value)"
/>
<Select
v-if="configFields.exportFormat"
:df="configFields.exportFormat"
:value="exportFormat"
:border="true"
@change="(value: ExportFormat) => (exportFormat = value)"
/>
<Int
v-if="configFields.limit"
:df="configFields.limit"
:value="limit"
:border="true"
@change="(value: number) => (limit = value)"
/>
</div>
<hr width="93%">
<div class="row ml-4 mb-4">
<div v-for="column in columns" :key="column.id" class="form-check mt-2 col-6">
<input :id="column.id" class="form-check-input" type="checkbox" v-model="column.checked">
<label class="form-check-label" :for="column.id">{{ column.content }}</label>
<hr />
<!-- Fields Selection -->
<div class="max-h-80 overflow-auto custom-scroll">
<!-- Main Fields -->
<div class="p-4">
<h2 class="text-sm font-semibold text-gray-800">
{{ fyo.schemaMap[schemaName]?.label ?? schemaName }}
</h2>
<div class="grid grid-cols-3 border rounded mt-1">
<Check
v-for="ef of fields"
:label-class="
ef.fieldtype === 'Table'
? 'text-sm text-gray-600 font-semibold'
: 'text-sm text-gray-600'
"
:key="ef.fieldname"
:df="getField(ef)"
:show-label="true"
:value="ef.export"
@change="(value: boolean) => setExportFieldValue(ef, value)"
/>
</div>
</div>
<div class="row footer-divider mb-3">
<div class="col-12" style="border-bottom:1px solid #e9ecef"></div>
<!-- Table Fields -->
<div class="p-4" v-for="efs of filteredTableFields" :key="efs.fieldname">
<h2 class="text-sm font-semibold text-gray-800">
{{ fyo.schemaMap[efs.target]?.label ?? schemaName }}
</h2>
<div class="grid grid-cols-3 border rounded mt-1">
<Check
v-for="ef of efs.fields"
:key="ef.fieldname"
:df="getField(ef)"
:show-label="true"
:value="ef.export"
@change="(value: boolean) => setExportFieldValue(ef, value, efs.target)"
/>
</div>
<div class="row">
<div class="col-12 text-right">
<f-button primary @click="save">{{ 'Download CSV' }}</f-button>
</div>
</div>
<!-- Export Button -->
<hr />
<div class="p-4 flex justify-between items-center">
<p class="text-sm text-gray-600">
{{ t`${numSelected} fields selected` }}
</p>
<Button type="primary" @click="exportData">{{ t`Export` }}</Button>
</div>
</div>
</template>
<script>
import FileSaver from 'file-saver'
<script lang="ts">
import { t } from 'fyo';
import { Field, FieldTypeEnum } from 'schemas/types';
import { fyo } from 'src/initFyo';
import {
getCsvExportData,
getExportFields,
getExportTableFields,
getJsonExportData
} from 'src/utils/export';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
import { ExportField, ExportFormat, ExportTableField } from 'src/utils/types';
import { QueryFilter } from 'utils/db/types';
import { defineComponent, PropType } from 'vue';
import Button from './Button.vue';
import Check from './Controls/Check.vue';
import Int from './Controls/Int.vue';
import Select from './Controls/Select.vue';
import FormHeader from './FormHeader.vue';
export default {
props: ['title', 'rows', 'columnData'],
interface ExportWizardData {
useListFilters: boolean;
exportFormat: ExportFormat;
fields: ExportField[];
limit: number | null;
tableFields: ExportTableField[];
numUnfilteredEntries: number;
}
export default defineComponent({
props: {
schemaName: { type: String, required: true },
listFilters: { type: Object as PropType<QueryFilter>, default: () => {} },
pageTitle: String,
},
data() {
const fields = fyo.schemaMap[this.schemaName]?.fields ?? [];
const exportFields = getExportFields(fields);
const exportTableFields = getExportTableFields(fields, fyo);
return {
selectAllFlag: true,
columns: this.columnData
};
limit: null,
useListFilters: true,
exportFormat: 'csv',
fields: exportFields,
tableFields: exportTableFields,
} as ExportWizardData;
},
methods: {
toggleSelect() {
this.columns = this.columns.map(column => {
getField(ef: ExportField): Field {
return {
id: column.id,
content: column.content,
checked: this.selectAllFlag
fieldtype: 'Check',
label: ef.label,
fieldname: ef.fieldname,
};
});
},
close() {
this.$modal.hide();
},
checkNoneSelected(columns) {
for (let column of columns) {
if (column.checked) return false;
getExportField(
fieldname: string,
target?: string
): ExportField | undefined {
let fields: ExportField[] | undefined;
if (!target) {
fields = this.fields;
} else {
fields = this.tableFields.find((f) => f.target === target)?.fields;
}
return true;
if (!fields) {
return undefined;
}
return fields.find((f) => f.fieldname === fieldname);
},
async save() {
if (this.checkNoneSelected(this.columns)) {
alert(
`No columns have been selected.\n` +
`Please select at least one column to perform export.`
setExportFieldValue(ef: ExportField, value: boolean, target?: string) {
const field = this.getExportField(ef.fieldname, target);
if (!field) {
return;
}
field.export = value;
},
async exportData() {
const filters = JSON.parse(
JSON.stringify(this.useListFilters ? this.listFilters : {})
);
let data: string;
if (this.exportFormat === 'json') {
data = await getJsonExportData(
this.schemaName,
this.fields,
this.tableFields,
this.limit,
filters,
fyo
);
} else {
let selectedColumnIds = this.columns.map(column => {
if (column.checked) return column.id;
});
let selectedColumns = this.columnData.filter(
column => selectedColumnIds.indexOf(column.id) != -1
data = await getCsvExportData(
this.schemaName,
this.fields,
this.tableFields,
this.limit,
filters,
fyo
);
await this.exportData(this.rows, selectedColumns);
this.$modal.hide();
}
await this.saveExportData(data);
},
async exportData(rows = this.rows, columns = this.columnData) {
let title = this.title;
let columnNames = columns.map(column => column.content).toString();
let columnIDs = columns.map(column => column.id);
let rowData = rows.map(row => columnIDs.map(id => row[id]).toString());
let csvDataArray = [columnNames, ...rowData];
let csvData = csvDataArray.join('\n');
let d = new Date();
let fileName = [
title.replace(/\s/g, '-'),
[d.getDate(), d.getMonth(), d.getFullYear()].join('-'),
`${d.getTime()}.csv`
].join('_');
var blob = new Blob([csvData], { type: 'text/plain;charset=utf-8' });
await FileSaver.saveAs(blob, fileName);
async saveExportData(data: string) {
const fileName = this.getFileName();
const { canceled, filePath } = await getSavePath(
fileName,
this.exportFormat
);
if (canceled || !filePath) {
return;
}
await saveData(data, filePath);
showExportInFolder(fyo.t`Export Successful`, filePath);
},
getFileName() {
const fileName = this.label.toLowerCase().replace(/\s/g, '-');
const dateString = new Date().toISOString().split('T')[0];
return `${fileName}_${dateString}`;
},
},
computed: {
label() {
if (this.pageTitle) {
return this.pageTitle;
}
return fyo.schemaMap?.[this.schemaName]?.label ?? '';
},
filteredTableFields() {
return this.tableFields.filter((f) => {
const ef = this.getExportField(f.fieldname);
return !!ef?.export;
});
},
numSelected() {
return (
this.filteredTableFields.reduce(
(acc, f) => f.fields.filter((f) => f.export).length + acc,
0
) +
this.fields.filter(
(f) => f.fieldtype !== FieldTypeEnum.Table && f.export
).length
);
},
configFields() {
return {
useListFilters: {
fieldtype: 'Check',
label: t`Use List Filters`,
fieldname: 'useListFilters',
},
limit: {
placeholder: 'Limit number of rows',
fieldtype: 'Int',
label: t`Limit`,
fieldname: 'limit',
},
exportFormat: {
fieldtype: 'Select',
label: t`Export Format`,
fieldname: 'exportFormat',
options: [
{ value: 'json', label: 'JSON' },
{ value: 'csv', label: 'CSV' },
],
},
};
},
},
components: { FormHeader, Check, Select, Button, Int },
});
</script>
<style scoped>
.fixed-btn-width {
width: 5vw !important;
}
.bold {
font-size: 1.1rem;
font-weight: 600;
}
#select-cbox {
width: 15px;
height: 15px;
}
#exportWizard {
overflow: hidden;
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<div
class="
px-4
text-xl
font-semibold
flex
justify-between
h-row-large
items-center
"
>
<h1>{{ formTitle }}</h1>
<p class="text-gray-600">
{{ formSubTitle }}
</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
formTitle: { type: String, default: '' },
formSubTitle: { type: String, default: '' },
},
});
</script>

View File

@ -16,7 +16,7 @@
v-if="openModal"
>
<div
class="bg-white rounded-lg shadow-2xl w-form"
class="bg-white rounded-lg shadow-2xl w-form border overflow-hidden"
v-bind="$attrs"
@click.stop
>
@ -25,14 +25,38 @@
</div>
</template>
<script>
export default {
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
openModal: {
default: false,
type: Boolean,
},
setCloseListener: {
default: true,
type: Boolean,
},
},
emits: ['closemodal'],
};
watch: {
openModal(value: boolean) {
if (value) {
document.addEventListener('keyup', this.escapeEventListener);
} else {
document.removeEventListener('keyup', this.escapeEventListener);
}
},
},
methods: {
escapeEventListener(event: KeyboardEvent) {
if (event.code !== 'Escape') {
return;
}
this.$emit('closemodal');
},
},
});
</script>

View File

@ -11,7 +11,7 @@
</div>
<!-- Search Modal -->
<Modal :open-modal="openModal" @closemodal="close">
<Modal :open-modal="openModal" @closemodal="close" :set-close-listener="false">
<!-- Search Input -->
<div class="p-1">
<input

View File

@ -9,7 +9,7 @@
items-center
mb-3
w-96
z-10
z-30
bg-white
rounded-lg
"

View File

@ -45,28 +45,14 @@
<!-- Invoice Form -->
<template #body v-if="doc">
<div
class="
px-4
text-xl
font-semibold
flex
justify-between
h-row-large
items-center
"
>
<h1>
{{ doc.notInserted ? t`New Entry` : doc.name }}
</h1>
<p class="text-gray-600">
{{
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="
doc.schemaName === 'SalesInvoice'
? t`Sales Invoice`
: t`Purchase Invoice`
}}
</p>
</div>
"
/>
<hr />
<div>
@ -298,6 +284,7 @@ import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
@ -323,6 +310,7 @@ export default {
FormContainer,
QuickEditForm,
ExchangeRate,
FormHeader,
},
provide() {
return {

View File

@ -24,24 +24,10 @@
<!-- Journal Entry Form -->
<template #body v-if="doc">
<div
class="
px-4
text-xl
font-semibold
flex
justify-between
h-row-large
items-center
"
>
<h1>
{{ doc.notInserted ? t`New Entry` : doc.name }}
</h1>
<p class="text-gray-600">
{{ t`Journal Entry` }}
</p>
</div>
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="t`Journal Entry`"
/>
<hr />
<div>
<div class="m-4 grid grid-cols-3 gap-y-4 gap-x-4">
@ -148,6 +134,7 @@ import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
@ -169,6 +156,7 @@ export default {
FormControl,
Table,
FormContainer,
FormHeader
},
provide() {
return {

View File

@ -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);
},
},
});

View File

@ -1,6 +1,9 @@
<template>
<div class="flex flex-col">
<PageHeader :title="title">
<Button :icon="false" @click="openExportModal = true">
{{ t`Export` }}
</Button>
<FilterDropdown
ref="filterDropdown"
@change="applyFilter"
@ -22,13 +25,23 @@
:listConfig="listConfig"
:filters="filters"
class="flex-1 flex h-full"
@updatedData="updatedData"
@makeNewDoc="makeNewDoc"
/>
<Modal :open-modal="openExportModal" @closemodal="openExportModal = false">
<ExportWizard
:schema-name="schemaName"
:title="pageTitle"
:list-filters="listFilters"
/>
</Modal>
</div>
</template>
<script>
import Button from 'src/components/Button.vue';
import ExportWizard from 'src/components/ExportWizard.vue';
import FilterDropdown from 'src/components/FilterDropdown.vue';
import Modal from 'src/components/Modal.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
@ -47,9 +60,15 @@ export default {
List,
Button,
FilterDropdown,
Modal,
ExportWizard,
},
data() {
return { listConfig: undefined };
return {
listConfig: undefined,
openExportModal: false,
listFilters: {},
};
},
async activated() {
if (typeof this.filters === 'object') {
@ -63,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);

10
src/shims-vue-custom.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { Fyo } from 'fyo';
import { TranslationLiteral } from 'fyo/utils/translation';
declare module 'vue' {
interface ComponentCustomProperties {
t: (...args: TranslationLiteral[]) => string;
fyo: Fyo;
platform: string;
}
}

334
src/utils/export.ts Normal file
View File

@ -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<string> {
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<string> {
const { childTableData, parentData } = await getExportData(
schemaName,
fields,
tableFields,
limit,
filters,
fyo
);
/**
* parentNameMap: Record<ParentName, Record<ParentFieldName, Rows[]>>
*/
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<string, RawValueMap[]>) {
const parentNameMap: Record<string, Record<string, RawValueMap[]>> = {};
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<string, RawValueMap[]>
) {
/**
* 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<string, RawValueMap[]> = {};
// 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);
}
}
}

View File

@ -1,3 +1,5 @@
import { FieldTypeEnum } from "schemas/types";
export interface MessageDialogButton {
label: string;
action: () => Promise<unknown> | 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';

View File

@ -37,6 +37,10 @@ export function getMapFromList<T, K extends keyof T>(
list: T[],
name: K
): Record<string, T> {
/**
* Do not convert function to use copies of T
* instead of references.
*/
const acc: Record<string, T> = {};
for (const t of list) {
const key = t[name];