mirror of
https://github.com/frappe/books.git
synced 2024-11-08 14:50:56 +00:00
incr: delete some stuff add typed doc.ts
This commit is contained in:
parent
98e1a44686
commit
cdb039d308
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
**/types.ts
|
@ -1,8 +1,7 @@
|
|||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import 'mocha';
|
import 'mocha';
|
||||||
import { getMapFromList } from 'schemas/helpers';
|
|
||||||
import { FieldTypeEnum, RawValue } from 'schemas/types';
|
import { FieldTypeEnum, RawValue } from 'schemas/types';
|
||||||
import { getValueMapFromList, sleep } from 'utils';
|
import { getMapFromList, getValueMapFromList, sleep } from 'utils';
|
||||||
import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../helpers';
|
import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../helpers';
|
||||||
import DatabaseCore from '../core';
|
import DatabaseCore from '../core';
|
||||||
import { FieldValueMap, SqliteTableInfo } from '../types';
|
import { FieldValueMap, SqliteTableInfo } from '../types';
|
||||||
|
@ -1,831 +0,0 @@
|
|||||||
import frappe from 'frappe';
|
|
||||||
import Observable from 'frappe/utils/observable';
|
|
||||||
import { knex } from 'knex';
|
|
||||||
import CacheManager from '../utils/cacheManager';
|
|
||||||
import { getRandomString } from '../utils/index';
|
|
||||||
|
|
||||||
export default class Database extends Observable {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.initTypeMap();
|
|
||||||
this.connectionParams = {};
|
|
||||||
this.cache = new CacheManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
this.knex = knex(this.connectionParams);
|
|
||||||
this.knex.on('query-error', (error) => {
|
|
||||||
error.type = this.getError(error);
|
|
||||||
});
|
|
||||||
this.executePostDbConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrate() {
|
|
||||||
for (let doctype in frappe.models) {
|
|
||||||
// check if controller module
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
if (!meta.isSingle) {
|
|
||||||
if (await this.tableExists(baseDoctype)) {
|
|
||||||
await this.alterTable(baseDoctype);
|
|
||||||
} else {
|
|
||||||
await this.createTable(baseDoctype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.commit();
|
|
||||||
await this.initializeSingles();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeSingles() {
|
|
||||||
let singleDoctypes = frappe
|
|
||||||
.getModels((model) => model.isSingle)
|
|
||||||
.map((model) => model.name);
|
|
||||||
|
|
||||||
for (let doctype of singleDoctypes) {
|
|
||||||
if (await this.singleExists(doctype)) {
|
|
||||||
const singleValues = await this.getSingleFieldsToInsert(doctype);
|
|
||||||
singleValues.forEach(({ fieldname, value }) => {
|
|
||||||
let singleValue = frappe.getNewDoc({
|
|
||||||
doctype: 'SingleValue',
|
|
||||||
parent: doctype,
|
|
||||||
fieldname,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
singleValue.insert();
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
if (meta.fields.every((df) => df.default == null)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let defaultValues = meta.fields.reduce((doc, df) => {
|
|
||||||
if (df.default != null) {
|
|
||||||
doc[df.fieldname] = df.default;
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}, {});
|
|
||||||
await this.updateSingle(doctype, defaultValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async singleExists(doctype) {
|
|
||||||
let res = await this.knex('SingleValue')
|
|
||||||
.count('parent as count')
|
|
||||||
.where('parent', doctype)
|
|
||||||
.first();
|
|
||||||
return res.count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSingleFieldsToInsert(doctype) {
|
|
||||||
const existingFields = (
|
|
||||||
await frappe.db
|
|
||||||
.knex('SingleValue')
|
|
||||||
.where({ parent: doctype })
|
|
||||||
.select('fieldname')
|
|
||||||
).map(({ fieldname }) => fieldname);
|
|
||||||
|
|
||||||
return frappe
|
|
||||||
.getMeta(doctype)
|
|
||||||
.fields.map(({ fieldname, default: value }) => ({
|
|
||||||
fieldname,
|
|
||||||
value,
|
|
||||||
}))
|
|
||||||
.filter(
|
|
||||||
({ fieldname, value }) =>
|
|
||||||
!existingFields.includes(fieldname) && value !== undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tableExists(table) {
|
|
||||||
return this.knex.schema.hasTable(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createTable(doctype, tableName = null) {
|
|
||||||
let fields = this.getValidFields(doctype);
|
|
||||||
return await this.runCreateTableQuery(tableName || doctype, fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
runCreateTableQuery(doctype, fields) {
|
|
||||||
return this.knex.schema.createTable(doctype, (table) => {
|
|
||||||
for (let field of fields) {
|
|
||||||
this.buildColumnForTable(table, field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async alterTable(doctype) {
|
|
||||||
// get columns
|
|
||||||
let diff = await this.getColumnDiff(doctype);
|
|
||||||
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
|
||||||
|
|
||||||
return this.knex.schema
|
|
||||||
.table(doctype, (table) => {
|
|
||||||
if (diff.added.length) {
|
|
||||||
for (let field of diff.added) {
|
|
||||||
this.buildColumnForTable(table, field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (diff.removed.length) {
|
|
||||||
this.removeColumns(doctype, diff.removed);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
if (newForeignKeys.length) {
|
|
||||||
return this.addForeignKeys(doctype, newForeignKeys);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buildColumnForTable(table, field) {
|
|
||||||
let columnType = this.getColumnType(field);
|
|
||||||
if (!columnType) {
|
|
||||||
// In case columnType is "Table"
|
|
||||||
// childTable links are handled using the childTable's "parent" field
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let column = table[columnType](field.fieldname);
|
|
||||||
|
|
||||||
// primary key
|
|
||||||
if (field.fieldname === 'name') {
|
|
||||||
column.primary();
|
|
||||||
}
|
|
||||||
|
|
||||||
// default value
|
|
||||||
if (!!field.default && !(field.default instanceof Function)) {
|
|
||||||
column.defaultTo(field.default);
|
|
||||||
}
|
|
||||||
|
|
||||||
// required
|
|
||||||
if (
|
|
||||||
(!!field.required && !(field.required instanceof Function)) ||
|
|
||||||
field.fieldtype === 'Currency'
|
|
||||||
) {
|
|
||||||
column.notNullable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// link
|
|
||||||
if (field.fieldtype === 'Link' && field.target) {
|
|
||||||
let meta = frappe.getMeta(field.target);
|
|
||||||
table
|
|
||||||
.foreign(field.fieldname)
|
|
||||||
.references('name')
|
|
||||||
.inTable(meta.getBaseDocType())
|
|
||||||
.onUpdate('CASCADE')
|
|
||||||
.onDelete('RESTRICT');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getColumnDiff(doctype) {
|
|
||||||
const tableColumns = await this.getTableColumns(doctype);
|
|
||||||
const validFields = this.getValidFields(doctype);
|
|
||||||
const diff = { added: [], removed: [] };
|
|
||||||
|
|
||||||
for (let field of validFields) {
|
|
||||||
if (
|
|
||||||
!tableColumns.includes(field.fieldname) &&
|
|
||||||
this.getColumnType(field)
|
|
||||||
) {
|
|
||||||
diff.added.push(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const validFieldNames = validFields.map((field) => field.fieldname);
|
|
||||||
for (let column of tableColumns) {
|
|
||||||
if (!validFieldNames.includes(column)) {
|
|
||||||
diff.removed.push(column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return diff;
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeColumns(doctype, removed) {
|
|
||||||
for (let column of removed) {
|
|
||||||
await this.runRemoveColumnQuery(doctype, column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNewForeignKeys(doctype) {
|
|
||||||
let foreignKeys = await this.getForeignKeys(doctype);
|
|
||||||
let newForeignKeys = [];
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
|
||||||
if (
|
|
||||||
field.fieldtype === 'Link' &&
|
|
||||||
!foreignKeys.includes(field.fieldname)
|
|
||||||
) {
|
|
||||||
newForeignKeys.push(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newForeignKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addForeignKeys(doctype, newForeignKeys) {
|
|
||||||
for (let field of newForeignKeys) {
|
|
||||||
this.addForeignKey(doctype, field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getForeignKeys(doctype, field) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTableColumns(doctype) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async get(doctype, name = null, fields = '*') {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let doc;
|
|
||||||
if (meta.isSingle) {
|
|
||||||
doc = await this.getSingle(doctype);
|
|
||||||
doc.name = doctype;
|
|
||||||
} else {
|
|
||||||
if (!name) {
|
|
||||||
throw new frappe.errors.ValueError('name is mandatory');
|
|
||||||
}
|
|
||||||
doc = await this.getOne(doctype, name, fields);
|
|
||||||
}
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.loadChildren(doc, meta);
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadChildren(doc, meta) {
|
|
||||||
// load children
|
|
||||||
let tableFields = meta.getTableFields();
|
|
||||||
for (let field of tableFields) {
|
|
||||||
doc[field.fieldname] = await this.getAll({
|
|
||||||
doctype: field.childtype,
|
|
||||||
fields: ['*'],
|
|
||||||
filters: { parent: doc.name },
|
|
||||||
orderBy: 'idx',
|
|
||||||
order: 'asc',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSingle(doctype) {
|
|
||||||
let values = await this.getAll({
|
|
||||||
doctype: 'SingleValue',
|
|
||||||
fields: ['fieldname', 'value'],
|
|
||||||
filters: { parent: doctype },
|
|
||||||
orderBy: 'fieldname',
|
|
||||||
order: 'asc',
|
|
||||||
});
|
|
||||||
let doc = {};
|
|
||||||
for (let row of values) {
|
|
||||||
doc[row.fieldname] = row.value;
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of values from the singles table.
|
|
||||||
* @param {...string | Object} fieldnames list of fieldnames to get the values of
|
|
||||||
* @returns {Array<Object>} array of {parent, value, fieldname}.
|
|
||||||
* @example
|
|
||||||
* Database.getSingleValues('internalPrecision');
|
|
||||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
|
||||||
* @example
|
|
||||||
* Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'});
|
|
||||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
|
||||||
*/
|
|
||||||
async getSingleValues(...fieldnames) {
|
|
||||||
fieldnames = fieldnames.map((fieldname) => {
|
|
||||||
if (typeof fieldname === 'string') {
|
|
||||||
return { fieldname };
|
|
||||||
}
|
|
||||||
return fieldname;
|
|
||||||
});
|
|
||||||
|
|
||||||
let builder = frappe.db.knex('SingleValue');
|
|
||||||
builder = builder.where(fieldnames[0]);
|
|
||||||
|
|
||||||
fieldnames.slice(1).forEach(({ fieldname, parent }) => {
|
|
||||||
if (typeof parent === 'undefined') {
|
|
||||||
builder = builder.orWhere({ fieldname });
|
|
||||||
} else {
|
|
||||||
builder = builder.orWhere({ fieldname, parent });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let values = [];
|
|
||||||
try {
|
|
||||||
values = await builder.select('fieldname', 'value', 'parent');
|
|
||||||
} catch (error) {
|
|
||||||
if (error.message.includes('no such table')) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return values.map((value) => {
|
|
||||||
const fields = frappe.getMeta(value.parent).fields;
|
|
||||||
return this.getDocFormattedDoc(fields, values);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOne(doctype, name, fields = '*') {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
|
|
||||||
const doc = await this.knex
|
|
||||||
.select(fields)
|
|
||||||
.from(baseDoctype)
|
|
||||||
.where('name', name)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!doc) {
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.getDocFormattedDoc(meta.fields, doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocFormattedDoc(fields, doc) {
|
|
||||||
// format for usage, not going into the db
|
|
||||||
const docFields = Object.keys(doc);
|
|
||||||
const filteredFields = fields.filter(({ fieldname }) =>
|
|
||||||
docFields.includes(fieldname)
|
|
||||||
);
|
|
||||||
|
|
||||||
const formattedValues = filteredFields.reduce((d, field) => {
|
|
||||||
const { fieldname } = field;
|
|
||||||
d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]);
|
|
||||||
return d;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return Object.assign(doc, formattedValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocFormattedValues(field, value) {
|
|
||||||
// format for usage, not going into the db
|
|
||||||
try {
|
|
||||||
if (field.fieldtype === 'Currency') {
|
|
||||||
return frappe.pesa(value);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${
|
|
||||||
field.fieldname
|
|
||||||
}', label: '${field.label}'`;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerChange(doctype, name) {
|
|
||||||
this.trigger(`change:${doctype}`, { name }, 500);
|
|
||||||
this.trigger(`change`, { doctype, name }, 500);
|
|
||||||
// also trigger change for basedOn doctype
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
if (meta.basedOn) {
|
|
||||||
this.triggerChange(meta.basedOn, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async insert(doctype, doc) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
|
||||||
|
|
||||||
// insert parent
|
|
||||||
if (meta.isSingle) {
|
|
||||||
await this.updateSingle(doctype, doc);
|
|
||||||
} else {
|
|
||||||
await this.insertOne(baseDoctype, doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert children
|
|
||||||
await this.insertChildren(meta, doc, baseDoctype);
|
|
||||||
|
|
||||||
this.triggerChange(doctype, doc.name);
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertChildren(meta, doc, doctype) {
|
|
||||||
let tableFields = meta.getTableFields();
|
|
||||||
for (let field of tableFields) {
|
|
||||||
let idx = 0;
|
|
||||||
for (let child of doc[field.fieldname] || []) {
|
|
||||||
this.prepareChild(doctype, doc.name, child, field, idx);
|
|
||||||
await this.insertOne(field.childtype, child);
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertOne(doctype, doc) {
|
|
||||||
let fields = this.getValidFields(doctype);
|
|
||||||
|
|
||||||
if (!doc.name) {
|
|
||||||
doc.name = getRandomString();
|
|
||||||
}
|
|
||||||
|
|
||||||
let formattedDoc = this.getFormattedDoc(fields, doc);
|
|
||||||
return this.knex(doctype).insert(formattedDoc);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(doctype, doc) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
doc = this.applyBaseDocTypeFilters(doctype, doc);
|
|
||||||
|
|
||||||
// update parent
|
|
||||||
if (meta.isSingle) {
|
|
||||||
await this.updateSingle(doctype, doc);
|
|
||||||
} else {
|
|
||||||
await this.updateOne(baseDoctype, doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert or update children
|
|
||||||
await this.updateChildren(meta, doc, baseDoctype);
|
|
||||||
|
|
||||||
this.triggerChange(doctype, doc.name);
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateChildren(meta, doc, doctype) {
|
|
||||||
let tableFields = meta.getTableFields();
|
|
||||||
for (let field of tableFields) {
|
|
||||||
let added = [];
|
|
||||||
for (let child of doc[field.fieldname] || []) {
|
|
||||||
this.prepareChild(doctype, doc.name, child, field, added.length);
|
|
||||||
if (await this.exists(field.childtype, child.name)) {
|
|
||||||
await this.updateOne(field.childtype, child);
|
|
||||||
} else {
|
|
||||||
await this.insertOne(field.childtype, child);
|
|
||||||
}
|
|
||||||
added.push(child.name);
|
|
||||||
}
|
|
||||||
await this.runDeleteOtherChildren(field, doc.name, added);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOne(doctype, doc) {
|
|
||||||
let validFields = this.getValidFields(doctype);
|
|
||||||
let fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name');
|
|
||||||
let fields = validFields.filter((df) =>
|
|
||||||
fieldsToUpdate.includes(df.fieldname)
|
|
||||||
);
|
|
||||||
let formattedDoc = this.getFormattedDoc(fields, doc);
|
|
||||||
|
|
||||||
return this.knex(doctype)
|
|
||||||
.where('name', doc.name)
|
|
||||||
.update(formattedDoc)
|
|
||||||
.then(() => {
|
|
||||||
let cacheKey = `${doctype}:${doc.name}`;
|
|
||||||
if (this.cache.hexists(cacheKey)) {
|
|
||||||
for (let fieldname in formattedDoc) {
|
|
||||||
let value = formattedDoc[fieldname];
|
|
||||||
this.cache.hset(cacheKey, fieldname, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
runDeleteOtherChildren(field, parent, added) {
|
|
||||||
// delete other children
|
|
||||||
return this.knex(field.childtype)
|
|
||||||
.where('parent', parent)
|
|
||||||
.andWhere('name', 'not in', added)
|
|
||||||
.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateSingle(doctype, doc) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
await this.deleteSingleValues(doctype);
|
|
||||||
for (let field of meta.getValidFields({ withChildren: false })) {
|
|
||||||
let value = doc[field.fieldname];
|
|
||||||
if (value != null) {
|
|
||||||
let singleValue = frappe.getNewDoc({
|
|
||||||
doctype: 'SingleValue',
|
|
||||||
parent: doctype,
|
|
||||||
fieldname: field.fieldname,
|
|
||||||
value: value,
|
|
||||||
});
|
|
||||||
await singleValue.insert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSingleValues(name) {
|
|
||||||
return this.knex('SingleValue').where('parent', name).delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rename(doctype, oldName, newName) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
await this.knex(baseDoctype)
|
|
||||||
.update({ name: newName })
|
|
||||||
.where('name', oldName)
|
|
||||||
.then(() => {
|
|
||||||
this.clearValueCache(doctype, oldName);
|
|
||||||
});
|
|
||||||
await frappe.db.commit();
|
|
||||||
|
|
||||||
this.triggerChange(doctype, newName);
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareChild(parenttype, parent, child, field, idx) {
|
|
||||||
if (!child.name) {
|
|
||||||
child.name = getRandomString();
|
|
||||||
}
|
|
||||||
child.parent = parent;
|
|
||||||
child.parenttype = parenttype;
|
|
||||||
child.parentfield = field.fieldname;
|
|
||||||
child.idx = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidFields(doctype) {
|
|
||||||
return frappe.getMeta(doctype).getValidFields({ withChildren: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedDoc(fields, doc) {
|
|
||||||
// format for storage, going into the db
|
|
||||||
let formattedDoc = {};
|
|
||||||
fields.map((field) => {
|
|
||||||
let value = doc[field.fieldname];
|
|
||||||
formattedDoc[field.fieldname] = this.getFormattedValue(field, value);
|
|
||||||
});
|
|
||||||
return formattedDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormattedValue(field, value) {
|
|
||||||
// format for storage, going into the db
|
|
||||||
const type = typeof value;
|
|
||||||
if (field.fieldtype === 'Currency') {
|
|
||||||
let currency = value;
|
|
||||||
|
|
||||||
if (type === 'number' || type === 'string') {
|
|
||||||
currency = frappe.pesa(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currencyValue = currency.store;
|
|
||||||
if (typeof currencyValue !== 'string') {
|
|
||||||
throw new Error(
|
|
||||||
`invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currencyValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Date) {
|
|
||||||
if (field.fieldtype === 'Date') {
|
|
||||||
// date
|
|
||||||
return value.toISOString().substr(0, 10);
|
|
||||||
} else {
|
|
||||||
// datetime
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
} else if (field.fieldtype === 'Link' && !value) {
|
|
||||||
// empty value must be null to satisfy
|
|
||||||
// foreign key constraint
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyBaseDocTypeFilters(doctype, doc) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
if (meta.filters) {
|
|
||||||
for (let fieldname in meta.filters) {
|
|
||||||
let value = meta.filters[fieldname];
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
doc[fieldname] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteMany(doctype, names) {
|
|
||||||
for (const name of names) {
|
|
||||||
await this.delete(doctype, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(doctype, name) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
await this.deleteOne(baseDoctype, name);
|
|
||||||
|
|
||||||
// delete children
|
|
||||||
let tableFields = frappe.getMeta(doctype).getTableFields();
|
|
||||||
for (let field of tableFields) {
|
|
||||||
await this.deleteChildren(field.childtype, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.triggerChange(doctype, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteOne(doctype, name) {
|
|
||||||
return this.knex(doctype)
|
|
||||||
.where('name', name)
|
|
||||||
.delete()
|
|
||||||
.then(() => {
|
|
||||||
this.clearValueCache(doctype, name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteChildren(parenttype, parent) {
|
|
||||||
return this.knex(parenttype).where('parent', parent).delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async exists(doctype, name) {
|
|
||||||
return (await this.getValue(doctype, name)) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getValue(doctype, filters, fieldname = 'name') {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
if (typeof filters === 'string') {
|
|
||||||
filters = { name: filters };
|
|
||||||
}
|
|
||||||
if (meta.filters) {
|
|
||||||
Object.assign(filters, meta.filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
let row = await this.getAll({
|
|
||||||
doctype: baseDoctype,
|
|
||||||
fields: [fieldname],
|
|
||||||
filters: filters,
|
|
||||||
start: 0,
|
|
||||||
limit: 1,
|
|
||||||
orderBy: 'name',
|
|
||||||
order: 'asc',
|
|
||||||
});
|
|
||||||
return row.length ? row[0][fieldname] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setValue(doctype, name, fieldname, value) {
|
|
||||||
return await this.setValues(doctype, name, {
|
|
||||||
[fieldname]: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setValues(doctype, name, fieldValuePair) {
|
|
||||||
let doc = Object.assign({}, fieldValuePair, { name });
|
|
||||||
return this.updateOne(doctype, doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCachedValue(doctype, name, fieldname) {
|
|
||||||
let value = this.cache.hget(`${doctype}:${name}`, fieldname);
|
|
||||||
if (value == null) {
|
|
||||||
value = await this.getValue(doctype, name, fieldname);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll({
|
|
||||||
doctype,
|
|
||||||
fields,
|
|
||||||
filters,
|
|
||||||
start,
|
|
||||||
limit,
|
|
||||||
groupBy,
|
|
||||||
orderBy = 'creation',
|
|
||||||
order = 'desc',
|
|
||||||
} = {}) {
|
|
||||||
let meta = frappe.getMeta(doctype);
|
|
||||||
let baseDoctype = meta.getBaseDocType();
|
|
||||||
if (!fields) {
|
|
||||||
fields = meta.getKeywordFields();
|
|
||||||
fields.push('name');
|
|
||||||
}
|
|
||||||
if (typeof fields === 'string') {
|
|
||||||
fields = [fields];
|
|
||||||
}
|
|
||||||
if (meta.filters) {
|
|
||||||
filters = Object.assign({}, filters, meta.filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
let builder = this.knex.select(fields).from(baseDoctype);
|
|
||||||
|
|
||||||
this.applyFiltersToBuilder(builder, filters);
|
|
||||||
|
|
||||||
if (orderBy) {
|
|
||||||
builder.orderBy(orderBy, order);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (groupBy) {
|
|
||||||
builder.groupBy(groupBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start) {
|
|
||||||
builder.offset(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (limit) {
|
|
||||||
builder.limit(limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
const docs = await builder;
|
|
||||||
return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc));
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFiltersToBuilder(builder, filters) {
|
|
||||||
// {"status": "Open"} => `status = "Open"`
|
|
||||||
|
|
||||||
// {"status": "Open", "name": ["like", "apple%"]}
|
|
||||||
// => `status="Open" and name like "apple%"
|
|
||||||
|
|
||||||
// {"date": [">=", "2017-09-09", "<=", "2017-11-01"]}
|
|
||||||
// => `date >= 2017-09-09 and date <= 2017-11-01`
|
|
||||||
|
|
||||||
let filtersArray = [];
|
|
||||||
|
|
||||||
for (let field in filters) {
|
|
||||||
let value = filters[field];
|
|
||||||
let operator = '=';
|
|
||||||
let comparisonValue = value;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
operator = value[0];
|
|
||||||
comparisonValue = value[1];
|
|
||||||
operator = operator.toLowerCase();
|
|
||||||
|
|
||||||
if (operator === 'includes') {
|
|
||||||
operator = 'like';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (operator === 'like' && !comparisonValue.includes('%')) {
|
|
||||||
comparisonValue = `%${comparisonValue}%`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filtersArray.push([field, operator, comparisonValue]);
|
|
||||||
|
|
||||||
if (Array.isArray(value) && value.length > 2) {
|
|
||||||
// multiple conditions
|
|
||||||
let operator = value[2];
|
|
||||||
let comparisonValue = value[3];
|
|
||||||
filtersArray.push([field, operator, comparisonValue]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filtersArray.map((filter) => {
|
|
||||||
const [field, operator, comparisonValue] = filter;
|
|
||||||
if (operator === '=') {
|
|
||||||
builder.where(field, comparisonValue);
|
|
||||||
} else {
|
|
||||||
builder.where(field, operator, comparisonValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
run(query, params) {
|
|
||||||
// run query
|
|
||||||
return this.sql(query, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
sql(query, params) {
|
|
||||||
// run sql
|
|
||||||
return this.knex.raw(query, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async commit() {
|
|
||||||
try {
|
|
||||||
await this.sql('commit');
|
|
||||||
} catch (e) {
|
|
||||||
if (e.type !== frappe.errors.CannotCommitError) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearValueCache(doctype, name) {
|
|
||||||
let cacheKey = `${doctype}:${name}`;
|
|
||||||
this.cache.hclear(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
getColumnType(field) {
|
|
||||||
return this.typeMap[field.fieldtype];
|
|
||||||
}
|
|
||||||
|
|
||||||
getError(err) {
|
|
||||||
return frappe.errors.DatabaseError;
|
|
||||||
}
|
|
||||||
|
|
||||||
initTypeMap() {
|
|
||||||
this.typeMap = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
executePostDbConnect() {
|
|
||||||
frappe.initializeMoneyMaker();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
export const sqliteTypeMap = {
|
|
||||||
AutoComplete: 'text',
|
|
||||||
Currency: 'text',
|
|
||||||
Int: 'integer',
|
|
||||||
Float: 'float',
|
|
||||||
Percent: 'float',
|
|
||||||
Check: 'integer',
|
|
||||||
Code: 'text',
|
|
||||||
Date: 'text',
|
|
||||||
Datetime: 'text',
|
|
||||||
Time: 'text',
|
|
||||||
Text: 'text',
|
|
||||||
Data: 'text',
|
|
||||||
Link: 'text',
|
|
||||||
DynamicLink: 'text',
|
|
||||||
Password: 'text',
|
|
||||||
Select: 'text',
|
|
||||||
File: 'text',
|
|
||||||
Attach: 'text',
|
|
||||||
AttachImage: 'text',
|
|
||||||
Color: 'text',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validTypes = Object.keys(sqliteTypeMap);
|
|
@ -1,115 +0,0 @@
|
|||||||
import frappe from 'frappe';
|
|
||||||
import Database from './database';
|
|
||||||
import { sqliteTypeMap } from './helpers';
|
|
||||||
|
|
||||||
export default class SqliteDatabase extends Database {
|
|
||||||
constructor({ dbPath }) {
|
|
||||||
super();
|
|
||||||
this.dbPath = dbPath;
|
|
||||||
this.connectionParams = {
|
|
||||||
client: 'sqlite3',
|
|
||||||
connection: {
|
|
||||||
filename: this.dbPath,
|
|
||||||
},
|
|
||||||
pool: {
|
|
||||||
afterCreate(conn, done) {
|
|
||||||
conn.run('PRAGMA foreign_keys=ON');
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
useNullAsDefault: true,
|
|
||||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async addForeignKeys(doctype, newForeignKeys) {
|
|
||||||
await this.sql('PRAGMA foreign_keys=OFF');
|
|
||||||
await this.sql('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
const tempName = 'TEMP' + doctype;
|
|
||||||
|
|
||||||
// create temp table
|
|
||||||
await this.createTable(doctype, tempName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// copy from old to new table
|
|
||||||
await this.knex(tempName).insert(this.knex.select().from(doctype));
|
|
||||||
} catch (err) {
|
|
||||||
await this.sql('ROLLBACK');
|
|
||||||
await this.sql('PRAGMA foreign_keys=ON');
|
|
||||||
|
|
||||||
const rows = await this.knex.select().from(doctype);
|
|
||||||
await this.prestigeTheTable(doctype, rows);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// drop old table
|
|
||||||
await this.knex.schema.dropTable(doctype);
|
|
||||||
|
|
||||||
// rename new table
|
|
||||||
await this.knex.schema.renameTable(tempName, doctype);
|
|
||||||
|
|
||||||
await this.sql('COMMIT');
|
|
||||||
await this.sql('PRAGMA foreign_keys=ON');
|
|
||||||
}
|
|
||||||
|
|
||||||
removeColumns() {
|
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTableColumns(doctype) {
|
|
||||||
return (await this.sql(`PRAGMA table_info(${doctype})`)).map((d) => d.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getForeignKeys(doctype) {
|
|
||||||
return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map(
|
|
||||||
(d) => d.from
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
initTypeMap() {
|
|
||||||
this.typeMap = sqliteTypeMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
getError(err) {
|
|
||||||
let errorType = frappe.errors.DatabaseError;
|
|
||||||
if (err.message.includes('FOREIGN KEY')) {
|
|
||||||
errorType = frappe.errors.LinkValidationError;
|
|
||||||
}
|
|
||||||
if (err.message.includes('SQLITE_ERROR: cannot commit')) {
|
|
||||||
errorType = frappe.errors.CannotCommitError;
|
|
||||||
}
|
|
||||||
if (err.message.includes('SQLITE_CONSTRAINT: UNIQUE constraint failed:')) {
|
|
||||||
errorType = frappe.errors.DuplicateEntryError;
|
|
||||||
}
|
|
||||||
return errorType;
|
|
||||||
}
|
|
||||||
|
|
||||||
async prestigeTheTable(tableName, tableRows) {
|
|
||||||
const max = 200;
|
|
||||||
|
|
||||||
// Alter table hacx for sqlite in case of schema change.
|
|
||||||
const tempName = `__${tableName}`;
|
|
||||||
await this.knex.schema.dropTableIfExists(tempName);
|
|
||||||
|
|
||||||
await this.knex.raw('PRAGMA foreign_keys=OFF');
|
|
||||||
await this.createTable(tableName, tempName);
|
|
||||||
|
|
||||||
if (tableRows.length > 200) {
|
|
||||||
const fi = Math.floor(tableRows.length / max);
|
|
||||||
for (let i = 0; i <= fi; i++) {
|
|
||||||
const rowSlice = tableRows.slice(i * max, i + 1 * max);
|
|
||||||
if (rowSlice.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await this.knex.batchInsert(tempName, rowSlice);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.knex.batchInsert(tempName, tableRows);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.knex.schema.dropTable(tableName);
|
|
||||||
await this.knex.schema.renameTable(tempName, tableName);
|
|
||||||
await this.knex.raw('PRAGMA foreign_keys=ON');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import { Frappe } from 'frappe/core/frappe';
|
import { Frappe } from 'frappe';
|
||||||
|
|
||||||
interface AuthConfig {
|
interface AuthConfig {
|
||||||
serverURL: string;
|
serverURL: string;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DatabaseDemux } from '@/demux/db';
|
import { DatabaseDemux } from '@/demux/db';
|
||||||
import { Frappe } from 'frappe/core/frappe';
|
import { Frappe } from 'frappe';
|
||||||
import Money from 'pesa/dist/types/src/money';
|
import Money from 'pesa/dist/types/src/money';
|
||||||
import { FieldType, FieldTypeEnum, RawValue, SchemaMap } from 'schemas/types';
|
import { FieldType, FieldTypeEnum, RawValue, SchemaMap } from 'schemas/types';
|
||||||
import { DatabaseBase, GetAllOptions } from 'utils/db/types';
|
import { DatabaseBase, GetAllOptions } from 'utils/db/types';
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Field, Model } from '@/types/model';
|
import { Field, Model } from '@/types/model';
|
||||||
import Doc from 'frappe/model/document';
|
import Doc from 'frappe/model/doc';
|
||||||
import Meta from 'frappe/model/meta';
|
import Meta from 'frappe/model/meta';
|
||||||
import { getDuplicates, getRandomString } from 'frappe/utils';
|
import { getDuplicates, getRandomString } from 'frappe/utils';
|
||||||
import Observable from 'frappe/utils/observable';
|
import Observable from 'frappe/utils/observable';
|
||||||
import { Frappe } from './frappe';
|
import { Frappe } from '..';
|
||||||
|
import { DocValue } from './types';
|
||||||
|
|
||||||
type DocMap = Record<string, Doc | undefined>;
|
type DocMap = Record<string, Doc | undefined>;
|
||||||
type MetaMap = Record<string, Meta | undefined>;
|
type MetaMap = Record<string, Meta | undefined>;
|
||||||
@ -109,8 +110,8 @@ export class DocHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDocFromCache(doctype: string, name: string): Doc | undefined {
|
getDocFromCache(schemaName: string, name: string): Doc | undefined {
|
||||||
const doc = (this.docs?.[doctype] as DocMap)?.[name];
|
const doc = (this.docs?.[schemaName] as DocMap)?.[name];
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,4 +252,10 @@ export class DocHandler {
|
|||||||
await doc.insert();
|
await doc.insert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCachedValue(
|
||||||
|
schemaName: string,
|
||||||
|
name: string,
|
||||||
|
fieldname: string
|
||||||
|
): DocValue {}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
import Doc from 'frappe/model/doc';
|
||||||
import Money from 'pesa/dist/types/src/money';
|
import Money from 'pesa/dist/types/src/money';
|
||||||
import { RawValue } from 'schemas/types';
|
import { RawValue } from 'schemas/types';
|
||||||
|
|
||||||
export type DocValue = string | number | boolean | Date | Money;
|
export type DocValue = string | number | boolean | Date | Money | null;
|
||||||
|
export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>;
|
||||||
export type DocValueMap = Record<string, DocValue | DocValueMap[]>;
|
|
||||||
export type RawValueMap = Record<string, RawValue | RawValueMap[]>;
|
export type RawValueMap = Record<string, RawValue | RawValueMap[]>;
|
||||||
|
|
||||||
export type SingleValue<T> = {
|
export type SingleValue<T> = {
|
||||||
|
321
frappe/index.js
321
frappe/index.js
@ -1,321 +0,0 @@
|
|||||||
import { getMoneyMaker } from 'pesa';
|
|
||||||
import { markRaw } from 'vue';
|
|
||||||
import { AuthHandler } from './core/authHandler';
|
|
||||||
import { asyncHandler, getDuplicates, getRandomString } from './utils';
|
|
||||||
import {
|
|
||||||
DEFAULT_DISPLAY_PRECISION,
|
|
||||||
DEFAULT_INTERNAL_PRECISION,
|
|
||||||
} from './utils/consts';
|
|
||||||
import * as errors from './utils/errors';
|
|
||||||
import { format } from './utils/format';
|
|
||||||
import Observable from './utils/observable';
|
|
||||||
import { t, T } from './utils/translation';
|
|
||||||
|
|
||||||
export class Frappe {
|
|
||||||
t = t;
|
|
||||||
T = T;
|
|
||||||
format = format;
|
|
||||||
|
|
||||||
errors = errors;
|
|
||||||
isElectron = false;
|
|
||||||
isServer = false;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.auth = new AuthHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeAndRegister(customModels = {}, force = false) {
|
|
||||||
this.init(force);
|
|
||||||
|
|
||||||
this.Meta = (await import('frappe/model/meta')).default;
|
|
||||||
this.Document = (await import('frappe/model/document')).default;
|
|
||||||
|
|
||||||
const coreModels = await import('frappe/models');
|
|
||||||
this.registerModels(coreModels.default);
|
|
||||||
this.registerModels(customModels);
|
|
||||||
}
|
|
||||||
|
|
||||||
init(force) {
|
|
||||||
if (this._initialized && !force) return;
|
|
||||||
|
|
||||||
// Initialize Globals
|
|
||||||
this.metaCache = {};
|
|
||||||
this.models = {};
|
|
||||||
|
|
||||||
this.methods = {};
|
|
||||||
this.errorLog = [];
|
|
||||||
|
|
||||||
// temp params while calling routes
|
|
||||||
this.temp = {};
|
|
||||||
|
|
||||||
this.docs = new Observable();
|
|
||||||
this.events = new Observable();
|
|
||||||
this._initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerModels(models) {
|
|
||||||
// register models from app/models/index.js
|
|
||||||
for (let doctype in models) {
|
|
||||||
let metaDefinition = models[doctype];
|
|
||||||
if (!metaDefinition.name) {
|
|
||||||
throw new Error(`Name is mandatory for ${doctype}`);
|
|
||||||
}
|
|
||||||
if (metaDefinition.name !== doctype) {
|
|
||||||
throw new Error(
|
|
||||||
`Model name mismatch for ${doctype}: ${metaDefinition.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let fieldnames = (metaDefinition.fields || [])
|
|
||||||
.map((df) => df.fieldname)
|
|
||||||
.sort();
|
|
||||||
let duplicateFieldnames = getDuplicates(fieldnames);
|
|
||||||
if (duplicateFieldnames.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.models[doctype] = metaDefinition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerMethod({ method, handler }) {
|
|
||||||
this.methods[method] = handler;
|
|
||||||
if (this.app) {
|
|
||||||
// add to router if client-server
|
|
||||||
this.app.post(
|
|
||||||
`/api/method/${method}`,
|
|
||||||
asyncHandler(async function (request, response) {
|
|
||||||
let data = await handler(request.body);
|
|
||||||
if (data === undefined) {
|
|
||||||
data = {};
|
|
||||||
}
|
|
||||||
return response.json(data);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initializeMoneyMaker(currency) {
|
|
||||||
currency ??= 'XXX';
|
|
||||||
|
|
||||||
// to be called after db initialization
|
|
||||||
const values =
|
|
||||||
(await frappe.db?.getSingleValues(
|
|
||||||
{
|
|
||||||
fieldname: 'internalPrecision',
|
|
||||||
parent: 'SystemSettings',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'displayPrecision',
|
|
||||||
parent: 'SystemSettings',
|
|
||||||
}
|
|
||||||
)) ?? [];
|
|
||||||
|
|
||||||
let { internalPrecision: precision, displayPrecision: display } =
|
|
||||||
values.reduce((acc, { fieldname, value }) => {
|
|
||||||
acc[fieldname] = value;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
if (typeof precision === 'undefined') {
|
|
||||||
precision = DEFAULT_INTERNAL_PRECISION;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof precision === 'string') {
|
|
||||||
precision = parseInt(precision);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof display === 'undefined') {
|
|
||||||
display = DEFAULT_DISPLAY_PRECISION;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof display === 'string') {
|
|
||||||
display = parseInt(display);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pesa = getMoneyMaker({
|
|
||||||
currency,
|
|
||||||
precision,
|
|
||||||
display,
|
|
||||||
wrapper: markRaw,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getModels(filterFunction) {
|
|
||||||
let models = [];
|
|
||||||
for (let doctype in this.models) {
|
|
||||||
models.push(this.models[doctype]);
|
|
||||||
}
|
|
||||||
return filterFunction ? models.filter(filterFunction) : models;
|
|
||||||
}
|
|
||||||
|
|
||||||
async call({ method, args }) {
|
|
||||||
if (this.isServer) {
|
|
||||||
if (this.methods[method]) {
|
|
||||||
return await this.methods[method](args);
|
|
||||||
} else {
|
|
||||||
throw new Error(`${method} not found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = `/api/method/${method}`;
|
|
||||||
let response = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(args || {}),
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
addToCache(doc) {
|
|
||||||
if (!this.docs) return;
|
|
||||||
|
|
||||||
// add to `docs` cache
|
|
||||||
if (doc.doctype && doc.name) {
|
|
||||||
if (!this.docs[doc.doctype]) {
|
|
||||||
this.docs[doc.doctype] = {};
|
|
||||||
}
|
|
||||||
this.docs[doc.doctype][doc.name] = doc;
|
|
||||||
|
|
||||||
// singles available as first level objects too
|
|
||||||
if (doc.doctype === doc.name) {
|
|
||||||
this[doc.name] = doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// propogate change to `docs`
|
|
||||||
doc.on('change', (params) => {
|
|
||||||
this.docs.trigger('change', params);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFromCache(doctype, name) {
|
|
||||||
try {
|
|
||||||
delete this.docs[doctype][name];
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Document ${doctype} ${name} does not exist`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isDirty(doctype, name) {
|
|
||||||
return (
|
|
||||||
(this.docs &&
|
|
||||||
this.docs[doctype] &&
|
|
||||||
this.docs[doctype][name] &&
|
|
||||||
this.docs[doctype][name]._dirty) ||
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocFromCache(doctype, name) {
|
|
||||||
if (this.docs && this.docs[doctype] && this.docs[doctype][name]) {
|
|
||||||
return this.docs[doctype][name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMeta(doctype) {
|
|
||||||
if (!this.metaCache[doctype]) {
|
|
||||||
let model = this.models[doctype];
|
|
||||||
if (!model) {
|
|
||||||
throw new Error(`${doctype} is not a registered doctype`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let metaClass = model.metaClass || this.Meta;
|
|
||||||
this.metaCache[doctype] = new metaClass(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.metaCache[doctype];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDoc(doctype, name, options = { skipDocumentCache: false }) {
|
|
||||||
let doc = options.skipDocumentCache
|
|
||||||
? null
|
|
||||||
: this.getDocFromCache(doctype, name);
|
|
||||||
if (!doc) {
|
|
||||||
doc = new (this.getDocumentClass(doctype))({
|
|
||||||
doctype: doctype,
|
|
||||||
name: name,
|
|
||||||
});
|
|
||||||
await doc.load();
|
|
||||||
this.addToCache(doc);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocumentClass(doctype) {
|
|
||||||
const meta = this.getMeta(doctype);
|
|
||||||
return meta.documentClass || this.Document;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSingle(doctype) {
|
|
||||||
return await this.getDoc(doctype, doctype);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDuplicate(doc) {
|
|
||||||
const newDoc = await this.getEmptyDoc(doc.doctype);
|
|
||||||
for (let field of this.getMeta(doc.doctype).getValidFields()) {
|
|
||||||
if (['name', 'submitted'].includes(field.fieldname)) continue;
|
|
||||||
if (field.fieldtype === 'Table') {
|
|
||||||
newDoc[field.fieldname] = (doc[field.fieldname] || []).map((d) => {
|
|
||||||
let newd = Object.assign({}, d);
|
|
||||||
newd.name = '';
|
|
||||||
return newd;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
newDoc[field.fieldname] = doc[field.fieldname];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newDoc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEmptyDoc(doctype, cacheDoc = true) {
|
|
||||||
let doc = this.getNewDoc({ doctype: doctype });
|
|
||||||
doc._notInserted = true;
|
|
||||||
doc.name = getRandomString();
|
|
||||||
|
|
||||||
if (cacheDoc) {
|
|
||||||
this.addToCache(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewDoc(data) {
|
|
||||||
let doc = new (this.getDocumentClass(data.doctype))(data);
|
|
||||||
doc.setDefaults();
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMeta(fields) {
|
|
||||||
return new this.Meta({ isCustom: 1, fields });
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncDoc(data) {
|
|
||||||
let doc;
|
|
||||||
if (await this.db.exists(data.doctype, data.name)) {
|
|
||||||
doc = await this.getDoc(data.doctype, data.name);
|
|
||||||
Object.assign(doc, data);
|
|
||||||
await doc.update();
|
|
||||||
} else {
|
|
||||||
doc = this.getNewDoc(data);
|
|
||||||
await doc.insert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.db.close();
|
|
||||||
this.auth.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
store = {
|
|
||||||
isDevelopment: false,
|
|
||||||
appVersion: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { T, t };
|
|
||||||
export default new Frappe();
|
|
@ -1,19 +1,17 @@
|
|||||||
import { ErrorLog } from '@/errorHandling';
|
import { ErrorLog } from '@/errorHandling';
|
||||||
import { Model } from '@/types/model';
|
import Doc from 'frappe/model/doc';
|
||||||
import Doc from 'frappe/model/document';
|
|
||||||
import Meta from 'frappe/model/meta';
|
|
||||||
import { getMoneyMaker, MoneyMaker } from 'pesa';
|
import { getMoneyMaker, MoneyMaker } from 'pesa';
|
||||||
import { markRaw } from 'vue';
|
import { markRaw } from 'vue';
|
||||||
|
import { AuthHandler } from './core/authHandler';
|
||||||
|
import { DatabaseHandler } from './core/dbHandler';
|
||||||
|
import { DocHandler } from './core/docHandler';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DISPLAY_PRECISION,
|
DEFAULT_DISPLAY_PRECISION,
|
||||||
DEFAULT_INTERNAL_PRECISION,
|
DEFAULT_INTERNAL_PRECISION,
|
||||||
} from '../utils/consts';
|
} from './utils/consts';
|
||||||
import * as errors from '../utils/errors';
|
import * as errors from './utils/errors';
|
||||||
import { format } from '../utils/format';
|
import { format } from './utils/format';
|
||||||
import { t, T } from '../utils/translation';
|
import { t, T } from './utils/translation';
|
||||||
import { AuthHandler } from './authHandler';
|
|
||||||
import { DatabaseHandler } from './dbHandler';
|
|
||||||
import { DocHandler } from './docHandler';
|
|
||||||
|
|
||||||
export class Frappe {
|
export class Frappe {
|
||||||
t = t;
|
t = t;
|
||||||
@ -30,8 +28,7 @@ export class Frappe {
|
|||||||
doc: DocHandler;
|
doc: DocHandler;
|
||||||
db: DatabaseHandler;
|
db: DatabaseHandler;
|
||||||
|
|
||||||
Meta?: typeof Meta;
|
Doc?: typeof Doc;
|
||||||
Document?: typeof Doc;
|
|
||||||
|
|
||||||
_initialized: boolean = false;
|
_initialized: boolean = false;
|
||||||
|
|
||||||
@ -66,8 +63,7 @@ export class Frappe {
|
|||||||
async initializeAndRegister(customModels = {}, force = false) {
|
async initializeAndRegister(customModels = {}, force = false) {
|
||||||
await this.init(force);
|
await this.init(force);
|
||||||
|
|
||||||
this.Meta = (await import('frappe/model/meta')).default;
|
this.Doc = (await import('frappe/model/doc')).default;
|
||||||
this.Document = (await import('frappe/model/document')).default;
|
|
||||||
|
|
||||||
const coreModels = await import('frappe/models');
|
const coreModels = await import('frappe/models');
|
||||||
this.doc.registerModels(coreModels.default as Record<string, Model>);
|
this.doc.registerModels(coreModels.default as Record<string, Model>);
|
691
frappe/model/doc.ts
Normal file
691
frappe/model/doc.ts
Normal file
@ -0,0 +1,691 @@
|
|||||||
|
import telemetry from '@/telemetry/telemetry';
|
||||||
|
import { Verb } from '@/telemetry/types';
|
||||||
|
import { DocValue, DocValueMap } from 'frappe/core/types';
|
||||||
|
import {
|
||||||
|
Conflict,
|
||||||
|
MandatoryError,
|
||||||
|
NotFoundError,
|
||||||
|
ValidationError,
|
||||||
|
} from 'frappe/utils/errors';
|
||||||
|
import Observable from 'frappe/utils/observable';
|
||||||
|
import Money from 'pesa/dist/types/src/money';
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldTypeEnum,
|
||||||
|
OptionField,
|
||||||
|
Schema,
|
||||||
|
TargetField,
|
||||||
|
} from 'schemas/types';
|
||||||
|
import { getIsNullOrUndef, getMapFromList } from 'utils';
|
||||||
|
import frappe from '..';
|
||||||
|
import { getRandomString, isPesa } from '../utils/index';
|
||||||
|
import {
|
||||||
|
areDocValuesEqual,
|
||||||
|
getMissingMandatoryMessage,
|
||||||
|
getPreDefaultValues,
|
||||||
|
shouldApplyFormula,
|
||||||
|
} from './helpers';
|
||||||
|
import { setName } from './naming';
|
||||||
|
import {
|
||||||
|
DefaultMap,
|
||||||
|
DependsOnMap,
|
||||||
|
FormulaMap,
|
||||||
|
RequiredMap,
|
||||||
|
ValidationMap,
|
||||||
|
} from './types';
|
||||||
|
import { validateSelect } from './validationFunction';
|
||||||
|
|
||||||
|
export default class Doc extends Observable<DocValue | Doc[]> {
|
||||||
|
name?: string;
|
||||||
|
schema: Readonly<Schema>;
|
||||||
|
fieldMap: Record<string, Field>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields below are used by child docs to maintain
|
||||||
|
* reference w.r.t their parent doc.
|
||||||
|
*/
|
||||||
|
idx?: number;
|
||||||
|
parentdoc?: Doc;
|
||||||
|
parentfield?: string;
|
||||||
|
|
||||||
|
_links?: Record<string, Doc>;
|
||||||
|
_dirty: boolean = true;
|
||||||
|
_notInserted: boolean = true;
|
||||||
|
|
||||||
|
flags = {
|
||||||
|
submitAction: false,
|
||||||
|
revertAction: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(schema: Schema, data: DocValueMap) {
|
||||||
|
super();
|
||||||
|
this.schema = schema;
|
||||||
|
this._setInitialValues(data);
|
||||||
|
this.fieldMap = getMapFromList(schema.fields, 'fieldname');
|
||||||
|
}
|
||||||
|
|
||||||
|
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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
_setInitialValues(data: DocValueMap) {
|
||||||
|
for (const fieldname in data) {
|
||||||
|
const value = data[fieldname];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const row of value) {
|
||||||
|
this.push(fieldname, row);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this[fieldname] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set unset fields as null
|
||||||
|
for (const field of this.schema.fields) {
|
||||||
|
if (this[field.fieldname] === undefined) {
|
||||||
|
this[field.fieldname] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirty(value: boolean) {
|
||||||
|
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') {
|
||||||
|
this.setMultiple(fieldname as DocValueMap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldname === 'numberSeries' && !this._notInserted) {
|
||||||
|
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);
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
this[fieldname] = value.map((row, i) => {
|
||||||
|
row.idx = i;
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const field = this.fieldMap[fieldname];
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
await this.applyChange(fieldname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMultiple(docValueMap: DocValueMap) {
|
||||||
|
for (const fieldname in docValueMap) {
|
||||||
|
await this.set(fieldname, docValueMap[fieldname] as DocValue | Doc[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyChange(fieldname: string) {
|
||||||
|
await this.applyFormula(fieldname);
|
||||||
|
await this.trigger('change', {
|
||||||
|
doc: this,
|
||||||
|
changed: fieldname,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaults() {
|
||||||
|
for (const field of this.schema.fields) {
|
||||||
|
if (!getIsNullOrUndef(this[field.fieldname])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultValue: DocValue | Doc[] = getPreDefaultValues(field.fieldtype);
|
||||||
|
const defaultFunction = this.defaults[field.fieldname];
|
||||||
|
|
||||||
|
if (defaultFunction !== undefined) {
|
||||||
|
defaultValue = defaultFunction();
|
||||||
|
} else if (field.default !== undefined) {
|
||||||
|
defaultValue = field.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
|
||||||
|
defaultValue = frappe.pesa!(defaultValue as string | number);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[field.fieldname] = defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
append(fieldname: string, docValueMap: Doc | DocValueMap = {}) {
|
||||||
|
// push child row and trigger change
|
||||||
|
this.push(fieldname, docValueMap);
|
||||||
|
this._dirty = true;
|
||||||
|
this.applyChange(fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
push(fieldname: string, docValueMap: Doc | DocValueMap = {}) {
|
||||||
|
// push child row without triggering change
|
||||||
|
this[fieldname] ??= [];
|
||||||
|
const childDoc = this._initChild(docValueMap, fieldname);
|
||||||
|
(this[fieldname] as Doc[]).push(childDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
_initChild(docValueMap: Doc | DocValueMap, fieldname: string): Doc {
|
||||||
|
if (docValueMap instanceof Doc) {
|
||||||
|
return docValueMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = Object.assign({}, docValueMap);
|
||||||
|
|
||||||
|
data.parent = this.name;
|
||||||
|
data.parenttype = this.schemaName;
|
||||||
|
data.parentfield = fieldname;
|
||||||
|
data.parentdoc = this;
|
||||||
|
|
||||||
|
if (!data.idx) {
|
||||||
|
data.idx = ((this[fieldname] as Doc[]) || []).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.name) {
|
||||||
|
data.name = getRandomString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const childSchemaName = this.fieldMap[fieldname] as TargetField;
|
||||||
|
const schema = frappe.db.schemaMap[childSchemaName.target];
|
||||||
|
const childDoc = new Doc(schema, data as DocValueMap);
|
||||||
|
childDoc.setDefaults();
|
||||||
|
return childDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateInsert() {
|
||||||
|
this.validateMandatory();
|
||||||
|
await this.validateFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateMandatory() {
|
||||||
|
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');
|
||||||
|
const message = frappe.t`Value missing for ${fields}`;
|
||||||
|
throw new MandatoryError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateFields() {
|
||||||
|
const fields = this.schema.fields;
|
||||||
|
for (const field of fields) {
|
||||||
|
if (field.fieldtype === FieldTypeEnum.Table) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.get(field.fieldname) as DocValue;
|
||||||
|
await this.validateField(field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateField(field: Field, value: DocValue) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
data[field.fieldname] = value;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseMetaValues() {
|
||||||
|
if (this.schema.isSubmittable && typeof this.submitted !== 'boolean') {
|
||||||
|
this.submitted = false;
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.createdBy) {
|
||||||
|
this.createdBy = frappe.auth.session.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.created) {
|
||||||
|
this.created = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateModified() {
|
||||||
|
this.modifiedBy = frappe.auth.session.user;
|
||||||
|
this.modified = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
if (this.name === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await frappe.db.get(this.schemaName, this.name);
|
||||||
|
if (data && data.name) {
|
||||||
|
this.syncValues(data);
|
||||||
|
if (this.schema.isSingle) {
|
||||||
|
this.setDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._links[fieldname] = await frappe.doc.getDoc(
|
||||||
|
field.target,
|
||||||
|
value as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLink(fieldname: string) {
|
||||||
|
return this._links ? this._links[fieldname] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncValues(data: DocValueMap) {
|
||||||
|
this.clearValues();
|
||||||
|
this._setInitialValues(data);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setChildIdx() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async compareWithCurrentDoc() {
|
||||||
|
if (this.isNew || !this.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDoc = await frappe.db.get(this.schemaName, this.name);
|
||||||
|
|
||||||
|
// check for conflict
|
||||||
|
if (
|
||||||
|
currentDoc &&
|
||||||
|
(this.modified as Date) !== (currentDoc.modified as Date)
|
||||||
|
) {
|
||||||
|
throw new Conflict(
|
||||||
|
frappe.t`Document ${this.doctype} ${this.name} has been modified after loading`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.submitted && !this.schema.isSubmittable) {
|
||||||
|
throw new ValidationError(
|
||||||
|
frappe.t`Document type ${this.doctype} is not submittable`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set submit action flag
|
||||||
|
if (this.submitted && !currentDoc.submitted) {
|
||||||
|
this.flags.submitAction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDoc.submitted && !this.submitted) {
|
||||||
|
this.flags.revertAction = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyFormula(fieldname?: string) {
|
||||||
|
if (fieldname && this.formulas[fieldname] === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
changed ||= await this.applyFormulaForFields(
|
||||||
|
formulaFields,
|
||||||
|
row,
|
||||||
|
fieldname
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parent or child row
|
||||||
|
const formulaFields = Object.keys(this.formulas).map(
|
||||||
|
(fn) => this.fieldMap[fn]
|
||||||
|
);
|
||||||
|
changed ||= await this.applyFormulaForFields(formulaFields, doc, fieldname);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyFormulaForFields(
|
||||||
|
formulaFields: Field[],
|
||||||
|
doc: Doc,
|
||||||
|
fieldname?: string
|
||||||
|
) {
|
||||||
|
let changed = false;
|
||||||
|
for (const field of formulaFields) {
|
||||||
|
if (!shouldApplyFormula(field, doc, fieldname)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVal = await this.getValueFromFormula(field, doc);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValueFromFormula(field: Field, doc: Doc) {
|
||||||
|
let value: Doc[] | DocValue;
|
||||||
|
|
||||||
|
const formula = doc.formulas[field.fieldtype];
|
||||||
|
if (formula === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = await formula();
|
||||||
|
|
||||||
|
if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) {
|
||||||
|
value = value.map((row) => this._initChild(row, field.fieldname));
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit() {
|
||||||
|
// re-run triggers
|
||||||
|
this.setChildIdx();
|
||||||
|
await this.applyFormula();
|
||||||
|
await this.trigger('validate', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert() {
|
||||||
|
await setName(this);
|
||||||
|
this.setBaseMetaValues();
|
||||||
|
await this.commit();
|
||||||
|
await this.validateInsert();
|
||||||
|
await this.trigger('beforeInsert', null);
|
||||||
|
|
||||||
|
const oldName = this.name!;
|
||||||
|
const data = await frappe.db.insert(this.schemaName, this.getValidDict());
|
||||||
|
this.syncValues(data);
|
||||||
|
|
||||||
|
if (oldName !== this.name) {
|
||||||
|
frappe.doc.removeFromCache(this.schemaName, oldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.trigger('afterInsert', null);
|
||||||
|
await this.trigger('afterSave', null);
|
||||||
|
|
||||||
|
telemetry.log(Verb.Created, this.schemaName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update() {
|
||||||
|
await this.compareWithCurrentDoc();
|
||||||
|
await this.commit();
|
||||||
|
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
|
||||||
|
this.updateModified();
|
||||||
|
|
||||||
|
const data = this.getValidDict();
|
||||||
|
await frappe.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');
|
||||||
|
if (this.flags.revertAction) await this.trigger('afterRevert');
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertOrUpdate() {
|
||||||
|
if (this._notInserted) {
|
||||||
|
return await this.insert();
|
||||||
|
} else {
|
||||||
|
return await this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete() {
|
||||||
|
await this.trigger('beforeDelete');
|
||||||
|
await frappe.db.delete(this.schemaName, this.name!);
|
||||||
|
await this.trigger('afterDelete');
|
||||||
|
|
||||||
|
telemetry.log(Verb.Deleted, this.schemaName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async submitOrRevert(isSubmit: boolean) {
|
||||||
|
const wasSubmitted = this.submitted;
|
||||||
|
this.submitted = isSubmit;
|
||||||
|
try {
|
||||||
|
await this.update();
|
||||||
|
} catch (e) {
|
||||||
|
this.submitted = wasSubmitted;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit() {
|
||||||
|
this.cancelled = false;
|
||||||
|
await this.submitOrRevert(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async revert() {
|
||||||
|
await this.submitOrRevert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async rename(newName: string) {
|
||||||
|
await this.trigger('beforeRename');
|
||||||
|
await frappe.db.rename(this.schemaName, this.name!, newName);
|
||||||
|
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 {
|
||||||
|
return frappe.pesa(value as string | number);
|
||||||
|
} catch (err) {
|
||||||
|
(
|
||||||
|
err as Error
|
||||||
|
).message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value as Money;
|
||||||
|
})
|
||||||
|
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||||
|
|
||||||
|
if (convertToFloat) {
|
||||||
|
return sum.float;
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFrom(schemaName: string, name: string, fieldname: string) {
|
||||||
|
if (name === undefined || fieldname === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return frappe.doc.getCachedValue(schemaName, name, fieldname);
|
||||||
|
}
|
||||||
|
|
||||||
|
async duplicate() {
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = frappe.doc.getEmptyDoc(this.schemaName, false);
|
||||||
|
await doc.setMultiple(updateMap);
|
||||||
|
await doc.insert();
|
||||||
|
}
|
||||||
|
|
||||||
|
formulas: FormulaMap = {};
|
||||||
|
defaults: DefaultMap = {};
|
||||||
|
validations: ValidationMap = {};
|
||||||
|
required: RequiredMap = {};
|
||||||
|
dependsOn: DependsOnMap = {};
|
||||||
|
}
|
103
frappe/model/helpers.ts
Normal file
103
frappe/model/helpers.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import frappe from 'frappe';
|
||||||
|
import { DocValue } from 'frappe/core/types';
|
||||||
|
import { isPesa } from 'frappe/utils';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import Money from 'pesa/dist/types/src/money';
|
||||||
|
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
|
||||||
|
import { getIsNullOrUndef } from 'utils';
|
||||||
|
import Doc from './doc';
|
||||||
|
|
||||||
|
export function areDocValuesEqual(
|
||||||
|
dvOne: DocValue | Doc[],
|
||||||
|
dvTwo: DocValue | Doc[]
|
||||||
|
): boolean {
|
||||||
|
if (['string', 'number'].includes(typeof dvOne) || dvOne instanceof Date) {
|
||||||
|
return dvOne === dvTwo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPesa(dvOne)) {
|
||||||
|
try {
|
||||||
|
return (dvOne as Money).eq(dvTwo as string | number);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isEqual(dvOne, dvTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPreDefaultValues(fieldtype: FieldType): DocValue | Doc[] {
|
||||||
|
switch (fieldtype) {
|
||||||
|
case FieldTypeEnum.Table:
|
||||||
|
return [] as Doc[];
|
||||||
|
case FieldTypeEnum.Currency:
|
||||||
|
return frappe.pesa!(0.0);
|
||||||
|
case FieldTypeEnum.Int:
|
||||||
|
case FieldTypeEnum.Float:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMissingMandatoryMessage(doc: Doc) {
|
||||||
|
const mandatoryFields = getMandatory(doc);
|
||||||
|
const message = mandatoryFields
|
||||||
|
.filter((f) => {
|
||||||
|
const value = doc.get(f.fieldname);
|
||||||
|
const isNullOrUndef = getIsNullOrUndef(value);
|
||||||
|
|
||||||
|
if (f.fieldtype === FieldTypeEnum.Table) {
|
||||||
|
return isNullOrUndef || (value as Doc[])?.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isNullOrUndef || value === '';
|
||||||
|
})
|
||||||
|
.map((f) => f.label)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
if (message && doc.schema.isChild && doc.parentdoc && doc.parentfield) {
|
||||||
|
const parentfield = doc.parentdoc.fieldMap[doc.parentfield];
|
||||||
|
return `${parentfield.label} Row ${(doc.idx ?? 0) + 1}: ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMandatory(doc: Doc): Field[] {
|
||||||
|
const mandatoryFields: Field[] = [];
|
||||||
|
for (const field of doc.schema.fields) {
|
||||||
|
if (field.required) {
|
||||||
|
mandatoryFields.push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredFunction = doc.required[field.fieldname];
|
||||||
|
if (typeof requiredFunction !== 'function') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredFunction()) {
|
||||||
|
mandatoryFields.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mandatoryFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldApplyFormula(field: Field, doc: Doc, fieldname?: string) {
|
||||||
|
if (!doc.formulas[field.fieldtype]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.readOnly) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependsOn = doc.dependsOn[field.fieldname] ?? [];
|
||||||
|
if (fieldname && dependsOn.includes(fieldname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = doc.get(field.fieldname);
|
||||||
|
return getIsNullOrUndef(value);
|
||||||
|
}
|
@ -1,114 +0,0 @@
|
|||||||
const cloneDeep = require('lodash/cloneDeep');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extend: (base, target, options = {}) => {
|
|
||||||
base = cloneDeep(base);
|
|
||||||
const fieldsToMerge = (target.fields || []).map(df => df.fieldname);
|
|
||||||
const fieldsToRemove = options.skipFields || [];
|
|
||||||
const overrideProps = options.overrideProps || [];
|
|
||||||
for (let prop of overrideProps) {
|
|
||||||
if (base.hasOwnProperty(prop)) {
|
|
||||||
delete base[prop];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mergeFields = (baseFields, targetFields) => {
|
|
||||||
let fields = cloneDeep(baseFields);
|
|
||||||
fields = fields
|
|
||||||
.filter(df => !fieldsToRemove.includes(df.fieldname))
|
|
||||||
.map(df => {
|
|
||||||
if (fieldsToMerge.includes(df.fieldname)) {
|
|
||||||
let copy = cloneDeep(df);
|
|
||||||
return Object.assign(
|
|
||||||
copy,
|
|
||||||
targetFields.find(tdf => tdf.fieldname === df.fieldname)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return df;
|
|
||||||
});
|
|
||||||
let fieldsAdded = fields.map(df => df.fieldname);
|
|
||||||
let fieldsToAdd = targetFields.filter(
|
|
||||||
df => !fieldsAdded.includes(df.fieldname)
|
|
||||||
);
|
|
||||||
return fields.concat(fieldsToAdd);
|
|
||||||
};
|
|
||||||
|
|
||||||
let fields = mergeFields(base.fields, target.fields || []);
|
|
||||||
let out = Object.assign(base, target);
|
|
||||||
out.fields = fields;
|
|
||||||
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
commonFields: [
|
|
||||||
{
|
|
||||||
fieldname: 'name',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
submittableFields: [
|
|
||||||
{
|
|
||||||
fieldname: 'submitted',
|
|
||||||
fieldtype: 'Check',
|
|
||||||
required: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
parentFields: [
|
|
||||||
{
|
|
||||||
fieldname: 'owner',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'modifiedBy',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'creation',
|
|
||||||
fieldtype: 'Datetime',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'modified',
|
|
||||||
fieldtype: 'Datetime',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'keywords',
|
|
||||||
fieldtype: 'Text'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
childFields: [
|
|
||||||
{
|
|
||||||
fieldname: 'idx',
|
|
||||||
fieldtype: 'Int',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parent',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parenttype',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'parentfield',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
treeFields: [
|
|
||||||
{
|
|
||||||
fieldname: 'lft',
|
|
||||||
fieldtype: 'Int'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldname: 'rgt',
|
|
||||||
fieldtype: 'Int'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
@ -1,334 +0,0 @@
|
|||||||
import frappe from 'frappe';
|
|
||||||
import { validTypes } from 'frappe/backends/helpers';
|
|
||||||
import { ValueError } from 'frappe/utils/errors';
|
|
||||||
import { indicators as indicatorColor } from '../../src/colors';
|
|
||||||
import { t } from '../utils/translation';
|
|
||||||
import Document from './document';
|
|
||||||
import model from './index';
|
|
||||||
|
|
||||||
export default class Meta extends Document {
|
|
||||||
filters;
|
|
||||||
basedOn;
|
|
||||||
|
|
||||||
constructor(data) {
|
|
||||||
if (data.basedOn) {
|
|
||||||
let config = frappe.models[data.basedOn];
|
|
||||||
Object.assign(data, config, {
|
|
||||||
name: data.name,
|
|
||||||
label: data.label,
|
|
||||||
filters: data.filters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
super(data);
|
|
||||||
this.setDefaultIndicators();
|
|
||||||
if (this.setupMeta) {
|
|
||||||
this.setupMeta();
|
|
||||||
}
|
|
||||||
if (!this.titleField) {
|
|
||||||
this.titleField = 'name';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setValues(data) {
|
|
||||||
Object.assign(this, data);
|
|
||||||
this.processFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
processFields() {
|
|
||||||
// add name field
|
|
||||||
if (!this.fields.find((df) => df.fieldname === 'name') && !this.isSingle) {
|
|
||||||
this.fields = [
|
|
||||||
{
|
|
||||||
label: t`ID`,
|
|
||||||
fieldname: 'name',
|
|
||||||
fieldtype: 'Data',
|
|
||||||
required: 1,
|
|
||||||
readOnly: 1,
|
|
||||||
},
|
|
||||||
].concat(this.fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fields = this.fields.map((df) => {
|
|
||||||
// name field is always required
|
|
||||||
if (df.fieldname === 'name') {
|
|
||||||
df.required = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return df;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
hasField(fieldname) {
|
|
||||||
return this.getField(fieldname) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getField(fieldname) {
|
|
||||||
if (!this._field_map) {
|
|
||||||
this._field_map = {};
|
|
||||||
for (let field of this.fields) {
|
|
||||||
this._field_map[field.fieldname] = field;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this._field_map[fieldname];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get fields filtered by filters
|
|
||||||
* @param {Object} filters
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* meta = frappe.getMeta('ToDo')
|
|
||||||
* dataFields = meta.getFieldsWith({ fieldtype: 'Data' })
|
|
||||||
*/
|
|
||||||
getFieldsWith(filters) {
|
|
||||||
return this.fields.filter((df) => {
|
|
||||||
let match = true;
|
|
||||||
for (const key in filters) {
|
|
||||||
const value = filters[key];
|
|
||||||
match = df[key] === value;
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getLabel(fieldname) {
|
|
||||||
let df = this.getField(fieldname);
|
|
||||||
return df.getLabel || df.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTableFields() {
|
|
||||||
if (this._tableFields === undefined) {
|
|
||||||
this._tableFields = this.fields.filter(
|
|
||||||
(field) => field.fieldtype === 'Table'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this._tableFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFormulaFields() {
|
|
||||||
if (this._formulaFields === undefined) {
|
|
||||||
this._formulaFields = this.fields.filter((field) => field.formula);
|
|
||||||
}
|
|
||||||
return this._formulaFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFormula() {
|
|
||||||
if (this._hasFormula === undefined) {
|
|
||||||
this._hasFormula = false;
|
|
||||||
if (this.getFormulaFields().length) {
|
|
||||||
this._hasFormula = true;
|
|
||||||
} else {
|
|
||||||
for (let tablefield of this.getTableFields()) {
|
|
||||||
if (frappe.getMeta(tablefield.childtype).getFormulaFields().length) {
|
|
||||||
this._hasFormula = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this._hasFormula;
|
|
||||||
}
|
|
||||||
|
|
||||||
getBaseDocType() {
|
|
||||||
return this.basedOn || this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(fieldname, value) {
|
|
||||||
this[fieldname] = value;
|
|
||||||
await this.trigger(fieldname);
|
|
||||||
}
|
|
||||||
|
|
||||||
get(fieldname) {
|
|
||||||
return this[fieldname];
|
|
||||||
}
|
|
||||||
|
|
||||||
getValidFields({ withChildren = true } = {}) {
|
|
||||||
if (!this._validFields) {
|
|
||||||
this._validFields = [];
|
|
||||||
this._validFieldsWithChildren = [];
|
|
||||||
|
|
||||||
const _add = (field) => {
|
|
||||||
this._validFields.push(field);
|
|
||||||
this._validFieldsWithChildren.push(field);
|
|
||||||
};
|
|
||||||
|
|
||||||
// fields validation
|
|
||||||
this.fields.forEach((df, i) => {
|
|
||||||
if (!df.fieldname) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`DocType ${this.name}: "fieldname" is required for field at index ${i}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!df.fieldtype) {
|
|
||||||
throw new ValidationError(
|
|
||||||
`DocType ${this.name}: "fieldtype" is required for field "${df.fieldname}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const doctypeFields = this.fields.map((field) => field.fieldname);
|
|
||||||
|
|
||||||
// standard fields
|
|
||||||
for (let field of model.commonFields) {
|
|
||||||
if (
|
|
||||||
validTypes.includes(field.fieldtype) &&
|
|
||||||
!doctypeFields.includes(field.fieldname)
|
|
||||||
) {
|
|
||||||
_add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isSubmittable) {
|
|
||||||
_add({
|
|
||||||
fieldtype: 'Check',
|
|
||||||
fieldname: 'submitted',
|
|
||||||
label: t`Submitted`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isChild) {
|
|
||||||
// child fields
|
|
||||||
for (let field of model.childFields) {
|
|
||||||
if (
|
|
||||||
validTypes.includes(field.fieldtype) &&
|
|
||||||
!doctypeFields.includes(field.fieldname)
|
|
||||||
) {
|
|
||||||
_add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// parent fields
|
|
||||||
for (let field of model.parentFields) {
|
|
||||||
if (
|
|
||||||
validTypes.includes(field.fieldtype) &&
|
|
||||||
!doctypeFields.includes(field.fieldname)
|
|
||||||
) {
|
|
||||||
_add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTree) {
|
|
||||||
// tree fields
|
|
||||||
for (let field of model.treeFields) {
|
|
||||||
if (
|
|
||||||
validTypes.includes(field.fieldtype) &&
|
|
||||||
!doctypeFields.includes(field.fieldname)
|
|
||||||
) {
|
|
||||||
_add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doctype fields
|
|
||||||
for (let field of this.fields) {
|
|
||||||
const include = validTypes.includes(field.fieldtype);
|
|
||||||
|
|
||||||
if (include) {
|
|
||||||
_add(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// include tables if (withChildren = True)
|
|
||||||
if (!include && field.fieldtype === 'Table') {
|
|
||||||
this._validFieldsWithChildren.push(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withChildren) {
|
|
||||||
return this._validFieldsWithChildren;
|
|
||||||
} else {
|
|
||||||
return this._validFields;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeywordFields() {
|
|
||||||
if (!this._keywordFields) {
|
|
||||||
this._keywordFields = this.keywordFields;
|
|
||||||
if (!(this._keywordFields && this._keywordFields.length && this.fields)) {
|
|
||||||
this._keywordFields = this.fields
|
|
||||||
.filter((field) => field.fieldtype !== 'Table' && field.required)
|
|
||||||
.map((field) => field.fieldname);
|
|
||||||
}
|
|
||||||
if (!(this._keywordFields && this._keywordFields.length)) {
|
|
||||||
this._keywordFields = ['name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this._keywordFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
getQuickEditFields() {
|
|
||||||
if (this.quickEditFields) {
|
|
||||||
return this.quickEditFields.map((fieldname) => this.getField(fieldname));
|
|
||||||
}
|
|
||||||
return this.getFieldsWith({ required: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
validateSelect(field, value) {
|
|
||||||
let options = field.options;
|
|
||||||
if (!options) return;
|
|
||||||
if (!field.required && value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let validValues = options;
|
|
||||||
|
|
||||||
if (typeof options === 'string') {
|
|
||||||
// values given as string
|
|
||||||
validValues = options.split('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof options[0] === 'object') {
|
|
||||||
// options as array of {label, value} pairs
|
|
||||||
validValues = options.map((o) => o.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validValues.includes(value)) {
|
|
||||||
throw new ValueError(
|
|
||||||
// prettier-ignore
|
|
||||||
`DocType ${this.name}: Invalid value "${value}" for "${field.label}". Must be one of ${options.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async trigger(event, params = {}) {
|
|
||||||
Object.assign(params, {
|
|
||||||
doc: this,
|
|
||||||
name: event,
|
|
||||||
});
|
|
||||||
|
|
||||||
await super.trigger(event, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultIndicators() {
|
|
||||||
if (!this.indicators) {
|
|
||||||
if (this.isSubmittable) {
|
|
||||||
this.indicators = {
|
|
||||||
key: 'submitted',
|
|
||||||
colors: {
|
|
||||||
0: indicatorColor.GRAY,
|
|
||||||
1: indicatorColor.BLUE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndicatorColor(doc) {
|
|
||||||
if (frappe.isDirty(this.name, doc.name)) {
|
|
||||||
return indicatorColor.ORANGE;
|
|
||||||
} else {
|
|
||||||
if (this.indicators) {
|
|
||||||
let value = doc[this.indicators.key];
|
|
||||||
if (value) {
|
|
||||||
return this.indicators.colors[value] || indicatorColor.GRAY;
|
|
||||||
} else {
|
|
||||||
return indicatorColor.GRAY;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return indicatorColor.GRAY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,8 @@
|
|||||||
import frappe from 'frappe';
|
import frappe from 'frappe';
|
||||||
import { getRandomString } from 'frappe/utils';
|
import { getRandomString } from 'frappe/utils';
|
||||||
|
|
||||||
export async function isNameAutoSet(doctype) {
|
export async function isNameAutoSet(schemaName: string) {
|
||||||
const doc = frappe.getEmptyDoc(doctype);
|
const doc = frappe.doc.getEmptyDoc(schemaName);
|
||||||
if (doc.meta.naming === 'autoincrement') {
|
if (doc.meta.naming === 'autoincrement') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
@ -1,26 +0,0 @@
|
|||||||
import frappe from 'frappe';
|
|
||||||
|
|
||||||
export default async function runPatches(patchList) {
|
|
||||||
const patchesAlreadyRun = (
|
|
||||||
await frappe.db.knex('PatchRun').select('name')
|
|
||||||
).map(({ name }) => name);
|
|
||||||
|
|
||||||
for (let patch of patchList) {
|
|
||||||
if (patchesAlreadyRun.includes(patch.patchName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await runPatch(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runPatch({ patchName, patchFunction }) {
|
|
||||||
try {
|
|
||||||
await patchFunction();
|
|
||||||
const patchRun = frappe.getEmptyDoc('PatchRun');
|
|
||||||
patchRun.name = patchName;
|
|
||||||
await patchRun.insert();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`could not run ${patchName}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
31
frappe/model/types.ts
Normal file
31
frappe/model/types.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { DocValue } from 'frappe/core/types';
|
||||||
|
import Doc from './doc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The functions below are used for dynamic evaluation
|
||||||
|
* and setting of field types.
|
||||||
|
*
|
||||||
|
* Since they are set directly on the doc, they can
|
||||||
|
* access the doc by using `this`.
|
||||||
|
*
|
||||||
|
* - `Formula`: Async function used for obtaining a computed value such as amount (rate * qty).
|
||||||
|
* - `Default`: Regular function used to dynamically set the default value, example new Date().
|
||||||
|
* - `Validation`: Async function that throw an error if the value is invalid.
|
||||||
|
* - `Required`: Regular function used to decide if a value is mandatory (there are !notnul in the db).
|
||||||
|
*/
|
||||||
|
export type Formula = () => Promise<DocValue>;
|
||||||
|
export type Default = () => DocValue;
|
||||||
|
export type Validation = (value: DocValue) => Promise<void>;
|
||||||
|
export type Required = () => boolean;
|
||||||
|
|
||||||
|
export type FormulaMap = Record<string, Formula | undefined>;
|
||||||
|
export type DefaultMap = Record<string, Default | undefined>;
|
||||||
|
export type ValidationMap = Record<string, Validation | undefined>;
|
||||||
|
export type RequiredMap = Record<string, Required | undefined>;
|
||||||
|
|
||||||
|
export type DependsOnMap = Record<string, string[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should add this for hidden too
|
||||||
|
|
||||||
|
*/
|
39
frappe/model/validationFunction.ts
Normal file
39
frappe/model/validationFunction.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { ValidationError, ValueError } from 'frappe/utils/errors';
|
||||||
|
import { t } from 'frappe/utils/translation';
|
||||||
|
import { OptionField } from 'schemas/types';
|
||||||
|
|
||||||
|
export function email(value: string) {
|
||||||
|
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new ValidationError(`Invalid email: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function phone(value: string) {
|
||||||
|
const isValid = /[+]{0,1}[\d ]+/.test(value);
|
||||||
|
if (!isValid) {
|
||||||
|
throw new ValidationError(`Invalid phone: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateSelect(field: OptionField, value: string) {
|
||||||
|
const options = field.options;
|
||||||
|
if (!options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.required && (value === null || value === undefined)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validValues = options.map((o) => o.value);
|
||||||
|
|
||||||
|
if (validValues.includes(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = options.map((o) => o.label).join(', ');
|
||||||
|
throw new ValueError(
|
||||||
|
t`Invalid value ${value} for ${field.label}. Must be one of ${labels}`
|
||||||
|
);
|
||||||
|
}
|
@ -4,7 +4,7 @@ enum EventType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class Observable<T> {
|
export default class Observable<T> {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown | T;
|
||||||
_isHot: Map<string, boolean>;
|
_isHot: Map<string, boolean>;
|
||||||
_eventQueue: Map<string, unknown[]>;
|
_eventQueue: Map<string, unknown[]>;
|
||||||
_map: Map<string, unknown>;
|
_map: Map<string, unknown>;
|
||||||
@ -25,8 +25,8 @@ export default class Observable<T> {
|
|||||||
* @param key
|
* @param key
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
get(key: string): unknown {
|
get(key: string): T {
|
||||||
return this[key];
|
return this[key] as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +35,7 @@ export default class Observable<T> {
|
|||||||
* @param key
|
* @param key
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
set(key: string, value: unknown) {
|
set(key: string, value: T) {
|
||||||
this[key] = value;
|
this[key] = value;
|
||||||
this.trigger('change', {
|
this.trigger('change', {
|
||||||
doc: this,
|
doc: this,
|
||||||
|
@ -199,9 +199,10 @@ export default class PaymentServer extends Document {
|
|||||||
async updateReferenceOutstandingAmount() {
|
async updateReferenceOutstandingAmount() {
|
||||||
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
|
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
|
||||||
const refDoc = await frappe.getDoc(referenceType, referenceName);
|
const refDoc = await frappe.getDoc(referenceType, referenceName);
|
||||||
refDoc.update({
|
refDoc.setMultiple({
|
||||||
outstandingAmount: refDoc.outstandingAmount.add(amount),
|
outstandingAmount: refDoc.outstandingAmount.add(amount),
|
||||||
});
|
});
|
||||||
|
refDoc.update();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,8 @@
|
|||||||
"label": "Address",
|
"label": "Address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Address",
|
"target": "Address",
|
||||||
"placeholder": "Click to create"
|
"placeholder": "Click to create",
|
||||||
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "template",
|
"fieldname": "template",
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
export function getMapFromList<T, K extends keyof T>(
|
|
||||||
list: T[],
|
|
||||||
name: K
|
|
||||||
): Record<string, T> {
|
|
||||||
const acc: Record<string, T> = {};
|
|
||||||
for (const t of list) {
|
|
||||||
const key = t[name];
|
|
||||||
if (key === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[String(key)] = t;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getListFromMap<T>(map: Record<string, T>): T[] {
|
|
||||||
return Object.keys(map).map((n) => map[n]);
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { getListFromMap, getMapFromList } from './helpers';
|
import { getListFromMap, getMapFromList } from 'utils';
|
||||||
import regionalSchemas from './regional';
|
import regionalSchemas from './regional';
|
||||||
import { appSchemas, coreSchemas, metaSchemas } from './schemas';
|
import { appSchemas, coreSchemas, metaSchemas } from './schemas';
|
||||||
import { Field, Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types';
|
import { Field, Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { cloneDeep, isEqual } from 'lodash';
|
import { cloneDeep, isEqual } from 'lodash';
|
||||||
import { describe } from 'mocha';
|
import { describe } from 'mocha';
|
||||||
import { getMapFromList } from '../helpers';
|
import { getMapFromList } from 'utils';
|
||||||
import {
|
import {
|
||||||
addMetaFields,
|
addMetaFields,
|
||||||
cleanSchemas,
|
cleanSchemas,
|
||||||
|
@ -59,7 +59,6 @@ export enum FieldTypeEnum {
|
|||||||
export type FieldType = keyof typeof FieldTypeEnum;
|
export type FieldType = keyof typeof FieldTypeEnum;
|
||||||
export type RawValue = string | number | boolean | null;
|
export type RawValue = string | number | boolean | null;
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
export interface BaseField {
|
export interface BaseField {
|
||||||
fieldname: string; // Column name in the db
|
fieldname: string; // Column name in the db
|
||||||
fieldtype: FieldType; // UI Descriptive field types that map to column types
|
fieldtype: FieldType; // UI Descriptive field types that map to column types
|
||||||
@ -73,6 +72,7 @@ export interface BaseField {
|
|||||||
groupBy?: string; // UI Facing used in dropdowns fields
|
groupBy?: string; // UI Facing used in dropdowns fields
|
||||||
computed?: boolean; // Indicates whether a value is computed, implies readonly
|
computed?: boolean; // Indicates whether a value is computed, implies readonly
|
||||||
meta?: boolean; // Field is a meta field, i.e. only for the db, not UI
|
meta?: boolean; // Field is a meta field, i.e. only for the db, not UI
|
||||||
|
inline?: boolean; // UI Facing config, whether to display doc inline.
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SelectOption = { value: string; label: string };
|
export type SelectOption = { value: string; label: string };
|
||||||
@ -84,19 +84,16 @@ export interface OptionField extends BaseField {
|
|||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
export interface TargetField extends BaseField {
|
export interface TargetField extends BaseField {
|
||||||
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
||||||
target: string; // Name of the table or group of tables to fetch values
|
target: string; // Name of the table or group of tables to fetch values
|
||||||
}
|
}
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
export interface DynamicLinkField extends BaseField {
|
export interface DynamicLinkField extends BaseField {
|
||||||
fieldtype: FieldTypeEnum.DynamicLink;
|
fieldtype: FieldTypeEnum.DynamicLink;
|
||||||
references: string; // Reference to an option field that links to schema
|
references: string; // Reference to an option field that links to schema
|
||||||
}
|
}
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
export interface NumberField extends BaseField {
|
export interface NumberField extends BaseField {
|
||||||
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int;
|
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int;
|
||||||
minvalue?: number; // UI Facing used to restrict lower bound
|
minvalue?: number; // UI Facing used to restrict lower bound
|
||||||
@ -112,7 +109,6 @@ export type Field =
|
|||||||
|
|
||||||
export type TreeSettings = { parentField: string };
|
export type TreeSettings = { parentField: string };
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
name: string; // Table name
|
name: string; // Table name
|
||||||
label: string; // Translateable UI facing name
|
label: string; // Translateable UI facing name
|
||||||
|
@ -87,10 +87,10 @@
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Icon from '@/components/Icon';
|
import Icon from '@/components/Icon';
|
||||||
import PageHeader from '@/components/PageHeader';
|
import PageHeader from '@/components/PageHeader';
|
||||||
import { IPC_MESSAGES } from 'utils/messages';
|
|
||||||
import { openSettings, routeTo } from '@/utils';
|
import { openSettings, routeTo } from '@/utils';
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import frappe, { t } from 'frappe';
|
import frappe, { t } from 'frappe';
|
||||||
|
import { IPC_MESSAGES } from 'utils/messages';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -112,8 +112,7 @@ export default {
|
|||||||
key: 'Invoice',
|
key: 'Invoice',
|
||||||
label: t`Invoice`,
|
label: t`Invoice`,
|
||||||
icon: 'invoice',
|
icon: 'invoice',
|
||||||
description:
|
description: t`Customize your invoices by adding a logo and address details`,
|
||||||
t`Customize your invoices by adding a logo and address details`,
|
|
||||||
fieldname: 'invoiceSetup',
|
fieldname: 'invoiceSetup',
|
||||||
action() {
|
action() {
|
||||||
openSettings('Invoice');
|
openSettings('Invoice');
|
||||||
@ -123,8 +122,7 @@ export default {
|
|||||||
key: 'General',
|
key: 'General',
|
||||||
label: t`General`,
|
label: t`General`,
|
||||||
icon: 'general',
|
icon: 'general',
|
||||||
description:
|
description: t`Setup your company information, email, country and fiscal year`,
|
||||||
t`Setup your company information, email, country and fiscal year`,
|
|
||||||
fieldname: 'companySetup',
|
fieldname: 'companySetup',
|
||||||
action() {
|
action() {
|
||||||
openSettings('General');
|
openSettings('General');
|
||||||
@ -134,8 +132,7 @@ export default {
|
|||||||
key: 'System',
|
key: 'System',
|
||||||
label: t`System`,
|
label: t`System`,
|
||||||
icon: 'system',
|
icon: 'system',
|
||||||
description:
|
description: t`Setup system defaults like date format and display precision`,
|
||||||
t`Setup system defaults like date format and display precision`,
|
|
||||||
fieldname: 'systemSetup',
|
fieldname: 'systemSetup',
|
||||||
action() {
|
action() {
|
||||||
openSettings('System');
|
openSettings('System');
|
||||||
@ -151,8 +148,7 @@ export default {
|
|||||||
key: 'Review Accounts',
|
key: 'Review Accounts',
|
||||||
label: t`Review Accounts`,
|
label: t`Review Accounts`,
|
||||||
icon: 'review-ac',
|
icon: 'review-ac',
|
||||||
description:
|
description: t`Review your chart of accounts, add any account or tax heads as needed`,
|
||||||
t`Review your chart of accounts, add any account or tax heads as needed`,
|
|
||||||
action: () => {
|
action: () => {
|
||||||
routeTo('/chart-of-accounts');
|
routeTo('/chart-of-accounts');
|
||||||
},
|
},
|
||||||
@ -165,8 +161,7 @@ export default {
|
|||||||
label: t`Opening Balances`,
|
label: t`Opening Balances`,
|
||||||
icon: 'opening-ac',
|
icon: 'opening-ac',
|
||||||
fieldname: 'openingBalanceChecked',
|
fieldname: 'openingBalanceChecked',
|
||||||
description:
|
description: t`Setup your opening balances before performing any accounting entries`,
|
||||||
t`Setup your opening balances before performing any accounting entries`,
|
|
||||||
documentation:
|
documentation:
|
||||||
'https://frappebooks.com/docs/setting-up#5-setup-opening-balances',
|
'https://frappebooks.com/docs/setting-up#5-setup-opening-balances',
|
||||||
},
|
},
|
||||||
@ -175,8 +170,7 @@ export default {
|
|||||||
label: t`Add Taxes`,
|
label: t`Add Taxes`,
|
||||||
icon: 'percentage',
|
icon: 'percentage',
|
||||||
fieldname: 'taxesAdded',
|
fieldname: 'taxesAdded',
|
||||||
description:
|
description: t`Setup your tax templates for your sales or purchase transactions`,
|
||||||
t`Setup your tax templates for your sales or purchase transactions`,
|
|
||||||
action: () => routeTo('/list/Tax'),
|
action: () => routeTo('/list/Tax'),
|
||||||
documentation:
|
documentation:
|
||||||
'https://frappebooks.com/docs/setting-up#2-add-taxes',
|
'https://frappebooks.com/docs/setting-up#2-add-taxes',
|
||||||
@ -191,8 +185,7 @@ export default {
|
|||||||
key: 'Add Sales Items',
|
key: 'Add Sales Items',
|
||||||
label: t`Add Items`,
|
label: t`Add Items`,
|
||||||
icon: 'item',
|
icon: 'item',
|
||||||
description:
|
description: t`Add products or services that you sell to your customers`,
|
||||||
t`Add products or services that you sell to your customers`,
|
|
||||||
action: () => routeTo('/list/Item'),
|
action: () => routeTo('/list/Item'),
|
||||||
fieldname: 'itemCreated',
|
fieldname: 'itemCreated',
|
||||||
documentation:
|
documentation:
|
||||||
@ -212,8 +205,7 @@ export default {
|
|||||||
key: 'Create Invoice',
|
key: 'Create Invoice',
|
||||||
label: t`Create Invoice`,
|
label: t`Create Invoice`,
|
||||||
icon: 'sales-invoice',
|
icon: 'sales-invoice',
|
||||||
description:
|
description: t`Create your first invoice and mail it to your customer`,
|
||||||
t`Create your first invoice and mail it to your customer`,
|
|
||||||
action: () => routeTo('/list/SalesInvoice'),
|
action: () => routeTo('/list/SalesInvoice'),
|
||||||
fieldname: 'invoiceCreated',
|
fieldname: 'invoiceCreated',
|
||||||
documentation: 'https://frappebooks.com/docs/invoices',
|
documentation: 'https://frappebooks.com/docs/invoices',
|
||||||
@ -228,8 +220,7 @@ export default {
|
|||||||
key: 'Add Purchase Items',
|
key: 'Add Purchase Items',
|
||||||
label: t`Add Items`,
|
label: t`Add Items`,
|
||||||
icon: 'item',
|
icon: 'item',
|
||||||
description:
|
description: t`Add products or services that you buy from your suppliers`,
|
||||||
t`Add products or services that you buy from your suppliers`,
|
|
||||||
action: () => routeTo('/list/Item'),
|
action: () => routeTo('/list/Item'),
|
||||||
fieldname: 'itemCreated',
|
fieldname: 'itemCreated',
|
||||||
},
|
},
|
||||||
@ -245,8 +236,7 @@ export default {
|
|||||||
key: 'Create Bill',
|
key: 'Create Bill',
|
||||||
label: t`Create Bill`,
|
label: t`Create Bill`,
|
||||||
icon: 'purchase-invoice',
|
icon: 'purchase-invoice',
|
||||||
description:
|
description: t`Create your first bill and mail it to your supplier`,
|
||||||
t`Create your first bill and mail it to your supplier`,
|
|
||||||
action: () => routeTo('/list/PurchaseInvoice'),
|
action: () => routeTo('/list/PurchaseInvoice'),
|
||||||
fieldname: 'billCreated',
|
fieldname: 'billCreated',
|
||||||
documentation: 'https://frappebooks.com/docs/bills',
|
documentation: 'https://frappebooks.com/docs/bills',
|
||||||
@ -316,7 +306,8 @@ export default {
|
|||||||
if (onboardingComplete) {
|
if (onboardingComplete) {
|
||||||
await this.updateChecks({ onboardingComplete });
|
await this.updateChecks({ onboardingComplete });
|
||||||
const systemSettings = await frappe.getSingle('SystemSettings');
|
const systemSettings = await frappe.getSingle('SystemSettings');
|
||||||
await systemSettings.update({ hideGetStarted: 1 });
|
await systemSettings.set({ hideGetStarted: 1 });
|
||||||
|
await systemSettings.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
return onboardingComplete;
|
return onboardingComplete;
|
||||||
@ -380,7 +371,8 @@ export default {
|
|||||||
await this.updateChecks(toUpdate);
|
await this.updateChecks(toUpdate);
|
||||||
},
|
},
|
||||||
async updateChecks(toUpdate) {
|
async updateChecks(toUpdate) {
|
||||||
await frappe.GetStarted.update(toUpdate);
|
await frappe.GetStarted.setMultiple(toUpdate);
|
||||||
|
await frappe.GetStarted.update();
|
||||||
frappe.GetStarted = await frappe.getSingle('GetStarted');
|
frappe.GetStarted = await frappe.getSingle('GetStarted');
|
||||||
},
|
},
|
||||||
isCompleted(item) {
|
isCompleted(item) {
|
||||||
|
@ -27,7 +27,7 @@ export default async function setupCompany(setupWizardValues) {
|
|||||||
const locale = countryList[country]['locale'] ?? DEFAULT_LOCALE;
|
const locale = countryList[country]['locale'] ?? DEFAULT_LOCALE;
|
||||||
await callInitializeMoneyMaker(currency);
|
await callInitializeMoneyMaker(currency);
|
||||||
|
|
||||||
await accountingSettings.update({
|
const accountingSettingsUpdateMap = {
|
||||||
companyName,
|
companyName,
|
||||||
country,
|
country,
|
||||||
fullname: name,
|
fullname: name,
|
||||||
@ -36,25 +36,34 @@ export default async function setupCompany(setupWizardValues) {
|
|||||||
fiscalYearStart,
|
fiscalYearStart,
|
||||||
fiscalYearEnd,
|
fiscalYearEnd,
|
||||||
currency,
|
currency,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await accountingSettings.setMultiple(accountingSettingsUpdateMap);
|
||||||
|
await accountingSettings.update();
|
||||||
|
|
||||||
const printSettings = await frappe.getSingle('PrintSettings');
|
const printSettings = await frappe.getSingle('PrintSettings');
|
||||||
printSettings.update({
|
const printSettingsUpdateMap = {
|
||||||
logo: companyLogo,
|
logo: companyLogo,
|
||||||
companyName,
|
companyName,
|
||||||
email,
|
email,
|
||||||
displayLogo: companyLogo ? 1 : 0,
|
displayLogo: companyLogo ? true : false,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
await printSettings.setMultiple(printSettingsUpdateMap);
|
||||||
|
await printSettings.update();
|
||||||
|
|
||||||
await setupGlobalCurrencies(countryList);
|
await setupGlobalCurrencies(countryList);
|
||||||
await setupChartOfAccounts(bankName, country, chartOfAccounts);
|
await setupChartOfAccounts(bankName, country, chartOfAccounts);
|
||||||
await setupRegionalChanges(country);
|
await setupRegionalChanges(country);
|
||||||
updateInitializationConfig();
|
updateInitializationConfig();
|
||||||
|
|
||||||
await accountingSettings.update({ setupComplete: 1 });
|
await accountingSettings.setMultiple({ setupComplete: true });
|
||||||
|
await accountingSettings.update();
|
||||||
frappe.AccountingSettings = accountingSettings;
|
frappe.AccountingSettings = accountingSettings;
|
||||||
|
|
||||||
(await frappe.getSingle('SystemSettings')).update({ locale });
|
const systemSettings = await frappe.getSingle('SystemSettings');
|
||||||
|
systemSettings.setMultiple({ locale });
|
||||||
|
systemSettings.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupGlobalCurrencies(countries) {
|
async function setupGlobalCurrencies(countries) {
|
||||||
|
@ -31,3 +31,27 @@ export function getRandomString(): string {
|
|||||||
export async function sleep(durationMilliseconds: number = 1000) {
|
export async function sleep(durationMilliseconds: number = 1000) {
|
||||||
return new Promise((r) => setTimeout(() => r(null), durationMilliseconds));
|
return new Promise((r) => setTimeout(() => r(null), durationMilliseconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMapFromList<T, K extends keyof T>(
|
||||||
|
list: T[],
|
||||||
|
name: K
|
||||||
|
): Record<string, T> {
|
||||||
|
const acc: Record<string, T> = {};
|
||||||
|
for (const t of list) {
|
||||||
|
const key = t[name];
|
||||||
|
if (key === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[String(key)] = t;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getListFromMap<T>(map: Record<string, T>): T[] {
|
||||||
|
return Object.keys(map).map((n) => map[n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIsNullOrUndef(value: unknown): boolean {
|
||||||
|
return value === null || value === undefined;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user