2
0
mirror of https://github.com/frappe/books.git synced 2025-01-12 11:04:12 +00:00

Merge pull request #152 from 18alantom/add-conditional-require

feat: add conditional `required`, fix formatting in document.js, fix async calls for submit and revert.
This commit is contained in:
Alan 2021-11-08 19:49:08 +05:30 committed by GitHub
commit b791f1af33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 37 deletions

View File

@ -113,6 +113,12 @@ module.exports = class Database extends Observable {
buildColumnForTable(table, field) { buildColumnForTable(table, field) {
let columnType = this.getColumnType(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); let column = table[columnType](field.fieldname);
// primary key // primary key
@ -121,12 +127,12 @@ module.exports = class Database extends Observable {
} }
// default value // default value
if (field.default) { if (!!field.default && !(field.default instanceof Function)) {
column.defaultTo(field.default); column.defaultTo(field.default);
} }
// required // required
if (field.required) { if (!!field.required && !(field.required instanceof Function)) {
column.notNullable(); column.notNullable();
} }

View File

@ -6,14 +6,20 @@ Note: A model is called `DocType` in Frappe.js
### Fields ### Fields
Every model must have a set of fields (these become database columns). All fields must have Every model must have a set of fields (these become database columns). All fields may have the following properties:
- `fieldname`: Column name in database / property name - `fieldname`: Column name in database / property name.
- `fieldtype`: Data type ([see details](fields.md)) - `fieldtype`: Data type ([see details](fields.md)).
- `label`: Display label - `label`: Display label.
- `required`: Is mandatory - `required`: Is mandatory.
- `hidden`: Is hidden - `hidden`: Is hidden.
- `disabled`: Is disabled - `disabled`: Is disabled.
### Conditional Fields
The following fields: `hidden`, `required` can be conditional, depending on the values of other fields.
This is done by setting it's value to a function that receives an object with all the models fields and their values and returns a boolean.
See _"Posting Date"_ in the example below.
### Example ### Example
@ -28,7 +34,7 @@ module.exports = {
"fieldname": "subject", "fieldname": "subject",
"label": "Subject", "label": "Subject",
"fieldtype": "Data", "fieldtype": "Data",
"reqd": 1 "required": 1
}, },
{ {
"fieldname": "description", "fieldname": "description",
@ -44,8 +50,14 @@ module.exports = {
"Closed" "Closed"
], ],
"default": "Open", "default": "Open",
"reqd": 1 "required": 1
} },
{
"fieldname": "postingDate",
"label": "Posting Date",
"fieldtype": "Date",
"required": (doc) => doc.status === "Closed"
},
] ]
} }
``` ```

View File

@ -99,7 +99,7 @@ module.exports = class BaseDocument extends Observable {
this.roundFloats(); this.roundFloats();
await this.trigger('change', { await this.trigger('change', {
doc: this, doc: this,
changed: fieldname changed: fieldname,
}); });
} }
@ -194,15 +194,16 @@ module.exports = class BaseDocument extends Observable {
} }
validateMandatory() { validateMandatory() {
const fieldValueMap = this.getFieldValueMap();
let checkForMandatory = [this]; let checkForMandatory = [this];
let tableFields = this.meta.fields.filter(df => df.fieldtype === 'Table'); let tableFields = this.meta.fields.filter((df) => df.fieldtype === 'Table');
tableFields.map(df => { tableFields.map((df) => {
let rows = this[df.fieldname]; let rows = this[df.fieldname];
checkForMandatory = [...checkForMandatory, ...rows]; checkForMandatory = [...checkForMandatory, ...rows];
}); });
let missingMandatory = checkForMandatory let missingMandatory = checkForMandatory
.map(doc => getMissingMandatory(doc)) .map((doc) => getMissingMandatory(doc))
.filter(Boolean); .filter(Boolean);
if (missingMandatory.length > 0) { if (missingMandatory.length > 0) {
@ -212,16 +213,21 @@ module.exports = class BaseDocument extends Observable {
} }
function getMissingMandatory(doc) { function getMissingMandatory(doc) {
let mandatoryFields = doc.meta.fields.filter(df => df.required); let mandatoryFields = doc.meta.fields.filter((df) => {
if (df.required instanceof Function) {
return df.required(fieldValueMap);
}
return df.required;
});
let message = mandatoryFields let message = mandatoryFields
.filter(df => { .filter((df) => {
let value = doc[df.fieldname]; let value = doc[df.fieldname];
if (df.fieldtype === 'Table') { if (df.fieldtype === 'Table') {
return value == null || value.length === 0; return value == null || value.length === 0;
} }
return value == null || value === ''; return value == null || value === '';
}) })
.map(df => { .map((df) => {
return `"${df.label}"`; return `"${df.label}"`;
}) })
.join(', '); .join(', ');
@ -277,7 +283,7 @@ module.exports = class BaseDocument extends Observable {
if (!isValid) { if (!isValid) {
throw new frappe.errors.ValidationError(`Invalid phone: ${value}`); throw new frappe.errors.ValidationError(`Invalid phone: ${value}`);
} }
} },
}; };
return functions[validator.type]; return functions[validator.type];
@ -288,7 +294,9 @@ module.exports = class BaseDocument extends Observable {
for (let field of this.meta.getValidFields()) { for (let field of this.meta.getValidFields()) {
let value = this[field.fieldname]; let value = this[field.fieldname];
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = value.map(doc => (doc.getValidDict ? doc.getValidDict() : doc)); value = value.map((doc) =>
doc.getValidDict ? doc.getValidDict() : doc
);
} }
data[field.fieldname] = value; data[field.fieldname] = value;
} }
@ -341,7 +349,7 @@ module.exports = class BaseDocument extends Observable {
async loadLinks() { async loadLinks() {
this._links = {}; this._links = {};
let inlineLinks = this.meta.fields.filter(df => df.inline); let inlineLinks = this.meta.fields.filter((df) => df.inline);
for (let df of inlineLinks) { for (let df of inlineLinks) {
await this.loadLink(df.fieldname); await this.loadLink(df.fieldname);
} }
@ -367,13 +375,13 @@ module.exports = class BaseDocument extends Observable {
this.setValues(data); this.setValues(data);
this._dirty = false; this._dirty = false;
this.trigger('change', { this.trigger('change', {
doc: this doc: this,
}); });
} }
clearValues() { clearValues() {
let toClear = ['_dirty', '_notInserted'].concat( let toClear = ['_dirty', '_notInserted'].concat(
this.meta.getValidFields().map(df => df.fieldname) this.meta.getValidFields().map((df) => df.fieldname)
); );
for (let key of toClear) { for (let key of toClear) {
this[key] = null; this[key] = null;
@ -400,7 +408,7 @@ module.exports = class BaseDocument extends Observable {
throw new frappe.errors.Conflict( throw new frappe.errors.Conflict(
frappe._('Document {0} {1} has been modified after loading', [ frappe._('Document {0} {1} has been modified after loading', [
this.doctype, this.doctype,
this.name this.name,
]) ])
); );
} }
@ -412,7 +420,7 @@ module.exports = class BaseDocument extends Observable {
} }
// set submit action flag // set submit action flag
this.flags = {} this.flags = {};
if (this.submitted && !currentDoc.submitted) { if (this.submitted && !currentDoc.submitted) {
this.flags.submitAction = true; this.flags.submitAction = true;
} }
@ -506,7 +514,7 @@ module.exports = class BaseDocument extends Observable {
} }
if (field.fieldtype === 'Table' && Array.isArray(value)) { if (field.fieldtype === 'Table' && Array.isArray(value)) {
value = value.map(row => { value = value.map((row) => {
let doc = this._initChild(row, field.fieldname); let doc = this._initChild(row, field.fieldname);
doc.roundFloats(); doc.roundFloats();
return doc; return doc;
@ -519,7 +527,7 @@ module.exports = class BaseDocument extends Observable {
roundFloats() { roundFloats() {
let fields = this.meta let fields = this.meta
.getValidFields() .getValidFields()
.filter(df => ['Float', 'Currency', 'Table'].includes(df.fieldtype)); .filter((df) => ['Float', 'Currency', 'Table'].includes(df.fieldtype));
for (let df of fields) { for (let df of fields) {
let value = this[df.fieldname]; let value = this[df.fieldname];
@ -528,7 +536,7 @@ module.exports = class BaseDocument extends Observable {
} }
// child // child
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.map(row => row.roundFloats()); value.map((row) => row.roundFloats());
continue; continue;
} }
// field // field
@ -600,11 +608,11 @@ module.exports = class BaseDocument extends Observable {
return this; return this;
} }
insertOrUpdate() { async insertOrUpdate() {
if (this._notInserted) { if (this._notInserted) {
return this.insert(); return await this.insert();
} else { } else {
return this.update(); return await this.update();
} }
} }
@ -614,14 +622,23 @@ module.exports = class BaseDocument extends Observable {
await this.trigger('afterDelete'); await this.trigger('afterDelete');
} }
async submitOrRevert(isSubmit) {
const wasSubmitted = this.submitted;
this.submitted = isSubmit;
try {
await this.update();
} catch (e) {
this.submitted = wasSubmitted;
throw e;
}
}
async submit() { async submit() {
this.submitted = 1; await this.submitOrRevert(1);
this.update();
} }
async revert() { async revert() {
this.submitted = 0; await this.submitOrRevert(0);
this.update();
} }
async rename(newName) { async rename(newName) {
@ -643,7 +660,7 @@ module.exports = class BaseDocument extends Observable {
// helper functions // helper functions
getSum(tablefield, childfield) { getSum(tablefield, childfield) {
return (this[tablefield] || []) return (this[tablefield] || [])
.map(d => parseFloat(d[childfield], 10) || 0) .map((d) => parseFloat(d[childfield], 10) || 0)
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
} }
@ -666,4 +683,22 @@ module.exports = class BaseDocument extends Observable {
isNew() { isNew() {
return this._notInserted; return this._notInserted;
} }
getFieldValueMap() {
const fieldValueMap = this.meta.fields
.map(({ fieldname, fieldtype }) => [fieldname, fieldtype])
.reduce((obj, [fieldname, fieldtype]) => {
let value = this[fieldname];
if (fieldtype == 'Table') {
value = value.map((childTableDoc) =>
childTableDoc.getFieldValueMap()
);
}
obj[fieldname] = value;
return obj;
}, {});
return Object.freeze(fieldValueMap);
}
}; };