2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 14:48:25 +00:00

fix: make setupInstance code work

This commit is contained in:
18alantom 2022-04-25 12:03:31 +05:30
parent 9877bf4fd3
commit 1b85fee5e6
19 changed files with 166 additions and 75 deletions

View File

@ -198,9 +198,9 @@ export async function assertDoesNotThrow(
await func();
} catch (err) {
throw new assert.AssertionError({
message: `Missing expected exception: ${message} Error: ${
message: `Got unwanted exception: ${message}\nError: ${
(err as Error).message
}`,
}\n${(err as Error).stack}`,
});
}
}

View File

@ -4,7 +4,7 @@ import { isPesa } from 'fyo/utils';
import { ValueError } from 'fyo/utils/errors';
import { DateTime } from 'luxon';
import Money from 'pesa/dist/types/src/money';
import { Field, FieldTypeEnum, RawValue } from 'schemas/types';
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
import { getIsNullOrUndef } from 'utils';
import { DatabaseHandler } from './dbHandler';
import { DocValue, DocValueMap, RawValueMap } from './types';
@ -70,7 +70,7 @@ export class Converter {
case FieldTypeEnum.Check:
return toDocCheck(value, field);
default:
return String(value);
return toDocString(value, field);
}
}
@ -102,8 +102,9 @@ export class Converter {
const rawValue = rawValueMap[fieldname];
if (Array.isArray(rawValue)) {
const parentSchemaName = (field as TargetField).target;
docValueMap[fieldname] = rawValue.map((rv) =>
this.#toDocValueMap(schemaName, rv)
this.#toDocValueMap(parentSchemaName, rv)
);
} else {
docValueMap[fieldname] = Converter.toDocValue(
@ -126,12 +127,14 @@ export class Converter {
const docValue = docValueMap[fieldname];
if (Array.isArray(docValue)) {
const parentSchemaName = (field as TargetField).target;
rawValueMap[fieldname] = docValue.map((value) => {
if (value instanceof Doc) {
return this.#toRawValueMap(schemaName, value.getValidDict());
return this.#toRawValueMap(parentSchemaName, value.getValidDict());
}
return this.#toRawValueMap(schemaName, value as DocValueMap);
return this.#toRawValueMap(parentSchemaName, value as DocValueMap);
});
} else {
rawValueMap[fieldname] = Converter.toRawValue(
@ -146,6 +149,18 @@ export class Converter {
}
}
function toDocString(value: RawValue, field: Field) {
if (typeof value === 'string') {
return value;
}
if (value === null) {
return value;
}
throwError(value, field, 'doc');
}
function toDocDate(value: RawValue, field: Field) {
if (typeof value !== 'number' && typeof value !== 'string') {
throwError(value, field, 'doc');

View File

@ -3,7 +3,7 @@ import { DocValue, DocValueMap } from 'fyo/core/types';
import { Verb } from 'fyo/telemetry/types';
import { DEFAULT_USER } from 'fyo/utils/consts';
import {
Conflict,
ConflictError,
MandatoryError,
NotFoundError,
ValidationError,
@ -57,7 +57,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
*/
idx?: number;
parentdoc?: Doc;
parentfield?: string;
parentFieldname?: string;
parentSchemaName?: string;
_links?: Record<string, Doc>;
_dirty: boolean = true;
@ -115,7 +116,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
this.push(fieldname, row);
}
} else {
this[fieldname] = value ?? null;
this[fieldname] = value ?? this[fieldname] ?? null;
}
}
}
@ -153,7 +154,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
// 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.parentdoc._applyChange(this.parentFieldname as string);
} else {
await this._applyChange(fieldname);
}
@ -196,23 +197,20 @@ export class Doc extends Observable<DocValue | Doc[]> {
_setDefaults() {
for (const field of this.schema.fields) {
if (!getIsNullOrUndef(this[field.fieldname])) {
continue;
}
let defaultValue: DocValue | Doc[] = getPreDefaultValues(
field.fieldtype,
this.fyo
);
const defaultFunction = this.defaults[field.fieldname];
const defaultFunction =
this.fyo.models[this.schemaName]?.defaults?.[field.fieldname];
if (defaultFunction !== undefined) {
defaultValue = defaultFunction();
} else if (field.default !== undefined) {
defaultValue = field.default;
}
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
if (field.fieldtype === FieldTypeEnum.Currency && !isPesa(defaultValue)) {
defaultValue = this.fyo.pesa!(defaultValue as string | number);
}
@ -242,8 +240,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
const data: Record<string, unknown> = Object.assign({}, docValueMap);
data.parent = this.name;
data.parenttype = this.schemaName;
data.parentfield = fieldname;
data.parentSchemaName = this.schemaName;
data.parentFieldname = fieldname;
data.parentdoc = this;
if (!data.idx) {
@ -329,6 +327,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
value = (value as Money).copy();
}
if (value === null && this.schema.isSingle) {
continue;
}
data[field.fieldname] = value;
}
return data;
@ -348,10 +350,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
this.created = new Date();
}
this._updateModified();
this._updateModifiedMetaValues();
}
_updateModified() {
_updateModifiedMetaValues() {
this.modifiedBy = this.fyo.auth.session.user || DEFAULT_USER;
this.modified = new Date();
}
@ -442,20 +444,19 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
async _compareWithCurrentDoc() {
if (this.isNew || !this.name) {
if (this.isNew || !this.name || this.schema.isSingle) {
return;
}
const currentDoc = await this.fyo.db.get(this.schemaName, this.name);
const dbValues = await this.fyo.db.get(this.schemaName, this.name);
const docModified = this.modified as Date;
const dbModified = dbValues.modified as Date;
// check for conflict
if (
currentDoc &&
(this.modified as Date) !== (currentDoc.modified as Date)
) {
throw new Conflict(
if (dbValues && docModified !== dbModified) {
throw new ConflictError(
this.fyo
.t`Document ${this.schemaName} ${this.name} has been modified after loading`
.t`Document ${this.schemaName} ${this.name} has been modified after loading` +
`${docModified?.toISOString()}, ${dbModified?.toISOString()}`
);
}
@ -466,11 +467,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
// set submit action flag
if (this.submitted && !currentDoc.submitted) {
if (this.submitted && !dbValues.submitted) {
this.flags.submitAction = true;
}
if (currentDoc.submitted && !this.submitted) {
if (dbValues.submitted && !this.submitted) {
this.flags.revertAction = true;
}
}
@ -568,15 +569,16 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this.trigger('beforeInsert', null);
const oldName = this.name!;
const data = await this.fyo.db.insert(this.schemaName, this.getValidDict());
const validDict = this.getValidDict();
const data = await this.fyo.db.insert(this.schemaName, validDict);
this.syncValues(data);
this._notInserted = false;
if (oldName !== this.name) {
this.fyo.doc.removeFromCache(this.schemaName, oldName);
}
await this.trigger('afterInsert', null);
await this.trigger('afterSave', null);
this.fyo.telemetry.log(Verb.Created, this.schemaName);
return this;
@ -592,14 +594,13 @@ export class Doc extends Observable<DocValue | Doc[]> {
if (this.flags.revertAction) await this.trigger('beforeRevert');
// update modifiedBy and modified
this._updateModified();
this._updateModifiedMetaValues();
const data = this.getValidDict();
await this.fyo.db.update(this.schemaName, data);
this.syncValues(data);
await this.trigger('afterUpdate');
await this.trigger('afterSave');
// after submit
if (this.flags.submitAction) await this.trigger('afterSubmit');
@ -609,11 +610,15 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
async sync() {
await this.trigger('beforeSync');
let doc;
if (this._notInserted) {
return await this._insert();
doc = await this._insert();
} else {
return await this._update();
doc = await this._update();
}
await this.trigger('afterSync');
return doc;
}
async delete() {
@ -738,14 +743,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
async afterInsert() {}
async beforeUpdate() {}
async afterUpdate() {}
async afterSave() {}
async beforeSync() {}
async afterSync() {}
async beforeDelete() {}
async afterDelete() {}
async beforeRevert() {}
async afterRevert() {}
formulas: FormulaMap = {};
defaults: DefaultMap = {};
validations: ValidationMap = {};
required: RequiredMap = {};
hidden: HiddenMap = {};
@ -755,6 +760,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
static lists: ListsMap = {};
static filters: FiltersMap = {};
static defaults: DefaultMap = {};
static emptyMessages: EmptyMessageMap = {};
static getListViewSettings(fyo: Fyo): ListViewSettings {

View File

@ -56,11 +56,11 @@ export function getMissingMandatoryMessage(doc: Doc) {
return isNullOrUndef || value === '';
})
.map((f) => f.label)
.map((f) => f.label ?? f.fieldname)
.join(', ');
if (message && doc.schema.isChild && doc.parentdoc && doc.parentfield) {
const parentfield = doc.parentdoc.fieldMap[doc.parentfield];
if (message && doc.schema.isChild && doc.parentdoc && doc.parentFieldname) {
const parentfield = doc.parentdoc.fieldMap[doc.parentFieldname];
return `${parentfield.label} Row ${(doc.idx ?? 0) + 1}: ${message}`;
}
@ -75,11 +75,7 @@ function getMandatory(doc: Doc): Field[] {
}
const requiredFunction = doc.required[field.fieldname];
if (typeof requiredFunction !== 'function') {
continue;
}
if (requiredFunction()) {
if (requiredFunction?.()) {
mandatoryFields.push(field);
}
}

View File

@ -67,5 +67,5 @@ export class CannotCommitError extends DatabaseError {
}
export class ValueError extends ValidationError {}
export class Conflict extends ValidationError {}
export class ConflictError extends ValidationError {}
export class InvalidFieldError extends ValidationError {}

View File

@ -1,6 +1,7 @@
import { Fyo } from 'fyo';
import { Doc } from 'fyo/model/doc';
import {
DefaultMap,
FiltersMap,
ListViewSettings,
TreeViewSettings,
@ -13,6 +14,16 @@ export class Account extends Doc {
accountType?: AccountType;
parentAccount?: string;
static defaults: DefaultMap = {
/**
* NestedSet indices are actually not used
* this needs updation as they may be required
* later on.
*/
lft: () => 0,
rgt: () => 0,
};
async beforeInsert() {
if (this.accountType || !this.parentAccount) {
return;

View File

@ -188,7 +188,7 @@ export abstract class Invoice extends Doc {
},
};
defaults: DefaultMap = {
static defaults: DefaultMap = {
date: () => new Date().toISOString().slice(0, 10),
};

View File

@ -48,7 +48,7 @@ export class JournalEntry extends Doc {
await this.getPosting().postReverse();
}
defaults: DefaultMap = {
static defaults: DefaultMap = {
date: () => DateTime.local().toISODate(),
};

View File

@ -281,7 +281,7 @@ export class Payment extends Doc {
);
}
defaults: DefaultMap = { date: () => new Date().toISOString() };
static defaults: DefaultMap = { date: () => new Date().toISOString() };
formulas: FormulaMap = {
account: async () => {

View File

@ -16,7 +16,7 @@
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps",
"script:translate": "ts-node -O '{\"module\":\"commonjs\"}' scripts/generateTranslations.ts",
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --reporter nyan --require ts-node/register --require tsconfig-paths/register ./**/tests/**/*.spec.ts"
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --reporter nyan --require ts-node/register --require tsconfig-paths/register ./**/tests/**/*.spec.ts --exit"
},
"dependencies": {
"@popperjs/core": "^2.10.2",
@ -52,6 +52,7 @@
"@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^9",
"babel-loader": "^8.2.3",
"dotenv": "^16.0.0",
"electron": "^15.3.5",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.1.1",

View File

@ -6,7 +6,7 @@
"isSubmittable": false,
"fields": [
{
"fieldname": "companyLogo",
"fieldname": "logo",
"label": "Company Logo",
"fieldtype": "AttachImage"
},

View File

@ -78,6 +78,7 @@
"fieldname": "countryCode",
"label": "Country Code",
"fieldtype": "Data",
"default": "in",
"description": "Country code used to initialize regional settings."
},
{

View File

@ -14,7 +14,6 @@ export enum FieldTypeEnum {
Currency = 'Currency',
Text = 'Text',
Color = 'Color',
Code = 'Code',
}
export type FieldType = keyof typeof FieldTypeEnum;

View File

@ -45,9 +45,9 @@
<div v-if="doc">
<div class="flex items-center px-6 py-5 mb-8 bg-brand rounded-xl">
<FormControl
:df="getField('companyLogo')"
:value="doc.companyLogo"
@change="(value) => setValue('companyLogo', value)"
:df="getField('logo')"
:value="doc.logo"
@change="(value) => setValue('logo', value)"
/>
<div class="ml-2">
<FormControl

View File

@ -70,15 +70,15 @@ async function updateAccountingSettings(
}
async function updatePrintSettings(
{ companyLogo, companyName, email }: SetupWizardOptions,
{ logo, companyName, email }: SetupWizardOptions,
fyo: Fyo
) {
const printSettings = await fyo.doc.getSingle('PrintSettings');
await printSettings.setAndSync({
logo: companyLogo,
logo,
companyName,
email,
displayLogo: companyLogo ? true : false,
displayLogo: logo ? true : false,
});
}
@ -91,10 +91,13 @@ async function updateSystemSettings(
const currency =
companyCurrency ?? countryOptions.currency ?? DEFAULT_CURRENCY;
const locale = countryOptions.locale ?? DEFAULT_LOCALE;
const countryCode = getCountryCodeFromCountry(country);
const systemSettings = await fyo.doc.getSingle('SystemSettings');
systemSettings.setAndSync({
locale,
currency,
countryCode,
});
}
@ -154,8 +157,7 @@ async function createAccountRecords(
async function completeSetup(companyName: string, fyo: Fyo) {
updateInitializationConfig(companyName, fyo);
await fyo.singles.AccountingSettings!.set('setupComplete', true);
await fyo.singles.AccountingSettings!.sync();
await fyo.singles.AccountingSettings!.setAndSync('setupComplete', true);
}
function updateInitializationConfig(companyName: string, fyo: Fyo) {

View File

@ -1,5 +1,5 @@
export interface SetupWizardOptions {
companyLogo: string;
logo: string | null;
companyName: string;
country: string;
fullname: string;

View File

@ -1,9 +1,10 @@
import { config } from 'dotenv';
import { SetupWizardOptions } from 'src/setup/types';
import { getFiscalYear } from 'utils/misc';
export function getTestSetupWizardOptions(): SetupWizardOptions {
return {
companyLogo: '',
logo: null,
companyName: 'Test Company',
country: 'India',
fullname: 'Test Person',
@ -15,3 +16,8 @@ export function getTestSetupWizardOptions(): SetupWizardOptions {
chartOfAccounts: 'India - Chart of Accounts',
};
}
export function getTestDbPath() {
config();
return process.env.TEST_DB_PATH ?? ':memory:';
}

View File

@ -1,25 +1,74 @@
import * as assert from 'assert';
import { DatabaseManager } from 'backend/database/manager';
import { assertDoesNotThrow } from 'backend/database/tests/helpers';
import { Fyo } from 'fyo';
import { DummyAuthDemux } from 'fyo/tests/helpers';
import 'mocha';
import setupInstance from 'src/setup/setupInstance';
import { getTestSetupWizardOptions } from './helpers';
import { SetupWizardOptions } from 'src/setup/types';
import { getValueMapFromList } from 'utils';
import { getTestDbPath, getTestSetupWizardOptions } from './helpers';
const DB_PATH = '/Users/alan/Desktop/test.db';
describe('setupInstance', function () {
const fyo = new Fyo({
DatabaseDemux: DatabaseManager,
AuthDemux: DummyAuthDemux,
isTest: true,
isElectron: false,
const dbPath = getTestDbPath();
const setupOptions = getTestSetupWizardOptions();
let fyo: Fyo;
this.beforeAll(function () {
fyo = new Fyo({
DatabaseDemux: DatabaseManager,
AuthDemux: DummyAuthDemux,
isTest: true,
isElectron: false,
});
});
this.afterAll(async function () {
await fyo.close();
});
const setupOptions = getTestSetupWizardOptions();
specify('setupInstance', async function () {
await setupInstance(DB_PATH, setupOptions, fyo);
await assertDoesNotThrow(async () => {
// await setupInstance(':memory:', setupOptions, fyo);
await setupInstance(dbPath, setupOptions, fyo);
}, 'setup instance failed');
});
specify('check setup Singles', async function () {
const setupFields = [
'companyName',
'country',
'fullname',
'email',
'bankName',
'fiscalYearStart',
'fiscalYearEnd',
'currency',
];
const setupSingles = await fyo.db.getSingleValues(...setupFields);
const singlesMap = getValueMapFromList(setupSingles, 'fieldname', 'value');
for (const field of setupFields) {
let dbValue = singlesMap[field];
const optionsValue = setupOptions[field as keyof SetupWizardOptions];
if (dbValue instanceof Date) {
dbValue = dbValue.toISOString().split('T')[0];
}
assert.strictEqual(dbValue as string, optionsValue, `${field} mismatch`);
}
});
specify('check null singles', async function () {
const nullFields = ['gstin', 'logo', 'phone', 'address'];
const nullSingles = await fyo.db.getSingleValues(...nullFields);
assert.strictEqual(
nullSingles.length,
0,
`null singles found ${JSON.stringify(nullSingles)}`
);
});
});

View File

@ -4680,6 +4680,11 @@ dotenv-expand@^5.1.0:
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
dotenv@^16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411"
integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==
dotenv@^8.2.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"