2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

incr: get dataimport to work

- need to test on child table docs
This commit is contained in:
18alantom 2022-05-05 16:14:26 +05:30
parent 0a3fe90990
commit bd6f110553
16 changed files with 192 additions and 172 deletions

View File

@ -164,7 +164,7 @@ function toDocString(value: RawValue, field: Field) {
}
function toDocDate(value: RawValue, field: Field) {
if (value === null) {
if (value === null || value === '') {
return null;
}
@ -181,6 +181,10 @@ function toDocDate(value: RawValue, field: Field) {
}
function toDocCurrency(value: RawValue, field: Field, fyo: Fyo) {
if (value === '') {
return fyo.pesa(0);
}
if (typeof value === 'string') {
return fyo.pesa(value);
}
@ -201,6 +205,10 @@ function toDocCurrency(value: RawValue, field: Field, fyo: Fyo) {
}
function toDocInt(value: RawValue, field: Field): number {
if (value === '') {
return 0;
}
if (typeof value === 'string') {
value = parseInt(value);
}
@ -209,6 +217,10 @@ function toDocInt(value: RawValue, field: Field): number {
}
function toDocFloat(value: RawValue, field: Field): number {
if (value === '') {
return 0;
}
if (typeof value === 'boolean') {
return Number(value);
}

View File

@ -2,24 +2,24 @@ import { Fyo } from 'fyo';
import NumberSeries from 'fyo/models/NumberSeries';
import { DEFAULT_SERIES_START } from 'fyo/utils/consts';
import { BaseError } from 'fyo/utils/errors';
import { Field, Schema } from 'schemas/types';
import { getRandomString } from 'utils';
import { Doc } from './doc';
export function getNumberSeries(schema: Schema): Field | undefined {
const numberSeries = schema.fields.find(
(f) => f.fieldname === 'numberSeries'
);
return numberSeries;
}
export function isNameAutoSet(schemaName: string, fyo: Fyo): boolean {
const schema = fyo.schemaMap[schemaName]!;
if (schema.naming === 'manual') {
return false;
}
if (schema.naming === 'autoincrement') {
return true;
}
const numberSeries = getNumberSeries(schema);
if (schema.naming === 'random') {
return true;
}
const numberSeries = fyo.getField(schema.name, 'numberSeries');
if (numberSeries) {
return true;
}

View File

@ -98,9 +98,9 @@ export abstract class InvoiceItem extends Doc {
const itemList = doc.parentdoc!.items as Doc[];
const items = itemList.map((d) => d.item as string).filter(Boolean);
let itemNotFor = 'sales';
let itemNotFor = 'Sales';
if (doc.isSales) {
itemNotFor = 'purchases';
itemNotFor = 'Purchases';
}
const baseFilter = { for: ['not in', [itemNotFor]] };

View File

@ -67,7 +67,7 @@ export class Item extends Doc {
return [
{
label: fyo.t`New Sale`,
condition: (doc) => !doc.notInserted && doc.for !== 'purchases',
condition: (doc) => !doc.notInserted && doc.for !== 'Purchases',
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('SalesInvoice');
await invoice.append('items', {
@ -80,7 +80,7 @@ export class Item extends Doc {
},
{
label: fyo.t`New Purchase`,
condition: (doc) => !doc.notInserted && doc.for !== 'sales',
condition: (doc) => !doc.notInserted && doc.for !== 'Sales',
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('PurchaseInvoice');
await invoice.append('items', {

View File

@ -74,20 +74,21 @@
"fieldtype": "Select",
"options": [
{
"value": "purchases",
"value": "Purchases",
"label": "Purchases"
},
{
"value": "sales",
"value": "Sales",
"label": "Sales"
},
{
"value": "both",
"value": "Both",
"label": "Both"
}
],
"readOnly": true,
"default": "both"
"required": true,
"default": "Both"
},
{
"fieldname": "incomeAccount",

View File

@ -65,7 +65,8 @@
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
"placeholder": "HSN/SAC Code",
"hidden": true
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],

View File

@ -66,7 +66,8 @@
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
"placeholder": "HSN/SAC Code",
"hidden": true
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],

View File

@ -206,6 +206,8 @@ export default {
// valid selection
let item = this.items[this.highlightedIndex];
await this.selectItem(item);
} else if (this.items.length === 1) {
await this.selectItem(this.items[0])
}
},
highlightItemUp() {

View File

@ -31,7 +31,9 @@ export default {
actions: { default: [] },
type: { type: String, default: 'secondary' },
},
inject: ['doc'],
inject: {
doc: { default: null },
},
components: {
Dropdown,
Button,

View File

@ -1,8 +1,10 @@
import { Fyo, t } from 'fyo';
import { Converter } from 'fyo/core/converter';
import { DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { isNameAutoSet } from 'fyo/model/naming';
import { Noun, Verb } from 'fyo/telemetry/types';
import { ModelNameEnum } from 'models/types';
import {
Field,
FieldType,
@ -11,16 +13,20 @@ import {
SelectOption,
TargetField,
} from 'schemas/types';
import { parseCSV } from '../utils/csvParser';
import {
getDefaultMapFromList,
getMapFromList,
getValueMapFromList,
} from 'utils';
import { generateCSV, parseCSV } from '../utils/csvParser';
export const importable = [
'SalesInvoice',
'PurchaseInvoice',
'Payment',
'JournalEntry',
'Customer',
'Supplier',
'Item',
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.Payment,
ModelNameEnum.Party,
ModelNameEnum.Item,
ModelNameEnum.JournalEntry,
];
type Status = {
@ -29,16 +35,7 @@ type Status = {
names: string[];
};
type Exclusion = {
[key: string]: string[];
};
type Map = Record<string, unknown>;
type ObjectMap = Record<string, Map>;
type LabelTemplateFieldMap = {
[key: string]: TemplateField;
};
type Exclusion = Record<string, string[]>;
type LoadingStatusCallback = (
isMakingEntries: boolean,
@ -56,33 +53,9 @@ interface TemplateField {
parentField: string;
}
function formatValue(value: string, fieldtype: FieldType): unknown {
switch (fieldtype) {
case FieldTypeEnum.Date:
if (value === '') {
return '';
}
return new Date(value);
case FieldTypeEnum.Currency:
// @ts-ignore
return this.fyo.pesa(value || 0);
case FieldTypeEnum.Int:
case FieldTypeEnum.Float: {
const n = parseFloat(value);
if (!Number.isNaN(n)) {
return n;
}
return 0;
}
default:
return value;
}
}
const exclusion: Exclusion = {
Item: ['image'],
Supplier: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'],
Party: ['address', 'outstandingAmount', 'image'],
};
function getFilteredDocFields(
@ -97,48 +70,68 @@ function getFilteredDocFields(
parentField = '';
}
// @ts-ignore
const primaryFields: Field[] = fyo.schemaMap[schemaName]?.fields ?? [];
const primaryFields: Field[] = fyo.schemaMap[schemaName]!.fields;
const fields: TemplateField[] = [];
const tableTypes: string[][] = [];
const exclusionFields: string[] = exclusion[schemaName] ?? [];
primaryFields.forEach((field) => {
const { label, fieldtype, fieldname, readOnly, required, hidden } = field;
for (const field of primaryFields) {
const { label, fieldtype, fieldname, required } = field;
if (
!(fieldname === 'name' && !parentField) &&
(readOnly ||
(hidden && typeof hidden === 'number') ||
exclusionFields.includes(fieldname))
) {
return;
if (shouldSkip(field, exclusionFields, parentField)) {
continue;
}
if (fieldtype === FieldTypeEnum.Table && (field as TargetField).target) {
tableTypes.push([(field as TargetField).target, fieldname]);
return;
if (fieldtype === FieldTypeEnum.Table) {
const { target } = field as TargetField;
tableTypes.push([target, fieldname]);
continue;
}
let options: SelectOption[] = [];
if ((field as OptionField).options !== undefined) {
options = (field as OptionField).options;
}
const options: SelectOption[] = (field as OptionField).options ?? [];
fields.push({
label,
fieldname,
schemaName: schemaName,
schemaName,
options,
fieldtype,
parentField,
required: Boolean(required ?? false),
required: required ?? false,
});
});
}
return [fields, tableTypes];
}
function shouldSkip(
field: Field,
exclusionFields: string[],
parentField: string
): boolean {
if (field.meta) {
return true;
}
if (field.fieldname === 'name' && parentField) {
return true;
}
if (field.required) {
return false;
}
if (exclusionFields.includes(field.fieldname)) {
return true;
}
if (field.hidden || field.readOnly) {
return true;
}
return false;
}
function getTemplateFields(schemaName: string, fyo: Fyo): TemplateField[] {
const fields: TemplateField[] = [];
if (!schemaName) {
@ -147,46 +140,28 @@ function getTemplateFields(schemaName: string, fyo: Fyo): TemplateField[] {
const schemaNames: string[][] = [[schemaName]];
while (schemaNames.length > 0) {
const dt = schemaNames.pop();
if (!dt) {
const sn = schemaNames.pop();
if (!sn) {
break;
}
const [templateFields, tableTypes] = getFilteredDocFields(dt, fyo);
const [templateFields, tableTypes] = getFilteredDocFields(sn, fyo);
fields.push(...templateFields);
schemaNames.push(...tableTypes);
}
return fields;
}
function getLabelFieldMap(templateFields: TemplateField[]): Map {
const map: Map = {};
templateFields.reduce((acc, tf) => {
const key = tf.label as string;
acc[key] = tf.fieldname;
return acc;
}, map);
return map;
}
function getTemplate(templateFields: TemplateField[]): string {
const labels = templateFields.map(({ label }) => `"${label}"`).join(',');
return [labels, ''].join('\n');
}
export class Importer {
schemaName: string;
templateFields: TemplateField[];
map: Map;
labelTemplateFieldMap: Record<string, TemplateField> = {};
template: string;
indices: number[] = [];
parsedLabels: string[] = [];
parsedValues: string[][] = [];
assignedMap: Map = {}; // target: import
requiredMap: Map = {};
labelTemplateFieldMap: LabelTemplateFieldMap = {};
assignedMap: Record<string, string> = {}; // target: import
requiredMap: Record<string, boolean> = {};
shouldSubmit: boolean = false;
labelIndex: number = -1;
csv: string[][] = [];
@ -196,38 +171,30 @@ export class Importer {
this.schemaName = schemaName;
this.fyo = fyo;
this.templateFields = getTemplateFields(schemaName, this.fyo);
this.map = getLabelFieldMap(this.templateFields);
this.template = getTemplate(this.templateFields);
this.assignedMap = this.assignableLabels.reduce((acc: Map, k) => {
acc[k] = '';
return acc;
}, {});
this.requiredMap = this.templateFields.reduce((acc: Map, k) => {
acc[k.label] = k.required;
return acc;
}, {});
this.labelTemplateFieldMap = this.templateFields.reduce(
(acc: LabelTemplateFieldMap, k) => {
acc[k.label] = k;
return acc;
},
{}
);
this.template = generateCSV([this.templateFields.map((t) => t.label)]);
this.labelTemplateFieldMap = getMapFromList(this.templateFields, 'label');
this.assignedMap = getDefaultMapFromList(this.templateFields, '', 'label');
this.requiredMap = getValueMapFromList(
this.templateFields,
'label',
'required'
) as Record<string, boolean>;
}
get assignableLabels() {
const req: string[] = [];
const nreq: string[] = [];
Object.keys(this.map).forEach((k) => {
if (this.requiredMap[k]) {
req.push(k);
return;
for (const label in this.labelTemplateFieldMap) {
if (this.requiredMap[label]) {
req.push(label);
continue;
}
nreq.push(k);
});
nreq.push(label);
}
return [...req, ...nreq];
return [req, nreq].flat();
}
get unassignedLabels() {
@ -332,23 +299,23 @@ export class Importer {
});
}
getDocs(): Map[] {
getDocs(): DocValueMap[] {
const fields = this.columnLabels.map((k) => this.labelTemplateFieldMap[k]);
const nameIndex = fields.findIndex(({ fieldname }) => fieldname === 'name');
const docMap: ObjectMap = {};
const docMap: Record<string, DocValueMap> = {};
const assignedMatrix = this.assignedMatrix;
for (let r = 0; r < assignedMatrix.length; r++) {
const row = assignedMatrix[r];
const cts: ObjectMap = {};
const cts: Record<string, DocValueMap> = {};
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);
const value = Converter.toDocValue(row[f], field, this.fyo);
if (field.parentField) {
cts[field.parentField] ??= {};
@ -361,7 +328,7 @@ export class Importer {
for (const k of Object.keys(cts)) {
docMap[name][k] ??= [];
(docMap[name][k] as Map[]).push(cts[k]);
(docMap[name][k] as DocValueMap[]).push(cts[k]);
}
}
@ -423,9 +390,8 @@ export class Importer {
this.parsedValues.push(emptyRow);
}
async makeEntry(doc: Doc, docObj: Map) {
await doc.setMultiple(docObj as DocValueMap);
await doc.sync();
async makeEntry(doc: Doc, docObj: DocValueMap) {
await doc.setAndSync(docObj);
if (this.shouldSubmit) {
await doc.submit();
}

View File

@ -1,5 +1,6 @@
<template>
<div class="flex flex-col overflow-hidden w-full">
<!-- Header -->
<PageHeader :title="t`Data Import`">
<DropdownWithActions
:actions="actions"
@ -13,6 +14,7 @@
>{{ primaryLabel }}</Button
>
</PageHeader>
<div
class="flex px-8 mt-2 text-base w-full flex-col gap-8"
v-if="!complete"
@ -21,8 +23,8 @@
<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"
class="w-40"
input-class="bg-transparent text-gray-900 text-base"
class="w-40 bg-gray-100 rounded"
:value="importType"
size="small"
@change="setImportType"
@ -330,9 +332,11 @@ import HowTo from 'src/components/HowTo.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { importable, Importer } from 'src/dataImport';
import { fyo } from 'src/initFyo';
import { getSavePath, saveData, showMessageDialog } from 'src/utils';
import { getSavePath, saveData } from 'src/utils/ipcCalls';
import { showMessageDialog } from 'src/utils/ui';
import { IPC_ACTIONS } from 'utils/messages';
import Loading from '../components/Loading.vue';
export default {
components: {
PageHeader,
@ -356,6 +360,11 @@ export default {
messageLoading: '',
};
},
mounted() {
if (fyo.store.isDevelopment) {
window.di = this;
}
},
computed: {
labelIndex() {
return this.importer.labelIndex;
@ -435,14 +444,14 @@ export default {
label: this.t`Import Type`,
fieldtype: 'AutoComplete',
placeholder: this.t`Import Type`,
getList: () => importable.map((i) => fyo.models[i].label),
options: Object.keys(this.labelSchemaNameMap)
};
},
labelSchemaNameMap() {
return importable
.map((i) => ({
name: i,
label: fyo.models[i].label,
label: fyo.schemaMap[i].label,
}))
.reduce((acc, { name, label }) => {
acc[label] = name;

View File

@ -1,8 +1,8 @@
import { NounEnum, Verb } from 'fyo/telemetry/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import DataImport from 'src/pages/DataImport.vue';
import GetStarted from 'src/pages/GetStarted.vue';
// import DataImport from 'src/pages/DataImport.vue';
import InvoiceForm from 'src/pages/InvoiceForm.vue';
import JournalEntryForm from 'src/pages/JournalEntryForm.vue';
import ListView from 'src/pages/ListView/ListView.vue';
@ -107,13 +107,12 @@ const routes: RouteRecordRaw[] = [
edit: (route) => route.query,
},
},
/*
{
path: '/data-import',
name: 'Data Import',
component: DataImport,
},
*/ {
{
path: '/settings',
name: 'Settings',
component: Settings,

View File

@ -87,17 +87,17 @@ function getCreateList(): SearchItem[] {
{
label: t`Sales Items`,
schemaName: ModelNameEnum.Item,
filter: { for: 'sales' },
filter: { for: 'Sales' },
},
{
label: t`Purchase Items`,
schemaName: ModelNameEnum.Item,
filter: { for: 'purchases' },
filter: { for: 'Purchases' },
},
{
label: t`Common Items`,
schemaName: ModelNameEnum.Item,
filter: { for: 'both' },
filter: { for: 'Both' },
},
].map(({ label, filter, schemaName }) => {
const fk = Object.keys(filter)[0] as 'for' | 'role';
@ -163,15 +163,15 @@ function getListViewList(): SearchItem[] {
{ label: t`Suppliers`, route: `/list/Party/role/Supplier/${t`Suppliers`}` },
{
label: t`Sales Items`,
route: `/list/Item/for/sales/${t`Sales Items`}`,
route: `/list/Item/for/Sales/${t`Sales Items`}`,
},
{
label: t`Purchase Items`,
route: `/list/Item/for/purchases/${t`Purchase Items`}`,
route: `/list/Item/for/Purchases/${t`Purchase Items`}`,
},
{
label: t`Common Items`,
route: `/list/Item/for/both/${t`Common Items`}`,
route: `/list/Item/for/Both/${t`Common Items`}`,
},
].map((i) => ({ ...i, group: 'List' } as SearchItem));

View File

@ -69,7 +69,7 @@ function getCompleteSidebar(): SidebarConfig {
{
label: t`Sales Items`,
name: 'sales-items',
route: `/list/Item/for/sales/${t`Sales Items`}`,
route: `/list/Item/for/Sales/${t`Sales Items`}`,
schemaName: 'Item',
},
],
@ -101,7 +101,7 @@ function getCompleteSidebar(): SidebarConfig {
{
label: t`Purchase Items`,
name: 'purchase-items',
route: `/list/Item/for/purchases/${t`Purchase Items`}`,
route: `/list/Item/for/Purchases/${t`Purchase Items`}`,
schemaName: 'Item',
},
],
@ -118,20 +118,21 @@ function getCompleteSidebar(): SidebarConfig {
route: '/list/JournalEntry',
schemaName: 'JournalEntry',
},
{
label: t`Common Items`,
name: 'common-items',
route: `/list/Item/for/both/${t`Common Items`}`,
schemaName: 'Item',
},
{
label: t`Party`,
name: 'party',
route: '/list/Party/role/Both',
schemaName: 'Party',
},
{
label: t`Common Items`,
name: 'common-items',
route: `/list/Item/for/Both/${t`Common Items`}`,
schemaName: 'Item',
},
],
},
/*
{
label: t`Reports`,
name: t`reports`,
@ -172,6 +173,7 @@ function getCompleteSidebar(): SidebarConfig {
},
],
},
*/
{
label: t`Setup`,
name: t`setup`,
@ -192,7 +194,7 @@ function getCompleteSidebar(): SidebarConfig {
{
label: t`Data Import`,
name: 'data-import',
route: '/data_import',
route: '/data-import',
},
{
label: t`Settings`,

View File

@ -47,16 +47,16 @@ export async function openQuickEdit({
defaults = Object.assign({
for:
purpose === 'sales'
? 'purchases'
: purpose === 'purchases'
? 'sales'
: 'both',
purpose === 'Sales'
? 'Purchases'
: purpose === 'Purchases'
? 'Sales'
: 'Both',
});
}
if (forWhat[0] === 'not in' && forWhat[1] === 'sales') {
defaults = Object.assign({ for: 'purchases' });
if (forWhat[0] === 'not in' && forWhat[1] === 'Sales') {
defaults = Object.assign({ for: 'Purchases' });
}
router[method]({

View File

@ -1,5 +1,4 @@
/**
* Functions in utils/*.ts can be used by the frontend or the backends
* And so should not contain and platforma specific imports.
*/
export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
@ -48,6 +47,32 @@ export function getMapFromList<T, K extends keyof T>(
return acc;
}
export function getDefaultMapFromList<T, K extends keyof T, D>(
list: T[] | string[],
defaultValue: D,
name?: K
): Record<string, D> {
const acc: Record<string, D> = {};
if (typeof list[0] === 'string') {
for (const l of list as string[]) {
acc[l] = defaultValue;
}
return acc;
}
if (!name) {
return {};
}
for (const l of list as T[]) {
const key = String(l[name]);
acc[key] = defaultValue;
}
return acc;
}
export function getListFromMap<T>(map: Record<string, T>): T[] {
return Object.keys(map).map((n) => map[n]);
}