mirror of
https://github.com/frappe/books.git
synced 2024-09-20 03:29:00 +00:00
feat: add should submit
- code for insertion - success state
This commit is contained in:
parent
f7937935fb
commit
916d0ecee4
@ -12,12 +12,26 @@ export const importable = [
|
||||
'Item',
|
||||
];
|
||||
|
||||
type Status = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
names: string[];
|
||||
};
|
||||
|
||||
type Exclusion = {
|
||||
[key: string]: string[];
|
||||
};
|
||||
|
||||
type Map = {
|
||||
[key: string]: string | boolean | object;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type ObjectMap = {
|
||||
[key: string]: Map;
|
||||
};
|
||||
|
||||
type LabelTemplateFieldMap = {
|
||||
[key: string]: TemplateField;
|
||||
};
|
||||
|
||||
interface TemplateField {
|
||||
@ -27,6 +41,27 @@ interface TemplateField {
|
||||
doctype: string;
|
||||
options?: string[];
|
||||
fieldtype: FieldType;
|
||||
parentField: string;
|
||||
}
|
||||
|
||||
function formatValue(value: string, fieldtype: FieldType): unknown {
|
||||
switch (fieldtype) {
|
||||
case FieldType.Date:
|
||||
return new Date(value);
|
||||
case FieldType.Currency:
|
||||
// @ts-ignore
|
||||
return frappe.pesa(value);
|
||||
case FieldType.Int:
|
||||
case FieldType.Float: {
|
||||
const n = parseFloat(value);
|
||||
if (!Number.isNaN(n)) {
|
||||
return n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const exclusion: Exclusion = {
|
||||
@ -35,11 +70,21 @@ const exclusion: Exclusion = {
|
||||
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
|
||||
};
|
||||
|
||||
function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
||||
const fields: TemplateField[] = [];
|
||||
function getFilteredDocFields(
|
||||
df: string | string[]
|
||||
): [TemplateField[], string[][]] {
|
||||
let doctype = df[0];
|
||||
let parentField = df[1] ?? '';
|
||||
|
||||
if (typeof df === 'string') {
|
||||
doctype = df;
|
||||
parentField = '';
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const primaryFields: Field[] = frappe.models[doctype].fields;
|
||||
const tableTypes: string[] = [];
|
||||
const fields: TemplateField[] = [];
|
||||
const tableTypes: string[][] = [];
|
||||
const exclusionFields: string[] = exclusion[doctype] ?? [];
|
||||
|
||||
primaryFields.forEach(
|
||||
@ -54,15 +99,16 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
||||
options,
|
||||
}) => {
|
||||
if (
|
||||
readOnly ||
|
||||
(hidden && typeof hidden === 'number') ||
|
||||
exclusionFields.includes(fieldname)
|
||||
!(fieldname === 'name' && !parentField) &&
|
||||
(readOnly ||
|
||||
(hidden && typeof hidden === 'number') ||
|
||||
exclusionFields.includes(fieldname))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fieldtype === FieldType.Table && childtype) {
|
||||
tableTypes.push(childtype);
|
||||
tableTypes.push([childtype, fieldname]);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -72,6 +118,7 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
|
||||
doctype,
|
||||
options,
|
||||
fieldtype,
|
||||
parentField,
|
||||
required: Boolean(required ?? false),
|
||||
});
|
||||
}
|
||||
@ -85,7 +132,7 @@ function getTemplateFields(doctype: string): TemplateField[] {
|
||||
if (!doctype) {
|
||||
return [];
|
||||
}
|
||||
const doctypes: string[] = [doctype];
|
||||
const doctypes: string[][] = [[doctype]];
|
||||
while (doctypes.length > 0) {
|
||||
const dt = doctypes.pop();
|
||||
if (!dt) {
|
||||
@ -126,7 +173,8 @@ export class Importer {
|
||||
parsedValues: string[][] = [];
|
||||
assignedMap: Map = {}; // target: import
|
||||
requiredMap: Map = {};
|
||||
labelFieldMap: Map = {};
|
||||
labelTemplateFieldMap: LabelTemplateFieldMap = {};
|
||||
shouldSubmit: boolean = false;
|
||||
|
||||
constructor(doctype: string) {
|
||||
this.doctype = doctype;
|
||||
@ -141,10 +189,13 @@ export class Importer {
|
||||
acc[k.label] = k.required;
|
||||
return acc;
|
||||
}, {});
|
||||
this.labelFieldMap = this.templateFields.reduce((acc: Map, k) => {
|
||||
acc[k.label] = k;
|
||||
return acc;
|
||||
}, {});
|
||||
this.labelTemplateFieldMap = this.templateFields.reduce(
|
||||
(acc: LabelTemplateFieldMap, k) => {
|
||||
acc[k.label] = k;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
get assignableLabels() {
|
||||
@ -225,9 +276,11 @@ export class Importer {
|
||||
this.parsedLabels = csv[0];
|
||||
const values = csv.slice(1);
|
||||
|
||||
/*
|
||||
if (values.some((v) => v.length !== this.parsedLabels.length)) {
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
|
||||
this.parsedValues = values;
|
||||
this._setAssigned();
|
||||
@ -244,9 +297,81 @@ export class Importer {
|
||||
this.assignedMap[l] = l;
|
||||
});
|
||||
}
|
||||
|
||||
getDocs(): Map[] {
|
||||
const fields = this.columnLabels.map((k) => this.labelTemplateFieldMap[k]);
|
||||
const nameIndex = fields.findIndex(({ fieldname }) => fieldname === 'name');
|
||||
|
||||
const docMap: ObjectMap = {};
|
||||
|
||||
for (let r = 0; r < this.assignedMatrix.length; r++) {
|
||||
const row = this.assignedMatrix[r];
|
||||
const cts: ObjectMap = {};
|
||||
const name = row[nameIndex];
|
||||
|
||||
docMap[name] ??= {};
|
||||
|
||||
for (let f = 0; f < fields.length; f++) {
|
||||
const field = fields[f];
|
||||
const value = formatValue(row[f], field.fieldtype);
|
||||
|
||||
if (field.parentField) {
|
||||
cts[field.parentField] ??= {};
|
||||
cts[field.parentField][field.fieldname] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
docMap[name][field.fieldname] = value;
|
||||
}
|
||||
|
||||
for (const k of Object.keys(cts)) {
|
||||
docMap[name][k] ??= [];
|
||||
(docMap[name][k] as Map[]).push(cts[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// return docObjs;
|
||||
return Object.keys(docMap).map((k) => docMap[k]);
|
||||
}
|
||||
|
||||
async importData(): Promise<Status> {
|
||||
const status: Status = { success: false, names: [], message: '' };
|
||||
|
||||
for (const docObj of this.getDocs()) {
|
||||
const doc = frappe.getNewDoc(this.doctype);
|
||||
await doc.update(docObj);
|
||||
|
||||
try {
|
||||
await doc.insert();
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
|
||||
const messages = [
|
||||
frappe.t`Could not import ${this.doctype} ${doc.name}.`,
|
||||
];
|
||||
if (message) {
|
||||
messages.push(frappe.t`Error: ${message}.`);
|
||||
}
|
||||
|
||||
if (status.names.length) {
|
||||
messages.push(
|
||||
frappe.t`The following ${
|
||||
status.names.length
|
||||
} entries were created: ${status.names.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
status.message = messages.join(' ');
|
||||
return status;
|
||||
}
|
||||
|
||||
status.names.push(doc.name);
|
||||
}
|
||||
|
||||
status.success = true;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.im = importable;
|
||||
// @ts-ignore
|
||||
window.gtf = getTemplateFields;
|
||||
|
@ -10,10 +10,10 @@
|
||||
<DropdownWithActions
|
||||
class="ml-2"
|
||||
:actions="actions"
|
||||
v-if="canCancel || importType"
|
||||
v-if="(canCancel || importType) && !complete"
|
||||
/>
|
||||
<Button
|
||||
v-if="importType"
|
||||
v-if="importType && !complete"
|
||||
type="primary"
|
||||
class="text-sm ml-2"
|
||||
@click="handlePrimaryClick"
|
||||
@ -21,9 +21,12 @@
|
||||
>
|
||||
</template>
|
||||
</PageHeader>
|
||||
<div class="flex px-8 mt-2 text-base w-full flex-col gap-8">
|
||||
<div
|
||||
class="flex px-8 mt-2 text-base w-full flex-col gap-8"
|
||||
v-if="!complete"
|
||||
>
|
||||
<!-- Type selector -->
|
||||
<div class="flex flex-row justify-start items-center w-full">
|
||||
<div class="flex flex-row justify-start items-center w-full gap-2">
|
||||
<FormControl
|
||||
:df="importableDf"
|
||||
input-class="bg-gray-100 text-gray-900 text-base"
|
||||
@ -32,6 +35,36 @@
|
||||
size="small"
|
||||
@change="setImportType"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="importType && isSubmittable"
|
||||
class="
|
||||
justify-center
|
||||
items-center
|
||||
gap-2
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
bg-gray-100
|
||||
px-2
|
||||
py-1
|
||||
rounded
|
||||
text-gray-900
|
||||
"
|
||||
>
|
||||
<p>Should Submit</p>
|
||||
<FormControl
|
||||
size="small"
|
||||
input-class="bg-gray-100"
|
||||
:df="{
|
||||
fieldname: 'shouldSubmit',
|
||||
label: this.t`Submit on Import`,
|
||||
fieldtype: 'Check',
|
||||
}"
|
||||
:value="Number(importer.shouldSubmit)"
|
||||
@change="(value) => (importer.shouldSubmit = !!value)"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
class="text-base text-base ml-2"
|
||||
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||
@ -49,7 +82,9 @@
|
||||
<!-- Label Assigner -->
|
||||
<div v-if="fileName" class="pb-4">
|
||||
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
|
||||
<div class="gap-2 mt-4 grid grid-flow-col overflow-x-scroll">
|
||||
<div
|
||||
class="gap-2 mt-4 grid grid-flow-col overflow-x-scroll no-scrollbar"
|
||||
>
|
||||
<div v-for="(f, k) in importer.assignableLabels" :key="f + '-' + k">
|
||||
<p class="text-gray-600 text-sm mb-1">
|
||||
{{ f }}
|
||||
@ -143,6 +178,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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-scroll">
|
||||
<div
|
||||
v-for="(n, i) in names"
|
||||
:key="i"
|
||||
class="grid grid-cols-2 gap-2 border-b pb-2 mb-2 pr-4 text-lg w-60"
|
||||
style="grid-template-columns: 2rem auto"
|
||||
>
|
||||
<p class="text-right">{{ i + 1 }}.</p>
|
||||
<p>
|
||||
{{ n }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" class="text-sm w-28" @click="showMe">{{
|
||||
t`Show Me`
|
||||
}}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -166,6 +236,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
complete: false,
|
||||
names: ['Bat', 'Baseball', 'Other Shit'],
|
||||
file: null,
|
||||
importer: null,
|
||||
importType: '',
|
||||
@ -186,7 +258,7 @@ export default {
|
||||
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
||||
},
|
||||
condition: () => true,
|
||||
action: this.cancel,
|
||||
action: this.clear,
|
||||
};
|
||||
|
||||
const secondaryAction = {
|
||||
@ -216,6 +288,13 @@ export default {
|
||||
primaryLabel() {
|
||||
return this.file ? this.t`Import Data` : this.t`Select File`;
|
||||
},
|
||||
isSubmittable() {
|
||||
const doctype = this.importer?.doctype;
|
||||
if (doctype) {
|
||||
return frappe.models[doctype].isSubmittable ?? false;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
importableDf() {
|
||||
return {
|
||||
fieldname: 'importType',
|
||||
@ -240,11 +319,21 @@ export default {
|
||||
return !!(this.file || this.importType);
|
||||
},
|
||||
},
|
||||
deactivated() {
|
||||
this.clear();
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
showMe() {
|
||||
this.clear();
|
||||
const doctype = this.importer?.doctype ?? 'Item';
|
||||
this.$router.push(`/list/${doctype}`);
|
||||
},
|
||||
clear() {
|
||||
this.file = null;
|
||||
this.names = [];
|
||||
this.importer = null;
|
||||
this.importType = '';
|
||||
this.complete = false;
|
||||
},
|
||||
handlePrimaryClick() {
|
||||
if (!this.file) {
|
||||
@ -292,10 +381,24 @@ export default {
|
||||
onValueChange(event, i, j) {
|
||||
this.importer.updateValue(event.target.value, i, j);
|
||||
},
|
||||
importData() {},
|
||||
async importData() {
|
||||
// TODO: pre import conditions
|
||||
/*
|
||||
if(){}
|
||||
*/
|
||||
|
||||
const { success, names } = await this.importer.importData();
|
||||
if (!success || !names.length) {
|
||||
// handle failure
|
||||
return;
|
||||
}
|
||||
|
||||
this.names = names;
|
||||
this.complete = true;
|
||||
},
|
||||
setImportType(importType) {
|
||||
if (this.importType) {
|
||||
this.cancel();
|
||||
this.clear();
|
||||
}
|
||||
this.importType = importType;
|
||||
this.importer = new Importer(this.labelDoctypeMap[this.importType]);
|
||||
|
Loading…
Reference in New Issue
Block a user