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

incr: get setupwizard to display

- make converter more explicit
This commit is contained in:
18alantom 2022-04-23 14:53:44 +05:30
parent b3533698c0
commit 76c61a5c29
31 changed files with 529 additions and 339 deletions

View File

@ -1,7 +1,11 @@
import { Fyo } from 'fyo';
import Doc from 'fyo/model/doc';
import { isPesa } from 'fyo/utils';
import { ValueError } from 'fyo/utils/errors';
import { DateTime } from 'luxon';
import Money from 'pesa/dist/types/src/money';
import { FieldType, FieldTypeEnum, RawValue } from 'schemas/types';
import { Field, FieldTypeEnum, RawValue } from 'schemas/types';
import { getIsNullOrUndef } from 'utils';
import { DatabaseHandler } from './dbHandler';
import { DocValue, DocValueMap, RawValueMap } from './types';
@ -51,55 +55,41 @@ export class Converter {
}
}
static toDocValue(
value: RawValue,
fieldtype: FieldType,
fyo: Fyo
): DocValue {
switch (fieldtype) {
static toDocValue(value: RawValue, field: Field, fyo: Fyo): DocValue {
switch (field.fieldtype) {
case FieldTypeEnum.Currency:
return fyo.pesa((value ?? 0) as string | number);
return toDocCurrency(value, field, fyo);
case FieldTypeEnum.Date:
return new Date(value as string);
return toDocDate(value, field);
case FieldTypeEnum.Datetime:
return new Date(value as string);
return toDocDate(value, field);
case FieldTypeEnum.Int:
return +(value as string | number);
return toDocInt(value, field);
case FieldTypeEnum.Float:
return +(value as string | number);
return toDocFloat(value, field);
case FieldTypeEnum.Check:
return Boolean(value as number);
return toDocCheck(value, field);
default:
return String(value);
}
}
static toRawValue(value: DocValue, fieldtype: FieldType): RawValue {
switch (fieldtype) {
static toRawValue(value: DocValue, field: Field, fyo: Fyo): RawValue {
switch (field.fieldtype) {
case FieldTypeEnum.Currency:
return (value as Money).store;
return toRawCurrency(value, fyo, field);
case FieldTypeEnum.Date:
return (value as Date).toISOString().split('T')[0];
return toRawDate(value, field);
case FieldTypeEnum.Datetime:
return (value as Date).toISOString();
case FieldTypeEnum.Int: {
if (typeof value === 'string') {
return parseInt(value);
}
return Math.floor(value as number);
}
case FieldTypeEnum.Float: {
if (typeof value === 'string') {
return parseFloat(value);
}
return value as number;
}
return toRawDateTime(value, field);
case FieldTypeEnum.Int:
return toRawInt(value, field);
case FieldTypeEnum.Float:
return toRawFloat(value, field);
case FieldTypeEnum.Check:
return Number(value);
return toRawCheck(value, field);
default:
return String(value);
return toRawString(value, field);
}
}
@ -118,7 +108,7 @@ export class Converter {
} else {
docValueMap[fieldname] = Converter.toDocValue(
rawValue,
field.fieldtype,
field,
this.fyo
);
}
@ -146,7 +136,8 @@ export class Converter {
} else {
rawValueMap[fieldname] = Converter.toRawValue(
docValue,
field.fieldtype
field,
this.fyo
);
}
}
@ -154,3 +145,181 @@ export class Converter {
return rawValueMap;
}
}
function toDocDate(value: RawValue, field: Field) {
if (typeof value !== 'number' && typeof value !== 'string') {
throwError(value, field, 'doc');
}
const date = new Date(value);
if (date.toString() === 'Invalid Date') {
throwError(value, field, 'doc');
}
return date;
}
function toDocCurrency(value: RawValue, field: Field, fyo: Fyo) {
if (typeof value === 'string') {
return fyo.pesa(value);
}
if (typeof value === 'number') {
return fyo.pesa(value);
}
if (typeof value === 'boolean') {
return fyo.pesa(Number(value));
}
throwError(value, field, 'doc');
}
function toDocInt(value: RawValue, field: Field): number {
if (typeof value === 'string') {
value = parseInt(value);
}
return toDocFloat(value, field);
}
function toDocFloat(value: RawValue, field: Field): number {
if (typeof value === 'boolean') {
return Number(value);
}
if (typeof value === 'string') {
value = parseFloat(value);
}
if (typeof value === 'number' && !Number.isNaN(value)) {
return value;
}
throwError(value, field, 'doc');
}
function toDocCheck(value: RawValue, field: Field): boolean {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'string') {
return value === '1';
}
if (typeof value === 'number') {
return Boolean(value);
}
throwError(value, field, 'doc');
}
function toRawCurrency(value: DocValue, fyo: Fyo, field: Field): string {
if (isPesa(value)) {
return (value as Money).store;
}
if (getIsNullOrUndef(value)) {
return fyo.pesa(0).store;
}
if (typeof value === 'number') {
return fyo.pesa(value).store;
}
if (typeof value === 'string') {
return fyo.pesa(value).store;
}
throwError(value, field, 'raw');
}
function toRawInt(value: DocValue, field: Field): number {
if (typeof value === 'string') {
return parseInt(value);
}
if (getIsNullOrUndef(value)) {
return 0;
}
if (typeof value === 'number') {
return Math.floor(value as number);
}
throwError(value, field, 'raw');
}
function toRawFloat(value: DocValue, field: Field): number {
if (typeof value === 'string') {
return parseFloat(value);
}
if (getIsNullOrUndef(value)) {
return 0;
}
if (typeof value === 'number') {
return value;
}
throwError(value, field, 'raw');
}
function toRawDate(value: DocValue, field: Field): string {
const dateTime = toRawDateTime(value, field);
return dateTime.split('T')[0];
}
function toRawDateTime(value: DocValue, field: Field): string {
if (getIsNullOrUndef(value)) {
return '';
}
if (typeof value === 'string') {
return value;
}
if (value instanceof Date) {
return (value as Date).toISOString();
}
if (value instanceof DateTime) {
return (value as DateTime).toISO();
}
throwError(value, field, 'raw');
}
function toRawCheck(value: DocValue, field: Field): number {
if (typeof value === 'number') {
value = Boolean(value);
}
if (typeof value === 'boolean') {
return Number(value);
}
throwError(value, field, 'raw');
}
function toRawString(value: DocValue, field: Field): string {
if (getIsNullOrUndef(value)) {
return '';
}
if (typeof value === 'string') {
return value;
}
throwError(value, field, 'raw');
}
function throwError<T>(value: T, field: Field, type: 'raw' | 'doc'): never {
throw new ValueError(
`invalid ${type} conversion '${value}' of type ${typeof value} found, field: ${JSON.stringify(
field
)}`
);
}

View File

@ -129,8 +129,8 @@ export class DatabaseHandler extends DatabaseBase {
const docSingleValue: SingleValue<DocValue> = [];
for (const sv of rawSingleValue) {
const fieldtype = this.fieldValueMap[sv.parent][sv.fieldname].fieldtype;
const value = Converter.toDocValue(sv.value, fieldtype, this.#fyo);
const field = this.fieldValueMap[sv.parent][sv.fieldname];
const value = Converter.toDocValue(sv.value, field, this.#fyo);
docSingleValue.push({
value,

View File

@ -1,6 +1,7 @@
import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import { Verb } from 'fyo/telemetry/types';
import { DEFAULT_USER } from 'fyo/utils/consts';
import {
Conflict,
MandatoryError,
@ -123,7 +124,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
}
setDirty(value: boolean) {
_setDirty(value: boolean) {
this._dirty = value;
if (this.schema.isChild && this.parentdoc) {
this.parentdoc._dirty = value;
@ -133,27 +134,15 @@ export default class Doc extends Observable<DocValue | Doc[]> {
// set value and trigger change
async set(fieldname: string | DocValueMap, value?: DocValue | Doc[]) {
if (typeof fieldname === 'object') {
this.setMultiple(fieldname as DocValueMap);
await this.setMultiple(fieldname as DocValueMap);
return;
}
if (fieldname === 'numberSeries' && !this._notInserted) {
if (!this._canSet(fieldname, value)) {
return;
}
if (value === undefined) {
return;
}
if (
this.fieldMap[fieldname] === undefined ||
(this[fieldname] !== undefined &&
areDocValuesEqual(this[fieldname] as DocValue, value as DocValue))
) {
return;
}
this.setDirty(true);
this._setDirty(true);
if (Array.isArray(value)) {
this[fieldname] = value.map((row, i) => {
row.idx = i;
@ -161,16 +150,16 @@ export default class Doc extends Observable<DocValue | Doc[]> {
});
} else {
const field = this.fieldMap[fieldname];
await this.validateField(field, value);
await this._validateField(field, value);
this[fieldname] = value;
}
// always run applyChange from the parentdoc
if (this.schema.isChild && this.parentdoc) {
await this.applyChange(fieldname);
await this.parentdoc.applyChange(this.parentfield as string);
await this._applyChange(fieldname);
await this.parentdoc._applyChange(this.parentfield as string);
} else {
await this.applyChange(fieldname);
await this._applyChange(fieldname);
}
}
@ -180,8 +169,29 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
}
async applyChange(fieldname: string) {
await this.applyFormula(fieldname);
_canSet(fieldname: string, value?: DocValue | Doc[]): boolean {
if (fieldname === 'numberSeries' && !this._notInserted) {
return false;
}
if (value === undefined) {
return false;
}
if (this.fieldMap[fieldname] === undefined) {
return false;
}
const currentValue = this.get(fieldname);
if (currentValue === undefined) {
return true;
}
return !areDocValuesEqual(currentValue as DocValue, value as DocValue);
}
async _applyChange(fieldname: string) {
await this._applyFormula(fieldname);
await this.trigger('change', {
doc: this,
changed: fieldname,
@ -218,7 +228,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
// push child row and trigger change
this.push(fieldname, docValueMap);
this._dirty = true;
this.applyChange(fieldname);
this._applyChange(fieldname);
}
push(fieldname: string, docValueMap: Doc | DocValueMap = {}) {
@ -256,11 +266,11 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
async validateInsert() {
this.validateMandatory();
await this.validateFields();
this._validateMandatory();
await this._validateFields();
}
validateMandatory() {
_validateMandatory() {
const checkForMandatory: Doc[] = [this];
const tableFields = this.schema.fields.filter(
(f) => f.fieldtype === FieldTypeEnum.Table
@ -282,7 +292,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
}
async validateFields() {
async _validateFields() {
const fields = this.schema.fields;
for (const field of fields) {
if (field.fieldtype === FieldTypeEnum.Table) {
@ -290,11 +300,11 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
const value = this.get(field.fieldname) as DocValue;
await this.validateField(field, value);
await this._validateField(field, value);
}
}
async validateField(field: Field, value: DocValue) {
async _validateField(field: Field, value: DocValue) {
if (field.fieldtype == 'Select') {
validateSelect(field as OptionField, value as string);
}
@ -329,25 +339,25 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return data;
}
setBaseMetaValues() {
_setBaseMetaValues() {
if (this.schema.isSubmittable && typeof this.submitted !== 'boolean') {
this.submitted = false;
this.cancelled = false;
}
if (!this.createdBy) {
this.createdBy = this.fyo.auth.session.user;
this.createdBy = this.fyo.auth.session.user || DEFAULT_USER;
}
if (!this.created) {
this.created = new Date();
}
this.updateModified();
this._updateModified();
}
updateModified() {
this.modifiedBy = this.fyo.auth.session.user;
_updateModified() {
this.modifiedBy = this.fyo.auth.session.user || DEFAULT_USER;
this.modified = new Date();
}
@ -474,11 +484,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
}
async applyFormula(fieldname?: string) {
if (fieldname && this.formulas[fieldname] === undefined) {
return false;
}
async _applyFormula(fieldname?: string) {
const doc = this;
let changed = false;
@ -492,7 +498,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
(fn) => this.fieldMap[fn]
);
changed ||= await this.applyFormulaForFields(
changed ||= await this._applyFormulaForFields(
formulaFields,
row,
fieldname
@ -503,22 +509,27 @@ export default class Doc extends Observable<DocValue | Doc[]> {
const formulaFields = Object.keys(this.formulas).map(
(fn) => this.fieldMap[fn]
);
changed ||= await this.applyFormulaForFields(formulaFields, doc, fieldname);
changed ||= await this._applyFormulaForFields(
formulaFields,
doc,
fieldname
);
return changed;
}
async applyFormulaForFields(
async _applyFormulaForFields(
formulaFields: Field[],
doc: Doc,
fieldname?: string
) {
let changed = false;
for (const field of formulaFields) {
if (!shouldApplyFormula(field, doc, fieldname)) {
const shouldApply = shouldApplyFormula(field, doc, fieldname);
if (!shouldApply) {
continue;
}
const newVal = await this.getValueFromFormula(field, doc);
const newVal = await this._getValueFromFormula(field, doc);
const previousVal = doc.get(field.fieldname);
const isSame = areDocValuesEqual(newVal as DocValue, previousVal);
if (newVal === undefined || isSame) {
@ -532,15 +543,18 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return changed;
}
async getValueFromFormula(field: Field, doc: Doc) {
let value: FormulaReturn;
const formula = doc.formulas[field.fieldtype];
async _getValueFromFormula(field: Field, doc: Doc) {
const formula = doc.formulas[field.fieldname];
if (formula === undefined) {
return;
}
value = await formula();
let value: FormulaReturn;
try {
value = await formula();
} catch {
return;
}
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
value = value.map((row) => this._initChild(row, field.fieldname));
}
@ -551,13 +565,13 @@ export default class Doc extends Observable<DocValue | Doc[]> {
async commit() {
// re-run triggers
this.setChildIdx();
await this.applyFormula();
await this._applyFormula();
await this.trigger('validate', null);
}
async insert() {
await setName(this, this.fyo);
this.setBaseMetaValues();
this._setBaseMetaValues();
await this.commit();
await this.validateInsert();
await this.trigger('beforeInsert', null);
@ -587,7 +601,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
if (this.flags.revertAction) await this.trigger('beforeRevert');
// update modifiedBy and modified
this.updateModified();
this._updateModified();
const data = this.getValidDict();
await this.fyo.db.update(this.schemaName, data);

View File

@ -88,7 +88,7 @@ function getMandatory(doc: Doc): Field[] {
}
export function shouldApplyFormula(field: Field, doc: Doc, fieldname?: string) {
if (!doc.formulas[field.fieldtype]) {
if (!doc.formulas[field.fieldname]) {
return false;
}

View File

@ -23,7 +23,7 @@ export function validateSelect(field: OptionField, value: string) {
return;
}
if (!field.required && (value === null || value === undefined)) {
if (!field.required && !value) {
return;
}

View File

@ -1,8 +1,9 @@
export const DEFAULT_INTERNAL_PRECISION = 11;
export const DEFAULT_DISPLAY_PRECISION = 2;
export const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd';
export const DEFAULT_DATE_FORMAT = 'MMM d, y';
export const DEFAULT_LOCALE = 'en-IN';
export const DEFAULT_COUNTRY_CODE = 'in';
export const DEFAULT_CURRENCY = 'INR';
export const DEFAULT_LANGUAGE = 'English';
export const DEFAULT_SERIES_START = 1001;
export const DEFAULT_SERIES_START = 1001;
export const DEFAULT_USER = 'Admin';

View File

@ -129,12 +129,14 @@ function getNumberFormatter(fyo: Fyo) {
}
function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string {
if (doc && doc.getCurrencies[field.fieldname]) {
return doc.getCurrencies[field.fieldname]();
let getCurrency = doc?.getCurrencies[field.fieldname];
if (getCurrency !== undefined) {
return getCurrency();
}
if (doc && doc.parentdoc?.getCurrencies[field.fieldname]) {
return doc.parentdoc.getCurrencies[field.fieldname]();
getCurrency = doc?.parentdoc?.getCurrencies[field.fieldname];
if (getCurrency !== undefined) {
return getCurrency();
}
return (fyo.singles.SystemSettings?.currency as string) ?? DEFAULT_CURRENCY;

View File

@ -1,7 +1,7 @@
import Doc from 'fyo/model/doc';
import { FiltersMap, ListsMap, ValidationMap } from 'fyo/model/types';
import { validateEmail } from 'fyo/model/validationFunction';
import countryInfo from '../../../fixtures/countryInfo.json';
import { getCountryInfo } from 'utils/misc';
export class AccountingSettings extends Doc {
static filters: FiltersMap = {
@ -20,6 +20,6 @@ export class AccountingSettings extends Doc {
};
static lists: ListsMap = {
country: () => Object.keys(countryInfo),
country: () => Object.keys(getCountryInfo()),
};
}

View File

@ -3,7 +3,7 @@ import Doc from 'fyo/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types';
import { stateCodeMap } from 'regional/in';
import { titleCase } from 'utils';
import countryInfo from '../../../fixtures/countryInfo.json';
import { getCountryInfo } from 'utils/misc';
export class Address extends Doc {
formulas: FormulaMap = {
@ -32,7 +32,7 @@ export class Address extends Doc {
}
},
country() {
return Object.keys(countryInfo).sort();
return Object.keys(getCountryInfo()).sort();
},
};

View File

@ -1,9 +1,14 @@
import { t } from 'fyo';
import Doc from 'fyo/model/doc';
import { FormulaMap, ListsMap, ValidationMap } from 'fyo/model/types';
import {
DependsOnMap,
FormulaMap,
ListsMap,
ValidationMap,
} from 'fyo/model/types';
import { validateEmail } from 'fyo/model/validationFunction';
import { DateTime } from 'luxon';
import countryInfo from '../../../fixtures/countryInfo.json';
import { getCountryInfo } from 'utils/misc';
export function getCOAList() {
return [
@ -26,14 +31,21 @@ export function getCOAList() {
}
export class SetupWizard extends Doc {
dependsOn: DependsOnMap = {
fiscalYearStart: ['country'],
fiscalYearEnd: ['country'],
currency: ['country'],
chartOfAccounts: ['country'],
};
formulas: FormulaMap = {
fiscalYearStart: async () => {
if (!this.country) return;
const today = DateTime.local();
// @ts-ignore
const fyStart = countryInfo[this.country].fiscal_year_start as
const countryInfo = getCountryInfo();
const fyStart = countryInfo[this.country as string]?.fiscal_year_start as
| string
| undefined;
@ -50,8 +62,8 @@ export class SetupWizard extends Doc {
const today = DateTime.local();
// @ts-ignore
const fyEnd = countryInfo[this.country].fiscal_year_end as
const countryInfo = getCountryInfo();
const fyEnd = countryInfo[this.country as string]?.fiscal_year_end as
| string
| undefined;
if (fyEnd) {
@ -64,8 +76,8 @@ export class SetupWizard extends Doc {
if (!this.country) {
return;
}
// @ts-ignore
return countryInfo[this.country].currency;
const countryInfo = getCountryInfo();
return countryInfo[this.country as string]?.currency;
},
chartOfAccounts: async () => {
const country = this.get('country') as string | undefined;
@ -73,7 +85,7 @@ export class SetupWizard extends Doc {
return;
}
// @ts-ignore
const countryInfo = getCountryInfo();
const code = (countryInfo[country] as undefined | { code: string })?.code;
if (code === undefined) {
return;
@ -94,7 +106,7 @@ export class SetupWizard extends Doc {
};
static lists: ListsMap = {
country: () => Object.keys(countryInfo),
country: () => Object.keys(getCountryInfo()),
chartOfAccounts: () => getCOAList().map(({ name }) => name),
};
}

View File

@ -10,6 +10,13 @@
v-if="activeScreen === 'Desk'"
@change-db-file="changeDbFile"
/>-->
<div
v-if="activeScreen === 'Desk'"
class="h-screen w-screen flex justify-center items-center bg-white"
>
<h1>Desk</h1>
</div>
<DatabaseSelector
v-if="activeScreen === 'DatabaseSelector'"
@file-selected="fileSelected"
@ -17,7 +24,7 @@
<SetupWizard
v-if="activeScreen === 'SetupWizard'"
@setup-complete="setupComplete"
@setup-canceled="setupCanceled"
@setup-canceled="changeDbFile"
/>
<div
id="toast-container"
@ -26,17 +33,23 @@
>
<div id="toast-target" />
</div>
<!-- TODO: check this and uncomment
<TelemetryModal />-->
<TelemetryModal />
</div>
</template>
<script>
import fs from 'fs/promises';
import { ConfigKeys } from 'fyo/core/types';
import {
getSetupComplete,
incrementOpenCount,
startTelemetry
} from 'src/utils/misc';
import TelemetryModal from './components/once/TelemetryModal.vue';
import WindowsTitleBar from './components/WindowsTitleBar.vue';
import { fyo, initializeInstance } from './initFyo';
import DatabaseSelector from './pages/DatabaseSelector.vue';
import SetupWizard from './pages/SetupWizard/SetupWizard.vue';
import setupInstance from './setup/setupInstance';
import './styles/index.css';
import { checkForUpdates } from './utils/ipcCalls';
import { routeTo } from './utils/ui';
@ -53,48 +66,45 @@ export default {
SetupWizard,
DatabaseSelector,
WindowsTitleBar,
// TelemetryModal,
TelemetryModal,
},
async mounted() {
fyo.telemetry.platform = this.platform;
/*
const lastSelectedFilePath = fyo.config.get('lastSelectedFilePath', null);
const { connectionSuccess, reason } = await connectToLocalDatabase(
lastSelectedFilePath
const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath,
null
);
if (connectionSuccess) {
this.showSetupWizardOrDesk(false);
if (lastSelectedFilePath) {
await this.fileSelected(lastSelectedFilePath, false);
return;
}
if (lastSelectedFilePath) {
const title = this.t`DB Connection Error`;
const content = `reason: ${reason}, filePath: ${lastSelectedFilePath}`;
await showErrorDialog(title, content);
}
*/
// this.activeScreen = 'DatabaseSelector';
this.activeScreen = 'SetupWizard';
this.activeScreen = 'DatabaseSelector';
},
methods: {
async setupComplete() {
// TODO: Complete this
// await postSetup();
// await this.showSetupWizardOrDesk(true);
async setDesk() {
this.activeScreen = 'Desk';
incrementOpenCount();
await startTelemetry();
await checkForUpdates(false);
await this.setDeskRoute();
},
async fileSelected(filePath, isNew) {
console.log('from App.vue', filePath, isNew);
fyo.config.set(ConfigKeys.LastSelectedFilePath, filePath);
if (isNew) {
this.activeScreen = 'SetupWizard';
return;
}
await this.showSetupWizardOrDesk(filePath);
},
async showSetupWizardOrDesk(filePath, resetRoute = false) {
async setupComplete(setupWizardOptions) {
const filePath = fyo.config.get(ConfigKeys.LastSelectedFilePath);
await setupInstance(filePath, setupWizardOptions);
await this.setDesk();
},
async showSetupWizardOrDesk(filePath) {
const countryCode = await fyo.db.connectToDatabase(filePath);
const setupComplete = await getSetupComplete();
@ -104,13 +114,7 @@ export default {
}
await initializeInstance(filePath, false, countryCode);
this.activeScreen = 'Desk';
await checkForUpdates(false);
if (!resetRoute) {
return;
}
await this.setDeskRoute();
await this.setDesk();
},
async setDeskRoute() {
const { onboardingComplete } = await fyo.doc.getSingle('GetStarted');
@ -128,13 +132,6 @@ export default {
fyo.purgeCache();
this.activeScreen = 'DatabaseSelector';
},
async setupCanceled() {
const filePath = fyo.config.get('lastSelectedFilePath');
if (filePath) {
await fs.unlink(filePath);
}
this.changeDbFile();
},
},
};
</script>

View File

@ -2,12 +2,18 @@
This is where all the frontend code lives
## Initialization
New Instance
1. Run _Setup Wizard_ for initialization values (eg: `countryCode`).
2.
## Fyo Initialization
Existing Instance
1. Connect to db
The initialization flows are different when the instance is new or is existing.
All of them are triggered from `src/App.vue`.
**New Instance**
1. Run _Setup Wizard_ for init values (eg: `country`).
2. Call `setupInstance.ts/setupInstance` using init values.
**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`
3. Call `initFyo/initializeInstance` with `dbPath` and `countryCode`

View File

@ -22,19 +22,19 @@
<script>
export default {
name: 'Base',
props: [
'df',
'value',
'inputClass',
'placeholder',
'size',
'showLabel',
'readOnly',
'autofocus',
],
props: {
df: Object,
value: [String, Number, Boolean, Object],
inputClass: [Function, String],
placeholder: String,
size: String,
showLabel: Boolean,
readOnly: Boolean,
autofocus: Boolean,
},
emits: ['focus', 'input', 'change'],
inject: {
doctype: {
schemaName: {
default: null,
},
name: {

View File

@ -3,6 +3,7 @@
<div class="text-gray-600 text-sm mb-1" v-if="showLabel">
{{ df.label }}
</div>
<DatePicker
ref="input"
:input-class="[inputClasses, 'cursor-text']"
@ -10,31 +11,31 @@
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
:format-value="formatValue"
@change="value => triggerChange(value)"
@change="(value) => triggerChange(value)"
/>
</div>
</template>
<script>
import { fyo } from 'src/initFyo';
import Base from './Base';
import DatePicker from '../DatePicker/DatePicker';
import Base from './Base';
export default {
name: 'Date',
extends: Base,
components: {
DatePicker
DatePicker,
},
computed: {
inputType() {
return 'date';
}
},
},
methods: {
formatValue(value) {
return fyo.format(value, this.df);
}
}
},
},
};
</script>

View File

@ -1,19 +1,19 @@
<script>
import { h } from 'vue';
import AttachImage from './AttachImage';
import AutoComplete from './AutoComplete';
import Check from './Check';
import Color from './Color';
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';
import AttachImage from './AttachImage.vue';
import AutoComplete from './AutoComplete.vue';
import Check from './Check.vue';
import Color from './Color.vue';
import Currency from './Currency.vue';
import Data from './Data.vue';
import Date from './Date.vue';
import DynamicLink from './DynamicLink.vue';
import Float from './Float.vue';
import Int from './Int.vue';
import Link from './Link.vue';
import Select from './Select.vue';
import Table from './Table.vue';
import Text from './Text.vue';
export default {
name: 'FormControl',

View File

@ -2,7 +2,7 @@
import { t } from 'fyo';
import Badge from 'src/components/Badge';
import { fyo } from 'src/initFyo';
import { openQuickEdit } from 'src/utils';
import { openQuickEdit } from 'src/utils/ui';
import { markRaw } from 'vue';
import AutoComplete from './AutoComplete';

View File

@ -109,7 +109,7 @@
togglePopover();
"
>
Clear
{{ t`Clear` }}
</div>
</div>
</div>

View File

@ -73,8 +73,8 @@
</template>
<script>
import Popover from './Popover';
import uniq from 'lodash/uniq';
import Popover from './Popover.vue';
export default {
name: 'Dropdown',

View File

@ -45,7 +45,7 @@ import { t } from 'fyo';
import reports from 'reports/view';
import Dropdown from 'src/components/Dropdown';
import { fyo } from 'src/initFyo';
import { routeTo } from 'src/utils';
import { routeTo } from 'src/utils/ui';
export default {

View File

@ -36,6 +36,7 @@
</template>
<script>
import { getColorClass } from 'src/utils/colors';
import FeatherIcon from './FeatherIcon.vue';
export default {
components: {

View File

@ -170,17 +170,16 @@ export default {
return evaluateReadOnly(df, this.doc);
},
onChange(df, value) {
if (value == null || df.inline) {
if (df.inline) {
return;
}
let oldValue = this.doc.get(df.fieldname);
this.errors[df.fieldname] = null;
if (oldValue === value) {
return;
}
this.errors[df.fieldname] = null;
if (this.emitChange) {
this.$emit('change', df, value, oldValue);
}

View File

@ -31,7 +31,7 @@
<script>
import { ipcRenderer } from 'electron';
import { runWindowAction } from 'src/utils';
import { runWindowAction } from 'src/utils/ipcCalls';
import { IPC_MESSAGES } from 'utils/messages';
export default {

View File

@ -1,5 +1,8 @@
<template>
<div>
<div
class="py-10 flex-1 bg-white flex justify-center items-center window-drag"
>
<!-- 0: Language Selection Slide -->
<Slide
@primary-clicked="handlePrimary"
@secondary-clicked="handleSecondary"
@ -26,6 +29,8 @@
{{ t`Next` }}
</template>
</Slide>
<!-- 1: Setup Wizard Slide -->
<Slide
:primary-disabled="!valuesFilled || loading"
@primary-clicked="handlePrimary"
@ -35,51 +40,48 @@
<template #title>
{{ t`Setup your organization` }}
</template>
<template #content>
<div v-if="doc">
<div
class="flex items-center px-6 py-5 mb-4 border bg-brand rounded-xl"
>
<div class="flex items-center px-6 py-5 mb-8 bg-brand rounded-xl">
<FormControl
:df="meta.getField('companyLogo')"
:df="getField('companyLogo')"
:value="doc.companyLogo"
@change="(value) => setValue('companyLogo', value)"
/>
<div class="ml-2">
<FormControl
ref="companyField"
:df="meta.getField('companyName')"
:df="getField('companyName')"
:value="doc.companyName"
@change="(value) => setValue('companyName', value)"
:input-class="
(classes) => [
() => [
'bg-transparent font-semibold text-xl text-white placeholder-blue-200 focus:outline-none focus:bg-blue-600 px-3 rounded py-1',
]
"
:autofocus="true"
/>
<Popover placement="auto" :show-popup="Boolean(emailError)">
<template #target>
<FormControl
:df="meta.getField('email')"
:value="doc.email"
@change="(value) => setValue('email', value)"
:input-class="
(classes) => [
'text-base bg-transparent text-white placeholder-blue-200 focus:bg-blue-600 focus:outline-none rounded px-3 py-1',
]
"
/>
</template>
<template #content>
<div class="p-2 text-sm">
{{ emailError }}
</div>
</template>
</Popover>
<FormControl
:df="getField('email')"
:value="doc.email"
@change="(value) => setValue('email', value)"
:input-class="
() => [
'text-base bg-transparent text-white placeholder-blue-200 focus:bg-blue-600 focus:outline-none rounded px-3 py-1',
]
"
/>
</div>
</div>
<TwoColumnForm :fields="fields" :doc="doc" />
<p
class="px-3 -mt-6 text-sm absolute text-red-400 w-full"
v-if="emailError"
>
{{ emailError }}
</p>
<TwoColumnForm :doc="doc" />
</div>
</template>
<template #secondaryButton>{{ t`Back` }}</template>
@ -90,22 +92,14 @@
<script>
import { ipcRenderer } from 'electron';
import fs from 'fs';
import path from 'path';
import FormControl from 'src/components/Controls/FormControl';
import FormControl from 'src/components/Controls/FormControl.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import Popover from 'src/components/Popover';
import TwoColumnForm from 'src/components/TwoColumnForm';
import { getErrorMessage } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { connectToLocalDatabase, purgeCache } from 'src/initialization';
import { setupInstance } from 'src/setup/setupInstance';
import { setLanguageMap, showMessageDialog } from 'src/utils';
import { getSetupWizardDoc } from 'src/utils/misc';
import { showMessageDialog } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages';
import {
getErrorMessage,
handleErrorWithDialog,
showErrorDialog
} from '../../errorHandling';
import Slide from './Slide.vue';
export default {
@ -122,14 +116,13 @@ export default {
},
provide() {
return {
doctype: 'SetupWizard',
schemaName: 'SetupWizard',
name: 'SetupWizard',
};
},
components: {
TwoColumnForm,
FormControl,
Popover,
Slide,
LanguageSelector,
},
@ -138,12 +131,15 @@ export default {
this.index = 1;
}
this.doc = await fyo.doc.getNewDoc('SetupWizard');
this.doc = await getSetupWizardDoc();
this.doc.on('change', () => {
this.valuesFilled = this.allValuesFilled();
});
},
methods: {
getField(fieldname) {
return this.doc.schema?.fields.find((f) => f.fieldname === fieldname);
},
openContributingTranslations() {
ipcRenderer.send(
IPC_MESSAGES.OPEN_EXTERNAL,
@ -164,10 +160,6 @@ export default {
this.$emit('setup-canceled');
}
},
async selectLanguage(value) {
const success = await setLanguageMap(value);
this.setValue('language', value);
},
setValue(fieldname, value) {
this.emailError = null;
this.doc.set(fieldname, value).catch((e) => {
@ -178,7 +170,7 @@ export default {
});
},
allValuesFilled() {
let values = this.meta.quickEditFields.map(
const values = this.doc.schema.quickEditFields.map(
(fieldname) => this.doc[fieldname]
);
return values.every(Boolean);
@ -189,61 +181,18 @@ export default {
return;
}
try {
this.loading = true;
await setupInstance(this.doc);
this.$emit('setup-complete');
} catch (e) {
this.loading = false;
if (e.type === fyo.errors.DuplicateEntryError) {
console.log(e);
console.log('retrying');
await this.renameDbFileAndRerunSetup();
} else {
handleErrorWithDialog(e, this.doc);
}
}
},
async renameDbFileAndRerunSetup() {
const filePath = fyo.config.get('lastSelectedFilePath');
renameDbFile(filePath);
await purgeCache();
const { connectionSuccess, reason } = await connectToLocalDatabase(
filePath
);
if (connectionSuccess) {
await setupInstance(this.doc);
this.$emit('setup-complete');
} else {
const title = this.t`DB Connection Error`;
const content = `reason: ${reason}, filePath: ${filePath}`;
await showErrorDialog(title, content);
}
this.loading = true;
this.$emit('setup-complete', this.doc.getValidDict());
},
},
computed: {
meta() {
return fyo.getMeta('SetupWizard');
},
fields() {
return this.meta.getQuickEditFields();
},
buttonText() {
return this.loading ? this.t`Setting Up...` : this.t`Submit`;
if (this.loading) {
return this.t`Submit`;
}
return this.t`Setting Up...`;
},
},
};
function renameDbFile(filePath) {
const dirname = path.dirname(filePath);
const basename = path.basename(filePath);
const backupPath = path.join(dirname, `_${basename}`);
if (fs.existsSync(backupPath)) {
fs.unlinkSync(backupPath);
}
fs.renameSync(filePath, backupPath);
}
</script>

View File

@ -7,7 +7,7 @@ import Badge from './components/Badge.vue';
import FeatherIcon from './components/FeatherIcon.vue';
import { getErrorHandled, handleError } from './errorHandling';
import { fyo } from './initFyo';
import { incrementOpenCount, outsideClickDirective } from './renderer/helpers';
import { outsideClickDirective } from './renderer/helpers';
import registerIpcRendererListeners from './renderer/registerIpcRendererListeners';
import router from './router';
import { stringifyCircular } from './utils';
@ -33,7 +33,7 @@ import { setLanguageMap } from './utils/language';
app.use(router);
app.component('App', App);
app.component('feather-icon', FeatherIcon);
app.component('FeatherIcon', FeatherIcon);
app.component('Badge', Badge);
app.directive('on-outside-click', outsideClickDirective);
@ -62,8 +62,6 @@ import { setLanguageMap } from './utils/language';
});
fyo.store.appVersion = await ipcRenderer.invoke(IPC_ACTIONS.GET_VERSION);
incrementOpenCount();
app.mount('body');
})();

View File

@ -1,5 +1,3 @@
import { ConfigKeys } from 'fyo/core/types';
import { fyo } from 'src/initFyo';
import { Directive } from 'vue';
const instances: OutsideClickCallback[] = [];
@ -35,14 +33,3 @@ function onDocumentClick(e: Event, el: HTMLElement, fn: OutsideClickCallback) {
fn(e);
}
}
export function incrementOpenCount() {
let openCount = fyo.config.get(ConfigKeys.OpenCount);
if (typeof openCount !== 'number') {
openCount = 1;
} else {
openCount += 1;
}
fyo.config.set(ConfigKeys.OpenCount, openCount);
}

View File

@ -1,6 +1,7 @@
import { ipcRenderer } from 'electron';
import { handleError } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { startTelemetry } from 'src/utils/misc';
import { showToast } from 'src/utils/ui';
import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
@ -56,7 +57,7 @@ export default function registerIpcRendererListeners() {
document.addEventListener('visibilitychange', function () {
const { visibilityState } = document;
if (visibilityState === 'visible' && !fyo.telemetry.started) {
fyo.telemetry.start();
startTelemetry();
}
if (visibilityState !== 'hidden') {

View File

@ -1,4 +1,3 @@
import countryInfo from 'fixtures/countryInfo.json';
import { ConfigFile, DocValueMap } from 'fyo/core/types';
import Doc from 'fyo/model/doc';
import { createNumberSeries } from 'fyo/model/naming';
@ -9,16 +8,21 @@ import {
DEFAULT_SERIES_START,
} from 'fyo/utils/consts';
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
import { fyo } from 'src/initFyo';
import { fyo, initializeInstance } from 'src/initFyo';
import { createRegionalRecords } from 'src/regional';
import { getCountryCodeFromCountry, getCountryInfo } from 'utils/misc';
import { CountryInfo } from 'utils/types';
import { createCOA } from './createCOA';
import { CountrySettings, SetupWizardOptions } from './types';
import { SetupWizardOptions } from './types';
export default async function setupInstance(
dbPath: string,
setupWizardOptions: SetupWizardOptions
) {
const { companyName, country, bankName, chartOfAccounts } =
setupWizardOptions;
await initializeDatabase(dbPath, country);
await updateSystemSettings(setupWizardOptions);
await updateAccountingSettings(setupWizardOptions);
await updatePrintSettings(setupWizardOptions);
@ -31,10 +35,15 @@ export default async function setupInstance(
await completeSetup(companyName);
}
async function initializeDatabase(dbPath: string, country: string) {
const countryCode = getCountryCodeFromCountry(country);
await initializeInstance(dbPath, true, countryCode);
}
async function updateAccountingSettings({
companyName,
country,
name,
fullname,
email,
bankName,
fiscalYearStart,
@ -46,7 +55,7 @@ async function updateAccountingSettings({
await accountingSettings.setAndUpdate({
companyName,
country,
fullname: name,
fullname,
email,
bankName,
fiscalYearStart,
@ -73,8 +82,8 @@ async function updateSystemSettings({
country,
currency: companyCurrency,
}: SetupWizardOptions) {
// @ts-ignore
const countryOptions = countryInfo[country] as CountrySettings;
const countryInfo = getCountryInfo();
const countryOptions = countryInfo[country] as CountryInfo;
const currency =
companyCurrency ?? countryOptions.currency ?? DEFAULT_CURRENCY;
const locale = countryOptions.locale ?? DEFAULT_LOCALE;
@ -88,10 +97,7 @@ async function updateSystemSettings({
async function createCurrencyRecords() {
const promises: Promise<Doc | undefined>[] = [];
const queue: string[] = [];
const countrySettings: CountrySettings[] = Object.values(
// @ts-ignore
countryInfo as Record<string, CountrySettings>
);
const countrySettings = Object.values(getCountryInfo()) as CountryInfo[];
for (const country of countrySettings) {
const {

View File

@ -3,7 +3,6 @@ export interface SetupWizardOptions {
companyName: string;
country: string;
fullname: string;
name: string;
email: string;
bankName: string;
currency: string;
@ -11,15 +10,3 @@ export interface SetupWizardOptions {
fiscalYearEnd: string;
chartOfAccounts: string;
}
export interface CountrySettings {
code: string;
currency: string;
fiscal_year_start: string;
fiscal_year_end: string;
locale: string;
currency_fraction?: string;
currency_fraction_units?: number;
smallest_currency_fraction_value?: number;
currency_symbol?: string;
}

View File

@ -1,3 +1,4 @@
import { ConfigKeys } from 'fyo/core/types';
import { getSingleValue } from 'fyo/utils';
import { DateTime } from 'luxon';
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
@ -51,3 +52,31 @@ export async function getSetupComplete(): Promise<boolean> {
fyo
));
}
export function incrementOpenCount() {
let openCount = fyo.config.get(ConfigKeys.OpenCount);
if (typeof openCount !== 'number') {
openCount = 1;
} else {
openCount += 1;
}
fyo.config.set(ConfigKeys.OpenCount, openCount);
}
export async function startTelemetry() {
fyo.telemetry.interestingDocs = [
ModelNameEnum.Payment,
ModelNameEnum.PaymentFor,
ModelNameEnum.SalesInvoice,
ModelNameEnum.SalesInvoiceItem,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.PurchaseInvoiceItem,
ModelNameEnum.JournalEntry,
ModelNameEnum.JournalEntryAccount,
ModelNameEnum.Party,
ModelNameEnum.Account,
ModelNameEnum.Tax,
];
await fyo.telemetry.start();
}

17
utils/misc.ts Normal file
View File

@ -0,0 +1,17 @@
import countryInfo from 'fixtures/countryInfo.json';
import { CountryInfoMap } from './types';
export function getCountryInfo(): CountryInfoMap {
// @ts-ignore
return countryInfo as CountryInfoMap;
}
export function getCountryCodeFromCountry(countryName: string): string {
const countryInfoMap = getCountryInfo();
const countryInfo = countryInfoMap[countryName];
if (countryInfo === undefined) {
return '';
}
return countryInfo.code;
}

View File

@ -1,2 +1,16 @@
export type Translation = { translation: string; context?: string };
export type LanguageMap = Record<string, Translation>;
export type CountryInfoMap = Record<string, CountryInfo | undefined>;
export interface CountryInfo {
code: string;
currency: string;
currency_fraction?: string;
currency_fraction_units?: number;
smallest_currency_fraction_value?: number;
currency_symbol?: string;
timezones?: string[];
fiscal_year_start: string;
fiscal_year_end: string;
locale: string;
}