mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
feat: start rewriting data import as the import wizard
This commit is contained in:
parent
5fd192174d
commit
b24577f9fc
@ -3,7 +3,6 @@ import { Doc } from 'fyo/model/doc';
|
|||||||
import { isPesa } from 'fyo/utils';
|
import { isPesa } from 'fyo/utils';
|
||||||
import { ValueError } from 'fyo/utils/errors';
|
import { ValueError } from 'fyo/utils/errors';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Money } from 'pesa';
|
|
||||||
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
||||||
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
|
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
|
||||||
import { DatabaseHandler } from './dbHandler';
|
import { DatabaseHandler } from './dbHandler';
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
|
import { DocValue } from 'fyo/core/types';
|
||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { Action } from 'fyo/model/types';
|
import { Action } from 'fyo/model/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { Field, OptionField, SelectOption } from 'schemas/types';
|
import { Field, FieldType, OptionField, SelectOption } from 'schemas/types';
|
||||||
import { getIsNullOrUndef, safeParseInt } from 'utils';
|
import { getIsNullOrUndef, safeParseInt } from 'utils';
|
||||||
|
|
||||||
export function slug(str: string) {
|
export function slug(str: string) {
|
||||||
@ -109,3 +110,34 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
|
|||||||
|
|
||||||
return getList(doc!);
|
return getList(doc!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEmptyValuesByFieldTypes(
|
||||||
|
fieldtype: FieldType,
|
||||||
|
fyo: Fyo
|
||||||
|
): DocValue {
|
||||||
|
switch (fieldtype) {
|
||||||
|
case 'Date':
|
||||||
|
case 'Datetime':
|
||||||
|
return new Date();
|
||||||
|
case 'Float':
|
||||||
|
case 'Int':
|
||||||
|
return 0;
|
||||||
|
case 'Currency':
|
||||||
|
return fyo.pesa(0);
|
||||||
|
case 'Check':
|
||||||
|
return false;
|
||||||
|
case 'DynamicLink':
|
||||||
|
case 'Link':
|
||||||
|
case 'Select':
|
||||||
|
case 'AutoComplete':
|
||||||
|
case 'Text':
|
||||||
|
case 'Data':
|
||||||
|
case 'Color':
|
||||||
|
return null;
|
||||||
|
case 'Table':
|
||||||
|
case 'Attachment':
|
||||||
|
case 'AttachImage':
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div :class="showMandatory ? 'show-mandatory' : ''">
|
<div :class="showMandatory ? 'show-mandatory' : ''">
|
||||||
<textarea
|
<textarea
|
||||||
ref="input"
|
ref="input"
|
||||||
rows="3"
|
:rows="rows"
|
||||||
:class="['resize-none', inputClasses, containerClasses]"
|
:class="['resize-none', inputClasses, containerClasses]"
|
||||||
:value="value"
|
:value="value"
|
||||||
:placeholder="inputPlaceholder"
|
:placeholder="inputPlaceholder"
|
||||||
@ -27,5 +27,6 @@ export default {
|
|||||||
name: 'Text',
|
name: 'Text',
|
||||||
extends: Base,
|
extends: Base,
|
||||||
emits: ['focus', 'input'],
|
emits: ['focus', 'input'],
|
||||||
|
props: { rows: { type: Number, default: 3 } },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
215
src/importer.ts
Normal file
215
src/importer.ts
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import { Fyo } from 'fyo';
|
||||||
|
import { DocValue } from 'fyo/core/types';
|
||||||
|
import { getEmptyValuesByFieldTypes } from 'fyo/utils';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
FieldTypeEnum,
|
||||||
|
RawValue,
|
||||||
|
Schema,
|
||||||
|
TargetField,
|
||||||
|
} from 'schemas/types';
|
||||||
|
import { generateCSV, parseCSV } from 'utils/csvParser';
|
||||||
|
|
||||||
|
type TemplateField = Field & {
|
||||||
|
schemaName: string;
|
||||||
|
schemaLabel: string;
|
||||||
|
fieldKey: string;
|
||||||
|
parentSchemaChildField?: TargetField;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValueMatrix = {
|
||||||
|
value: DocValue;
|
||||||
|
rawValue?: RawValue;
|
||||||
|
error?: boolean;
|
||||||
|
}[][];
|
||||||
|
|
||||||
|
const skippedFieldsTypes: FieldType[] = [
|
||||||
|
FieldTypeEnum.AttachImage,
|
||||||
|
FieldTypeEnum.Attachment,
|
||||||
|
FieldTypeEnum.Table,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool that
|
||||||
|
* - Can make bulk entries for any kind of Doc
|
||||||
|
* - Takes in unstructured CSV data, converts it into Docs
|
||||||
|
* - Saves and or Submits the converted Docs
|
||||||
|
*/
|
||||||
|
export class Importer {
|
||||||
|
schemaName: string;
|
||||||
|
fyo: Fyo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of template fields that have been assigned a column, in
|
||||||
|
* the order they have been assigned.
|
||||||
|
*/
|
||||||
|
assignedTemplateFields: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of all the template fields that can be imported.
|
||||||
|
*/
|
||||||
|
templateFieldsMap: Map<string, TemplateField>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of Fields that have been picked, i.e.
|
||||||
|
* - Fields which will be included in the template
|
||||||
|
* - Fields for which values will be provided
|
||||||
|
*/
|
||||||
|
templateFieldsPicked: Map<string, boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of Fields that have been assigned columns
|
||||||
|
*/
|
||||||
|
templateFieldsAssigned: Map<string, number>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the schema type being imported has table fields
|
||||||
|
*/
|
||||||
|
hasChildTables: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix containing the raw values which will be converted to
|
||||||
|
* doc values before importing.
|
||||||
|
*/
|
||||||
|
valueMatrix: ValueMatrix;
|
||||||
|
|
||||||
|
constructor(schemaName: string, fyo: Fyo) {
|
||||||
|
if (!fyo.schemaMap[schemaName]) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Invalid schemaName ${schemaName} found in importer`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasChildTables = false;
|
||||||
|
this.schemaName = schemaName;
|
||||||
|
this.fyo = fyo;
|
||||||
|
this.valueMatrix = [];
|
||||||
|
|
||||||
|
const templateFields = getTemplateFields(schemaName, fyo, this);
|
||||||
|
this.assignedTemplateFields = templateFields.map((f) => f.fieldKey);
|
||||||
|
this.templateFieldsMap = new Map();
|
||||||
|
this.templateFieldsPicked = new Map();
|
||||||
|
this.templateFieldsAssigned = new Map();
|
||||||
|
|
||||||
|
templateFields.forEach((f, i) => {
|
||||||
|
this.templateFieldsMap.set(f.fieldKey, f);
|
||||||
|
this.templateFieldsPicked.set(f.fieldKey, true);
|
||||||
|
this.templateFieldsAssigned.set(f.fieldKey, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFile(data: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = parseCSV(data);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// createValueMatrixFromParsedCSV() {}
|
||||||
|
|
||||||
|
addRow() {
|
||||||
|
const valueRow: ValueMatrix[number] = this.assignedTemplateFields.map(
|
||||||
|
(key) => {
|
||||||
|
const { fieldtype } = this.templateFieldsMap.get(key) ?? {};
|
||||||
|
let value = null;
|
||||||
|
if (fieldtype) {
|
||||||
|
value = getEmptyValuesByFieldTypes(fieldtype, this.fyo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.valueMatrix.push(valueRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRow(index: number) {
|
||||||
|
this.valueMatrix = this.valueMatrix.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCSVTemplate(): string {
|
||||||
|
const schemaLabels: string[] = [];
|
||||||
|
const fieldLabels: string[] = [];
|
||||||
|
const fieldKey: string[] = [];
|
||||||
|
|
||||||
|
for (const [name, picked] of this.templateFieldsPicked.entries()) {
|
||||||
|
if (!picked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = this.templateFieldsMap.get(name);
|
||||||
|
if (!field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaLabels.push(field.schemaLabel);
|
||||||
|
fieldLabels.push(field.label);
|
||||||
|
fieldKey.push(field.fieldKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateCSV([schemaLabels, fieldLabels, fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateFields(
|
||||||
|
schemaName: string,
|
||||||
|
fyo: Fyo,
|
||||||
|
importer: Importer
|
||||||
|
): TemplateField[] {
|
||||||
|
const schemas: { schema: Schema; parentSchemaChildField?: TargetField }[] = [
|
||||||
|
{ schema: fyo.schemaMap[schemaName]! },
|
||||||
|
];
|
||||||
|
const fields: TemplateField[] = [];
|
||||||
|
|
||||||
|
while (schemas.length) {
|
||||||
|
const { schema, parentSchemaChildField } = schemas.pop() ?? {};
|
||||||
|
if (!schema) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of schema.fields) {
|
||||||
|
if (field.computed || field.meta || field.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.fieldtype === FieldTypeEnum.Table) {
|
||||||
|
importer.hasChildTables = true;
|
||||||
|
schemas.push({
|
||||||
|
schema: fyo.schemaMap[field.target]!,
|
||||||
|
parentSchemaChildField: field,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skippedFieldsTypes.includes(field.fieldtype)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.readOnly && field.required) {
|
||||||
|
field.readOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.readOnly) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaName = schema.name;
|
||||||
|
const schemaLabel = schema.label;
|
||||||
|
const fieldKey = `${schema.name}.${field.fieldname}`;
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
...field,
|
||||||
|
schemaName,
|
||||||
|
schemaLabel,
|
||||||
|
fieldKey,
|
||||||
|
parentSchemaChildField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
629
src/pages/ImportWizard.vue
Normal file
629
src/pages/ImportWizard.vue
Normal file
@ -0,0 +1,629 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden w-full">
|
||||||
|
<!-- Header -->
|
||||||
|
<PageHeader :title="t`Import Wizard`">
|
||||||
|
<DropdownWithActions :actions="actions" v-if="hasImporter && !complete" />
|
||||||
|
<Button
|
||||||
|
v-if="hasImporter && !complete"
|
||||||
|
class="text-sm"
|
||||||
|
@click="saveTemplate"
|
||||||
|
>{{ t`Save Template` }}</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-if="importType && !complete"
|
||||||
|
type="primary"
|
||||||
|
class="text-sm"
|
||||||
|
@click="handlePrimaryClick"
|
||||||
|
>{{ primaryLabel }}</Button
|
||||||
|
>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<!-- Main Body of the Wizard -->
|
||||||
|
<div class="flex text-base w-full flex-col" v-if="!complete">
|
||||||
|
<!-- Select Import Type -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
flex flex-row
|
||||||
|
justify-start
|
||||||
|
items-center
|
||||||
|
w-full
|
||||||
|
gap-2
|
||||||
|
border-b
|
||||||
|
p-4
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
:df="importableDf"
|
||||||
|
input-class="bg-transparent text-gray-900 text-base"
|
||||||
|
class="w-40 bg-gray-100 rounded"
|
||||||
|
:value="importType"
|
||||||
|
size="small"
|
||||||
|
@change="setImportType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p
|
||||||
|
class="text-base ms-2"
|
||||||
|
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||||
|
>
|
||||||
|
<span v-if="fileName" class="font-normal"
|
||||||
|
>{{ t`Selected file` }}
|
||||||
|
</span>
|
||||||
|
{{ helperText }}{{ fileName ? ',' : '' }}
|
||||||
|
<span v-if="fileName" class="font-normal">
|
||||||
|
{{ t`verify the imported data and click on` }} </span
|
||||||
|
>{{ ' ' }}<span v-if="fileName">{{ t`Import Data` }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bulk Entries Grid -->
|
||||||
|
<div v-if="hasImporter">
|
||||||
|
<div class="flex justify-start">
|
||||||
|
<!-- Index Column -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
w-12
|
||||||
|
p-4
|
||||||
|
border-e
|
||||||
|
flex-shrink-0
|
||||||
|
text-gray-600
|
||||||
|
grid grid-cols-1
|
||||||
|
items-center
|
||||||
|
justify-items-center
|
||||||
|
gap-4
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div class="h-6">#</div>
|
||||||
|
<div
|
||||||
|
v-for="(_, i) of importer.valueMatrix"
|
||||||
|
:key="i"
|
||||||
|
class="flex items-center group h-6"
|
||||||
|
>
|
||||||
|
<span class="hidden group-hover:inline-block">
|
||||||
|
<feather-icon
|
||||||
|
name="x"
|
||||||
|
class="w-4 h-4 -ms-1 cursor-pointer"
|
||||||
|
:button="true"
|
||||||
|
@click="importer.removeRow(i)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="group-hover:hidden">
|
||||||
|
{{ i + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grid -->
|
||||||
|
<div
|
||||||
|
class="overflow-x-scroll gap-4 p-4 grid"
|
||||||
|
:style="`grid-template-columns: repeat(${pickedArray.length}, 10rem)`"
|
||||||
|
>
|
||||||
|
<!-- Grid Title Row Cells, Allow Column Selection -->
|
||||||
|
<AutoComplete
|
||||||
|
class="flex-shrink-0"
|
||||||
|
v-for="(_, index) in pickedArray"
|
||||||
|
size="small"
|
||||||
|
:border="true"
|
||||||
|
:key="index"
|
||||||
|
:df="gridColumnTitleDf"
|
||||||
|
:value="importer.assignedTemplateFields[index]"
|
||||||
|
@change="(v:string) => importer.assignedTemplateFields[index] = v"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Grid Value Row Cells, Allow Editing Values -->
|
||||||
|
<template v-for="(row, ridx) of importer.valueMatrix">
|
||||||
|
<template
|
||||||
|
v-for="(val, cidx) of row"
|
||||||
|
:key="`cell-${ridx}-${cidx}`"
|
||||||
|
>
|
||||||
|
<div v-if="!importer.assignedTemplateFields[cidx]">
|
||||||
|
{{ val.value }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
:df="
|
||||||
|
importer.templateFieldsMap.get(
|
||||||
|
importer.assignedTemplateFields[cidx]
|
||||||
|
)
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
:rows="1"
|
||||||
|
:border="true"
|
||||||
|
:value="val.value"
|
||||||
|
@change="(value: DocValue)=> {
|
||||||
|
importer.valueMatrix[ridx][cidx]!.value = value
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- Add Row Button -->
|
||||||
|
<button
|
||||||
|
class="
|
||||||
|
text-gray-600
|
||||||
|
hover:bg-gray-50
|
||||||
|
flex flex-row
|
||||||
|
w-full
|
||||||
|
px-4
|
||||||
|
h-row-mid
|
||||||
|
border-b
|
||||||
|
items-center
|
||||||
|
outline-none
|
||||||
|
"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
importer.addRow();
|
||||||
|
canReset = true;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="plus" class="w-4 h-4" />
|
||||||
|
<p class="ps-4">
|
||||||
|
{{ t`Add Row` }}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Complete Success -->
|
||||||
|
<div v-if="complete" class="flex justify-center h-full items-center">
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
flex flex-col
|
||||||
|
justify-center
|
||||||
|
items-center
|
||||||
|
gap-8
|
||||||
|
rounded-lg
|
||||||
|
shadow-md
|
||||||
|
p-6
|
||||||
|
"
|
||||||
|
style="width: 450px"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-semibold mt-4">{{ t`Import Success` }} 🎉</h2>
|
||||||
|
<p class="text-lg text-center">
|
||||||
|
{{ t`Successfully created the following ${names.length} entries:` }}
|
||||||
|
</p>
|
||||||
|
<div class="max-h-96 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(n, i) in names"
|
||||||
|
:key="'name-' + i"
|
||||||
|
class="grid grid-cols-2 gap-2 border-b pb-2 mb-2 pe-4 text-lg w-60"
|
||||||
|
style="grid-template-columns: 2rem auto"
|
||||||
|
>
|
||||||
|
<p class="text-end">{{ i + 1 }}.</p>
|
||||||
|
<p>
|
||||||
|
{{ n }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<Button type="secondary" class="text-sm w-32" @click="clear">{{
|
||||||
|
t`Import More`
|
||||||
|
}}</Button>
|
||||||
|
<Button type="primary" class="text-sm w-32" @click="showMe">{{
|
||||||
|
t`Show Me`
|
||||||
|
}}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!importType"
|
||||||
|
class="flex justify-center h-full w-full items-center mb-16"
|
||||||
|
>
|
||||||
|
<HowTo
|
||||||
|
link="https://youtu.be/ukHAgcnVxTQ"
|
||||||
|
class="text-gray-900 rounded-lg text-base border px-3 py-2"
|
||||||
|
>
|
||||||
|
{{ t`How to Use the Import Wizard` }}
|
||||||
|
</HowTo>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Loading
|
||||||
|
v-if="isMakingEntries"
|
||||||
|
:open="isMakingEntries"
|
||||||
|
:percent="percentLoading"
|
||||||
|
:message="messageLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { log } from 'console';
|
||||||
|
import { DocValue } from 'fyo/core/types';
|
||||||
|
import { Action as BaseAction } from 'fyo/model/types';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { FieldTypeEnum, OptionField, SelectOption } from 'schemas/types';
|
||||||
|
import Button from 'src/components/Button.vue';
|
||||||
|
import AutoComplete from 'src/components/Controls/AutoComplete.vue';
|
||||||
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
|
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||||
|
import HowTo from 'src/components/HowTo.vue';
|
||||||
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
|
import { Importer } from 'src/importer';
|
||||||
|
import { fyo } from 'src/initFyo';
|
||||||
|
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
||||||
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
import { showMessageDialog } from 'src/utils/ui';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import Loading from '../components/Loading.vue';
|
||||||
|
|
||||||
|
type Action = Pick<BaseAction, 'condition' | 'component'> & {
|
||||||
|
action: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataImportData = {
|
||||||
|
canReset: boolean;
|
||||||
|
complete: boolean;
|
||||||
|
names: string[];
|
||||||
|
file: null | { name: string; filePath: string; text: string };
|
||||||
|
nullOrImporter: null | Importer;
|
||||||
|
importType: string;
|
||||||
|
isMakingEntries: boolean;
|
||||||
|
percentLoading: number;
|
||||||
|
messageLoading: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
FormControl,
|
||||||
|
Button,
|
||||||
|
DropdownWithActions,
|
||||||
|
HowTo,
|
||||||
|
Loading,
|
||||||
|
AutoComplete,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
canReset: false,
|
||||||
|
complete: false,
|
||||||
|
names: ['Bat', 'Baseball', 'Other Shit'],
|
||||||
|
file: null,
|
||||||
|
nullOrImporter: null,
|
||||||
|
importType: '',
|
||||||
|
isMakingEntries: false,
|
||||||
|
percentLoading: 0,
|
||||||
|
messageLoading: '',
|
||||||
|
} as DataImportData;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (fyo.store.isDevelopment) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.bew = this;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasImporter(): boolean {
|
||||||
|
return !!this.nullOrImporter;
|
||||||
|
},
|
||||||
|
importer(): Importer {
|
||||||
|
if (!this.nullOrImporter) {
|
||||||
|
throw new ValidationError(this.t`Importer not set, reload tool`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.nullOrImporter as Importer;
|
||||||
|
},
|
||||||
|
importableSchemaNames(): ModelNameEnum[] {
|
||||||
|
const importables = [
|
||||||
|
ModelNameEnum.SalesInvoice,
|
||||||
|
ModelNameEnum.PurchaseInvoice,
|
||||||
|
ModelNameEnum.Payment,
|
||||||
|
ModelNameEnum.Party,
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
ModelNameEnum.JournalEntry,
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasInventory = fyo.doc.singles.AccountingSettings?.enableInventory;
|
||||||
|
if (hasInventory) {
|
||||||
|
importables.push(
|
||||||
|
ModelNameEnum.StockMovement,
|
||||||
|
ModelNameEnum.Shipment,
|
||||||
|
ModelNameEnum.PurchaseReceipt,
|
||||||
|
ModelNameEnum.Location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return importables;
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
labelIndex(): number {
|
||||||
|
return this.importer.labelIndex ?? 0;
|
||||||
|
},
|
||||||
|
requiredUnassigned(): string[] {
|
||||||
|
return this.importer.assignableLabels.filter(
|
||||||
|
(k) => this.importer.requiredMap[k] && !this.importer.assignedMap[k]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
isRequiredUnassigned(): boolean {
|
||||||
|
return this.requiredUnassigned.length > 0;
|
||||||
|
},
|
||||||
|
assignedMatrix(): string[][] {
|
||||||
|
return this.nullOrImporter?.assignedMatrix ?? [];
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
actions(): Action[] {
|
||||||
|
const actions: Action[] = [];
|
||||||
|
|
||||||
|
if (this.file) {
|
||||||
|
actions.push({
|
||||||
|
component: {
|
||||||
|
template: '<span>{{ t`Change File` }}</span>',
|
||||||
|
},
|
||||||
|
condition: () => true,
|
||||||
|
action: this.selectFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelAction = {
|
||||||
|
component: {
|
||||||
|
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
||||||
|
},
|
||||||
|
condition: () => true,
|
||||||
|
action: this.clear,
|
||||||
|
};
|
||||||
|
actions.push(cancelAction);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
},
|
||||||
|
fileName(): string {
|
||||||
|
if (!this.file) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.file.name;
|
||||||
|
},
|
||||||
|
helperText(): string {
|
||||||
|
if (!this.importType) {
|
||||||
|
return this.t`Set an Import Type`;
|
||||||
|
} else if (!this.fileName) {
|
||||||
|
return this.t`Select a file for import`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fileName;
|
||||||
|
},
|
||||||
|
primaryLabel(): string {
|
||||||
|
return this.file ? this.t`Import Data` : this.t`Select File`;
|
||||||
|
},
|
||||||
|
isSubmittable(): boolean {
|
||||||
|
const schemaName = this.importer.schemaName;
|
||||||
|
return fyo.schemaMap[schemaName]?.isSubmittable ?? false;
|
||||||
|
},
|
||||||
|
importableDf(): OptionField {
|
||||||
|
return {
|
||||||
|
fieldname: 'importType',
|
||||||
|
label: this.t`Import Type`,
|
||||||
|
fieldtype: FieldTypeEnum.AutoComplete,
|
||||||
|
options: Object.keys(this.labelSchemaNameMap).map((k) => ({
|
||||||
|
value: k,
|
||||||
|
label: k,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
labelSchemaNameMap(): Record<string, string> {
|
||||||
|
return this.importableSchemaNames
|
||||||
|
.map((i) => ({
|
||||||
|
name: i,
|
||||||
|
label: fyo.schemaMap[i]?.label ?? i,
|
||||||
|
}))
|
||||||
|
.reduce((acc, { name, label }) => {
|
||||||
|
acc[label] = name;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
},
|
||||||
|
gridColumnTitleDf(): OptionField {
|
||||||
|
const options: SelectOption[] = [];
|
||||||
|
for (const field of this.importer.templateFieldsMap.values()) {
|
||||||
|
const value = field.fieldKey;
|
||||||
|
|
||||||
|
let label = field.label;
|
||||||
|
if (field.parentSchemaChildField) {
|
||||||
|
label = `${label} (${field.parentSchemaChildField.label})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldname: 'col',
|
||||||
|
fieldtype: 'AutoComplete',
|
||||||
|
options,
|
||||||
|
} as OptionField;
|
||||||
|
},
|
||||||
|
pickedArray(): string[] {
|
||||||
|
return [...this.importer.templateFieldsPicked.entries()]
|
||||||
|
.filter(([key, picked]) => picked)
|
||||||
|
.map(([key, _]) => key);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activated(): void {
|
||||||
|
docsPathRef.value = docsPathMap.DataImport ?? '';
|
||||||
|
},
|
||||||
|
deactivated(): void {
|
||||||
|
docsPathRef.value = '';
|
||||||
|
if (!this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
log: console.log,
|
||||||
|
showMe(): void {
|
||||||
|
const schemaName = this.importer.schemaName;
|
||||||
|
this.clear();
|
||||||
|
this.$router.push(`/list/${schemaName}`);
|
||||||
|
},
|
||||||
|
clear(): void {
|
||||||
|
this.file = null;
|
||||||
|
this.names = [];
|
||||||
|
this.nullOrImporter = null;
|
||||||
|
this.importType = '';
|
||||||
|
this.complete = false;
|
||||||
|
this.canReset = false;
|
||||||
|
this.isMakingEntries = false;
|
||||||
|
this.percentLoading = 0;
|
||||||
|
this.messageLoading = '';
|
||||||
|
},
|
||||||
|
async handlePrimaryClick(): Promise<void> {
|
||||||
|
if (!this.file) {
|
||||||
|
return await this.selectFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.importData();
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
setLabelIndex(e: Event): void {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const labelIndex = Number(target?.value ?? '1') - 1;
|
||||||
|
this.nullOrImporter?.initialize(labelIndex);
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
async saveTemplate(): Promise<void> {
|
||||||
|
const template = this.importer.getCSVTemplate();
|
||||||
|
const templateName = this.importType + ' ' + this.t`Template`;
|
||||||
|
const { canceled, filePath } = await getSavePath(templateName, 'csv');
|
||||||
|
|
||||||
|
if (canceled || !filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveData(template, filePath);
|
||||||
|
},
|
||||||
|
/*
|
||||||
|
getAssignerField(targetLabel: string): OptionField {
|
||||||
|
const assigned = this.importer.assignedMap[targetLabel];
|
||||||
|
return {
|
||||||
|
fieldname: 'assignerField',
|
||||||
|
label: targetLabel,
|
||||||
|
placeholder: `Select Label`,
|
||||||
|
fieldtype: FieldTypeEnum.Select,
|
||||||
|
options: [
|
||||||
|
'',
|
||||||
|
...(assigned ? [assigned] : []),
|
||||||
|
...this.importer.unassignedLabels,
|
||||||
|
].map((i) => ({ value: i, label: i })),
|
||||||
|
default: assigned ?? '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onAssignedChange(target: string, value: string): void {
|
||||||
|
this.importer.assignedMap[target] = value;
|
||||||
|
},
|
||||||
|
onValueChange(event: Event, i: number, j: number): void {
|
||||||
|
this.importer.updateValue((event.target as HTMLInputElement).value, i, j);
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
async importData(): Promise<void> {
|
||||||
|
/*
|
||||||
|
if (this.isMakingEntries || this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isRequiredUnassigned) {
|
||||||
|
return await showMessageDialog({
|
||||||
|
message: this.t`Required Fields not Assigned`,
|
||||||
|
detail: this
|
||||||
|
.t`Please assign the following fields ${this.requiredUnassigned.join(
|
||||||
|
', '
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.importer.assignedMatrix.length === 0) {
|
||||||
|
return await showMessageDialog({
|
||||||
|
message: this.t`No Data to Import`,
|
||||||
|
detail: this.t`Please select a file with data to import.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, names, message } = await this.importer.importData(
|
||||||
|
this.setLoadingStatus
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
return await showMessageDialog({
|
||||||
|
message: this.t`Import Failed`,
|
||||||
|
detail: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.names = names;
|
||||||
|
this.complete = true;
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
setImportType(importType: string): void {
|
||||||
|
this.clear();
|
||||||
|
if (!importType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importType = importType;
|
||||||
|
this.nullOrImporter = new Importer(
|
||||||
|
this.labelSchemaNameMap[this.importType],
|
||||||
|
fyo
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setLoadingStatus(
|
||||||
|
isMakingEntries: boolean,
|
||||||
|
entriesMade: number,
|
||||||
|
totalEntries: number
|
||||||
|
): void {
|
||||||
|
this.isMakingEntries = isMakingEntries;
|
||||||
|
this.percentLoading = entriesMade / totalEntries;
|
||||||
|
this.messageLoading = isMakingEntries
|
||||||
|
? `${entriesMade} entries made out of ${totalEntries}...`
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
async selectFile(): Promise<void> {
|
||||||
|
const options = {
|
||||||
|
title: this.t`Select File`,
|
||||||
|
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { success, canceled, filePath, data, name } = await selectFile(
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success && !canceled) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message: this.t`File selection failed.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success || canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = new TextDecoder().decode(data);
|
||||||
|
const isValid = this.importer.selectFile(text);
|
||||||
|
if (!isValid) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message: this.t`Bad import data`,
|
||||||
|
detail: this.t`Could not read file`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.file = {
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Add Pick Modal
|
||||||
|
* TODO: Build Grid Body
|
||||||
|
* TODO: View raw values
|
||||||
|
* TODO: If field not assigned to column show raw value
|
||||||
|
* TODO: If field assigned to column show respective FormControl
|
||||||
|
* TODO: If error in parsing the value, mark as error and call
|
||||||
|
* - for editing value
|
||||||
|
* TODO: View parsed values (after columns have been assigned)
|
||||||
|
*/
|
||||||
|
</script>
|
@ -2,6 +2,7 @@ import { ModelNameEnum } from 'models/types';
|
|||||||
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
|
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
|
||||||
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
|
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
|
||||||
import DataImport from 'src/pages/DataImport.vue';
|
import DataImport from 'src/pages/DataImport.vue';
|
||||||
|
import ImportWizard from 'src/pages/ImportWizard.vue';
|
||||||
import GeneralForm from 'src/pages/GeneralForm.vue';
|
import GeneralForm from 'src/pages/GeneralForm.vue';
|
||||||
import GetStarted from 'src/pages/GetStarted.vue';
|
import GetStarted from 'src/pages/GetStarted.vue';
|
||||||
import InvoiceForm from 'src/pages/InvoiceForm.vue';
|
import InvoiceForm from 'src/pages/InvoiceForm.vue';
|
||||||
@ -142,6 +143,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'Data Import',
|
name: 'Data Import',
|
||||||
component: DataImport,
|
component: DataImport,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/import-wizard',
|
||||||
|
name: 'Import Wizard',
|
||||||
|
component: ImportWizard,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
|
@ -272,6 +272,11 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
|
|||||||
name: 'data-import',
|
name: 'data-import',
|
||||||
route: '/data-import',
|
route: '/data-import',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t`Import Wizard`,
|
||||||
|
name: 'import-wizard',
|
||||||
|
route: '/import-wizard',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t`Settings`,
|
label: t`Settings`,
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
14
tests/testImporter.spec.ts
Normal file
14
tests/testImporter.spec.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { Importer } from 'src/importer';
|
||||||
|
import test from 'tape';
|
||||||
|
import { closeTestFyo, getTestFyo, setupTestFyo } from './helpers';
|
||||||
|
|
||||||
|
const fyo = getTestFyo();
|
||||||
|
setupTestFyo(fyo, __filename);
|
||||||
|
|
||||||
|
test('importer', async (t) => {
|
||||||
|
const importer = new Importer(ModelNameEnum.SalesInvoice, fyo);
|
||||||
|
t.ok(importer.getCSVTemplate());
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTestFyo(fyo, __filename);
|
Loading…
Reference in New Issue
Block a user