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 'mocha';
|
||||
import { getMapFromList } from 'schemas/helpers';
|
||||
import { FieldTypeEnum, RawValue } from 'schemas/types';
|
||||
import { getValueMapFromList, sleep } from 'utils';
|
||||
import { getMapFromList, getValueMapFromList, sleep } from 'utils';
|
||||
import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../helpers';
|
||||
import DatabaseCore from '../core';
|
||||
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 {
|
||||
serverURL: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DatabaseDemux } from '@/demux/db';
|
||||
import { Frappe } from 'frappe/core/frappe';
|
||||
import { Frappe } from 'frappe';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { FieldType, FieldTypeEnum, RawValue, SchemaMap } from 'schemas/types';
|
||||
import { DatabaseBase, GetAllOptions } from 'utils/db/types';
|
||||
|
@ -1,9 +1,10 @@
|
||||
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 { getDuplicates, getRandomString } from 'frappe/utils';
|
||||
import Observable from 'frappe/utils/observable';
|
||||
import { Frappe } from './frappe';
|
||||
import { Frappe } from '..';
|
||||
import { DocValue } from './types';
|
||||
|
||||
type DocMap = Record<string, Doc | undefined>;
|
||||
type MetaMap = Record<string, Meta | undefined>;
|
||||
@ -109,8 +110,8 @@ export class DocHandler {
|
||||
}
|
||||
}
|
||||
|
||||
getDocFromCache(doctype: string, name: string): Doc | undefined {
|
||||
const doc = (this.docs?.[doctype] as DocMap)?.[name];
|
||||
getDocFromCache(schemaName: string, name: string): Doc | undefined {
|
||||
const doc = (this.docs?.[schemaName] as DocMap)?.[name];
|
||||
return doc;
|
||||
}
|
||||
|
||||
@ -251,4 +252,10 @@ export class DocHandler {
|
||||
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 { RawValue } from 'schemas/types';
|
||||
|
||||
export type DocValue = string | number | boolean | Date | Money;
|
||||
|
||||
export type DocValueMap = Record<string, DocValue | DocValueMap[]>;
|
||||
export type DocValue = string | number | boolean | Date | Money | null;
|
||||
export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>;
|
||||
export type RawValueMap = Record<string, RawValue | RawValueMap[]>;
|
||||
|
||||
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 { Model } from '@/types/model';
|
||||
import Doc from 'frappe/model/document';
|
||||
import Meta from 'frappe/model/meta';
|
||||
import Doc from 'frappe/model/doc';
|
||||
import { getMoneyMaker, MoneyMaker } from 'pesa';
|
||||
import { markRaw } from 'vue';
|
||||
import { AuthHandler } from './core/authHandler';
|
||||
import { DatabaseHandler } from './core/dbHandler';
|
||||
import { DocHandler } from './core/docHandler';
|
||||
import {
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
} from '../utils/consts';
|
||||
import * as errors from '../utils/errors';
|
||||
import { format } from '../utils/format';
|
||||
import { t, T } from '../utils/translation';
|
||||
import { AuthHandler } from './authHandler';
|
||||
import { DatabaseHandler } from './dbHandler';
|
||||
import { DocHandler } from './docHandler';
|
||||
} from './utils/consts';
|
||||
import * as errors from './utils/errors';
|
||||
import { format } from './utils/format';
|
||||
import { t, T } from './utils/translation';
|
||||
|
||||
export class Frappe {
|
||||
t = t;
|
||||
@ -30,8 +28,7 @@ export class Frappe {
|
||||
doc: DocHandler;
|
||||
db: DatabaseHandler;
|
||||
|
||||
Meta?: typeof Meta;
|
||||
Document?: typeof Doc;
|
||||
Doc?: typeof Doc;
|
||||
|
||||
_initialized: boolean = false;
|
||||
|
||||
@ -66,8 +63,7 @@ export class Frappe {
|
||||
async initializeAndRegister(customModels = {}, force = false) {
|
||||
await this.init(force);
|
||||
|
||||
this.Meta = (await import('frappe/model/meta')).default;
|
||||
this.Document = (await import('frappe/model/document')).default;
|
||||
this.Doc = (await import('frappe/model/doc')).default;
|
||||
|
||||
const coreModels = await import('frappe/models');
|
||||
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 { getRandomString } from 'frappe/utils';
|
||||
|
||||
export async function isNameAutoSet(doctype) {
|
||||
const doc = frappe.getEmptyDoc(doctype);
|
||||
export async function isNameAutoSet(schemaName: string) {
|
||||
const doc = frappe.doc.getEmptyDoc(schemaName);
|
||||
if (doc.meta.naming === 'autoincrement') {
|
||||
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> {
|
||||
[key: string]: unknown;
|
||||
[key: string]: unknown | T;
|
||||
_isHot: Map<string, boolean>;
|
||||
_eventQueue: Map<string, unknown[]>;
|
||||
_map: Map<string, unknown>;
|
||||
@ -25,8 +25,8 @@ export default class Observable<T> {
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
get(key: string): unknown {
|
||||
return this[key];
|
||||
get(key: string): T {
|
||||
return this[key] as T;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +35,7 @@ export default class Observable<T> {
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
set(key: string, value: unknown) {
|
||||
set(key: string, value: T) {
|
||||
this[key] = value;
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
|
@ -199,9 +199,10 @@ export default class PaymentServer extends Document {
|
||||
async updateReferenceOutstandingAmount() {
|
||||
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
|
||||
const refDoc = await frappe.getDoc(referenceType, referenceName);
|
||||
refDoc.update({
|
||||
refDoc.setMultiple({
|
||||
outstandingAmount: refDoc.outstandingAmount.add(amount),
|
||||
});
|
||||
refDoc.update();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,8 @@
|
||||
"label": "Address",
|
||||
"fieldtype": "Link",
|
||||
"target": "Address",
|
||||
"placeholder": "Click to create"
|
||||
"placeholder": "Click to create",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"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 { getListFromMap, getMapFromList } from './helpers';
|
||||
import { getListFromMap, getMapFromList } from 'utils';
|
||||
import regionalSchemas from './regional';
|
||||
import { appSchemas, coreSchemas, metaSchemas } from './schemas';
|
||||
import { Field, Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as assert from 'assert';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import { describe } from 'mocha';
|
||||
import { getMapFromList } from '../helpers';
|
||||
import { getMapFromList } from 'utils';
|
||||
import {
|
||||
addMetaFields,
|
||||
cleanSchemas,
|
||||
|
@ -59,7 +59,6 @@ export enum FieldTypeEnum {
|
||||
export type FieldType = keyof typeof FieldTypeEnum;
|
||||
export type RawValue = string | number | boolean | null;
|
||||
|
||||
// prettier-ignore
|
||||
export interface BaseField {
|
||||
fieldname: string; // Column name in the db
|
||||
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
|
||||
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
|
||||
inline?: boolean; // UI Facing config, whether to display doc inline.
|
||||
}
|
||||
|
||||
export type SelectOption = { value: string; label: string };
|
||||
@ -84,19 +84,16 @@ export interface OptionField extends BaseField {
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export interface TargetField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
||||
target: string; // Name of the table or group of tables to fetch values
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
export interface DynamicLinkField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.DynamicLink;
|
||||
references: string; // Reference to an option field that links to schema
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
export interface NumberField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int;
|
||||
minvalue?: number; // UI Facing used to restrict lower bound
|
||||
@ -112,7 +109,6 @@ export type Field =
|
||||
|
||||
export type TreeSettings = { parentField: string };
|
||||
|
||||
// @formatter:off
|
||||
export interface Schema {
|
||||
name: string; // Table name
|
||||
label: string; // Translateable UI facing name
|
||||
|
@ -87,10 +87,10 @@
|
||||
import Button from '@/components/Button';
|
||||
import Icon from '@/components/Icon';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
import { IPC_MESSAGES } from 'utils/messages';
|
||||
import { openSettings, routeTo } from '@/utils';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import frappe, { t } from 'frappe';
|
||||
import { IPC_MESSAGES } from 'utils/messages';
|
||||
import { h } from 'vue';
|
||||
|
||||
export default {
|
||||
@ -112,8 +112,7 @@ export default {
|
||||
key: 'Invoice',
|
||||
label: t`Invoice`,
|
||||
icon: 'invoice',
|
||||
description:
|
||||
t`Customize your invoices by adding a logo and address details`,
|
||||
description: t`Customize your invoices by adding a logo and address details`,
|
||||
fieldname: 'invoiceSetup',
|
||||
action() {
|
||||
openSettings('Invoice');
|
||||
@ -123,8 +122,7 @@ export default {
|
||||
key: 'General',
|
||||
label: t`General`,
|
||||
icon: 'general',
|
||||
description:
|
||||
t`Setup your company information, email, country and fiscal year`,
|
||||
description: t`Setup your company information, email, country and fiscal year`,
|
||||
fieldname: 'companySetup',
|
||||
action() {
|
||||
openSettings('General');
|
||||
@ -134,8 +132,7 @@ export default {
|
||||
key: 'System',
|
||||
label: t`System`,
|
||||
icon: 'system',
|
||||
description:
|
||||
t`Setup system defaults like date format and display precision`,
|
||||
description: t`Setup system defaults like date format and display precision`,
|
||||
fieldname: 'systemSetup',
|
||||
action() {
|
||||
openSettings('System');
|
||||
@ -151,8 +148,7 @@ export default {
|
||||
key: 'Review Accounts',
|
||||
label: t`Review Accounts`,
|
||||
icon: 'review-ac',
|
||||
description:
|
||||
t`Review your chart of accounts, add any account or tax heads as needed`,
|
||||
description: t`Review your chart of accounts, add any account or tax heads as needed`,
|
||||
action: () => {
|
||||
routeTo('/chart-of-accounts');
|
||||
},
|
||||
@ -165,8 +161,7 @@ export default {
|
||||
label: t`Opening Balances`,
|
||||
icon: 'opening-ac',
|
||||
fieldname: 'openingBalanceChecked',
|
||||
description:
|
||||
t`Setup your opening balances before performing any accounting entries`,
|
||||
description: t`Setup your opening balances before performing any accounting entries`,
|
||||
documentation:
|
||||
'https://frappebooks.com/docs/setting-up#5-setup-opening-balances',
|
||||
},
|
||||
@ -175,8 +170,7 @@ export default {
|
||||
label: t`Add Taxes`,
|
||||
icon: 'percentage',
|
||||
fieldname: 'taxesAdded',
|
||||
description:
|
||||
t`Setup your tax templates for your sales or purchase transactions`,
|
||||
description: t`Setup your tax templates for your sales or purchase transactions`,
|
||||
action: () => routeTo('/list/Tax'),
|
||||
documentation:
|
||||
'https://frappebooks.com/docs/setting-up#2-add-taxes',
|
||||
@ -191,8 +185,7 @@ export default {
|
||||
key: 'Add Sales Items',
|
||||
label: t`Add Items`,
|
||||
icon: 'item',
|
||||
description:
|
||||
t`Add products or services that you sell to your customers`,
|
||||
description: t`Add products or services that you sell to your customers`,
|
||||
action: () => routeTo('/list/Item'),
|
||||
fieldname: 'itemCreated',
|
||||
documentation:
|
||||
@ -212,8 +205,7 @@ export default {
|
||||
key: 'Create Invoice',
|
||||
label: t`Create Invoice`,
|
||||
icon: 'sales-invoice',
|
||||
description:
|
||||
t`Create your first invoice and mail it to your customer`,
|
||||
description: t`Create your first invoice and mail it to your customer`,
|
||||
action: () => routeTo('/list/SalesInvoice'),
|
||||
fieldname: 'invoiceCreated',
|
||||
documentation: 'https://frappebooks.com/docs/invoices',
|
||||
@ -228,8 +220,7 @@ export default {
|
||||
key: 'Add Purchase Items',
|
||||
label: t`Add Items`,
|
||||
icon: 'item',
|
||||
description:
|
||||
t`Add products or services that you buy from your suppliers`,
|
||||
description: t`Add products or services that you buy from your suppliers`,
|
||||
action: () => routeTo('/list/Item'),
|
||||
fieldname: 'itemCreated',
|
||||
},
|
||||
@ -245,8 +236,7 @@ export default {
|
||||
key: 'Create Bill',
|
||||
label: t`Create Bill`,
|
||||
icon: 'purchase-invoice',
|
||||
description:
|
||||
t`Create your first bill and mail it to your supplier`,
|
||||
description: t`Create your first bill and mail it to your supplier`,
|
||||
action: () => routeTo('/list/PurchaseInvoice'),
|
||||
fieldname: 'billCreated',
|
||||
documentation: 'https://frappebooks.com/docs/bills',
|
||||
@ -316,7 +306,8 @@ export default {
|
||||
if (onboardingComplete) {
|
||||
await this.updateChecks({ onboardingComplete });
|
||||
const systemSettings = await frappe.getSingle('SystemSettings');
|
||||
await systemSettings.update({ hideGetStarted: 1 });
|
||||
await systemSettings.set({ hideGetStarted: 1 });
|
||||
await systemSettings.update();
|
||||
}
|
||||
|
||||
return onboardingComplete;
|
||||
@ -380,7 +371,8 @@ export default {
|
||||
await this.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');
|
||||
},
|
||||
isCompleted(item) {
|
||||
|
@ -27,7 +27,7 @@ export default async function setupCompany(setupWizardValues) {
|
||||
const locale = countryList[country]['locale'] ?? DEFAULT_LOCALE;
|
||||
await callInitializeMoneyMaker(currency);
|
||||
|
||||
await accountingSettings.update({
|
||||
const accountingSettingsUpdateMap = {
|
||||
companyName,
|
||||
country,
|
||||
fullname: name,
|
||||
@ -36,25 +36,34 @@ export default async function setupCompany(setupWizardValues) {
|
||||
fiscalYearStart,
|
||||
fiscalYearEnd,
|
||||
currency,
|
||||
});
|
||||
};
|
||||
|
||||
await accountingSettings.setMultiple(accountingSettingsUpdateMap);
|
||||
await accountingSettings.update();
|
||||
|
||||
const printSettings = await frappe.getSingle('PrintSettings');
|
||||
printSettings.update({
|
||||
const printSettingsUpdateMap = {
|
||||
logo: companyLogo,
|
||||
companyName,
|
||||
email,
|
||||
displayLogo: companyLogo ? 1 : 0,
|
||||
});
|
||||
displayLogo: companyLogo ? true : false,
|
||||
};
|
||||
|
||||
await printSettings.setMultiple(printSettingsUpdateMap);
|
||||
await printSettings.update();
|
||||
|
||||
await setupGlobalCurrencies(countryList);
|
||||
await setupChartOfAccounts(bankName, country, chartOfAccounts);
|
||||
await setupRegionalChanges(country);
|
||||
updateInitializationConfig();
|
||||
|
||||
await accountingSettings.update({ setupComplete: 1 });
|
||||
await accountingSettings.setMultiple({ setupComplete: true });
|
||||
await accountingSettings.update();
|
||||
frappe.AccountingSettings = accountingSettings;
|
||||
|
||||
(await frappe.getSingle('SystemSettings')).update({ locale });
|
||||
const systemSettings = await frappe.getSingle('SystemSettings');
|
||||
systemSettings.setMultiple({ locale });
|
||||
systemSettings.update();
|
||||
}
|
||||
|
||||
async function setupGlobalCurrencies(countries) {
|
||||
|
@ -31,3 +31,27 @@ export function getRandomString(): string {
|
||||
export async function sleep(durationMilliseconds: number = 1000) {
|
||||
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