diff --git a/backend/database/tests/helpers.ts b/backend/database/tests/helpers.ts index 448816e1..c6b48658 100644 --- a/backend/database/tests/helpers.ts +++ b/backend/database/tests/helpers.ts @@ -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}`, }); } } diff --git a/fyo/core/converter.ts b/fyo/core/converter.ts index b06a8abf..870f6322 100644 --- a/fyo/core/converter.ts +++ b/fyo/core/converter.ts @@ -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'); diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 54fa7500..7710aa1e 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -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 { */ idx?: number; parentdoc?: Doc; - parentfield?: string; + parentFieldname?: string; + parentSchemaName?: string; _links?: Record; _dirty: boolean = true; @@ -115,7 +116,7 @@ export class Doc extends Observable { this.push(fieldname, row); } } else { - this[fieldname] = value ?? null; + this[fieldname] = value ?? this[fieldname] ?? null; } } } @@ -153,7 +154,7 @@ export class Doc extends Observable { // 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 { _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 { const data: Record = 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 { 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 { 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 { } 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 { } // 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 { 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 { 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 { } 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 { 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 { static lists: ListsMap = {}; static filters: FiltersMap = {}; + static defaults: DefaultMap = {}; static emptyMessages: EmptyMessageMap = {}; static getListViewSettings(fyo: Fyo): ListViewSettings { diff --git a/fyo/model/helpers.ts b/fyo/model/helpers.ts index 6b0678a9..2bd6b6f3 100644 --- a/fyo/model/helpers.ts +++ b/fyo/model/helpers.ts @@ -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); } } diff --git a/fyo/utils/errors.ts b/fyo/utils/errors.ts index 5cf534d1..636f80db 100644 --- a/fyo/utils/errors.ts +++ b/fyo/utils/errors.ts @@ -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 {} diff --git a/models/baseModels/Account/Account.ts b/models/baseModels/Account/Account.ts index 88728328..30709c12 100644 --- a/models/baseModels/Account/Account.ts +++ b/models/baseModels/Account/Account.ts @@ -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; diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 74095ec2..b3f08c4e 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -188,7 +188,7 @@ export abstract class Invoice extends Doc { }, }; - defaults: DefaultMap = { + static defaults: DefaultMap = { date: () => new Date().toISOString().slice(0, 10), }; diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts index 64ab48a1..a2b9cca9 100644 --- a/models/baseModels/JournalEntry/JournalEntry.ts +++ b/models/baseModels/JournalEntry/JournalEntry.ts @@ -48,7 +48,7 @@ export class JournalEntry extends Doc { await this.getPosting().postReverse(); } - defaults: DefaultMap = { + static defaults: DefaultMap = { date: () => DateTime.local().toISODate(), }; diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 9eb86ec5..40808739 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -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 () => { diff --git a/package.json b/package.json index de560894..93c7b865 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/schemas/app/SetupWizard.json b/schemas/app/SetupWizard.json index a7caab4c..b712b4a7 100644 --- a/schemas/app/SetupWizard.json +++ b/schemas/app/SetupWizard.json @@ -6,7 +6,7 @@ "isSubmittable": false, "fields": [ { - "fieldname": "companyLogo", + "fieldname": "logo", "label": "Company Logo", "fieldtype": "AttachImage" }, diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 392681b7..317928de 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -78,6 +78,7 @@ "fieldname": "countryCode", "label": "Country Code", "fieldtype": "Data", + "default": "in", "description": "Country code used to initialize regional settings." }, { diff --git a/schemas/types.ts b/schemas/types.ts index c00ac1e3..54f485aa 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -14,7 +14,6 @@ export enum FieldTypeEnum { Currency = 'Currency', Text = 'Text', Color = 'Color', - Code = 'Code', } export type FieldType = keyof typeof FieldTypeEnum; diff --git a/src/pages/SetupWizard/SetupWizard.vue b/src/pages/SetupWizard/SetupWizard.vue index 6164e53d..300b4fe0 100644 --- a/src/pages/SetupWizard/SetupWizard.vue +++ b/src/pages/SetupWizard/SetupWizard.vue @@ -45,9 +45,9 @@
{ - // 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)}` + ); + }); }); diff --git a/yarn.lock b/yarn.lock index 9b85286f..92cb8962 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"