2018-01-16 06:09:17 +00:00
|
|
|
const frappe = require('frappejs');
|
2018-02-13 11:54:57 +00:00
|
|
|
const Observable = require('frappejs/utils/observable');
|
2018-03-05 16:45:21 +00:00
|
|
|
const naming = require('./naming');
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2018-02-13 11:54:57 +00:00
|
|
|
module.exports = class BaseDocument extends Observable {
|
2019-07-30 11:01:06 +00:00
|
|
|
constructor(data) {
|
|
|
|
super();
|
|
|
|
this.fetchValuesCache = {};
|
|
|
|
this.flags = {};
|
|
|
|
this.setup();
|
|
|
|
Object.assign(this, data);
|
|
|
|
|
|
|
|
// clear fetch-values cache
|
|
|
|
frappe.db.on(
|
|
|
|
'change',
|
|
|
|
params => (this.fetchValuesCache[`${params.doctype}:${params.name}`] = {})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
setup() {
|
|
|
|
// add listeners
|
|
|
|
}
|
|
|
|
|
|
|
|
get meta() {
|
|
|
|
if (this.isCustom) {
|
|
|
|
this._meta = frappe.createMeta(this.fields);
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
if (!this._meta) {
|
|
|
|
this._meta = frappe.getMeta(this.doctype);
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
return this._meta;
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
async getSettings() {
|
|
|
|
if (!this._settings) {
|
|
|
|
this._settings = await frappe.getSingle(this.meta.settings);
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
return this._settings;
|
|
|
|
}
|
|
|
|
|
|
|
|
// set value and trigger change
|
|
|
|
async set(fieldname, value) {
|
|
|
|
if (typeof fieldname === 'object') {
|
|
|
|
const valueDict = fieldname;
|
|
|
|
for (let fieldname in valueDict) {
|
|
|
|
await this.set(fieldname, valueDict[fieldname]);
|
|
|
|
}
|
|
|
|
return;
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
if (this[fieldname] !== value) {
|
|
|
|
this._dirty = true;
|
|
|
|
this[fieldname] = await this.validateField(fieldname, value);
|
|
|
|
await this.applyChange(fieldname);
|
2018-02-16 13:13:46 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async applyChange(fieldname) {
|
|
|
|
if (await this.applyFormula()) {
|
|
|
|
// multiple changes
|
|
|
|
await this.trigger('change', {
|
2019-08-28 08:54:11 +00:00
|
|
|
doc: this,
|
|
|
|
changed: fieldname
|
2019-07-30 11:01:06 +00:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// no other change, trigger control refresh
|
|
|
|
await this.trigger('change', {
|
|
|
|
doc: this,
|
2019-08-28 08:54:11 +00:00
|
|
|
fieldname: fieldname,
|
|
|
|
changed: fieldname
|
2019-07-30 11:01:06 +00:00
|
|
|
});
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setDefaults() {
|
|
|
|
for (let field of this.meta.fields) {
|
|
|
|
if (
|
|
|
|
this[field.fieldname] === null ||
|
|
|
|
this[field.fieldname] === undefined
|
|
|
|
) {
|
|
|
|
let defaultValue = null;
|
|
|
|
|
|
|
|
if (field.fieldtype === 'Table') {
|
|
|
|
defaultValue = [];
|
2018-02-12 12:01:31 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
if (field.default) {
|
|
|
|
defaultValue = field.default;
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
this[field.fieldname] = defaultValue;
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
setKeywords() {
|
|
|
|
let keywords = [];
|
|
|
|
for (let fieldname of this.meta.getKeywordFields()) {
|
|
|
|
keywords.push(this[fieldname]);
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
this.keywords = keywords.join(', ');
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
append(key, document) {
|
|
|
|
if (!this[key]) {
|
|
|
|
this[key] = [];
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
this[key].push(this.initDoc(document));
|
|
|
|
}
|
|
|
|
|
|
|
|
initDoc(data) {
|
|
|
|
if (data.prototype instanceof Document) {
|
|
|
|
return data;
|
|
|
|
} else {
|
|
|
|
return new Document(data);
|
|
|
|
}
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
async validateField(key, value) {
|
|
|
|
let field = this.meta.getField(key);
|
|
|
|
if (field && field.fieldtype == 'Select') {
|
|
|
|
return this.meta.validateSelect(field, value);
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
return value;
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
getValidDict() {
|
|
|
|
let data = {};
|
|
|
|
for (let field of this.meta.getValidFields()) {
|
|
|
|
data[field.fieldname] = this[field.fieldname];
|
2018-03-05 16:45:21 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
getFullDict() {
|
|
|
|
let data = this.getValidDict();
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
setStandardValues() {
|
|
|
|
// set standard values on server-side only
|
|
|
|
if (frappe.isServer) {
|
|
|
|
let now = new Date().toISOString();
|
|
|
|
if (!this.submitted) {
|
|
|
|
this.submitted = 0;
|
|
|
|
}
|
2018-03-05 16:45:21 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
if (!this.owner) {
|
|
|
|
this.owner = frappe.session.user;
|
|
|
|
}
|
2018-05-07 04:23:20 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
if (!this.creation) {
|
|
|
|
this.creation = now;
|
|
|
|
}
|
2018-05-07 04:23:20 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
if (!this.modifiedBy) {
|
|
|
|
this.modifiedBy = frappe.session.user;
|
|
|
|
}
|
|
|
|
this.modified = now;
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async load() {
|
|
|
|
let data = await frappe.db.get(this.doctype, this.name);
|
|
|
|
if (data.name) {
|
|
|
|
this.syncValues(data);
|
|
|
|
if (this.meta.isSingle) {
|
|
|
|
this.setDefaults();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new frappe.errors.NotFound(
|
|
|
|
`Not Found: ${this.doctype} ${this.name}`
|
|
|
|
);
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
syncValues(data) {
|
|
|
|
this.clearValues();
|
|
|
|
Object.assign(this, data);
|
|
|
|
this._dirty = false;
|
|
|
|
this.trigger('change', {
|
|
|
|
doc: this
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
clearValues() {
|
|
|
|
for (let field of this.meta.getValidFields()) {
|
|
|
|
if (this[field.fieldname]) {
|
|
|
|
delete this[field.fieldname];
|
|
|
|
}
|
2018-02-01 11:07:36 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setChildIdx() {
|
|
|
|
// renumber children
|
|
|
|
for (let field of this.meta.getValidFields()) {
|
|
|
|
if (field.fieldtype === 'Table') {
|
|
|
|
for (let i = 0; i < (this[field.fieldname] || []).length; i++) {
|
|
|
|
this[field.fieldname][i].idx = i;
|
2018-02-01 11:07:36 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
2018-02-01 11:07:36 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async compareWithCurrentDoc() {
|
|
|
|
if (frappe.isServer && !this.isNew()) {
|
|
|
|
let currentDoc = await frappe.db.get(this.doctype, this.name);
|
|
|
|
|
|
|
|
// check for conflict
|
|
|
|
if (currentDoc && this.modified != currentDoc.modified) {
|
|
|
|
throw new frappe.errors.Conflict(
|
|
|
|
frappe._('Document {0} {1} has been modified after loading', [
|
|
|
|
this.doctype,
|
|
|
|
this.name
|
|
|
|
])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.submitted && !this.meta.isSubmittable) {
|
|
|
|
throw new frappe.errors.ValidationError(
|
|
|
|
frappe._('Document type {1} is not submittable', [this.doctype])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// set submit action flag
|
|
|
|
if (this.submitted && !currentDoc.submitted) {
|
|
|
|
this.flags.submitAction = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (currentDoc.submitted && !this.submitted) {
|
|
|
|
this.flags.revertAction = true;
|
|
|
|
}
|
2018-02-07 13:23:52 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
2018-02-07 13:23:52 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
async applyFormula() {
|
|
|
|
if (!this.meta.hasFormula()) {
|
|
|
|
return false;
|
2018-02-27 16:29:14 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
let doc = this;
|
|
|
|
|
|
|
|
// children
|
|
|
|
for (let tablefield of this.meta.getTableFields()) {
|
|
|
|
let formulaFields = frappe
|
|
|
|
.getMeta(tablefield.childtype)
|
|
|
|
.getFormulaFields();
|
|
|
|
if (formulaFields.length) {
|
|
|
|
// for each row
|
|
|
|
for (let row of this[tablefield.fieldname]) {
|
|
|
|
for (let field of formulaFields) {
|
|
|
|
if (shouldApplyFormula(field, row)) {
|
|
|
|
const val = await field.formula(row, doc);
|
|
|
|
if (val !== false && val !== undefined) {
|
|
|
|
row[field.fieldname] = val;
|
|
|
|
}
|
2018-02-08 11:45:32 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
2018-02-08 11:45:32 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
2018-02-07 13:23:52 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
// parent
|
|
|
|
for (let field of this.meta.getFormulaFields()) {
|
|
|
|
if (shouldApplyFormula(field, doc)) {
|
|
|
|
const val = await field.formula(doc);
|
|
|
|
if (val !== false && val !== undefined) {
|
|
|
|
doc[field.fieldname] = val;
|
|
|
|
}
|
|
|
|
}
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
return true;
|
2018-03-05 16:45:21 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
function shouldApplyFormula(field, doc) {
|
|
|
|
if (field.readOnly) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-03-05 16:45:21 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
if (!frappe.isServer || frappe.isElectron) {
|
|
|
|
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
|
|
|
return true;
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
return false;
|
2018-01-12 12:25:07 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async commit() {
|
|
|
|
// re-run triggers
|
|
|
|
this.setStandardValues();
|
|
|
|
this.setKeywords();
|
|
|
|
this.setChildIdx();
|
|
|
|
await this.applyFormula();
|
|
|
|
await this.trigger('validate');
|
|
|
|
}
|
|
|
|
|
|
|
|
async insert() {
|
|
|
|
await naming.setName(this);
|
|
|
|
await this.commit();
|
|
|
|
await this.trigger('beforeInsert');
|
|
|
|
|
|
|
|
const data = await frappe.db.insert(this.doctype, this.getValidDict());
|
|
|
|
this.syncValues(data);
|
|
|
|
|
|
|
|
await this.trigger('afterInsert');
|
|
|
|
await this.trigger('afterSave');
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
const data = await frappe.db.update(this.doctype, this.getValidDict());
|
|
|
|
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 delete() {
|
|
|
|
await this.trigger('beforeDelete');
|
|
|
|
await frappe.db.delete(this.doctype, this.name);
|
|
|
|
await this.trigger('afterDelete');
|
|
|
|
}
|
|
|
|
|
|
|
|
async submit() {
|
|
|
|
this.submitted = 1;
|
|
|
|
this.update();
|
|
|
|
}
|
|
|
|
|
|
|
|
async revert() {
|
|
|
|
this.submitted = 0;
|
|
|
|
this.update();
|
|
|
|
}
|
|
|
|
|
|
|
|
// trigger methods on the class if they match
|
|
|
|
// with the trigger name
|
|
|
|
async trigger(event, params) {
|
|
|
|
if (this[event]) {
|
|
|
|
await this[event](params);
|
2018-02-09 12:55:55 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
await super.trigger(event, params);
|
|
|
|
}
|
|
|
|
|
|
|
|
// helper functions
|
|
|
|
getSum(tablefield, childfield) {
|
|
|
|
return this[tablefield]
|
2019-08-28 08:54:11 +00:00
|
|
|
.map(d => frappe.parseNumber(d[childfield]) || 0)
|
2019-07-30 11:01:06 +00:00
|
|
|
.reduce((a, b) => a + b, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getFrom(doctype, name, fieldname) {
|
|
|
|
if (!name) return '';
|
|
|
|
let _values =
|
|
|
|
this.fetchValuesCache[`${doctype}:${name}`] ||
|
|
|
|
(this.fetchValuesCache[`${doctype}:${name}`] = {});
|
|
|
|
if (!_values[fieldname]) {
|
|
|
|
_values[fieldname] = await frappe.db.getValue(doctype, name, fieldname);
|
2018-02-09 12:55:55 +00:00
|
|
|
}
|
2019-07-30 11:01:06 +00:00
|
|
|
return _values[fieldname];
|
|
|
|
}
|
2018-11-09 19:07:14 +00:00
|
|
|
|
2019-07-30 11:01:06 +00:00
|
|
|
isNew() {
|
|
|
|
return this._notInserted;
|
|
|
|
}
|
|
|
|
};
|