2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 15:17:30 +00:00

incr: update DatabaseSelector

This commit is contained in:
18alantom 2022-04-22 16:32:03 +05:30
parent b6163b4cac
commit 6accde0e17
35 changed files with 457 additions and 264 deletions

View File

@ -2,5 +2,5 @@ import { Patch } from '../database/types';
import testPatch from './testPatch'; import testPatch from './testPatch';
export default [ export default [
{ name: 'testPatch', version: '0.4.2-beta.0', patch: testPatch }, { name: 'testPatch', version: '0.5.0-beta.0', patch: testPatch },
] as Patch[]; ] as Patch[];

View File

@ -97,6 +97,8 @@ export class AuthHandler {
// TODO: Implement this with auth flow // TODO: Implement this with auth flow
} }
purgeCache() {}
#getServerURL() { #getServerURL() {
return this.#config.serverURL || ''; return this.#config.serverURL || '';
} }

View File

@ -37,6 +37,10 @@ export class DatabaseHandler extends DatabaseBase {
} }
} }
get isConnected() {
return !!this.dbPath;
}
async createNewDatabase(dbPath: string, countryCode: string) { async createNewDatabase(dbPath: string, countryCode: string) {
countryCode = await this.#demux.createNewDatabase(dbPath, countryCode); countryCode = await this.#demux.createNewDatabase(dbPath, countryCode);
await this.init(); await this.init();
@ -60,6 +64,12 @@ export class DatabaseHandler extends DatabaseBase {
} }
} }
purgeCache() {
this.dbPath = undefined;
this.schemaMap = {};
this.fieldValueMap = {};
}
async insert( async insert(
schemaName: string, schemaName: string,
docValueMap: DocValueMap docValueMap: DocValueMap
@ -162,6 +172,7 @@ export class DatabaseHandler extends DatabaseBase {
// Other // Other
async close(): Promise<void> { async close(): Promise<void> {
await this.#demux.call('close'); await this.#demux.call('close');
this.purgeCache();
} }
async exists(schemaName: string, name?: string): Promise<boolean> { async exists(schemaName: string, name?: string): Promise<boolean> {

View File

@ -2,6 +2,7 @@ import Doc from 'fyo/model/doc';
import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types'; import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types';
import { coreModels } from 'fyo/models'; import { coreModels } from 'fyo/models';
import Observable from 'fyo/utils/observable'; import Observable from 'fyo/utils/observable';
import { Schema } from 'schemas/types';
import { getRandomString } from 'utils'; import { getRandomString } from 'utils';
import { Fyo } from '..'; import { Fyo } from '..';
import { DocValue, DocValueMap } from './types'; import { DocValue, DocValueMap } from './types';
@ -18,9 +19,14 @@ export class DocHandler {
init() { init() {
this.models = {}; this.models = {};
this.singles = {};
this.docs = new Observable(); this.docs = new Observable();
} }
purgeCache() {
this.init();
}
registerModels(models: ModelMap, regionalModels: ModelMap = {}) { registerModels(models: ModelMap, regionalModels: ModelMap = {}) {
for (const schemaName in this.fyo.db.schemaMap) { for (const schemaName in this.fyo.db.schemaMap) {
if (coreModels[schemaName] !== undefined) { if (coreModels[schemaName] !== undefined) {
@ -157,9 +163,14 @@ export class DocHandler {
return doc; return doc;
} }
getNewDoc(schemaName: string, data: DocValueMap = {}): Doc { getNewDoc(
const Model = this.getModel(schemaName); schemaName: string,
const schema = this.fyo.schemaMap[schemaName]; data: DocValueMap = {},
schema?: Schema,
Model?: typeof Doc
): Doc {
Model ??= this.getModel(schemaName);
schema ??= this.fyo.schemaMap[schemaName];
if (schema === undefined) { if (schema === undefined) {
throw new Error(`Schema not found for ${schemaName}`); throw new Error(`Schema not found for ${schemaName}`);
} }

View File

@ -34,8 +34,7 @@ export class Fyo {
_initialized: boolean = false; _initialized: boolean = false;
errorLog?: ErrorLog[]; errorLog: ErrorLog[] = [];
methods?: Record<string, Function>;
temp?: Record<string, unknown>; temp?: Record<string, unknown>;
currencyFormatter?: Intl.NumberFormat; currencyFormatter?: Intl.NumberFormat;
@ -113,9 +112,6 @@ export class Fyo {
} }
async #initializeModules() { async #initializeModules() {
this.methods = {};
this.errorLog = [];
// temp params while calling routes // temp params while calling routes
this.temp = {}; this.temp = {};
@ -170,6 +166,25 @@ export class Fyo {
return schema?.fields.find((f) => f.fieldname === fieldname); return schema?.fields.find((f) => f.fieldname === fieldname);
} }
purgeCache() {
this.pesa = getMoneyMaker({
currency: DEFAULT_CURRENCY,
precision: DEFAULT_INTERNAL_PRECISION,
display: DEFAULT_DISPLAY_PRECISION,
wrapper: markRaw,
});
this._initialized = false;
this.temp = {};
this.currencyFormatter = undefined;
this.currencySymbols = {};
this.errorLog = [];
this.temp = {};
this.db.purgeCache();
this.auth.purgeCache();
this.doc.purgeCache();
}
store = { store = {
isDevelopment: false, isDevelopment: false,
appVersion: '', appVersion: '',

View File

@ -37,6 +37,7 @@ import {
HiddenMap, HiddenMap,
ListsMap, ListsMap,
ListViewSettings, ListViewSettings,
ReadOnlyMap,
RequiredMap, RequiredMap,
TreeViewSettings, TreeViewSettings,
ValidationMap, ValidationMap,
@ -96,6 +97,11 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return this._dirty; return this._dirty;
} }
get quickEditFields() {
const fieldnames = this.schema.quickEditFields ?? ['name'];
return fieldnames.map((f) => this.fieldMap[f]);
}
_setInitialValues(data: DocValueMap) { _setInitialValues(data: DocValueMap) {
for (const fieldname in data) { for (const fieldname in data) {
const value = data[fieldname]; const value = data[fieldname];
@ -393,8 +399,13 @@ export default class Doc extends Observable<DocValue | Doc[]> {
); );
} }
getLink(fieldname: string) { getLink(fieldname: string): Doc | null {
return this._links ? this._links[fieldname] : null; const link = this._links?.[fieldname];
if (link === undefined) {
return null;
}
return link;
} }
syncValues(data: DocValueMap) { syncValues(data: DocValueMap) {
@ -736,6 +747,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
validations: ValidationMap = {}; validations: ValidationMap = {};
required: RequiredMap = {}; required: RequiredMap = {};
hidden: HiddenMap = {}; hidden: HiddenMap = {};
readOnly: ReadOnlyMap = {};
dependsOn: DependsOnMap = {}; dependsOn: DependsOnMap = {};
getCurrencies: CurrenciesMap = {}; getCurrencies: CurrenciesMap = {};

View File

@ -21,9 +21,10 @@ import Doc from './doc';
export type FormulaReturn = DocValue | DocValueMap[] | undefined | Doc[]; export type FormulaReturn = DocValue | DocValueMap[] | undefined | Doc[];
export type Formula = () => Promise<FormulaReturn> | FormulaReturn; export type Formula = () => Promise<FormulaReturn> | FormulaReturn;
export type Default = () => DocValue; export type Default = () => DocValue;
export type Validation = (value: DocValue) => Promise<void>; export type Validation = (value: DocValue) => Promise<void> | void;
export type Required = () => boolean; export type Required = () => boolean;
export type Hidden = () => boolean; export type Hidden = () => boolean;
export type ReadOnly = () => boolean;
export type GetCurrency = () => string; export type GetCurrency = () => string;
export type FormulaMap = Record<string, Formula | undefined>; export type FormulaMap = Record<string, Formula | undefined>;
@ -32,6 +33,7 @@ export type ValidationMap = Record<string, Validation | undefined>;
export type RequiredMap = Record<string, Required | undefined>; export type RequiredMap = Record<string, Required | undefined>;
export type CurrenciesMap = Record<string, GetCurrency>; export type CurrenciesMap = Record<string, GetCurrency>;
export type HiddenMap = Record<string, Hidden>; export type HiddenMap = Record<string, Hidden>;
export type ReadOnlyMap = Record<string, ReadOnly>;
export type DependsOnMap = Record<string, string[]>; export type DependsOnMap = Record<string, string[]>;
/** /**

View File

@ -1,16 +1,17 @@
import { DocValue } from 'fyo/core/types';
import { ValidationError, ValueError } from 'fyo/utils/errors'; import { ValidationError, ValueError } from 'fyo/utils/errors';
import { t } from 'fyo/utils/translation'; import { t } from 'fyo/utils/translation';
import { OptionField } from 'schemas/types'; import { OptionField } from 'schemas/types';
export function email(value: string) { export function validateEmail(value: DocValue) {
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value); const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value as string);
if (!isValid) { if (!isValid) {
throw new ValidationError(`Invalid email: ${value}`); throw new ValidationError(`Invalid email: ${value}`);
} }
} }
export function phone(value: string) { export function validatePhoneNumber(value: DocValue) {
const isValid = /[+]{0,1}[\d ]+/.test(value); const isValid = /[+]{0,1}[\d ]+/.test(value as string);
if (!isValid) { if (!isValid) {
throw new ValidationError(`Invalid phone: ${value}`); throw new ValidationError(`Invalid phone: ${value}`);
} }

View File

@ -1,3 +1,4 @@
import { Fyo } from 'fyo';
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 { pesa } from 'pesa'; import { pesa } from 'pesa';
@ -51,3 +52,24 @@ export function getActions(doc: Doc): Action[] {
return Model.getActions(doc.fyo); return Model.getActions(doc.fyo);
} }
export async function getSingleValue(
fieldname: string,
parent: string,
fyo: Fyo
) {
if (!fyo.db.isConnected) {
return undefined;
}
const res = await fyo.db.getSingleValues({ fieldname, parent });
const singleValue = res.find(
(f) => f.fieldname === fieldname && f.parent === parent
);
if (singleValue === undefined) {
return undefined;
}
return singleValue.value;
}

View File

@ -1,5 +1,6 @@
import Doc from 'fyo/model/doc'; import Doc from 'fyo/model/doc';
import { FiltersMap, ListsMap } from 'fyo/model/types'; import { FiltersMap, ListsMap, ValidationMap } from 'fyo/model/types';
import { validateEmail } from 'fyo/model/validationFunction';
import countryInfo from '../../../fixtures/countryInfo.json'; import countryInfo from '../../../fixtures/countryInfo.json';
export class AccountingSettings extends Doc { export class AccountingSettings extends Doc {
@ -14,6 +15,10 @@ export class AccountingSettings extends Doc {
}), }),
}; };
validations: ValidationMap = {
email: validateEmail,
};
static lists: ListsMap = { static lists: ListsMap = {
country: () => Object.keys(countryInfo), country: () => Object.keys(countryInfo),
}; };

View File

@ -5,7 +5,12 @@ import {
FiltersMap, FiltersMap,
FormulaMap, FormulaMap,
ListViewSettings, ListViewSettings,
ValidationMap,
} from 'fyo/model/types'; } from 'fyo/model/types';
import {
validateEmail,
validatePhoneNumber,
} from 'fyo/model/validationFunction';
import { PartyRole } from './types'; import { PartyRole } from './types';
export class Party extends Doc { export class Party extends Doc {
@ -71,6 +76,11 @@ export class Party extends Doc {
}, },
}; };
validations: ValidationMap = {
email: validateEmail,
phone: validatePhoneNumber,
};
static filters: FiltersMap = { static filters: FiltersMap = {
defaultAccount: (doc: Doc) => { defaultAccount: (doc: Doc) => {
const role = doc.role as PartyRole; const role = doc.role as PartyRole;

View File

@ -1,6 +1,7 @@
import { t } from 'fyo'; import { t } from 'fyo';
import Doc from 'fyo/model/doc'; import Doc from 'fyo/model/doc';
import { FormulaMap, ListsMap } from 'fyo/model/types'; import { FormulaMap, ListsMap, ValidationMap } from 'fyo/model/types';
import { validateEmail } from 'fyo/model/validationFunction';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import countryInfo from '../../../fixtures/countryInfo.json'; import countryInfo from '../../../fixtures/countryInfo.json';
@ -88,6 +89,10 @@ export class SetupWizard extends Doc {
}, },
}; };
validations: ValidationMap = {
email: validateEmail,
};
static lists: ListsMap = { static lists: ListsMap = {
country: () => Object.keys(countryInfo), country: () => Object.keys(countryInfo),
chartOfAccounts: () => getCOAList().map(({ name }) => name), chartOfAccounts: () => getCOAList().map(({ name }) => name),

View File

@ -40,6 +40,7 @@ function removeFields(schemaMap: SchemaMap): SchemaMap {
schema.keywordFields = schema.keywordFields?.filter( schema.keywordFields = schema.keywordFields?.filter(
(fn) => fn !== fieldname (fn) => fn !== fieldname
); );
if (schema.inlineEditDisplayField === fieldname) { if (schema.inlineEditDisplayField === fieldname) {
delete schema.inlineEditDisplayField; delete schema.inlineEditDisplayField;
} }

View File

@ -3,7 +3,8 @@
id="app" id="app"
class="h-screen flex flex-col font-sans overflow-hidden antialiased" class="h-screen flex flex-col font-sans overflow-hidden antialiased"
> >
<!-- <WindowsTitleBar v-if="platform === 'Windows'" /> <WindowsTitleBar v-if="platform === 'Windows'" />
<!--
<Desk <Desk
class="flex-1" class="flex-1"
v-if="activeScreen === 'Desk'" v-if="activeScreen === 'Desk'"
@ -11,9 +12,9 @@
/>--> />-->
<DatabaseSelector <DatabaseSelector
v-if="activeScreen === 'DatabaseSelector'" v-if="activeScreen === 'DatabaseSelector'"
@database-connect="showSetupWizardOrDesk(true)" @file-selected="fileSelected"
/> />
<!--<SetupWizard <SetupWizard
v-if="activeScreen === 'SetupWizard'" v-if="activeScreen === 'SetupWizard'"
@setup-complete="setupComplete" @setup-complete="setupComplete"
@setup-canceled="setupCanceled" @setup-canceled="setupCanceled"
@ -25,18 +26,20 @@
> >
<div id="toast-target" /> <div id="toast-target" />
</div> </div>
<!-- TODO: check this and uncomment
<TelemetryModal />--> <TelemetryModal />-->
</div> </div>
</template> </template>
<script> <script>
import fs from 'fs/promises'; import fs from 'fs/promises';
import { fyo } from './initFyo'; import WindowsTitleBar from './components/WindowsTitleBar.vue';
import DatabaseSelector from './pages/DatabaseSelector'; import { fyo, initializeInstance } from './initFyo';
// import Desk from './pages/Desk'; import DatabaseSelector from './pages/DatabaseSelector.vue';
// import SetupWizard from './pages/SetupWizard/SetupWizard'; import SetupWizard from './pages/SetupWizard/SetupWizard.vue';
import './styles/index.css'; import './styles/index.css';
import { checkForUpdates, routeTo } from './utils'; import { checkForUpdates } from './utils/ipcCalls';
import { routeTo } from './utils/ui';
export default { export default {
name: 'App', name: 'App',
@ -47,9 +50,9 @@ export default {
}, },
components: { components: {
// Desk, // Desk,
// SetupWizard, SetupWizard,
DatabaseSelector, DatabaseSelector,
// WindowsTitleBar, WindowsTitleBar,
// TelemetryModal, // TelemetryModal,
}, },
async mounted() { async mounted() {
@ -73,29 +76,45 @@ export default {
} }
*/ */
this.activeScreen = 'DatabaseSelector'; // this.activeScreen = 'DatabaseSelector';
this.activeScreen = 'SetupWizard';
}, },
methods: { methods: {
async setupComplete() { async setupComplete() {
// TODO: Complete this // TODO: Complete this
// await postSetup(); // await postSetup();
await this.showSetupWizardOrDesk(true); // await this.showSetupWizardOrDesk(true);
}, },
async showSetupWizardOrDesk(resetRoute = false) { async fileSelected(filePath, isNew) {
const { setupComplete } = fyo.singles.AccountingSettings; console.log('from App.vue', filePath, isNew);
if (!setupComplete) { if (isNew) {
this.activeScreen = 'SetupWizard'; this.activeScreen = 'SetupWizard';
} else { return;
this.activeScreen = 'Desk';
await checkForUpdates(false);
} }
await this.showSetupWizardOrDesk(filePath);
},
async showSetupWizardOrDesk(filePath, resetRoute = false) {
const countryCode = await fyo.db.connectToDatabase(filePath);
const setupComplete = await getSetupComplete();
if (!setupComplete) {
this.activeScreen = 'SetupWizard';
return;
}
await initializeInstance(filePath, false, countryCode);
this.activeScreen = 'Desk';
await checkForUpdates(false);
if (!resetRoute) { if (!resetRoute) {
return; return;
} }
const { onboardingComplete } = await fyo.getSingle('GetStarted'); await this.setDeskRoute();
const { hideGetStarted } = await fyo.getSingle('SystemSettings'); },
async setDeskRoute() {
const { onboardingComplete } = await fyo.doc.getSingle('GetStarted');
const { hideGetStarted } = await fyo.doc.getSingle('SystemSettings');
if (hideGetStarted || onboardingComplete) { if (hideGetStarted || onboardingComplete) {
routeTo('/'); routeTo('/');
@ -105,14 +124,15 @@ export default {
}, },
async changeDbFile() { async changeDbFile() {
fyo.config.set('lastSelectedFilePath', null); fyo.config.set('lastSelectedFilePath', null);
telemetry.stop(); fyo.telemetry.stop();
// TODO: purgeCache(true) fyo.purgeCache();
// await purgeCache(true);
this.activeScreen = 'DatabaseSelector'; this.activeScreen = 'DatabaseSelector';
}, },
async setupCanceled() { async setupCanceled() {
const filePath = fyo.config.get('lastSelectedFilePath'); const filePath = fyo.config.get('lastSelectedFilePath');
await fs.unlink(filePath); if (filePath) {
await fs.unlink(filePath);
}
this.changeDbFile(); this.changeDbFile();
}, },
}, },

13
src/README.md Normal file
View File

@ -0,0 +1,13 @@
# src
This is where all the frontend code lives
## Initialization
New Instance
1. Run _Setup Wizard_ for initialization values (eg: `countryCode`).
2.
Existing Instance
1. Connect to db
2. Check if _Setup Wizard_ has been completed, if not, jump to **New Instance**
3. Call `initFyo/initializeInstance` with `dbPath` and `countryCode`

View File

@ -1,18 +1,19 @@
import Data from './Data'; <script>
import Select from './Select'; import { h } from 'vue';
import Link from './Link'; import AttachImage from './AttachImage';
import Date from './Date';
import Table from './Table';
import AutoComplete from './AutoComplete'; import AutoComplete from './AutoComplete';
import Check from './Check'; import Check from './Check';
import AttachImage from './AttachImage';
import DynamicLink from './DynamicLink';
import Int from './Int';
import Float from './Float';
import Currency from './Currency';
import Text from './Text';
import Color from './Color'; import Color from './Color';
import { h } from 'vue'; import Currency from './Currency';
import Data from './Data';
import Date from './Date';
import DynamicLink from './DynamicLink';
import Float from './Float';
import Int from './Int';
import Link from './Link';
import Select from './Select';
import Table from './Table';
import Text from './Text';
export default { export default {
name: 'FormControl', name: 'FormControl',
@ -48,3 +49,4 @@ export default {
}, },
}, },
}; };
</script>

View File

@ -9,9 +9,8 @@
<script> <script>
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts'; import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { setLanguageMap } from 'src/utils'; import { languageCodeMap, setLanguageMap } from 'src/utils/language';
import { languageCodeMap } from 'src/utils/language'; import FormControl from './FormControl.vue';
import FormControl from './FormControl';
export default { export default {
methods: { methods: {
@ -37,7 +36,7 @@ export default {
return { return {
fieldname: 'language', fieldname: 'language',
label: this.t`Language`, label: this.t`Language`,
fieldtype: 'Select', fieldtype: 'AutoComplete',
options: Object.keys(languageCodeMap), options: Object.keys(languageCodeMap),
default: fyo.config.get('language') ?? DEFAULT_LANGUAGE, default: fyo.config.get('language') ?? DEFAULT_LANGUAGE,
description: this.t`Set the display language.`, description: this.t`Set the display language.`,

View File

@ -119,7 +119,7 @@
import { t } from 'fyo'; import { t } from 'fyo';
import { getRandomString } from 'utils'; import { getRandomString } from 'utils';
import Button from './Button'; import Button from './Button';
import FormControl from './Controls/FormControl'; import FormControl from './Controls/FormControl.vue';
import Icon from './Icon'; import Icon from './Icon';
import Popover from './Popover'; import Popover from './Popover';

View File

@ -1,9 +1,10 @@
<template> <template>
<div class="text-sm" :class="{ 'border-t': !noBorder }"> <div class="text-sm" :class="{ 'border-t': !noBorder }">
<template v-for="df in formFields"> <template v-for="df in formFields">
<!-- Table Field Form (Eg: PaymentFor) -->
<FormControl <FormControl
:key="df.fieldname"
v-if="df.fieldtype === 'Table'" v-if="df.fieldtype === 'Table'"
:key="`${df.fieldname}-table`"
ref="controls" ref="controls"
size="small" size="small"
:df="df" :df="df"
@ -11,91 +12,93 @@
:read-only="evaluateReadOnly(df)" :read-only="evaluateReadOnly(df)"
@change="(value) => onChange(df, value)" @change="(value) => onChange(df, value)"
/> />
<template v-else>
<div class="border-b" :key="df.fieldname" v-if="renderInline(df)"> <!-- Inline Field Form (Eg: Address) -->
<TwoColumnForm <div
ref="inlineEditForm" v-else-if="renderInline(df)"
:doc="inlineEditDoc" class="border-b"
:fields="inlineEditFields" :key="`${df.fieldname}-inline`"
:column-ratio="columnRatio" >
:no-border="true" <TwoColumnForm
:focus-first-input="true" ref="inlineEditForm"
:autosave="false" :doc="inlineEditDoc"
@error="(msg) => $emit('error', msg)" :fields="inlineEditFields"
/> :column-ratio="columnRatio"
<div class="flex px-4 pb-2"> :no-border="true"
<Button class="w-1/2 text-gray-900" @click="inlineEditField = null"> :focus-first-input="true"
{{ t`Cancel` }} :autosave="false"
</Button> @error="(msg) => $emit('error', msg)"
<Button />
type="primary" <div class="flex px-4 pb-2">
class="ml-2 w-1/2 text-white" <Button class="w-1/2 text-gray-900" @click="inlineEditField = null">
@click="() => saveInlineEditDoc(df)" {{ t`Cancel` }}
> </Button>
{{ df.inlineSaveText || t`Save` }} <Button
</Button> type="primary"
</div> class="ml-2 w-1/2 text-white"
</div> @click="() => saveInlineEditDoc(df)"
<div
:key="df.fieldname + '-else'"
v-else
class="grid"
:class="{ 'border-b': !noBorder }"
:style="style"
>
<div class="py-2 pl-4 flex text-gray-600">
<div class="py-1">
{{ df.label }}
</div>
</div>
<div
class="py-2 pr-4"
@click="activateInlineEditing(df)"
:class="{
'pl-2': df.fieldtype === 'AttachImage',
}"
> >
<FormControl {{ df.inlineSaveText || t`Save` }}
ref="controls" </Button>
size="small" </div>
:df="df" </div>
:value="
df.inline && doc.getLink(df.fieldname) <!-- Regular Field Form -->
? doc.getLink(df.fieldname)[ <div
doc.getLink(df.fieldname).meta.inlineEditDisplayField || v-else
'name' class="grid"
] :class="{ 'border-b': !noBorder }"
: doc[df.fieldname] :key="`${df.fieldname}-regular`"
" :style="style"
:class="{ 'p-2': df.fieldtype === 'Check' }" >
:read-only="evaluateReadOnly(df)" <div class="py-2 pl-4 flex text-gray-600">
@change="(value) => onChange(df, value)" <div class="py-1">
@focus="activateInlineEditing(df)" {{ df.label }}
@new-doc="(newdoc) => onChange(df, newdoc.name)"
/>
<div
class="text-sm text-red-600 mt-2 pl-2"
v-if="errors[df.fieldname]"
>
{{ errors[df.fieldname] }}
</div>
</div> </div>
</div> </div>
</template>
<div
class="py-2 pr-4"
@click="activateInlineEditing(df)"
:class="{
'pl-2': df.fieldtype === 'AttachImage',
}"
>
<FormControl
ref="controls"
size="small"
:df="df"
:value="getRegularValue(df)"
:class="{ 'p-2': df.fieldtype === 'Check' }"
:read-only="evaluateReadOnly(df)"
@change="(value) => onChange(df, value)"
@focus="activateInlineEditing(df)"
@new-doc="(newdoc) => onChange(df, newdoc.name)"
/>
<div
class="text-sm text-red-600 mt-2 pl-2"
v-if="errors[df.fieldname]"
>
{{ errors[df.fieldname] }}
</div>
</div>
</div>
</template> </template>
</div> </div>
</template> </template>
<script> <script>
import Button from 'src/components/Button'; import Doc from 'fyo/model/doc';
import FormControl from 'src/components/Controls/FormControl'; import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import { getErrorMessage, handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getErrorMessage, handleErrorWithDialog } from '../errorHandling'; import { evaluateHidden, evaluateReadOnly } from 'src/utils/doc';
let TwoColumnForm = { export default {
name: 'TwoColumnForm', name: 'TwoColumnForm',
emits: ['error', 'change'], emits: ['error', 'change'],
props: { props: {
doc: Object, doc: Doc,
fields: Array, fields: Array,
autosave: Boolean, autosave: Boolean,
columnRatio: { columnRatio: {
@ -120,7 +123,7 @@ let TwoColumnForm = {
}, },
provide() { provide() {
return { return {
doctype: this.doc.doctype, schemaName: this.doc.schemaName,
name: this.doc.name, name: this.doc.name,
doc: this.doc, doc: this.doc,
}; };
@ -134,24 +137,27 @@ let TwoColumnForm = {
if (this.focusFirstInput) { if (this.focusFirstInput) {
this.$refs['controls'][0].focus(); this.$refs['controls'][0].focus();
} }
window.tcf = this;
}, },
methods: { methods: {
getRegularValue(df) {
if (!df.inline) {
return this.doc[df.fieldname];
}
const link = this.doc.getLink(df.fieldname);
if (!link) {
return this.doc[df.fieldname];
}
const fieldname = link.schema.inlineEditDisplayField ?? 'name';
return link[fieldname];
},
renderInline(df) { renderInline(df) {
return ( return (
this.inlineEditField?.fieldname === df?.fieldname && this.inlineEditDoc this.inlineEditField?.fieldname === df?.fieldname && this.inlineEditDoc
); );
}, },
evaluateBoolean(fieldProp, defaultValue) {
const type = typeof fieldProp;
switch (type) {
case 'undefined':
return defaultValue;
case 'function':
return fieldProp(this.doc);
default:
return !!fieldProp;
}
},
evaluateReadOnly(df) { evaluateReadOnly(df) {
if (df.fieldname === 'numberSeries' && !this.doc._notInserted) { if (df.fieldname === 'numberSeries' && !this.doc._notInserted) {
return true; return true;
@ -160,10 +166,8 @@ let TwoColumnForm = {
if (this.submitted) { if (this.submitted) {
return true; return true;
} }
return this.evaluateBoolean(df.readOnly, false);
}, return evaluateReadOnly(df, this.doc);
evaluateHidden(df) {
return this.evaluateBoolean(df.hidden, false);
}, },
onChange(df, value) { onChange(df, value) {
if (value == null || df.inline) { if (value == null || df.inline) {
@ -229,8 +233,8 @@ let TwoColumnForm = {
} }
this.inlineEditDisplayField = this.inlineEditDisplayField =
this.doc.meta.inlineEditDisplayField || 'name'; this.doc.schema.inlineEditDisplayField ?? 'name';
this.inlineEditFields = fyo.getMeta(df.target).getQuickEditFields(); this.inlineEditFields = fyo.schemaMap[df.target].quickEditFields ?? [];
}, },
async saveInlineEditDoc(df) { async saveInlineEditDoc(df) {
if (!this.inlineEditDoc) { if (!this.inlineEditDoc) {
@ -248,8 +252,8 @@ let TwoColumnForm = {
}, },
computed: { computed: {
formFields() { formFields() {
return (this.fields || this.doc.meta.getQuickEditFields()).filter( return (this.fields || this.doc.quickEditFields).filter(
(df) => !this.evaluateHidden(df) (field) => !evaluateHidden(field, this.doc)
); );
}, },
style() { style() {
@ -261,10 +265,8 @@ let TwoColumnForm = {
}; };
}, },
submitted() { submitted() {
return Boolean(this.doc.meta.isSubmittable && this.doc.submitted); return Boolean(this.doc.schema.isSubmittable && this.doc.submitted);
}, },
}, },
}; };
export default TwoColumnForm;
</script> </script>

View File

@ -54,7 +54,7 @@ import { getTelemetryOptions } from 'fyo/telemetry/helpers';
import { TelemetrySetting } from 'fyo/telemetry/types'; import { TelemetrySetting } from 'fyo/telemetry/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import Button from '../Button.vue'; import Button from '../Button.vue';
import FormControl from '../Controls/FormControl'; import FormControl from '../Controls/FormControl.vue';
import FeatherIcon from '../FeatherIcon.vue'; import FeatherIcon from '../FeatherIcon.vue';
import HowTo from '../HowTo.vue'; import HowTo from '../HowTo.vue';
import Modal from '../Modal.vue'; import Modal from '../Modal.vue';

View File

@ -1,15 +1,49 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { getRegionalModels, models } from 'models'; import { getRegionalModels, models } from 'models';
import { getValueMapFromList } from 'utils';
export const fyo = new Fyo({ isTest: false, isElectron: true }); export const fyo = new Fyo({ isTest: false, isElectron: true });
export async function initializeModels(dbPath: string, countryCode?: string) { async function closeDbIfConnected() {
if (countryCode) { if (!fyo.db.isConnected) {
return;
}
await fyo.db.close();
}
export async function initializeInstance(
dbPath: string,
isNew: boolean,
countryCode: string
) {
if (isNew) {
await closeDbIfConnected();
countryCode = await fyo.db.createNewDatabase(dbPath, countryCode); countryCode = await fyo.db.createNewDatabase(dbPath, countryCode);
} else { } else if (!fyo.db.isConnected) {
countryCode = await fyo.db.connectToDatabase(dbPath); countryCode = await fyo.db.connectToDatabase(dbPath);
} }
const regionalModels = await getRegionalModels(countryCode); const regionalModels = await getRegionalModels(countryCode);
await fyo.initializeAndRegister(models, regionalModels); await fyo.initializeAndRegister(models, regionalModels);
await setSingles();
await setCurrencySymbols();
}
async function setSingles() {
await fyo.doc.getSingle('AccountingSettings');
await fyo.doc.getSingle('GetStarted');
}
async function setCurrencySymbols() {
const currencies = (await fyo.db.getAll('Currency', {
fields: ['name', 'symbol'],
})) as { name: string; symbol: string }[];
fyo.currencySymbols = getValueMapFromList(
currencies,
'name',
'symbol'
) as Record<string, string | undefined>;
} }

View File

@ -338,7 +338,7 @@
<script> <script>
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue'; import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FeatherIcon from 'src/components/FeatherIcon.vue'; import FeatherIcon from 'src/components/FeatherIcon.vue';
import HowTo from 'src/components/HowTo.vue'; import HowTo from 'src/components/HowTo.vue';

View File

@ -1,13 +1,16 @@
<template> <template>
<div <div
class="py-10 flex-1 bg-white" class="py-10 flex-1 bg-white flex justify-center items-center"
:class="{ :class="{
'pointer-events-none': loadingDatabase, 'pointer-events-none': loadingDatabase,
'window-drag': platform !== 'Windows', 'window-drag': platform !== 'Windows',
}" }"
> >
<div class="w-full"> <div
<div class="px-12"> class="w-full w-600 shadow rounded-lg border relative"
style="height: 700px"
>
<div class="px-6 py-8">
<h1 class="text-2xl font-semibold"> <h1 class="text-2xl font-semibold">
{{ t`Welcome to Frappe Books` }} {{ t`Welcome to Frappe Books` }}
</h1> </h1>
@ -20,7 +23,7 @@
{{ t`Select a file to load the company transactions` }} {{ t`Select a file to load the company transactions` }}
</p> </p>
</div> </div>
<div class="px-12 mt-10 window-no-drag" v-if="!showFiles"> <div class="px-12 mt-6 window-no-drag" v-if="!showFiles">
<div class="flex"> <div class="flex">
<div <div
@click="newDatabase" @click="newDatabase"
@ -135,7 +138,7 @@
</span> </span>
</div> </div>
<div class="text-gray-700"> <div class="text-gray-700">
{{ getFileLastModified(file.filePath) }} {{ file.modified }}
</div> </div>
</div> </div>
</div> </div>
@ -148,26 +151,28 @@
</a> </a>
</div> </div>
</div> </div>
</div> <div
<div class="w-full flex justify-end absolute px-6 py-6"
class="w-full flex justify-end absolute px-8" style="top: 100%; transform: translateY(-100%)"
style="top: 100%; transform: translateY(-175%)" >
> <LanguageSelector class="w-40" />
<LanguageSelector class="w-28" input-class="text-base" /> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import fs from 'fs'; import fs from 'fs';
import { cloneDeep } from 'lodash';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue'; import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getSavePath } from 'src/utils/ipcCalls';
import { IPC_ACTIONS } from 'utils/messages'; import { IPC_ACTIONS } from 'utils/messages';
export default { export default {
name: 'DatabaseSelector', name: 'DatabaseSelector',
emits: ['database-connect'], emits: ['file-selected'],
data() { data() {
return { return {
loadingDatabase: false, loadingDatabase: false,
@ -187,17 +192,23 @@ export default {
}, },
methods: { methods: {
setFiles() { setFiles() {
this.files = fyo.config this.files = cloneDeep(fyo.config.get('files', [])).filter(
.get('files', []) ({ filePath }) => fs.existsSync(filePath)
.filter(({ filePath }) => fs.existsSync(filePath)); );
for (const file of this.files) {
const stats = fs.statSync(file.filePath);
file.modified = DateTime.fromJSDate(stats.mtime).toRelative();
}
}, },
async newDatabase() { async newDatabase() {
/*
TODO: Refactor this
this.fileSelectedFrom = 'New File'; this.fileSelectedFrom = 'New File';
let filePath = await createNewDatabase(); const { filePath, canceled } = await getSavePath('books', 'db');
this.connectToDatabase(filePath); if (canceled || !filePath) {
*/ return;
}
this.connectToDatabase(filePath, true);
}, },
async existingDatabase() { async existingDatabase() {
this.fileSelectedFrom = 'Existing File'; this.fileSelectedFrom = 'Existing File';
@ -214,35 +225,17 @@ export default {
this.fileSelectedFrom = file; this.fileSelectedFrom = file;
await this.connectToDatabase(file.filePath); await this.connectToDatabase(file.filePath);
}, },
async connectToDatabase(filePath) { async connectToDatabase(filePath, isNew) {
/*
TODO: Refactor this
if (!filePath) { if (!filePath) {
return; return;
} }
this.loadingDatabase = true;
const { connectionSuccess, reason } = await connectToLocalDatabase( if (isNew) {
filePath this.$emit('file-selected', filePath, isNew);
);
this.loadingDatabase = false;
if (connectionSuccess) {
this.$emit('database-connect');
return; return;
} }
const title = this.t`DB Connection Error`;
let content = this.$emit('file-selected', filePath, !!isNew);
this.t`Please select an existing database or create a new one.` +
` reason: ${reason}, filePath: ${filePath}`;
if (reason === DB_CONN_FAILURE.CANT_OPEN) {
content = this
.t`Can't open database file: ${filePath}. Please create a new file.`;
}
await showErrorDialog(title, content);
*/
},
getFileLastModified(filePath) {
let stats = fs.statSync(filePath);
return DateTime.fromJSDate(stats.mtime).toRelative();
}, },
}, },
components: { LanguageSelector }, components: { LanguageSelector },

View File

@ -201,7 +201,7 @@
import { getInvoiceStatus } from 'models/helpers'; import { getInvoiceStatus } from 'models/helpers';
import BackLink from 'src/components/BackLink'; import BackLink from 'src/components/BackLink';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions'; import DropdownWithActions from 'src/components/DropdownWithActions';
import PageHeader from 'src/components/PageHeader'; import PageHeader from 'src/components/PageHeader';
import StatusBadge from 'src/components/StatusBadge'; import StatusBadge from 'src/components/StatusBadge';

View File

@ -130,7 +130,7 @@
<script> <script>
import BackLink from 'src/components/BackLink'; import BackLink from 'src/components/BackLink';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions'; import DropdownWithActions from 'src/components/DropdownWithActions';
import PageHeader from 'src/components/PageHeader'; import PageHeader from 'src/components/PageHeader';
import StatusBadge from 'src/components/StatusBadge'; import StatusBadge from 'src/components/StatusBadge';

View File

@ -79,7 +79,7 @@
<script> <script>
import { t } from 'fyo'; import { t } from 'fyo';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions'; import DropdownWithActions from 'src/components/DropdownWithActions';
import StatusBadge from 'src/components/StatusBadge'; import StatusBadge from 'src/components/StatusBadge';
import TwoColumnForm from 'src/components/TwoColumnForm'; import TwoColumnForm from 'src/components/TwoColumnForm';

View File

@ -139,7 +139,7 @@
import { getReportData } from 'reports/index'; import { getReportData } from 'reports/index';
import reportViewConfig from 'reports/view'; import reportViewConfig from 'reports/view';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue'; import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FeatherIcon from 'src/components/FeatherIcon.vue'; import FeatherIcon from 'src/components/FeatherIcon.vue';
import PageHeader from 'src/components/PageHeader'; import PageHeader from 'src/components/PageHeader';

View File

@ -47,7 +47,7 @@
</template> </template>
<script> <script>
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import TwoColumnForm from 'src/components/TwoColumnForm'; import TwoColumnForm from 'src/components/TwoColumnForm';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { IPC_ACTIONS } from 'utils/messages'; import { IPC_ACTIONS } from 'utils/messages';

View File

@ -41,7 +41,7 @@
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { getTelemetryOptions } from 'fyo/telemetry/helpers'; import { getTelemetryOptions } from 'fyo/telemetry/helpers';
import { TelemetrySetting } from 'fyo/telemetry/types'; import { TelemetrySetting } from 'fyo/telemetry/types';
import FormControl from 'src/components/Controls/FormControl'; import FormControl from 'src/components/Controls/FormControl.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue'; import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import TwoColumnForm from 'src/components/TwoColumnForm'; import TwoColumnForm from 'src/components/TwoColumnForm';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';

View File

@ -1,28 +1,24 @@
<template> <template>
<div <div class="w-600 shadow rounded-lg border relative" style="height: 700px">
class="flex-1 py-10 bg-white h-screen" <div class="px-6 py-8">
:class="{
'window-drag': platform !== 'Windows',
}"
>
<div class="px-12">
<h1 class="text-2xl font-semibold"><slot name="title"></slot></h1> <h1 class="text-2xl font-semibold"><slot name="title"></slot></h1>
</div> </div>
<div class="px-8 mt-5 window-no-drag"> <div class="px-6 window-no-drag">
<slot name="content"></slot> <slot name="content"></slot>
</div> </div>
<div <div
class="flex justify-between px-8 mt-5 window-no-drag absolute w-full" class="flex justify-between px-6 pb-6 window-no-drag absolute w-600"
style="top: 100%; transform: translateY(-260%)" style="top: 100%; transform: translateY(-100%)"
> >
<Button class="text-sm text-grey-900" @click="$emit('secondary-clicked')"> <Button class="text-sm text-grey-900 w-28" @click="$emit('secondary-clicked')">
<slot name="secondaryButton"></slot> <slot name="secondaryButton"></slot>
</Button> </Button>
<Button <Button
@click="$emit('primary-clicked')" @click="$emit('primary-clicked')"
type="primary" type="primary"
class="text-sm text-white" class="text-sm text-white w-28"
:disabled="primaryDisabled" :disabled="primaryDisabled"
> >
<slot name="primaryButton"></slot> <slot name="primaryButton"></slot>

View File

@ -1,39 +0,0 @@
import { createNumberSeries } from 'fyo/model/naming';
import { DEFAULT_SERIES_START } from 'fyo/utils/consts';
import { getValueMapFromList } from 'utils';
import { fyo } from './initFyo';
export async function postStart() {
await createDefaultNumberSeries();
await setSingles();
await setCurrencySymbols();
}
async function createDefaultNumberSeries() {
await createNumberSeries('SINV-', 'SalesInvoice', DEFAULT_SERIES_START, fyo);
await createNumberSeries(
'PINV-',
'PurchaseInvoice',
DEFAULT_SERIES_START,
fyo
);
await createNumberSeries('PAY-', 'Payment', DEFAULT_SERIES_START, fyo);
await createNumberSeries('JV-', 'JournalEntry', DEFAULT_SERIES_START, fyo);
}
async function setSingles() {
await fyo.doc.getSingle('AccountingSettings');
await fyo.doc.getSingle('GetStarted');
}
async function setCurrencySymbols() {
const currencies = (await fyo.db.getAll('Currency', {
fields: ['name', 'symbol'],
})) as { name: string; symbol: string }[];
fyo.currencySymbols = getValueMapFromList(
currencies,
'name',
'symbol'
) as Record<string, string | undefined>;
}

View File

@ -1,8 +1,13 @@
import countryInfo from 'fixtures/countryInfo.json'; import countryInfo from 'fixtures/countryInfo.json';
import { ConfigFile, DocValueMap } from 'fyo/core/types'; import { ConfigFile, DocValueMap } from 'fyo/core/types';
import Doc from 'fyo/model/doc'; import Doc from 'fyo/model/doc';
import { createNumberSeries } from 'fyo/model/naming';
import { getId } from 'fyo/telemetry/helpers'; import { getId } from 'fyo/telemetry/helpers';
import { DEFAULT_CURRENCY, DEFAULT_LOCALE } from 'fyo/utils/consts'; import {
DEFAULT_CURRENCY,
DEFAULT_LOCALE,
DEFAULT_SERIES_START,
} from 'fyo/utils/consts';
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings'; import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { createRegionalRecords } from 'src/regional'; import { createRegionalRecords } from 'src/regional';
@ -21,6 +26,7 @@ export default async function setupInstance(
await createCurrencyRecords(); await createCurrencyRecords();
await createAccountRecords(bankName, country, chartOfAccounts); await createAccountRecords(bankName, country, chartOfAccounts);
await createRegionalRecords(country); await createRegionalRecords(country);
await createDefaultNumberSeries();
await completeSetup(companyName); await completeSetup(companyName);
} }
@ -215,3 +221,15 @@ async function getBankAccountParentName(country: string) {
return parentBankAccount[0].name; return parentBankAccount[0].name;
} }
async function createDefaultNumberSeries() {
await createNumberSeries('SINV-', 'SalesInvoice', DEFAULT_SERIES_START, fyo);
await createNumberSeries(
'PINV-',
'PurchaseInvoice',
DEFAULT_SERIES_START,
fyo
);
await createNumberSeries('PAY-', 'Payment', DEFAULT_SERIES_START, fyo);
await createNumberSeries('JV-', 'JournalEntry', DEFAULT_SERIES_START, fyo);
}

View File

@ -51,4 +51,8 @@ html {
.no-scrollbar::-webkit-scrollbar { .no-scrollbar::-webkit-scrollbar {
display: none; display: none;
} }
.w-600 {
width: 600px;
}

28
src/utils/doc.ts Normal file
View File

@ -0,0 +1,28 @@
import Doc from 'fyo/model/doc';
import { Field } from 'schemas/types';
export function evaluateReadOnly(field: Field, doc: Doc) {
if (field.readOnly !== undefined) {
return field.readOnly;
}
const readOnlyFunc = doc.readOnly[field.fieldname];
if (readOnlyFunc !== undefined) {
return readOnlyFunc();
}
return false;
}
export function evaluateHidden(field: Field, doc: Doc) {
if (field.hidden !== undefined) {
return field.hidden;
}
const hiddenFunction = doc.hidden[field.fieldname];
if (hiddenFunction !== undefined) {
return hiddenFunction();
}
return false;
}

View File

@ -1,4 +1,9 @@
import { getSingleValue } from 'fyo/utils';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
import { ModelNameEnum } from 'models/types';
import SetupWizardSchema from 'schemas/app/SetupWizard.json';
import { Schema } from 'schemas/types';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
export async function getDatesAndPeriodicity( export async function getDatesAndPeriodicity(
@ -25,3 +30,24 @@ export async function getDatesAndPeriodicity(
periodicity, periodicity,
}; };
} }
export async function getSetupWizardDoc() {
/**
* This is used cause when setup wizard is running
* the database isn't yet initialized.
*/
return await fyo.doc.getNewDoc(
'SetupWizard',
{},
SetupWizardSchema as Schema,
SetupWizard
);
}
export async function getSetupComplete(): Promise<boolean> {
return !!(await getSingleValue(
'setupComplete',
ModelNameEnum.AccountingSettings,
fyo
));
}