2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

feat: add should submit

- code for insertion
- success state
This commit is contained in:
18alantom 2022-02-23 16:01:55 +05:30
parent f7937935fb
commit 916d0ecee4
2 changed files with 253 additions and 25 deletions

View File

@ -12,12 +12,26 @@ export const importable = [
'Item', 'Item',
]; ];
type Status = {
success: boolean;
message: string;
names: string[];
};
type Exclusion = { type Exclusion = {
[key: string]: string[]; [key: string]: string[];
}; };
type Map = { type Map = {
[key: string]: string | boolean | object; [key: string]: unknown;
};
type ObjectMap = {
[key: string]: Map;
};
type LabelTemplateFieldMap = {
[key: string]: TemplateField;
}; };
interface TemplateField { interface TemplateField {
@ -27,6 +41,27 @@ interface TemplateField {
doctype: string; doctype: string;
options?: string[]; options?: string[];
fieldtype: FieldType; 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 = { const exclusion: Exclusion = {
@ -35,11 +70,21 @@ const exclusion: Exclusion = {
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'], Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
}; };
function getFilteredDocFields(doctype: string): [TemplateField[], string[]] { function getFilteredDocFields(
const fields: TemplateField[] = []; df: string | string[]
): [TemplateField[], string[][]] {
let doctype = df[0];
let parentField = df[1] ?? '';
if (typeof df === 'string') {
doctype = df;
parentField = '';
}
// @ts-ignore // @ts-ignore
const primaryFields: Field[] = frappe.models[doctype].fields; const primaryFields: Field[] = frappe.models[doctype].fields;
const tableTypes: string[] = []; const fields: TemplateField[] = [];
const tableTypes: string[][] = [];
const exclusionFields: string[] = exclusion[doctype] ?? []; const exclusionFields: string[] = exclusion[doctype] ?? [];
primaryFields.forEach( primaryFields.forEach(
@ -54,15 +99,16 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
options, options,
}) => { }) => {
if ( if (
readOnly || !(fieldname === 'name' && !parentField) &&
(readOnly ||
(hidden && typeof hidden === 'number') || (hidden && typeof hidden === 'number') ||
exclusionFields.includes(fieldname) exclusionFields.includes(fieldname))
) { ) {
return; return;
} }
if (fieldtype === FieldType.Table && childtype) { if (fieldtype === FieldType.Table && childtype) {
tableTypes.push(childtype); tableTypes.push([childtype, fieldname]);
return; return;
} }
@ -72,6 +118,7 @@ function getFilteredDocFields(doctype: string): [TemplateField[], string[]] {
doctype, doctype,
options, options,
fieldtype, fieldtype,
parentField,
required: Boolean(required ?? false), required: Boolean(required ?? false),
}); });
} }
@ -85,7 +132,7 @@ function getTemplateFields(doctype: string): TemplateField[] {
if (!doctype) { if (!doctype) {
return []; return [];
} }
const doctypes: string[] = [doctype]; const doctypes: string[][] = [[doctype]];
while (doctypes.length > 0) { while (doctypes.length > 0) {
const dt = doctypes.pop(); const dt = doctypes.pop();
if (!dt) { if (!dt) {
@ -126,7 +173,8 @@ export class Importer {
parsedValues: string[][] = []; parsedValues: string[][] = [];
assignedMap: Map = {}; // target: import assignedMap: Map = {}; // target: import
requiredMap: Map = {}; requiredMap: Map = {};
labelFieldMap: Map = {}; labelTemplateFieldMap: LabelTemplateFieldMap = {};
shouldSubmit: boolean = false;
constructor(doctype: string) { constructor(doctype: string) {
this.doctype = doctype; this.doctype = doctype;
@ -141,10 +189,13 @@ export class Importer {
acc[k.label] = k.required; acc[k.label] = k.required;
return acc; return acc;
}, {}); }, {});
this.labelFieldMap = this.templateFields.reduce((acc: Map, k) => { this.labelTemplateFieldMap = this.templateFields.reduce(
(acc: LabelTemplateFieldMap, k) => {
acc[k.label] = k; acc[k.label] = k;
return acc; return acc;
}, {}); },
{}
);
} }
get assignableLabels() { get assignableLabels() {
@ -225,9 +276,11 @@ export class Importer {
this.parsedLabels = csv[0]; this.parsedLabels = csv[0];
const values = csv.slice(1); const values = csv.slice(1);
/*
if (values.some((v) => v.length !== this.parsedLabels.length)) { if (values.some((v) => v.length !== this.parsedLabels.length)) {
return false; return false;
} }
*/
this.parsedValues = values; this.parsedValues = values;
this._setAssigned(); this._setAssigned();
@ -244,9 +297,81 @@ export class Importer {
this.assignedMap[l] = l; 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 // @ts-ignore
window.gtf = getTemplateFields; window.gtf = getTemplateFields;

View File

@ -10,10 +10,10 @@
<DropdownWithActions <DropdownWithActions
class="ml-2" class="ml-2"
:actions="actions" :actions="actions"
v-if="canCancel || importType" v-if="(canCancel || importType) && !complete"
/> />
<Button <Button
v-if="importType" v-if="importType && !complete"
type="primary" type="primary"
class="text-sm ml-2" class="text-sm ml-2"
@click="handlePrimaryClick" @click="handlePrimaryClick"
@ -21,9 +21,12 @@
> >
</template> </template>
</PageHeader> </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 --> <!-- 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 <FormControl
:df="importableDf" :df="importableDf"
input-class="bg-gray-100 text-gray-900 text-base" input-class="bg-gray-100 text-gray-900 text-base"
@ -32,6 +35,36 @@
size="small" size="small"
@change="setImportType" @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 <p
class="text-base text-base ml-2" class="text-base text-base ml-2"
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'" :class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
@ -49,7 +82,9 @@
<!-- Label Assigner --> <!-- Label Assigner -->
<div v-if="fileName" class="pb-4"> <div v-if="fileName" class="pb-4">
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2> <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"> <div v-for="(f, k) in importer.assignableLabels" :key="f + '-' + k">
<p class="text-gray-600 text-sm mb-1"> <p class="text-gray-600 text-sm mb-1">
{{ f }} {{ f }}
@ -143,6 +178,41 @@
</div> </div>
</div> </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> </div>
</template> </template>
<script> <script>
@ -166,6 +236,8 @@ export default {
}, },
data() { data() {
return { return {
complete: false,
names: ['Bat', 'Baseball', 'Other Shit'],
file: null, file: null,
importer: null, importer: null,
importType: '', importType: '',
@ -186,7 +258,7 @@ export default {
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>', template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
}, },
condition: () => true, condition: () => true,
action: this.cancel, action: this.clear,
}; };
const secondaryAction = { const secondaryAction = {
@ -216,6 +288,13 @@ export default {
primaryLabel() { primaryLabel() {
return this.file ? this.t`Import Data` : this.t`Select File`; 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() { importableDf() {
return { return {
fieldname: 'importType', fieldname: 'importType',
@ -240,11 +319,21 @@ export default {
return !!(this.file || this.importType); return !!(this.file || this.importType);
}, },
}, },
deactivated() {
this.clear();
},
methods: { methods: {
cancel() { showMe() {
this.clear();
const doctype = this.importer?.doctype ?? 'Item';
this.$router.push(`/list/${doctype}`);
},
clear() {
this.file = null; this.file = null;
this.names = [];
this.importer = null; this.importer = null;
this.importType = ''; this.importType = '';
this.complete = false;
}, },
handlePrimaryClick() { handlePrimaryClick() {
if (!this.file) { if (!this.file) {
@ -292,10 +381,24 @@ export default {
onValueChange(event, i, j) { onValueChange(event, i, j) {
this.importer.updateValue(event.target.value, 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) { setImportType(importType) {
if (this.importType) { if (this.importType) {
this.cancel(); this.clear();
} }
this.importType = importType; this.importType = importType;
this.importer = new Importer(this.labelDoctypeMap[this.importType]); this.importer = new Importer(this.labelDoctypeMap[this.importType]);