2022-04-19 05:59:36 +00:00
|
|
|
import { Fyo } from 'fyo';
|
|
|
|
import { DocValue, DocValueMap } from 'fyo/core/types';
|
|
|
|
import { Verb } from 'fyo/telemetry/types';
|
2022-04-23 09:23:44 +00:00
|
|
|
import { DEFAULT_USER } from 'fyo/utils/consts';
|
2022-04-01 09:35:51 +00:00
|
|
|
import {
|
2022-04-25 06:33:31 +00:00
|
|
|
ConflictError,
|
2022-04-01 09:35:51 +00:00
|
|
|
MandatoryError,
|
|
|
|
NotFoundError,
|
|
|
|
ValidationError,
|
2022-04-19 05:59:36 +00:00
|
|
|
} from 'fyo/utils/errors';
|
|
|
|
import Observable from 'fyo/utils/observable';
|
2022-04-01 09:35:51 +00:00
|
|
|
import Money from 'pesa/dist/types/src/money';
|
|
|
|
import {
|
|
|
|
Field,
|
|
|
|
FieldTypeEnum,
|
|
|
|
OptionField,
|
|
|
|
Schema,
|
|
|
|
TargetField,
|
|
|
|
} from 'schemas/types';
|
2022-04-21 13:08:36 +00:00
|
|
|
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
|
|
|
|
import { isPesa } from '../utils/index';
|
2022-04-01 09:35:51 +00:00
|
|
|
import {
|
|
|
|
areDocValuesEqual,
|
|
|
|
getMissingMandatoryMessage,
|
|
|
|
getPreDefaultValues,
|
|
|
|
shouldApplyFormula,
|
|
|
|
} from './helpers';
|
|
|
|
import { setName } from './naming';
|
|
|
|
import {
|
2022-04-11 12:44:36 +00:00
|
|
|
Action,
|
2022-04-18 08:01:41 +00:00
|
|
|
CurrenciesMap,
|
2022-04-01 09:35:51 +00:00
|
|
|
DefaultMap,
|
|
|
|
DependsOnMap,
|
2022-04-11 12:44:36 +00:00
|
|
|
EmptyMessageMap,
|
2022-04-11 09:41:49 +00:00
|
|
|
FiltersMap,
|
2022-04-01 09:35:51 +00:00
|
|
|
FormulaMap,
|
2022-04-14 09:22:45 +00:00
|
|
|
FormulaReturn,
|
2022-04-14 05:24:11 +00:00
|
|
|
HiddenMap,
|
2022-04-11 09:41:49 +00:00
|
|
|
ListsMap,
|
|
|
|
ListViewSettings,
|
2022-04-22 11:02:03 +00:00
|
|
|
ReadOnlyMap,
|
2022-04-01 09:35:51 +00:00
|
|
|
RequiredMap,
|
2022-04-11 09:41:49 +00:00
|
|
|
TreeViewSettings,
|
2022-04-01 09:35:51 +00:00
|
|
|
ValidationMap,
|
|
|
|
} from './types';
|
|
|
|
import { validateSelect } from './validationFunction';
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
export class Doc extends Observable<DocValue | Doc[]> {
|
2022-04-01 09:35:51 +00:00
|
|
|
name?: string;
|
|
|
|
schema: Readonly<Schema>;
|
2022-04-19 05:59:36 +00:00
|
|
|
fyo: Fyo;
|
2022-04-01 09:35:51 +00:00
|
|
|
fieldMap: Record<string, Field>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fields below are used by child docs to maintain
|
|
|
|
* reference w.r.t their parent doc.
|
|
|
|
*/
|
|
|
|
idx?: number;
|
|
|
|
parentdoc?: Doc;
|
2022-04-25 06:33:31 +00:00
|
|
|
parentFieldname?: string;
|
|
|
|
parentSchemaName?: string;
|
2022-04-01 09:35:51 +00:00
|
|
|
|
|
|
|
_links?: Record<string, Doc>;
|
|
|
|
_dirty: boolean = true;
|
|
|
|
_notInserted: boolean = true;
|
|
|
|
|
|
|
|
flags = {
|
|
|
|
submitAction: false,
|
|
|
|
revertAction: false,
|
|
|
|
};
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
|
2022-04-01 09:35:51 +00:00
|
|
|
super();
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo = fyo;
|
2022-04-01 09:35:51 +00:00
|
|
|
this.schema = schema;
|
|
|
|
this.fieldMap = getMapFromList(schema.fields, 'fieldname');
|
2022-04-18 11:29:20 +00:00
|
|
|
|
|
|
|
if (this.schema.isSingle) {
|
|
|
|
this.name = this.schemaName;
|
|
|
|
}
|
2022-04-24 06:48:44 +00:00
|
|
|
|
|
|
|
this._setDefaults();
|
|
|
|
this._setValuesWithoutChecks(data);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get schemaName(): string {
|
|
|
|
return this.schema.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
get isNew(): boolean {
|
|
|
|
return this._notInserted;
|
|
|
|
}
|
|
|
|
|
|
|
|
get tableFields(): TargetField[] {
|
|
|
|
return this.schema.fields.filter(
|
|
|
|
(f) => f.fieldtype === FieldTypeEnum.Table
|
|
|
|
) as TargetField[];
|
|
|
|
}
|
|
|
|
|
2022-04-07 06:20:14 +00:00
|
|
|
get dirty() {
|
|
|
|
return this._dirty;
|
|
|
|
}
|
|
|
|
|
2022-04-22 11:02:03 +00:00
|
|
|
get quickEditFields() {
|
|
|
|
const fieldnames = this.schema.quickEditFields ?? ['name'];
|
|
|
|
return fieldnames.map((f) => this.fieldMap[f]);
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
_setValuesWithoutChecks(data: DocValueMap) {
|
|
|
|
for (const field of this.schema.fields) {
|
|
|
|
const fieldname = field.fieldname;
|
|
|
|
const value = data[field.fieldname];
|
2022-04-01 09:35:51 +00:00
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
for (const row of value) {
|
|
|
|
this.push(fieldname, row);
|
|
|
|
}
|
|
|
|
} else {
|
2022-04-25 06:33:31 +00:00
|
|
|
this[fieldname] = value ?? this[fieldname] ?? null;
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
_setDirty(value: boolean) {
|
2022-04-01 09:35:51 +00:00
|
|
|
this._dirty = value;
|
|
|
|
if (this.schema.isChild && this.parentdoc) {
|
|
|
|
this.parentdoc._dirty = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set value and trigger change
|
|
|
|
async set(fieldname: string | DocValueMap, value?: DocValue | Doc[]) {
|
|
|
|
if (typeof fieldname === 'object') {
|
2022-04-23 09:23:44 +00:00
|
|
|
await this.setMultiple(fieldname as DocValueMap);
|
2022-04-01 09:35:51 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
if (!this._canSet(fieldname, value)) {
|
2022-04-01 09:35:51 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
this._setDirty(true);
|
2022-04-01 09:35:51 +00:00
|
|
|
if (Array.isArray(value)) {
|
|
|
|
this[fieldname] = value.map((row, i) => {
|
|
|
|
row.idx = i;
|
|
|
|
return row;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
const field = this.fieldMap[fieldname];
|
2022-04-23 09:23:44 +00:00
|
|
|
await this._validateField(field, value);
|
2022-04-01 09:35:51 +00:00
|
|
|
this[fieldname] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
// always run applyChange from the parentdoc
|
|
|
|
if (this.schema.isChild && this.parentdoc) {
|
2022-04-23 09:23:44 +00:00
|
|
|
await this._applyChange(fieldname);
|
2022-04-25 06:33:31 +00:00
|
|
|
await this.parentdoc._applyChange(this.parentFieldname as string);
|
2022-04-01 09:35:51 +00:00
|
|
|
} else {
|
2022-04-23 09:23:44 +00:00
|
|
|
await this._applyChange(fieldname);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async setMultiple(docValueMap: DocValueMap) {
|
|
|
|
for (const fieldname in docValueMap) {
|
|
|
|
await this.set(fieldname, docValueMap[fieldname] as DocValue | Doc[]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
_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);
|
2022-04-01 09:35:51 +00:00
|
|
|
await this.trigger('change', {
|
|
|
|
doc: this,
|
|
|
|
changed: fieldname,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
_setDefaults() {
|
2022-04-01 09:35:51 +00:00
|
|
|
for (const field of this.schema.fields) {
|
2022-04-18 11:29:20 +00:00
|
|
|
let defaultValue: DocValue | Doc[] = getPreDefaultValues(
|
|
|
|
field.fieldtype,
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo
|
2022-04-18 11:29:20 +00:00
|
|
|
);
|
2022-04-01 09:35:51 +00:00
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
const defaultFunction =
|
|
|
|
this.fyo.models[this.schemaName]?.defaults?.[field.fieldname];
|
2022-04-01 09:35:51 +00:00
|
|
|
if (defaultFunction !== undefined) {
|
|
|
|
defaultValue = defaultFunction();
|
|
|
|
} else if (field.default !== undefined) {
|
|
|
|
defaultValue = field.default;
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
if (field.fieldtype === FieldTypeEnum.Currency && !isPesa(defaultValue)) {
|
2022-04-19 05:59:36 +00:00
|
|
|
defaultValue = this.fyo.pesa!(defaultValue as string | number);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this[field.fieldname] = defaultValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async append(fieldname: string, docValueMap: Doc | DocValueMap = {}) {
|
2022-04-01 09:35:51 +00:00
|
|
|
// push child row and trigger change
|
|
|
|
this.push(fieldname, docValueMap);
|
|
|
|
this._dirty = true;
|
2022-04-24 06:48:44 +00:00
|
|
|
await this._applyChange(fieldname);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
push(fieldname: string, docValueMap: Doc | DocValueMap = {}) {
|
|
|
|
// push child row without triggering change
|
|
|
|
this[fieldname] ??= [];
|
2022-04-24 06:48:44 +00:00
|
|
|
const childDoc = this._getChildDoc(docValueMap, fieldname);
|
2022-04-01 09:35:51 +00:00
|
|
|
(this[fieldname] as Doc[]).push(childDoc);
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
_getChildDoc(docValueMap: Doc | DocValueMap, fieldname: string): Doc {
|
2022-04-01 09:35:51 +00:00
|
|
|
if (docValueMap instanceof Doc) {
|
|
|
|
return docValueMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
const data: Record<string, unknown> = Object.assign({}, docValueMap);
|
|
|
|
|
|
|
|
data.parent = this.name;
|
2022-04-25 06:33:31 +00:00
|
|
|
data.parentSchemaName = this.schemaName;
|
|
|
|
data.parentFieldname = fieldname;
|
2022-04-01 09:35:51 +00:00
|
|
|
data.parentdoc = this;
|
|
|
|
|
|
|
|
if (!data.idx) {
|
|
|
|
data.idx = ((this[fieldname] as Doc[]) || []).length;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!data.name) {
|
|
|
|
data.name = getRandomString();
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
const targetField = this.fieldMap[fieldname] as TargetField;
|
|
|
|
const schema = this.fyo.db.schemaMap[targetField.target] as Schema;
|
2022-04-19 05:59:36 +00:00
|
|
|
const childDoc = new Doc(schema, data as DocValueMap, this.fyo);
|
2022-04-01 09:35:51 +00:00
|
|
|
return childDoc;
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _validateInsert() {
|
2022-04-23 09:23:44 +00:00
|
|
|
this._validateMandatory();
|
|
|
|
await this._validateFields();
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
_validateMandatory() {
|
2022-04-01 09:35:51 +00:00
|
|
|
const checkForMandatory: Doc[] = [this];
|
|
|
|
const tableFields = this.schema.fields.filter(
|
|
|
|
(f) => f.fieldtype === FieldTypeEnum.Table
|
|
|
|
) as TargetField[];
|
|
|
|
|
|
|
|
for (const field of tableFields) {
|
|
|
|
const childDocs = this.get(field.fieldname) as Doc[];
|
|
|
|
checkForMandatory.push(...childDocs);
|
|
|
|
}
|
|
|
|
|
|
|
|
const missingMandatoryMessage = checkForMandatory
|
|
|
|
.map((doc) => getMissingMandatoryMessage(doc))
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
if (missingMandatoryMessage.length > 0) {
|
|
|
|
const fields = missingMandatoryMessage.join('\n');
|
2022-04-19 05:59:36 +00:00
|
|
|
const message = this.fyo.t`Value missing for ${fields}`;
|
2022-04-01 09:35:51 +00:00
|
|
|
throw new MandatoryError(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
async _validateFields() {
|
2022-04-01 09:35:51 +00:00
|
|
|
const fields = this.schema.fields;
|
|
|
|
for (const field of fields) {
|
|
|
|
if (field.fieldtype === FieldTypeEnum.Table) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = this.get(field.fieldname) as DocValue;
|
2022-04-23 09:23:44 +00:00
|
|
|
await this._validateField(field, value);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
async _validateField(field: Field, value: DocValue) {
|
2022-04-01 09:35:51 +00:00
|
|
|
if (field.fieldtype == 'Select') {
|
|
|
|
validateSelect(field as OptionField, value as string);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value === null || value === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const validator = this.validations[field.fieldname];
|
|
|
|
if (validator === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await validator(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
getValidDict(): DocValueMap {
|
|
|
|
const data: DocValueMap = {};
|
|
|
|
for (const field of this.schema.fields) {
|
|
|
|
let value = this[field.fieldname] as DocValue | DocValueMap[];
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
value = value.map((doc) => (doc as Doc).getValidDict());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isPesa(value)) {
|
|
|
|
value = (value as Money).copy();
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
if (value === null && this.schema.isSingle) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-04-01 09:35:51 +00:00
|
|
|
data[field.fieldname] = value;
|
|
|
|
}
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
_setBaseMetaValues() {
|
2022-04-01 09:35:51 +00:00
|
|
|
if (this.schema.isSubmittable && typeof this.submitted !== 'boolean') {
|
|
|
|
this.submitted = false;
|
|
|
|
this.cancelled = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.createdBy) {
|
2022-04-23 09:23:44 +00:00
|
|
|
this.createdBy = this.fyo.auth.session.user || DEFAULT_USER;
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.created) {
|
|
|
|
this.created = new Date();
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
this._updateModifiedMetaValues();
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
_updateModifiedMetaValues() {
|
2022-04-23 09:23:44 +00:00
|
|
|
this.modifiedBy = this.fyo.auth.session.user || DEFAULT_USER;
|
2022-04-01 09:35:51 +00:00
|
|
|
this.modified = new Date();
|
|
|
|
}
|
|
|
|
|
|
|
|
async load() {
|
|
|
|
if (this.name === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
const data = await this.fyo.db.get(this.schemaName, this.name);
|
2022-04-18 11:29:20 +00:00
|
|
|
if (this.schema.isSingle && !data?.name) {
|
|
|
|
data.name = this.name!;
|
|
|
|
}
|
|
|
|
|
2022-04-01 09:35:51 +00:00
|
|
|
if (data && data.name) {
|
|
|
|
this.syncValues(data);
|
|
|
|
await this.loadLinks();
|
|
|
|
} else {
|
|
|
|
throw new NotFoundError(`Not Found: ${this.schemaName} ${this.name}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadLinks() {
|
|
|
|
this._links = {};
|
|
|
|
const inlineLinks = this.schema.fields.filter((f) => f.inline);
|
|
|
|
for (const f of inlineLinks) {
|
|
|
|
await this.loadLink(f.fieldname);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadLink(fieldname: string) {
|
|
|
|
this._links ??= {};
|
|
|
|
const field = this.fieldMap[fieldname] as TargetField;
|
|
|
|
if (field === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = this.get(fieldname);
|
|
|
|
if (getIsNullOrUndef(value) || field.target === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
this._links[fieldname] = await this.fyo.doc.getDoc(
|
2022-04-01 09:35:51 +00:00
|
|
|
field.target,
|
|
|
|
value as string
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-04-22 11:02:03 +00:00
|
|
|
getLink(fieldname: string): Doc | null {
|
|
|
|
const link = this._links?.[fieldname];
|
|
|
|
if (link === undefined) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return link;
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
syncValues(data: DocValueMap) {
|
|
|
|
this.clearValues();
|
2022-04-24 06:48:44 +00:00
|
|
|
this._setValuesWithoutChecks(data);
|
2022-04-01 09:35:51 +00:00
|
|
|
this._dirty = false;
|
|
|
|
this.trigger('change', {
|
|
|
|
doc: this,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
clearValues() {
|
|
|
|
for (const { fieldname } of this.schema.fields) {
|
|
|
|
this[fieldname] = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._dirty = true;
|
|
|
|
this._notInserted = true;
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
_setChildIdx() {
|
2022-04-01 09:35:51 +00:00
|
|
|
const childFields = this.schema.fields.filter(
|
|
|
|
(f) => f.fieldtype === FieldTypeEnum.Table
|
|
|
|
) as TargetField[];
|
|
|
|
|
|
|
|
for (const field of childFields) {
|
|
|
|
const childDocs = (this.get(field.fieldname) as Doc[]) ?? [];
|
|
|
|
|
|
|
|
for (let i = 0; i < childDocs.length; i++) {
|
|
|
|
childDocs[i].idx = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _compareWithCurrentDoc() {
|
2022-04-25 06:33:31 +00:00
|
|
|
if (this.isNew || !this.name || this.schema.isSingle) {
|
2022-04-01 09:35:51 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
const dbValues = await this.fyo.db.get(this.schemaName, this.name);
|
|
|
|
const docModified = this.modified as Date;
|
|
|
|
const dbModified = dbValues.modified as Date;
|
2022-04-01 09:35:51 +00:00
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
if (dbValues && docModified !== dbModified) {
|
|
|
|
throw new ConflictError(
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo
|
2022-04-25 06:33:31 +00:00
|
|
|
.t`Document ${this.schemaName} ${this.name} has been modified after loading` +
|
|
|
|
`${docModified?.toISOString()}, ${dbModified?.toISOString()}`
|
2022-04-01 09:35:51 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.submitted && !this.schema.isSubmittable) {
|
|
|
|
throw new ValidationError(
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo.t`Document type ${this.schemaName} is not submittable`
|
2022-04-01 09:35:51 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// set submit action flag
|
2022-04-25 06:33:31 +00:00
|
|
|
if (this.submitted && !dbValues.submitted) {
|
2022-04-01 09:35:51 +00:00
|
|
|
this.flags.submitAction = true;
|
|
|
|
}
|
|
|
|
|
2022-04-25 06:33:31 +00:00
|
|
|
if (dbValues.submitted && !this.submitted) {
|
2022-04-01 09:35:51 +00:00
|
|
|
this.flags.revertAction = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
async _applyFormula(fieldname?: string) {
|
2022-04-01 09:35:51 +00:00
|
|
|
const doc = this;
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
const childDocs = this.tableFields
|
|
|
|
.map((f) => (this.get(f.fieldname) as Doc[]) ?? [])
|
|
|
|
.flat();
|
|
|
|
|
|
|
|
// children
|
|
|
|
for (const row of childDocs) {
|
|
|
|
const formulaFields = Object.keys(this.formulas).map(
|
|
|
|
(fn) => this.fieldMap[fn]
|
|
|
|
);
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
changed ||= await this._applyFormulaForFields(
|
2022-04-01 09:35:51 +00:00
|
|
|
formulaFields,
|
|
|
|
row,
|
|
|
|
fieldname
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// parent or child row
|
|
|
|
const formulaFields = Object.keys(this.formulas).map(
|
|
|
|
(fn) => this.fieldMap[fn]
|
|
|
|
);
|
2022-04-23 09:23:44 +00:00
|
|
|
changed ||= await this._applyFormulaForFields(
|
|
|
|
formulaFields,
|
|
|
|
doc,
|
|
|
|
fieldname
|
|
|
|
);
|
2022-04-01 09:35:51 +00:00
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
async _applyFormulaForFields(
|
2022-04-01 09:35:51 +00:00
|
|
|
formulaFields: Field[],
|
|
|
|
doc: Doc,
|
|
|
|
fieldname?: string
|
|
|
|
) {
|
|
|
|
let changed = false;
|
|
|
|
for (const field of formulaFields) {
|
2022-04-23 09:23:44 +00:00
|
|
|
const shouldApply = shouldApplyFormula(field, doc, fieldname);
|
|
|
|
if (!shouldApply) {
|
2022-04-01 09:35:51 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
const newVal = await this._getValueFromFormula(field, doc);
|
2022-04-01 09:35:51 +00:00
|
|
|
const previousVal = doc.get(field.fieldname);
|
|
|
|
const isSame = areDocValuesEqual(newVal as DocValue, previousVal);
|
|
|
|
if (newVal === undefined || isSame) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
doc[field.fieldname] = newVal;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return changed;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
async _getValueFromFormula(field: Field, doc: Doc) {
|
|
|
|
const formula = doc.formulas[field.fieldname];
|
2022-04-01 09:35:51 +00:00
|
|
|
if (formula === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-04-23 09:23:44 +00:00
|
|
|
let value: FormulaReturn;
|
|
|
|
try {
|
|
|
|
value = await formula();
|
|
|
|
} catch {
|
|
|
|
return;
|
|
|
|
}
|
2022-04-01 09:35:51 +00:00
|
|
|
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
|
2022-04-24 06:48:44 +00:00
|
|
|
value = value.map((row) => this._getChildDoc(row, field.fieldname));
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _commit() {
|
2022-04-01 09:35:51 +00:00
|
|
|
// re-run triggers
|
2022-04-24 06:48:44 +00:00
|
|
|
this._setChildIdx();
|
2022-04-23 09:23:44 +00:00
|
|
|
await this._applyFormula();
|
2022-04-01 09:35:51 +00:00
|
|
|
await this.trigger('validate', null);
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _insert() {
|
2022-04-19 05:59:36 +00:00
|
|
|
await setName(this, this.fyo);
|
2022-04-23 09:23:44 +00:00
|
|
|
this._setBaseMetaValues();
|
2022-04-24 06:48:44 +00:00
|
|
|
await this._commit();
|
|
|
|
await this._validateInsert();
|
2022-04-01 09:35:51 +00:00
|
|
|
await this.trigger('beforeInsert', null);
|
|
|
|
|
|
|
|
const oldName = this.name!;
|
2022-04-25 06:33:31 +00:00
|
|
|
const validDict = this.getValidDict();
|
|
|
|
const data = await this.fyo.db.insert(this.schemaName, validDict);
|
2022-04-01 09:35:51 +00:00
|
|
|
this.syncValues(data);
|
2022-04-25 06:33:31 +00:00
|
|
|
this._notInserted = false;
|
2022-04-01 09:35:51 +00:00
|
|
|
|
|
|
|
if (oldName !== this.name) {
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo.doc.removeFromCache(this.schemaName, oldName);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
await this.trigger('afterInsert', null);
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo.telemetry.log(Verb.Created, this.schemaName);
|
2022-04-01 09:35:51 +00:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _update() {
|
|
|
|
await this._compareWithCurrentDoc();
|
|
|
|
await this._commit();
|
2022-04-01 09:35:51 +00:00
|
|
|
await this.trigger('beforeUpdate');
|
|
|
|
|
|
|
|
// before submit
|
|
|
|
if (this.flags.submitAction) await this.trigger('beforeSubmit');
|
|
|
|
if (this.flags.revertAction) await this.trigger('beforeRevert');
|
|
|
|
|
|
|
|
// update modifiedBy and modified
|
2022-04-25 06:33:31 +00:00
|
|
|
this._updateModifiedMetaValues();
|
2022-04-01 09:35:51 +00:00
|
|
|
|
|
|
|
const data = this.getValidDict();
|
2022-04-19 05:59:36 +00:00
|
|
|
await this.fyo.db.update(this.schemaName, data);
|
2022-04-01 09:35:51 +00:00
|
|
|
this.syncValues(data);
|
|
|
|
|
|
|
|
await this.trigger('afterUpdate');
|
|
|
|
|
|
|
|
// after submit
|
|
|
|
if (this.flags.submitAction) await this.trigger('afterSubmit');
|
|
|
|
if (this.flags.revertAction) await this.trigger('afterRevert');
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async sync() {
|
2022-04-25 06:33:31 +00:00
|
|
|
await this.trigger('beforeSync');
|
|
|
|
let doc;
|
2022-04-01 09:35:51 +00:00
|
|
|
if (this._notInserted) {
|
2022-04-25 06:33:31 +00:00
|
|
|
doc = await this._insert();
|
2022-04-01 09:35:51 +00:00
|
|
|
} else {
|
2022-04-25 06:33:31 +00:00
|
|
|
doc = await this._update();
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
2022-04-25 06:33:31 +00:00
|
|
|
await this.trigger('afterSync');
|
|
|
|
return doc;
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async delete() {
|
|
|
|
await this.trigger('beforeDelete');
|
2022-04-19 05:59:36 +00:00
|
|
|
await this.fyo.db.delete(this.schemaName, this.name!);
|
2022-04-01 09:35:51 +00:00
|
|
|
await this.trigger('afterDelete');
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
this.fyo.telemetry.log(Verb.Deleted, this.schemaName);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async _submitOrRevert(isSubmit: boolean) {
|
2022-04-01 09:35:51 +00:00
|
|
|
const wasSubmitted = this.submitted;
|
|
|
|
this.submitted = isSubmit;
|
|
|
|
try {
|
2022-04-24 06:48:44 +00:00
|
|
|
await this.sync();
|
2022-04-01 09:35:51 +00:00
|
|
|
} catch (e) {
|
|
|
|
this.submitted = wasSubmitted;
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async submit() {
|
|
|
|
this.cancelled = false;
|
2022-04-24 06:48:44 +00:00
|
|
|
await this._submitOrRevert(true);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async revert() {
|
2022-04-24 06:48:44 +00:00
|
|
|
await this._submitOrRevert(false);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async rename(newName: string) {
|
|
|
|
await this.trigger('beforeRename');
|
2022-04-19 05:59:36 +00:00
|
|
|
await this.fyo.db.rename(this.schemaName, this.name!, newName);
|
2022-04-01 09:35:51 +00:00
|
|
|
this.name = newName;
|
|
|
|
await this.trigger('afterRename');
|
|
|
|
}
|
|
|
|
|
|
|
|
async trigger(event: string, params?: unknown) {
|
|
|
|
if (this[event]) {
|
|
|
|
await (this[event] as Function)(params);
|
|
|
|
}
|
|
|
|
|
|
|
|
await super.trigger(event, params);
|
|
|
|
}
|
|
|
|
|
|
|
|
getSum(tablefield: string, childfield: string, convertToFloat = true) {
|
|
|
|
const childDocs = (this.get(tablefield) as Doc[]) ?? [];
|
|
|
|
const sum = childDocs
|
|
|
|
.map((d) => {
|
|
|
|
const value = d.get(childfield) ?? 0;
|
|
|
|
if (!isPesa(value)) {
|
|
|
|
try {
|
2022-04-19 05:59:36 +00:00
|
|
|
return this.fyo.pesa(value as string | number);
|
2022-04-01 09:35:51 +00:00
|
|
|
} catch (err) {
|
|
|
|
(
|
|
|
|
err as Error
|
|
|
|
).message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return value as Money;
|
|
|
|
})
|
2022-04-19 05:59:36 +00:00
|
|
|
.reduce((a, b) => a.add(b), this.fyo.pesa(0));
|
2022-04-01 09:35:51 +00:00
|
|
|
|
|
|
|
if (convertToFloat) {
|
|
|
|
return sum.float;
|
|
|
|
}
|
|
|
|
return sum;
|
|
|
|
}
|
|
|
|
|
|
|
|
getFrom(schemaName: string, name: string, fieldname: string) {
|
|
|
|
if (name === undefined || fieldname === undefined) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
return this.fyo.doc.getCachedValue(schemaName, name, fieldname);
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async setAndSync(fieldname: string | DocValueMap, value?: DocValue | Doc[]) {
|
2022-04-20 10:41:27 +00:00
|
|
|
await this.set(fieldname, value);
|
2022-04-24 06:48:44 +00:00
|
|
|
return await this.sync();
|
2022-04-20 10:41:27 +00:00
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
async duplicate(shouldSync: boolean = true): Promise<Doc> {
|
2022-04-01 09:35:51 +00:00
|
|
|
const updateMap: DocValueMap = {};
|
|
|
|
const docValueMap = this.getValidDict();
|
|
|
|
const fieldnames = this.schema.fields.map((f) => f.fieldname);
|
|
|
|
|
|
|
|
for (const fn of fieldnames) {
|
|
|
|
const value = docValueMap[fn];
|
|
|
|
if (getIsNullOrUndef(value)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
value.forEach((row) => {
|
|
|
|
delete row.name;
|
|
|
|
delete row.parent;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
updateMap[fn] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.numberSeries) {
|
|
|
|
delete updateMap.name;
|
|
|
|
} else {
|
|
|
|
updateMap.name = updateMap.name + ' CPY';
|
|
|
|
}
|
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
const doc = this.fyo.doc.getNewDoc(this.schemaName, {}, false);
|
2022-04-01 09:35:51 +00:00
|
|
|
await doc.setMultiple(updateMap);
|
2022-04-07 06:20:14 +00:00
|
|
|
|
2022-04-24 06:48:44 +00:00
|
|
|
if (shouldSync) {
|
|
|
|
await doc.sync();
|
2022-04-07 06:20:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return doc;
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-04-18 11:29:20 +00:00
|
|
|
async beforeInsert() {}
|
|
|
|
async afterInsert() {}
|
|
|
|
async beforeUpdate() {}
|
|
|
|
async afterUpdate() {}
|
2022-04-25 06:33:31 +00:00
|
|
|
async beforeSync() {}
|
|
|
|
async afterSync() {}
|
2022-04-18 11:29:20 +00:00
|
|
|
async beforeDelete() {}
|
|
|
|
async afterDelete() {}
|
|
|
|
async beforeRevert() {}
|
|
|
|
async afterRevert() {}
|
|
|
|
|
2022-04-01 09:35:51 +00:00
|
|
|
formulas: FormulaMap = {};
|
|
|
|
validations: ValidationMap = {};
|
|
|
|
required: RequiredMap = {};
|
2022-04-14 05:24:11 +00:00
|
|
|
hidden: HiddenMap = {};
|
2022-04-22 11:02:03 +00:00
|
|
|
readOnly: ReadOnlyMap = {};
|
2022-04-01 09:35:51 +00:00
|
|
|
dependsOn: DependsOnMap = {};
|
2022-04-18 08:01:41 +00:00
|
|
|
getCurrencies: CurrenciesMap = {};
|
2022-04-11 09:41:49 +00:00
|
|
|
|
|
|
|
static lists: ListsMap = {};
|
|
|
|
static filters: FiltersMap = {};
|
2022-04-25 06:33:31 +00:00
|
|
|
static defaults: DefaultMap = {};
|
2022-04-11 12:44:36 +00:00
|
|
|
static emptyMessages: EmptyMessageMap = {};
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
static getListViewSettings(fyo: Fyo): ListViewSettings {
|
2022-04-18 11:29:20 +00:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
static getTreeSettings(fyo: Fyo): TreeViewSettings | void {}
|
2022-04-18 11:29:20 +00:00
|
|
|
|
2022-04-19 05:59:36 +00:00
|
|
|
static getActions(fyo: Fyo): Action[] {
|
2022-04-18 11:29:20 +00:00
|
|
|
return [];
|
|
|
|
}
|
2022-04-01 09:35:51 +00:00
|
|
|
}
|