mirror of
https://github.com/frappe/books.git
synced 2025-01-26 16:48:28 +00:00
Add tailwind, DocType based on DocType and much more (#108)
Add tailwind, DocType based on DocType and much more
This commit is contained in:
commit
fa004db117
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
2.7.16
|
28
.vscode/launch.json
vendored
Normal file
28
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Mocha Current File",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"--timeout",
|
||||
"999999",
|
||||
"--colors",
|
||||
"${file}"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"skipFiles": [
|
||||
"node_modules/**/*.js",
|
||||
"lib/**/*.js",
|
||||
"async_hooks.js",
|
||||
"inspector_async_hook.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -19,11 +19,12 @@ module.exports = class Database extends Observable {
|
||||
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(doctype)) {
|
||||
await this.alterTable(doctype);
|
||||
if (await this.tableExists(baseDoctype)) {
|
||||
await this.alterTable(baseDoctype);
|
||||
} else {
|
||||
await this.createTable(doctype);
|
||||
await this.createTable(baseDoctype);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -197,20 +198,27 @@ module.exports = class Database extends Observable {
|
||||
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(meta, doc, doctype);
|
||||
} else {
|
||||
await this.insertOne(doctype, doc);
|
||||
await this.insertOne(baseDoctype, doc);
|
||||
}
|
||||
|
||||
// insert children
|
||||
await this.insertChildren(meta, doc, doctype);
|
||||
await this.insertChildren(meta, doc, baseDoctype);
|
||||
|
||||
this.triggerChange(doctype, doc.name);
|
||||
|
||||
@ -236,16 +244,18 @@ module.exports = class Database extends Observable {
|
||||
|
||||
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(meta, doc, doctype);
|
||||
} else {
|
||||
await this.updateOne(doctype, doc);
|
||||
await this.updateOne(baseDoctype, doc);
|
||||
}
|
||||
|
||||
// insert or update children
|
||||
await this.updateChildren(meta, doc, doctype);
|
||||
await this.updateChildren(meta, doc, baseDoctype);
|
||||
|
||||
this.triggerChange(doctype, doc.name);
|
||||
|
||||
@ -299,6 +309,10 @@ module.exports = class Database extends Observable {
|
||||
// await frappe.db.run('delete from SingleValue where parent=?', name)
|
||||
}
|
||||
|
||||
async rename(doctype, oldName, newName) {
|
||||
// await frappe.db.run('update doctype set name = ? where name = ?', name)
|
||||
}
|
||||
|
||||
prepareChild(parenttype, parent, child, field, idx) {
|
||||
if (!child.name) {
|
||||
child.name = frappe.getRandomString();
|
||||
@ -339,6 +353,19 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@ -346,7 +373,9 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
async delete(doctype, name) {
|
||||
await this.deleteOne(doctype, name);
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
await this.deleteOne(baseDoctype, name);
|
||||
|
||||
// delete children
|
||||
let tableFields = frappe.getMeta(doctype).getTableFields();
|
||||
@ -370,12 +399,17 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
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: doctype,
|
||||
doctype: baseDoctype,
|
||||
fields: [fieldname],
|
||||
filters: filters,
|
||||
start: 0,
|
||||
|
@ -74,17 +74,22 @@ module.exports = class sqliteDatabase extends Database {
|
||||
columns.push(def);
|
||||
|
||||
if (field.fieldtype === 'Link' && field.target) {
|
||||
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${field.target} ON UPDATE CASCADE ON DELETE RESTRICT`);
|
||||
let meta = frappe.getMeta(field.target);
|
||||
indexes.push(`FOREIGN KEY (${field.fieldname}) REFERENCES ${meta.getBaseDocType()} ON UPDATE CASCADE ON DELETE RESTRICT`);
|
||||
}
|
||||
}
|
||||
|
||||
getColumnDefinition(field) {
|
||||
let defaultValue = field.default;
|
||||
if (typeof defaultValue === 'string') {
|
||||
defaultValue = `'${defaultValue}'`
|
||||
}
|
||||
let def = [
|
||||
field.fieldname,
|
||||
this.typeMap[field.fieldtype],
|
||||
field.fieldname === 'name' ? 'PRIMARY KEY NOT NULL' : '',
|
||||
field.required ? 'NOT NULL' : '',
|
||||
field.default ? `DEFAULT ${field.default}` : ''
|
||||
field.default ? `DEFAULT ${defaultValue}` : ''
|
||||
].join(' ');
|
||||
|
||||
return def;
|
||||
@ -103,9 +108,11 @@ module.exports = class sqliteDatabase extends Database {
|
||||
}
|
||||
|
||||
getOne(doctype, name, fields = '*') {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
fields = this.prepareFields(fields);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.conn.get(`select ${fields} from ${doctype}
|
||||
this.conn.get(`select ${fields} from ${baseDoctype}
|
||||
where name = ?`, name,
|
||||
(err, row) => {
|
||||
resolve(row || {});
|
||||
@ -159,8 +166,16 @@ module.exports = class sqliteDatabase extends Database {
|
||||
await frappe.db.run('delete from SingleValue where parent=?', name)
|
||||
}
|
||||
|
||||
async rename(doctype, oldName, newName) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
await frappe.db.run(`update ${baseDoctype} set name = ? where name = ?`, [newName, oldName]);
|
||||
await frappe.db.commit();
|
||||
}
|
||||
|
||||
async setValues(doctype, name, fieldValuePair) {
|
||||
const meta = frappe.getMeta(doctype);
|
||||
const baseDoctype = meta.getBaseDocType();
|
||||
const validFields = this.getKeys(doctype);
|
||||
const validFieldnames = validFields.map(df => df.fieldname);
|
||||
const fieldsToUpdate = Object.keys(fieldValuePair)
|
||||
@ -179,22 +194,27 @@ module.exports = class sqliteDatabase extends Database {
|
||||
// additional name for where clause
|
||||
values.push(name);
|
||||
|
||||
return await this.run(`update ${doctype}
|
||||
return await this.run(`update ${baseDoctype}
|
||||
set ${assigns.join(', ')} where name=?`, values);
|
||||
}
|
||||
|
||||
getAll({ doctype, fields, filters, start, limit, orderBy = 'modified', groupBy, order = 'desc' } = {}) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
if (!fields) {
|
||||
fields = frappe.getMeta(doctype).getKeywordFields();
|
||||
fields = meta.getKeywordFields();
|
||||
}
|
||||
if (typeof fields === 'string') {
|
||||
fields = [fields];
|
||||
}
|
||||
if (meta.filters) {
|
||||
filters = Object.assign({}, filters, meta.filters);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let conditions = this.getFilterConditions(filters);
|
||||
let query = `select ${fields.join(", ")}
|
||||
from ${doctype}
|
||||
from ${baseDoctype}
|
||||
${conditions.conditions ? "where" : ""} ${conditions.conditions}
|
||||
${groupBy ? ("group by " + groupBy.join(', ')) : ""}
|
||||
${orderBy ? ("order by " + orderBy) : ""} ${orderBy ? (order || "asc") : ""}
|
||||
|
558
index.js
558
index.js
@ -1,261 +1,317 @@
|
||||
const Observable = require('./utils/observable');
|
||||
|
||||
module.exports = {
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this.initConfig();
|
||||
this.initGlobals();
|
||||
this.docs = new Observable();
|
||||
this.events = new Observable();
|
||||
this._initialized = true;
|
||||
},
|
||||
async init() {
|
||||
if (this._initialized) return;
|
||||
this.initConfig();
|
||||
this.initGlobals();
|
||||
this.docs = new Observable();
|
||||
this.events = new Observable();
|
||||
this._initialized = true;
|
||||
},
|
||||
|
||||
initConfig() {
|
||||
this.config = {
|
||||
serverURL: '',
|
||||
backend: 'sqlite',
|
||||
port: 8000
|
||||
};
|
||||
},
|
||||
initConfig() {
|
||||
this.config = {
|
||||
serverURL: '',
|
||||
backend: 'sqlite',
|
||||
port: 8000
|
||||
};
|
||||
},
|
||||
|
||||
initGlobals() {
|
||||
this.metaCache = {};
|
||||
this.models = {};
|
||||
this.forms = {};
|
||||
this.views = {};
|
||||
this.flags = {};
|
||||
this.methods = {};
|
||||
// temp params while calling routes
|
||||
this.params = {};
|
||||
},
|
||||
initGlobals() {
|
||||
this.metaCache = {};
|
||||
this.models = {};
|
||||
this.forms = {};
|
||||
this.views = {};
|
||||
this.flags = {};
|
||||
this.methods = {};
|
||||
// temp params while calling routes
|
||||
this.params = {};
|
||||
},
|
||||
|
||||
registerLibs(common) {
|
||||
// add standard libs and utils to frappe
|
||||
common.initLibs(this);
|
||||
},
|
||||
registerLibs(common) {
|
||||
// add standard libs and utils to frappe
|
||||
common.initLibs(this);
|
||||
},
|
||||
|
||||
registerModels(models, type) {
|
||||
// register models from app/models/index.js
|
||||
const toAdd = Object.assign({}, models.models);
|
||||
registerModels(models, type) {
|
||||
// register models from app/models/index.js
|
||||
const toAdd = Object.assign({}, models.models);
|
||||
|
||||
// post process based on type
|
||||
if (models[type]) {
|
||||
models[type](toAdd);
|
||||
}
|
||||
|
||||
Object.assign(this.models, toAdd);
|
||||
},
|
||||
|
||||
registerView(view, name, module) {
|
||||
if (!this.views[view]) this.views[view] = {};
|
||||
this.views[view][name] = module;
|
||||
},
|
||||
|
||||
registerMethod({method, handler}) {
|
||||
this.methods[method] = handler;
|
||||
if (this.app) {
|
||||
// add to router if client-server
|
||||
this.app.post(`/api/method/${method}`, this.asyncHandler(async function(request, response) {
|
||||
let data = await handler(request.body);
|
||||
if (data === undefined) {
|
||||
data = {}
|
||||
}
|
||||
return response.json(data);
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
async call({method, args}) {
|
||||
if (this.isServer) {
|
||||
if (this.methods[method]) {
|
||||
return await this.methods[method](args);
|
||||
} else {
|
||||
throw `${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);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
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 `${doctype} is not a registered doctype`;
|
||||
}
|
||||
let metaClass = model.metaClass || this.BaseMeta;
|
||||
this.metaCache[doctype] = new metaClass(model);
|
||||
}
|
||||
|
||||
return this.metaCache[doctype];
|
||||
},
|
||||
|
||||
async getDoc(doctype, name) {
|
||||
let doc = 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.BaseDocument;
|
||||
},
|
||||
|
||||
async getSingle(doctype) {
|
||||
return await this.getDoc(doctype, doctype);
|
||||
},
|
||||
|
||||
async getDuplicate(doc) {
|
||||
const newDoc = await this.getNewDoc(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;
|
||||
},
|
||||
|
||||
async getNewDoc(doctype) {
|
||||
let doc = this.newDoc({doctype: doctype});
|
||||
doc._notInserted = true;
|
||||
doc.name = this.getRandomString();
|
||||
this.addToCache(doc);
|
||||
return doc;
|
||||
},
|
||||
|
||||
newDoc(data) {
|
||||
let doc = new (this.getDocumentClass(data.doctype))(data);
|
||||
doc.setDefaults();
|
||||
return doc;
|
||||
},
|
||||
|
||||
async insert(data) {
|
||||
return await (this.newDoc(data)).insert();
|
||||
},
|
||||
|
||||
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.newDoc(data);
|
||||
await doc.insert();
|
||||
}
|
||||
},
|
||||
|
||||
// only for client side
|
||||
async login(email, password) {
|
||||
if (email === 'Administrator') {
|
||||
this.session = {
|
||||
user: 'Administrator'
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await fetch(this.getServerURL() + '/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const res = await response.json();
|
||||
|
||||
this.session = {
|
||||
user: email,
|
||||
token: res.token
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async signup(email, fullName, password) {
|
||||
let response = await fetch(this.getServerURL() + '/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, fullName, password })
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
getServerURL() {
|
||||
return this.config.serverURL || '';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
// post process based on type
|
||||
if (models[type]) {
|
||||
models[type](toAdd);
|
||||
}
|
||||
|
||||
Object.assign(this.models, toAdd);
|
||||
},
|
||||
|
||||
getDoctypeList(filters) {
|
||||
let doctypeList = [];
|
||||
if (filters && Object.keys(filters).length) {
|
||||
for (let model in this.models) {
|
||||
let doctypeName = model;
|
||||
let doctype = this.models[doctypeName];
|
||||
let matchedFields = 0;
|
||||
for (let key in filters) {
|
||||
let field = key;
|
||||
let value = filters[field];
|
||||
|
||||
if (Boolean(doctype[field]) === Boolean(value)) {
|
||||
matchedFields++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedFields === Object.keys(filters).length)
|
||||
doctypeList.push(doctypeName);
|
||||
}
|
||||
}
|
||||
|
||||
return doctypeList;
|
||||
},
|
||||
|
||||
registerView(view, name, module) {
|
||||
if (!this.views[view]) this.views[view] = {};
|
||||
this.views[view][name] = module;
|
||||
},
|
||||
|
||||
registerMethod({ method, handler }) {
|
||||
this.methods[method] = handler;
|
||||
if (this.app) {
|
||||
// add to router if client-server
|
||||
this.app.post(
|
||||
`/api/method/${method}`,
|
||||
this.asyncHandler(async function(request, response) {
|
||||
let data = await handler(request.body);
|
||||
if (data === undefined) {
|
||||
data = {};
|
||||
}
|
||||
return response.json(data);
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
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.BaseMeta;
|
||||
this.metaCache[doctype] = new metaClass(model);
|
||||
}
|
||||
|
||||
return this.metaCache[doctype];
|
||||
},
|
||||
|
||||
async getDoc(doctype, name) {
|
||||
let doc = 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.BaseDocument;
|
||||
},
|
||||
|
||||
async getSingle(doctype) {
|
||||
return await this.getDoc(doctype, doctype);
|
||||
},
|
||||
|
||||
async getDuplicate(doc) {
|
||||
const newDoc = await this.getNewDoc(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;
|
||||
},
|
||||
|
||||
async getNewDoc(doctype) {
|
||||
let doc = this.newDoc({ doctype: doctype });
|
||||
doc._notInserted = true;
|
||||
doc.name = this.getRandomString();
|
||||
this.addToCache(doc);
|
||||
return doc;
|
||||
},
|
||||
|
||||
async newCustomDoc(fields) {
|
||||
let doc = new this.BaseDocument({ isCustom: 1, fields });
|
||||
doc._notInserted = true;
|
||||
doc.name = this.getRandomString();
|
||||
this.addToCache(doc);
|
||||
return doc;
|
||||
},
|
||||
|
||||
createMeta(fields) {
|
||||
let meta = new this.BaseMeta({ isCustom: 1, fields });
|
||||
return meta;
|
||||
},
|
||||
|
||||
newDoc(data) {
|
||||
let doc = new (this.getDocumentClass(data.doctype))(data);
|
||||
doc.setDefaults();
|
||||
return doc;
|
||||
},
|
||||
|
||||
async insert(data) {
|
||||
return await this.newDoc(data).insert();
|
||||
},
|
||||
|
||||
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.newDoc(data);
|
||||
await doc.insert();
|
||||
}
|
||||
},
|
||||
|
||||
// only for client side
|
||||
async login(email, password) {
|
||||
if (email === 'Administrator') {
|
||||
this.session = {
|
||||
user: 'Administrator'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await fetch(this.getServerURL() + '/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const res = await response.json();
|
||||
|
||||
this.session = {
|
||||
user: email,
|
||||
token: res.token
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
async signup(email, fullName, password) {
|
||||
let response = await fetch(this.getServerURL() + '/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, fullName, password })
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
|
||||
getServerURL() {
|
||||
return this.config.serverURL || '';
|
||||
},
|
||||
|
||||
close() {
|
||||
this.db.close();
|
||||
|
||||
if (this.server) {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -3,354 +3,441 @@ const Observable = require('frappejs/utils/observable');
|
||||
const naming = require('./naming');
|
||||
|
||||
module.exports = class BaseDocument extends Observable {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.fetchValuesCache = {};
|
||||
this.flags = {};
|
||||
this.setup();
|
||||
Object.assign(this, data);
|
||||
constructor(data) {
|
||||
super();
|
||||
this.fetchValuesCache = {};
|
||||
this.flags = {};
|
||||
this.setup();
|
||||
this.setValues(data);
|
||||
|
||||
// clear fetch-values cache
|
||||
frappe.db.on('change', (params) => this.fetchValuesCache[`${params.doctype}:${params.name}`] = {});
|
||||
}
|
||||
// clear fetch-values cache
|
||||
frappe.db.on(
|
||||
'change',
|
||||
params => (this.fetchValuesCache[`${params.doctype}:${params.name}`] = {})
|
||||
);
|
||||
}
|
||||
|
||||
setup() {
|
||||
// add listeners
|
||||
}
|
||||
setup() {
|
||||
// add listeners
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.getMeta(this.doctype);
|
||||
setValues(data) {
|
||||
for (let fieldname in data) {
|
||||
let value = data[fieldname];
|
||||
if (fieldname.startsWith('_')) {
|
||||
// private property
|
||||
this[fieldname] = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
for (let row of value) {
|
||||
this.append(fieldname, row);
|
||||
}
|
||||
return this._meta;
|
||||
} else {
|
||||
this[fieldname] = value;
|
||||
}
|
||||
}
|
||||
// set unset fields as null
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
// check for null or undefined
|
||||
if (this[field.fieldname] == null) {
|
||||
this[field.fieldname] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get meta() {
|
||||
if (this.isCustom) {
|
||||
this._meta = frappe.createMeta(this.fields);
|
||||
}
|
||||
if (!this._meta) {
|
||||
this._meta = frappe.getMeta(this.doctype);
|
||||
}
|
||||
return this._meta;
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
if (!this._settings) {
|
||||
this._settings = await frappe.getSingle(this.meta.settings);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
async getSettings() {
|
||||
if (!this._settings) {
|
||||
this._settings = await frappe.getSingle(this.meta.settings);
|
||||
if (this[fieldname] !== value) {
|
||||
this._dirty = true;
|
||||
if (Array.isArray(value)) {
|
||||
this[fieldname] = [];
|
||||
for (let row of value) {
|
||||
this.append(fieldname, row);
|
||||
}
|
||||
return this._settings;
|
||||
} else {
|
||||
this[fieldname] = await this.validateField(fieldname, value);
|
||||
}
|
||||
await this.applyChange(fieldname);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
async applyChange(fieldname) {
|
||||
if (await this.applyFormula()) {
|
||||
// multiple changes
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
changed: fieldname
|
||||
});
|
||||
} else {
|
||||
// no other change, trigger control refresh
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
fieldname: fieldname,
|
||||
changed: fieldname
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setDefaults() {
|
||||
for (let field of this.meta.fields) {
|
||||
if (this[field.fieldname] == null) {
|
||||
let defaultValue = null;
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
defaultValue = [];
|
||||
}
|
||||
if (field.default) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
|
||||
if (this[fieldname] !== value) {
|
||||
this._dirty = true;
|
||||
this[fieldname] = await this.validateField(fieldname, value);
|
||||
await this.applyChange(fieldname);
|
||||
}
|
||||
this[field.fieldname] = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
async applyChange(fieldname) {
|
||||
if (await this.applyFormula()) {
|
||||
// multiple changes
|
||||
await this.trigger('change', {
|
||||
doc: this
|
||||
});
|
||||
} else {
|
||||
// no other change, trigger control refresh
|
||||
await this.trigger('change', {
|
||||
doc: this,
|
||||
fieldname: fieldname
|
||||
});
|
||||
}
|
||||
if (this.meta.basedOn && this.meta.filters) {
|
||||
this.setValues(this.meta.filters);
|
||||
}
|
||||
}
|
||||
|
||||
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 = [];
|
||||
}
|
||||
if (field.default) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
|
||||
this[field.fieldname] = defaultValue;
|
||||
}
|
||||
}
|
||||
setKeywords() {
|
||||
let keywords = [];
|
||||
for (let fieldname of this.meta.getKeywordFields()) {
|
||||
keywords.push(this[fieldname]);
|
||||
}
|
||||
this.keywords = keywords.join(', ');
|
||||
}
|
||||
|
||||
setKeywords() {
|
||||
let keywords = [];
|
||||
for (let fieldname of this.meta.getKeywordFields()) {
|
||||
keywords.push(this[fieldname]);
|
||||
}
|
||||
this.keywords = keywords.join(', ');
|
||||
append(key, document = {}) {
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this._initChild(document, key));
|
||||
this._dirty = true;
|
||||
this.applyChange(key);
|
||||
}
|
||||
|
||||
append(key, document) {
|
||||
if (!this[key]) {
|
||||
this[key] = [];
|
||||
}
|
||||
this[key].push(this.initDoc(document));
|
||||
_initChild(data, key) {
|
||||
if (data instanceof BaseDocument) {
|
||||
return data;
|
||||
} else {
|
||||
data.doctype = this.meta.getField(key).childtype;
|
||||
data.parent = this.name;
|
||||
data.parenttype = this.doctype;
|
||||
data.parentfield = key;
|
||||
data.parentdoc = this;
|
||||
|
||||
if (!data.idx) {
|
||||
data.idx = (this[key] || []).length;
|
||||
}
|
||||
|
||||
if (!data.name) {
|
||||
data.name = frappe.getRandomString();
|
||||
}
|
||||
|
||||
return new BaseDocument(data);
|
||||
}
|
||||
}
|
||||
|
||||
initDoc(data) {
|
||||
if (data.prototype instanceof Document) {
|
||||
return data;
|
||||
} else {
|
||||
return new Document(data);
|
||||
}
|
||||
async validateField(key, value) {
|
||||
let field = this.meta.getField(key);
|
||||
if (field && field.fieldtype == 'Select') {
|
||||
return this.meta.validateSelect(field, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async validateField(key, value) {
|
||||
let field = this.meta.getField(key);
|
||||
if (field && field.fieldtype == 'Select') {
|
||||
return this.meta.validateSelect(field, value);
|
||||
}
|
||||
return value;
|
||||
getValidDict() {
|
||||
let data = {};
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
data[field.fieldname] = this[field.fieldname];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
getValidDict() {
|
||||
let data = {};
|
||||
for (let field of this.meta.getValidFields()) {
|
||||
data[field.fieldname] = this[field.fieldname];
|
||||
}
|
||||
return data;
|
||||
}
|
||||
getFullDict() {
|
||||
let data = this.getValidDict();
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.session.user;
|
||||
}
|
||||
|
||||
if (!this.creation) {
|
||||
this.creation = now;
|
||||
}
|
||||
|
||||
if (!this.modifiedBy) {
|
||||
this.modifiedBy = frappe.session.user;
|
||||
}
|
||||
this.modified = now;
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async applyFormula() {
|
||||
if (!this.meta.hasFormula()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
function shouldApplyFormula(field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!frappe.isServer || frappe.isElectron) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
setStandardValues() {
|
||||
// set standard values on server-side only
|
||||
if (frappe.isServer) {
|
||||
let now = new Date().toISOString();
|
||||
if (!this.submitted) {
|
||||
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);
|
||||
if (!this.owner) {
|
||||
this.owner = frappe.session.user;
|
||||
}
|
||||
|
||||
if (!this.creation) {
|
||||
this.creation = now;
|
||||
}
|
||||
|
||||
if (!this.modifiedBy) {
|
||||
this.modifiedBy = frappe.session.user;
|
||||
}
|
||||
this.modified = now;
|
||||
}
|
||||
}
|
||||
|
||||
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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
syncValues(data) {
|
||||
this.clearValues();
|
||||
this.setValues(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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async applyFormula() {
|
||||
if (!this.meta.hasFormula()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// helper functions
|
||||
getSum(tablefield, childfield) {
|
||||
return this[tablefield].map(d => (d[childfield] || 0)).reduce((a, b) => a + b, 0);
|
||||
}
|
||||
let doc = this;
|
||||
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return _values[fieldname];
|
||||
}
|
||||
}
|
||||
|
||||
isNew() {
|
||||
return this._notInserted;
|
||||
// parent or child row
|
||||
for (let field of this.meta.getFormulaFields()) {
|
||||
if (shouldApplyFormula(field, doc)) {
|
||||
let val;
|
||||
if (this.meta.isChild) {
|
||||
val = await field.formula(doc, this.parentdoc);
|
||||
} else {
|
||||
val = await field.formula(doc);
|
||||
}
|
||||
if (val !== false && val !== undefined) {
|
||||
doc[field.fieldname] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
|
||||
function shouldApplyFormula(field, doc) {
|
||||
if (field.readOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!frappe.isServer || frappe.isElectron) {
|
||||
if (doc[field.fieldname] == null || doc[field.fieldname] == '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
let oldName = this.name;
|
||||
const data = await frappe.db.insert(this.doctype, this.getValidDict());
|
||||
this.syncValues(data);
|
||||
|
||||
if (oldName !== this.name) {
|
||||
frappe.removeFromCache(this.doctype, oldName);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
async rename(newName) {
|
||||
await this.trigger('beforeRename');
|
||||
await frappe.db.rename(this.doctype, this.name, newName);
|
||||
this.name = newName;
|
||||
await this.trigger('afterRename');
|
||||
}
|
||||
|
||||
// trigger methods on the class if they match
|
||||
// with the trigger name
|
||||
async trigger(event, params) {
|
||||
if (this[event]) {
|
||||
await this[event](params);
|
||||
}
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
|
||||
// helper functions
|
||||
getSum(tablefield, childfield) {
|
||||
return this[tablefield]
|
||||
.map(d => d[childfield] || 0)
|
||||
.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);
|
||||
}
|
||||
return _values[fieldname];
|
||||
}
|
||||
|
||||
isNew() {
|
||||
return this._notInserted;
|
||||
}
|
||||
};
|
||||
|
469
model/meta.js
469
model/meta.js
@ -1,232 +1,287 @@
|
||||
const BaseDocument = require('./document');
|
||||
const frappe = require('frappejs');
|
||||
const model = require('./index')
|
||||
const model = require('./index');
|
||||
const indicatorColor = require('frappejs/ui/constants/indicators');
|
||||
|
||||
module.exports = class BaseMeta extends BaseDocument {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
this.setDefaultIndicators();
|
||||
if (this.setupMeta) {
|
||||
this.setupMeta();
|
||||
}
|
||||
if (!this.titleField) {
|
||||
this.titleField = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
constructor(data) {
|
||||
if (data.basedOn) {
|
||||
let config = frappe.models[data.basedOn];
|
||||
Object.assign(data, config, {
|
||||
name: data.name,
|
||||
label: data.label,
|
||||
filters: data.filters
|
||||
});
|
||||
}
|
||||
|
||||
getLabel(fieldname) {
|
||||
return this.getField(fieldname).label;
|
||||
super(data);
|
||||
this.setDefaultIndicators();
|
||||
if (this.setupMeta) {
|
||||
this.setupMeta();
|
||||
}
|
||||
if (!this.titleField) {
|
||||
this.titleField = 'name';
|
||||
}
|
||||
}
|
||||
|
||||
getTableFields() {
|
||||
if (this._tableFields===undefined) {
|
||||
this._tableFields = this.fields.filter(field => field.fieldtype === 'Table');
|
||||
setValues(data) {
|
||||
Object.assign(this, data);
|
||||
if (!this.fields.find(df => df.fieldname === 'name')) {
|
||||
this.fields = [
|
||||
{
|
||||
label: frappe._('Name'),
|
||||
fieldname: 'name',
|
||||
fieldtype: 'Data',
|
||||
required: 1
|
||||
}
|
||||
return this._tableFields;
|
||||
].concat(this.fields);
|
||||
}
|
||||
}
|
||||
|
||||
getFormulaFields() {
|
||||
if (this._formulaFields===undefined) {
|
||||
this._formulaFields = this.fields.filter(field => field.formula);
|
||||
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._formulaFields;
|
||||
}
|
||||
}
|
||||
return this._hasFormula;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
const doctype_fields = this.fields.map(field => field.fieldname);
|
||||
|
||||
// standard fields
|
||||
for (let field of model.commonFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctype_fields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
return this._hasFormula;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const doctype_fields = this.fields.map((field) => field.fieldname);
|
||||
|
||||
// standard fields
|
||||
for (let field of model.commonFields) {
|
||||
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isSubmittable) {
|
||||
_add({fieldtype:'Check', fieldname: 'submitted', label: frappe._('Submitted')})
|
||||
}
|
||||
|
||||
if (this.isChild) {
|
||||
// child fields
|
||||
for (let field of model.childFields) {
|
||||
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parent fields
|
||||
for (let field of model.parentFields) {
|
||||
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isTree) {
|
||||
// tree fields
|
||||
for (let field of model.treeFields) {
|
||||
if (frappe.db.typeMap[field.fieldtype] && !doctype_fields.includes(field.fieldname)) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doctype fields
|
||||
for (let field of this.fields) {
|
||||
let include = frappe.db.typeMap[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;
|
||||
}
|
||||
|
||||
validateSelect(field, value) {
|
||||
let options = field.options;
|
||||
if (!options) return;
|
||||
|
||||
if (typeof options === 'string') {
|
||||
// values given as string
|
||||
options = field.options.split('\n');
|
||||
}
|
||||
if (!options.includes(value)) {
|
||||
throw new frappe.errors.ValueError(`${value} must be one of ${options.join(", ")}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async trigger(event, params = {}) {
|
||||
Object.assign(params, {
|
||||
doc: this,
|
||||
name: event
|
||||
if (this.isSubmittable) {
|
||||
_add({
|
||||
fieldtype: 'Check',
|
||||
fieldname: 'submitted',
|
||||
label: frappe._('Submitted')
|
||||
});
|
||||
}
|
||||
|
||||
await super.trigger(event, params);
|
||||
}
|
||||
|
||||
setDefaultIndicators() {
|
||||
if (!this.indicators) {
|
||||
if (this.isSubmittable) {
|
||||
this.indicators = {
|
||||
key: 'submitted',
|
||||
colors: {
|
||||
0: indicatorColor.GRAY,
|
||||
1: indicatorColor.BLUE
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.isChild) {
|
||||
// child fields
|
||||
for (let field of model.childFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctype_fields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// parent fields
|
||||
for (let field of model.parentFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctype_fields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isTree) {
|
||||
// tree fields
|
||||
for (let field of model.treeFields) {
|
||||
if (
|
||||
frappe.db.typeMap[field.fieldtype] &&
|
||||
!doctype_fields.includes(field.fieldname)
|
||||
) {
|
||||
_add(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doctype fields
|
||||
for (let field of this.fields) {
|
||||
let include = frappe.db.typeMap[field.fieldtype];
|
||||
|
||||
if (include) {
|
||||
_add(field);
|
||||
}
|
||||
|
||||
// include tables if (withChildren = True)
|
||||
if (!include && field.fieldtype === 'Table') {
|
||||
this._validFieldsWithChildren.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getIndicatorColor(doc) {
|
||||
if (frappe.isDirty(this.name, doc.name)) {
|
||||
return indicatorColor.ORANGE;
|
||||
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 (typeof options === 'string') {
|
||||
// values given as string
|
||||
options = field.options.split('\n');
|
||||
}
|
||||
if (!options.includes(value)) {
|
||||
throw new frappe.errors.ValueError(
|
||||
`${value} 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 {
|
||||
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;
|
||||
}
|
||||
return indicatorColor.GRAY;
|
||||
}
|
||||
} else {
|
||||
return indicatorColor.GRAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
37
package.json
37
package.json
@ -8,15 +8,16 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test mocha --timeout 3000 tests",
|
||||
"test-watch": "NODE_ENV=test mocha --timeout 3000 tests --watch --reporter=min",
|
||||
"start": "nodemon app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"awesomplete": "^1.1.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.5",
|
||||
"bcrypt": "^2.0.1",
|
||||
"bcrypt": "^3.0.6",
|
||||
"body-parser": "^1.18.2",
|
||||
"bootstrap": "^4.1.2",
|
||||
"bootstrap": "^4.3.1",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.1.2",
|
||||
"codemirror": "^5.35.0",
|
||||
"commander": "^2.13.0",
|
||||
@ -25,44 +26,50 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"deepmerge": "^2.1.0",
|
||||
"electron": "2.0.12",
|
||||
"electron-builder": "^20.28.4",
|
||||
"electron": "5.0.0",
|
||||
"electron-builder": "^21.0.15",
|
||||
"electron-debug": "^2.0.0",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"express": "^4.16.2",
|
||||
"file-saver": "^2.0.2",
|
||||
"feather-icons": "^4.7.3",
|
||||
"file-loader": "^1.1.11",
|
||||
"flatpickr": "^4.3.2",
|
||||
"frappe-datatable": "^1.3.1",
|
||||
"flatpickr": "^4.6.2",
|
||||
"frappe-datatable": "^1.13.5",
|
||||
"friendly-errors-webpack-plugin": "^1.7.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"jwt-simple": "^0.5.1",
|
||||
"luxon": "^1.0.0",
|
||||
"popper.js": "^1.14.3",
|
||||
"mkdirp": "^0.5.1",
|
||||
"morgan": "^1.9.0",
|
||||
"multer": "^1.3.1",
|
||||
"mysql": "^2.15.0",
|
||||
"node-fetch": "^1.7.3",
|
||||
"node-sass": "^4.7.2",
|
||||
"node-sass": "^4.12.0",
|
||||
"nunjucks": "^3.1.0",
|
||||
"octicons": "^7.2.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"puppeteer": "^1.2.0",
|
||||
"puppeteer-core": "^1.19.0",
|
||||
"sass-loader": "^7.0.3",
|
||||
"sharp": "^0.20.8",
|
||||
"sharp": "^0.23.0",
|
||||
"showdown": "^1.8.6",
|
||||
"socket.io": "^2.0.4",
|
||||
"sqlite3": "^4.0.2",
|
||||
"vue": "^2.5.16",
|
||||
"vue-flatpickr-component": "^7.0.4",
|
||||
"sqlite3": "^4.0.9",
|
||||
"vue": "^2.6.10",
|
||||
"vue-flatpickr-component": "^8.1.2",
|
||||
"vue-loader": "^15.2.6",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"vue-router": "^3.0.7",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^4.16.1",
|
||||
"webpack-dev-server": "^3.1.4",
|
||||
"webpack-hot-middleware": "^2.22.3"
|
||||
"webpack-hot-middleware": "^2.22.3",
|
||||
"csvjson-csv2json": "5.0.6",
|
||||
"postcss-loader": "3.0.0",
|
||||
"tailwindcss": "1.1.1",
|
||||
"autoprefixer": "9.6.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
138
server/pdf.js
138
server/pdf.js
@ -1,5 +1,5 @@
|
||||
const frappe = require('frappejs');
|
||||
const puppeteer = require('puppeteer');
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getTmpDir } = require('frappejs/server/utils');
|
||||
@ -7,87 +7,109 @@ const { getHTML } = require('frappejs/common/print');
|
||||
const { getRandomString } = require('frappejs/utils');
|
||||
|
||||
async function makePDF(html, filepath) {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html);
|
||||
await page.addStyleTag({
|
||||
url: 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'
|
||||
})
|
||||
await page.pdf({
|
||||
path: filepath,
|
||||
format: 'A4'
|
||||
});
|
||||
await browser.close();
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setContent(html);
|
||||
await page.addStyleTag({
|
||||
url:
|
||||
'https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'
|
||||
});
|
||||
await page.pdf({
|
||||
path: filepath,
|
||||
format: 'A4'
|
||||
});
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function getPDFForElectron(doctype, name, destination, htmlContent) {
|
||||
const { remote, shell } = require('electron');
|
||||
const { BrowserWindow } = remote;
|
||||
const html = htmlContent || await getHTML(doctype, name);
|
||||
const filepath = path.join(destination, name + '.pdf');
|
||||
const { remote, shell } = require('electron');
|
||||
const { BrowserWindow } = remote;
|
||||
const html = htmlContent || (await getHTML(doctype, name));
|
||||
if (!destination) {
|
||||
destination =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? path.resolve('.')
|
||||
: remote.getGlobal('documentsPath');
|
||||
}
|
||||
|
||||
const fs = require('fs')
|
||||
let printWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
show: false
|
||||
})
|
||||
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
|
||||
const filepath = path.resolve(
|
||||
path.join(destination, '/frappe-accounting/' + name + '.pdf')
|
||||
);
|
||||
|
||||
printWindow.on('closed', () => {
|
||||
printWindow = null;
|
||||
});
|
||||
const fs = require('fs');
|
||||
let printWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
height: 800,
|
||||
show: false
|
||||
});
|
||||
|
||||
const code = `
|
||||
const __static = remote.getGlobal('__static') || __static;
|
||||
|
||||
printWindow.loadURL(`file://${path.join(__static, 'print.html')}`);
|
||||
|
||||
printWindow.on('closed', () => {
|
||||
printWindow = null;
|
||||
});
|
||||
|
||||
const code = `
|
||||
document.body.innerHTML = \`${html}\`;
|
||||
`;
|
||||
|
||||
printWindow.webContents.executeJavaScript(code);
|
||||
printWindow.webContents.executeJavaScript(code);
|
||||
|
||||
const printPromise = new Promise(resolve => {
|
||||
printWindow.webContents.on('did-finish-load', () => {
|
||||
printWindow.webContents.printToPDF({
|
||||
const printPromise = new Promise(resolve => {
|
||||
printWindow.webContents.on('did-finish-load', () => {
|
||||
printWindow.webContents.printToPDF(
|
||||
{
|
||||
marginsType: 1, // no margin
|
||||
pageSize: 'A4',
|
||||
printBackground: true
|
||||
}, (error, data) => {
|
||||
if (error) throw error
|
||||
},
|
||||
(error, data) => {
|
||||
if (error) throw error;
|
||||
printWindow.close();
|
||||
fs.writeFile(filepath, data, (error) => {
|
||||
if (error) throw error
|
||||
fs.writeFile(filepath, data, error => {
|
||||
if (error) throw error;
|
||||
resolve(shell.openItem(filepath));
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await printPromise;
|
||||
// await makePDF(html, filepath);
|
||||
await printPromise;
|
||||
// await makePDF(html, filepath);
|
||||
}
|
||||
|
||||
function setupExpressRoute() {
|
||||
if (!frappe.app) return;
|
||||
frappe.app.post('/api/method/pdf', frappe.asyncHandler(handlePDFRequest));
|
||||
if (!frappe.app) return;
|
||||
frappe.app.post('/api/method/pdf', frappe.asyncHandler(handlePDFRequest));
|
||||
}
|
||||
|
||||
async function handlePDFRequest(req, res) {
|
||||
const args = req.body;
|
||||
const { doctype, name } = args;
|
||||
const html = await getHTML(doctype, name);
|
||||
const args = req.body;
|
||||
const { doctype, name } = args;
|
||||
const html = await getHTML(doctype, name);
|
||||
|
||||
const filepath = path.join(getTmpDir(), `frappe-pdf-${getRandomString()}.pdf`);
|
||||
await makePDF(html, filepath);
|
||||
const filepath = path.join(
|
||||
getTmpDir(),
|
||||
`frappe-pdf-${getRandomString()}.pdf`
|
||||
);
|
||||
await makePDF(html, filepath);
|
||||
|
||||
const file = fs.createReadStream(filepath);
|
||||
const stat = fs.statSync(filepath);
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=${path.basename(filepath)}`);
|
||||
file.pipe(res);
|
||||
const file = fs.createReadStream(filepath);
|
||||
const stat = fs.statSync(filepath);
|
||||
res.setHeader('Content-Length', stat.size);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`attachment; filename=${path.basename(filepath)}`
|
||||
);
|
||||
file.pipe(res);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makePDF,
|
||||
setupExpressRoute,
|
||||
getPDFForElectron
|
||||
}
|
||||
makePDF,
|
||||
setupExpressRoute,
|
||||
getPDFForElectron
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
const assert = require('assert');
|
||||
const frappe = require('frappejs');
|
||||
const helpers = require('./helpers');
|
||||
const BaseDocument = require('frappejs/model/document');
|
||||
|
||||
describe('Document', () => {
|
||||
before(async function() {
|
||||
@ -118,6 +119,46 @@ describe('Document', () => {
|
||||
assert.equal(user.roles[0].parentfield, 'roles');
|
||||
});
|
||||
|
||||
it('should convert children objects to BaseDocument', async () => {
|
||||
if (!await frappe.db.exists('Role', 'Test Role 1')) {
|
||||
await frappe.insert({doctype: 'Role', name: 'Test Role'});
|
||||
await frappe.insert({doctype: 'Role', name: 'Test Role 1'});
|
||||
}
|
||||
|
||||
let user = frappe.newDoc({
|
||||
doctype: 'User',
|
||||
name: frappe.getRandomString(),
|
||||
fullName: 'Test User',
|
||||
password: frappe.getRandomString(),
|
||||
roles: [
|
||||
{
|
||||
role: 'Test Role'
|
||||
}
|
||||
]
|
||||
});
|
||||
await user.insert();
|
||||
assert.ok(user.roles[0] instanceof BaseDocument);
|
||||
assert.equal(user.roles[0].parent, user.name);
|
||||
assert.equal(user.roles[0].parenttype, 'User');
|
||||
assert.equal(user.roles[0].parentfield, 'roles');
|
||||
assert.equal(user.roles[0].idx, 0);
|
||||
|
||||
user.append('roles', { role: 'Test Role 1'});
|
||||
assert.equal(user.roles[1].role, 'Test Role 1');
|
||||
assert.ok(user.roles[1] instanceof BaseDocument);
|
||||
assert.equal(user.roles[1].parent, user.name);
|
||||
assert.equal(user.roles[1].parenttype, 'User');
|
||||
assert.equal(user.roles[1].parentfield, 'roles');
|
||||
assert.equal(user.roles[1].idx, 1);
|
||||
|
||||
user.set('roles', [{ role: 'Test Role' }]);
|
||||
assert.equal(user.roles.length, 1);
|
||||
assert.ok(user.roles[0] instanceof BaseDocument);
|
||||
assert.equal(user.roles[0].parent, user.name);
|
||||
assert.equal(user.roles[0].parenttype, 'User');
|
||||
assert.equal(user.roles[0].parentfield, 'roles');
|
||||
assert.equal(user.roles[0].idx, 0);
|
||||
});
|
||||
});
|
||||
|
||||
function test_doc() {
|
||||
@ -127,4 +168,4 @@ function test_doc() {
|
||||
subject: 'testing 1',
|
||||
description: 'test description 1'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ const common = require('frappejs/common');
|
||||
|
||||
var test_server;
|
||||
|
||||
describe.only('REST', () => {
|
||||
describe('REST', () => {
|
||||
before(async function() {
|
||||
test_server = spawn('node', ['tests/test_server.js'], { stdio: 'inherit' });
|
||||
|
||||
@ -88,4 +88,4 @@ describe.only('REST', () => {
|
||||
assert.equal(await frappe.db.exists(doc2.doctype, doc2.name), false);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,25 +1,25 @@
|
||||
<template>
|
||||
<div class="frappe-form">
|
||||
<form-actions
|
||||
class="p-3 border-bottom"
|
||||
v-if="shouldRenderForm"
|
||||
:doc="doc"
|
||||
:links="links"
|
||||
@save="save"
|
||||
@submit="submit"
|
||||
@revert="revert"
|
||||
@print="print"
|
||||
/>
|
||||
<form-layout
|
||||
class="p-3"
|
||||
v-if="shouldRenderForm"
|
||||
:doc="doc"
|
||||
:fields="meta.fields"
|
||||
:layout="meta.layout"
|
||||
:invalid="invalid"
|
||||
/>
|
||||
<not-found v-if="notFound" />
|
||||
</div>
|
||||
<div class="frappe-form">
|
||||
<form-actions
|
||||
class="p-3 border-bottom"
|
||||
v-if="shouldRenderForm"
|
||||
:doc="doc"
|
||||
:links="links"
|
||||
@save="save"
|
||||
@submit="submit"
|
||||
@revert="revert"
|
||||
@print="print"
|
||||
/>
|
||||
<form-layout
|
||||
class="p-3"
|
||||
v-if="shouldRenderForm"
|
||||
:doc="doc"
|
||||
:fields="meta.fields"
|
||||
:layout="meta.layout"
|
||||
:invalid="invalid"
|
||||
/>
|
||||
<not-found v-if="notFound" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
@ -40,7 +40,8 @@ export default {
|
||||
notFound: false,
|
||||
invalid: false,
|
||||
invalidFields: [],
|
||||
links: []
|
||||
links: [],
|
||||
defaults: this.defaultValues
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -52,25 +53,36 @@ export default {
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
if (!this.defaults) {
|
||||
this.defaults = {};
|
||||
}
|
||||
this.meta.fields.forEach(field => {
|
||||
if (field.defaultValue)
|
||||
this.defaults[field.fieldname] = field.defaultValue;
|
||||
});
|
||||
if (!this.name) return;
|
||||
try {
|
||||
this.doc = await frappe.getDoc(this.doctype, this.name);
|
||||
|
||||
if (this.doc.isNew() && this.meta.fields.map(df => df.fieldname).includes('name')) {
|
||||
if (
|
||||
this.doc.isNew() &&
|
||||
this.meta.fields.map(df => df.fieldname).includes('name')
|
||||
) {
|
||||
// For a user editable name field,
|
||||
// it should be unset since it is autogenerated
|
||||
this.doc.set('name', '');
|
||||
}
|
||||
|
||||
if (this.defaultValues) {
|
||||
for (let fieldname in this.defaultValues) {
|
||||
const value = this.defaultValues[fieldname];
|
||||
this.doc.set(fieldname, value);
|
||||
if (this.doc.isNew() && this.defaults) {
|
||||
for (let fieldname in this.defaults) {
|
||||
const value = this.defaults[fieldname];
|
||||
await this.doc.set(fieldname, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.docLoaded = true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.notFound = true;
|
||||
}
|
||||
this.setLinks();
|
||||
@ -89,7 +101,6 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('save', this.doc);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
@ -112,13 +123,23 @@ export default {
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.doc.set('submitted', 1);
|
||||
await this.save();
|
||||
await this.doc.set('submitted', 1);
|
||||
try {
|
||||
await this.save();
|
||||
} catch (e) {
|
||||
await this.doc.set('submitted', 0);
|
||||
await this.doc.set('_dirty', false);
|
||||
}
|
||||
},
|
||||
|
||||
async revert() {
|
||||
this.doc.set('submitted', 0);
|
||||
await this.save();
|
||||
await this.doc.set('submitted', 0);
|
||||
try {
|
||||
await this.save();
|
||||
} catch (e) {
|
||||
await this.doc.set('submitted', 1);
|
||||
await this.doc.set('_dirty', false);
|
||||
}
|
||||
},
|
||||
|
||||
print() {
|
||||
|
@ -2,7 +2,12 @@
|
||||
<div class="frappe-form-actions d-flex justify-content-between align-items-center">
|
||||
<h5 class="m-0">{{ title }}</h5>
|
||||
<div class="d-flex">
|
||||
<f-button primary v-if="showSave" :disabled="disableSave" @click="$emit('save')">{{ _('Save') }}</f-button>
|
||||
<f-button
|
||||
primary
|
||||
v-if="showSave"
|
||||
:disabled="disableSave"
|
||||
@click="$emit('save')"
|
||||
>{{ _('Save') }}</f-button>
|
||||
<f-button primary v-if="showSubmit" @click="$emit('submit')">{{ _('Submit') }}</f-button>
|
||||
<f-button secondary v-if="showRevert" @click="$emit('revert')">{{ _('Revert') }}</f-button>
|
||||
<div class="ml-2" v-if="showPrint">
|
||||
@ -31,7 +36,7 @@ export default {
|
||||
showNextAction: false,
|
||||
showPrint: false,
|
||||
disableSave: false
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.doc.on('change', () => {
|
||||
@ -44,36 +49,45 @@ export default {
|
||||
this.isDirty = this.doc._dirty;
|
||||
|
||||
this.showSubmit =
|
||||
this.meta.isSubmittable
|
||||
&& !this.isDirty
|
||||
&& !this.doc.isNew()
|
||||
&& this.doc.submitted === 0;
|
||||
this.meta.isSubmittable &&
|
||||
!this.isDirty &&
|
||||
!this.doc.isNew() &&
|
||||
this.doc.submitted === 0;
|
||||
|
||||
this.showRevert =
|
||||
this.meta.isSubmittable
|
||||
&& !this.isDirty
|
||||
&& !this.doc.isNew()
|
||||
&& this.doc.submitted === 1;
|
||||
this.meta.isSubmittable &&
|
||||
!this.isDirty &&
|
||||
!this.doc.isNew() &&
|
||||
this.doc.submitted === 1;
|
||||
|
||||
this.showNextAction = 1
|
||||
this.showNextAction = 1;
|
||||
|
||||
this.showNextAction =
|
||||
!this.doc.isNew()
|
||||
&& this.links.length;
|
||||
this.showNextAction = !this.doc.isNew() && this.links.length;
|
||||
|
||||
this.showPrint =
|
||||
this.doc.submitted === 1
|
||||
&& this.meta.print
|
||||
this.showPrint = this.doc.submitted === 1 && this.meta.print;
|
||||
|
||||
this.showSave =
|
||||
this.doc.isNew() ?
|
||||
true :
|
||||
this.meta.isSubmittable ?
|
||||
(this.isDirty ? true : false) :
|
||||
true;
|
||||
this.showSave = this.doc.isNew()
|
||||
? true
|
||||
: this.meta.isSubmittable
|
||||
? this.isDirty
|
||||
? true
|
||||
: false
|
||||
: true;
|
||||
|
||||
this.disableSave =
|
||||
this.doc.isNew() ? false : !this.isDirty;
|
||||
this.disableSave = this.doc.isNew() ? false : !this.isDirty;
|
||||
},
|
||||
getFormTitle() {
|
||||
const _ = this._;
|
||||
|
||||
try {
|
||||
return _(
|
||||
this.meta.getFormTitle(this.doc) ||
|
||||
this.meta.label ||
|
||||
this.doc.doctype
|
||||
);
|
||||
} catch (e) {
|
||||
return _(this.meta.label || this.doc.doctype);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -84,12 +98,12 @@ export default {
|
||||
const _ = this._;
|
||||
|
||||
if (this.doc.isNew()) {
|
||||
return _('New {0}', _(this.doc.doctype));
|
||||
return _('New {0}', this.getFormTitle());
|
||||
}
|
||||
|
||||
const titleField = this.meta.titleField || 'name';
|
||||
return this.doc[titleField];
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,17 +1,21 @@
|
||||
<template>
|
||||
<form :class="['frappe-form-layout', { 'was-validated': invalid }]">
|
||||
<div class="form-row" v-if="layoutConfig"
|
||||
v-for="(section, i) in layoutConfig.sections" :key="i"
|
||||
v-show="showSection(i)"
|
||||
<div
|
||||
class="form-row"
|
||||
v-if="layoutConfig && showSection(i)"
|
||||
v-for="(section, i) in layoutConfig.sections"
|
||||
:key="i"
|
||||
>
|
||||
<div class="col" v-for="(column, j) in section.columns" :key="j">
|
||||
<frappe-control
|
||||
v-for="fieldname in column.fields"
|
||||
ref="frappe-control"
|
||||
v-for="(fieldname, k) in column.fields"
|
||||
v-if="shouldRenderField(fieldname)"
|
||||
:key="fieldname"
|
||||
:key="getDocField(fieldname).label"
|
||||
:docfield="getDocField(fieldname)"
|
||||
:value="$data[fieldname]"
|
||||
:doc="doc"
|
||||
:autofocus="doc.isNew() && (i === currentSection || i === 0) && j === 0 && k === 0 && !$data[fieldname]"
|
||||
@change="value => updateDoc(fieldname, value)"
|
||||
/>
|
||||
</div>
|
||||
@ -45,14 +49,27 @@ export default {
|
||||
this[df.fieldname] = doc[df.fieldname];
|
||||
});
|
||||
}
|
||||
this.updateLabels();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
updateLabels() {
|
||||
this.$refs['frappe-control'].forEach(control => {
|
||||
control.docfield.label = control.docfield.getLabel
|
||||
? control.docfield.getLabel(this.doc)
|
||||
: control.docfield.label;
|
||||
});
|
||||
},
|
||||
getDocField(fieldname) {
|
||||
return this.fields.find(df => df.fieldname === fieldname);
|
||||
},
|
||||
shouldRenderField(fieldname) {
|
||||
const hidden = Boolean(this.getDocField(fieldname).hidden);
|
||||
let hidden;
|
||||
try {
|
||||
hidden = Boolean(this.getDocField(fieldname).hidden(this.doc));
|
||||
} catch (e) {
|
||||
hidden = Boolean(this.getDocField(fieldname).hidden) || false;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return false;
|
||||
@ -84,15 +101,17 @@ export default {
|
||||
|
||||
if (!layout) {
|
||||
const fields = this.fields.map(df => df.fieldname);
|
||||
layout = [{
|
||||
columns: [{ fields }]
|
||||
}];
|
||||
layout = [
|
||||
{
|
||||
columns: [{ fields }]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (Array.isArray(layout)) {
|
||||
layout = {
|
||||
sections: layout
|
||||
}
|
||||
};
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<div class="frappe-list">
|
||||
<list-actions
|
||||
:doctype="doctype"
|
||||
:showDelete="checkList.length"
|
||||
@new="$emit('newDoc')"
|
||||
@delete="deleteCheckedItems"
|
||||
/>
|
||||
<ul class="list-group">
|
||||
<list-item v-for="doc of data" :key="doc.name"
|
||||
:id="doc.name"
|
||||
:isActive="doc.name === $route.params.name"
|
||||
:isChecked="isChecked(doc.name)"
|
||||
@clickItem="openForm(doc.name)"
|
||||
@checkItem="toggleCheck(doc.name)"
|
||||
>
|
||||
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
|
||||
<span class="d-inline-block ml-2">
|
||||
{{ doc[meta.titleField || 'name'] }}
|
||||
</span>
|
||||
</list-item>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="frappe-list">
|
||||
<list-actions
|
||||
:doctype="doctype"
|
||||
:showDelete="checkList.length"
|
||||
@new="$emit('newDoc')"
|
||||
@delete="deleteCheckedItems"
|
||||
/>
|
||||
<ul class="list-group">
|
||||
<list-item
|
||||
v-for="doc of data"
|
||||
:key="doc.name"
|
||||
:id="doc.name"
|
||||
:isActive="doc.name === $route.params.name"
|
||||
:isChecked="isChecked(doc.name)"
|
||||
@clickItem="openForm(doc.name)"
|
||||
@checkItem="toggleCheck(doc.name)"
|
||||
>
|
||||
<indicator v-if="hasIndicator" :color="getIndicatorColor(doc)" />
|
||||
<span class="d-inline-block ml-2">{{ doc[meta.titleField || 'name'] }}</span>
|
||||
</list-item>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
|
@ -1,8 +1,8 @@
|
||||
import ModalContainer from './ModalContainer';
|
||||
import frappe from 'frappejs';
|
||||
|
||||
const Plugin = {
|
||||
install (Vue) {
|
||||
|
||||
install(Vue) {
|
||||
this.event = new Vue();
|
||||
|
||||
Vue.prototype.$modal = {
|
||||
@ -13,13 +13,15 @@ const Plugin = {
|
||||
hide(id) {
|
||||
Plugin.event.$emit('hide', id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
frappe.showModal = Vue.prototype.$modal.show;
|
||||
|
||||
// create modal container
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
new Vue({ render: h => h(ModalContainer) }).$mount(div);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Plugin;
|
||||
export default Plugin;
|
||||
|
@ -1,118 +1,158 @@
|
||||
<script>
|
||||
export default {
|
||||
render(h) {
|
||||
if (this.onlyInput) {
|
||||
return this.getInputElement(h);
|
||||
}
|
||||
return this.getWrapperElement(h);
|
||||
},
|
||||
props: {
|
||||
docfield: Object,
|
||||
value: [String, Number, Array, FileList],
|
||||
onlyInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: Boolean,
|
||||
autofocus: Boolean
|
||||
},
|
||||
mounted() {
|
||||
if (this.autofocus) {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return this.docfield.fieldname + '-'
|
||||
+ document.querySelectorAll(`[data-fieldname="${this.docfield.fieldname}"]`).length;
|
||||
},
|
||||
inputClass() {
|
||||
return [];
|
||||
},
|
||||
wrapperClass() {
|
||||
return [];
|
||||
},
|
||||
labelClass() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWrapperElement(h) {
|
||||
return h('div', {
|
||||
class: ['form-group', this.onlyInput ? 'mb-0' : '', ...this.wrapperClass],
|
||||
attrs: {
|
||||
'data-fieldname': this.docfield.fieldname,
|
||||
'data-fieldtype': this.docfield.fieldtype
|
||||
}
|
||||
}, this.getChildrenElement(h));
|
||||
},
|
||||
getChildrenElement(h) {
|
||||
return [this.getLabelElement(h), this.getInputElement(h)]
|
||||
},
|
||||
getLabelElement(h) {
|
||||
return h('label', {
|
||||
class: [this.labelClass, 'text-muted'],
|
||||
attrs: {
|
||||
for: this.id
|
||||
},
|
||||
domProps: {
|
||||
textContent: this.docfield.label
|
||||
}
|
||||
});
|
||||
},
|
||||
getInputElement(h) {
|
||||
return h(this.getInputTag(), {
|
||||
class: this.getInputClass(),
|
||||
attrs: this.getInputAttrs(),
|
||||
on: this.getInputListeners(),
|
||||
domProps: this.getDomProps(),
|
||||
ref: 'input'
|
||||
}, this.getInputChildren(h));
|
||||
},
|
||||
getInputTag() {
|
||||
return 'input';
|
||||
},
|
||||
getInputClass() {
|
||||
return ['form-control', ...this.inputClass];
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
value: this.value,
|
||||
required: this.docfield.required,
|
||||
disabled: this.disabled
|
||||
}
|
||||
},
|
||||
getInputListeners() {
|
||||
return {
|
||||
change: (e) => {
|
||||
this.handleChange(e.target.value);
|
||||
}
|
||||
};
|
||||
},
|
||||
getInputChildren() {
|
||||
return null;
|
||||
},
|
||||
getDomProps() {
|
||||
return null;
|
||||
},
|
||||
async handleChange(value) {
|
||||
value = this.parse(value);
|
||||
const isValid = await this.validate(value);
|
||||
this.$refs.input.setCustomValidity(isValid === false ? 'error' : '');
|
||||
this.$emit('change', value);
|
||||
},
|
||||
getValueFromInput(e) {
|
||||
|
||||
},
|
||||
validate() {
|
||||
return true;
|
||||
},
|
||||
parse(value) {
|
||||
return value;
|
||||
}
|
||||
render(h) {
|
||||
if (this.onlyInput) {
|
||||
return this.getInputElement(h);
|
||||
}
|
||||
}
|
||||
return this.getWrapperElement(h);
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
label: this.docfield.label
|
||||
};
|
||||
},
|
||||
props: {
|
||||
docfield: Object,
|
||||
value: [String, Number, Array, FileList],
|
||||
onlyInput: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: Boolean,
|
||||
autofocus: Boolean,
|
||||
doc: Object
|
||||
},
|
||||
mounted() {
|
||||
if (this.autofocus) {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return (
|
||||
this.docfield.fieldname +
|
||||
'-' +
|
||||
document.querySelectorAll(
|
||||
`[data-fieldname="${this.docfield.fieldname}"]`
|
||||
).length
|
||||
);
|
||||
},
|
||||
inputClass() {
|
||||
return [];
|
||||
},
|
||||
wrapperClass() {
|
||||
return [];
|
||||
},
|
||||
labelClass() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getWrapperElement(h) {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
class: [
|
||||
'form-group',
|
||||
this.onlyInput ? 'mb-0' : '',
|
||||
...this.wrapperClass
|
||||
],
|
||||
attrs: {
|
||||
'data-fieldname': this.docfield.fieldname,
|
||||
'data-fieldtype': this.docfield.fieldtype
|
||||
}
|
||||
},
|
||||
this.getChildrenElement(h)
|
||||
);
|
||||
},
|
||||
getChildrenElement(h) {
|
||||
return [
|
||||
this.getLabelElement(h),
|
||||
this.getInputElement(h),
|
||||
this.getDescriptionElement(h)
|
||||
];
|
||||
},
|
||||
getLabelElement(h) {
|
||||
return h('label', {
|
||||
class: [this.labelClass, 'text-muted'],
|
||||
attrs: {
|
||||
for: this.id
|
||||
},
|
||||
domProps: {
|
||||
textContent: this.label
|
||||
}
|
||||
});
|
||||
},
|
||||
getDescriptionElement(h) {
|
||||
return h('small', {
|
||||
class: ['form-text', 'text-muted'],
|
||||
domProps: {
|
||||
textContent: this.docfield.description || ''
|
||||
}
|
||||
});
|
||||
},
|
||||
getInputElement(h) {
|
||||
return h(
|
||||
this.getInputTag(),
|
||||
{
|
||||
class: this.getInputClass(),
|
||||
attrs: this.getInputAttrs(),
|
||||
on: this.getInputListeners(),
|
||||
domProps: this.getDomProps(),
|
||||
ref: 'input'
|
||||
},
|
||||
this.getInputChildren(h)
|
||||
);
|
||||
},
|
||||
getInputTag() {
|
||||
return 'input';
|
||||
},
|
||||
getFormControlSize() {
|
||||
return this.docfield.size === 'small'
|
||||
? 'form-control-sm'
|
||||
: this.size === 'large'
|
||||
? 'form-control-lg'
|
||||
: '';
|
||||
},
|
||||
getInputClass() {
|
||||
return ['form-control', this.getFormControlSize(), ...this.inputClass];
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: 'text',
|
||||
placeholder: this.docfield.placeholder || '',
|
||||
value: this.value,
|
||||
required: this.docfield.required,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
getInputListeners() {
|
||||
return {
|
||||
change: e => {
|
||||
this.handleChange(e.target.value);
|
||||
}
|
||||
};
|
||||
},
|
||||
getInputChildren() {
|
||||
return null;
|
||||
},
|
||||
getDomProps() {
|
||||
return null;
|
||||
},
|
||||
async handleChange(value) {
|
||||
value = this.parse(value);
|
||||
const isValid = await this.validate(value);
|
||||
this.$refs.input.setCustomValidity(isValid === false ? 'error' : '');
|
||||
this.$emit('change', value);
|
||||
},
|
||||
getValueFromInput(e) {},
|
||||
validate() {
|
||||
return true;
|
||||
},
|
||||
parse(value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -3,10 +3,12 @@
|
||||
<label v-if="!onlyInput">{{ docfield.label }}</label>
|
||||
<flat-pickr
|
||||
class="form-control"
|
||||
:class="getFormControlSize()"
|
||||
:placeholder="docfield.placeholder"
|
||||
:value="value"
|
||||
:config="config"
|
||||
@on-change="emitChange">
|
||||
</flat-pickr>
|
||||
@on-change="emitChange"
|
||||
></flat-pickr>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -27,5 +29,8 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~flatpickr/dist/flatpickr.css";
|
||||
@import '~flatpickr/dist/flatpickr.css';
|
||||
.flat-pickr-input {
|
||||
background-color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,6 +1,14 @@
|
||||
<script>
|
||||
import Float from './Float';
|
||||
export default {
|
||||
extends: Float
|
||||
}
|
||||
extends: Float,
|
||||
methods: {
|
||||
parse(value) {
|
||||
return frappe.format(value, 'Currency');
|
||||
},
|
||||
validate(value) {
|
||||
return !isNaN(frappe.parseNumber(value));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -6,7 +6,21 @@ export default {
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
default: () => {
|
||||
let dateFormat = {
|
||||
'dd/MM/yyyy': 'd/m/Y',
|
||||
'MM/dd/yyyy': 'm/d/Y',
|
||||
'dd-MM-yyyy': 'd-m-Y',
|
||||
'MM-dd-yyyy': 'm-d-Y',
|
||||
'yyyy-MM-dd': 'Y-m-d'
|
||||
};
|
||||
let altFormat = dateFormat[frappe.SystemSettings.dateFormat];
|
||||
return {
|
||||
altInput: true,
|
||||
dateFormat: 'Y-m-d',
|
||||
altFormat: altFormat
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -6,10 +6,12 @@
|
||||
:onlyInput="onlyInput"
|
||||
:disabled="isDisabled"
|
||||
:autofocus="autofocus"
|
||||
:doc="doc"
|
||||
@change="$emit('change', $event)"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
import frappe from 'frappejs';
|
||||
import Base from './Base';
|
||||
import Autocomplete from './Autocomplete';
|
||||
import Check from './Check';
|
||||
@ -77,7 +79,7 @@ export default {
|
||||
return this.doc[reference];
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
|
@ -10,9 +10,9 @@ export default {
|
||||
extends: Autocomplete,
|
||||
methods: {
|
||||
async getList(query) {
|
||||
let filters = this.docfield.getFilters ?
|
||||
this.docfield.getFilters(query) :
|
||||
null;
|
||||
let filters = this.docfield.getFilters
|
||||
? this.docfield.getFilters(query, this.doc)
|
||||
: null;
|
||||
|
||||
if (query) {
|
||||
if (!filters) filters = {};
|
||||
@ -42,6 +42,7 @@ export default {
|
||||
}))
|
||||
.concat({
|
||||
label: plusIcon + ' New ' + this.getTarget(),
|
||||
filters,
|
||||
value: '__newItem'
|
||||
});
|
||||
},
|
||||
@ -138,18 +139,23 @@ export default {
|
||||
},
|
||||
onItemClick(item) {
|
||||
if (item.value === '__newItem') {
|
||||
this.openFormModal();
|
||||
this.openFormModal(item.filters);
|
||||
} else {
|
||||
this.handleChange(item.value);
|
||||
}
|
||||
},
|
||||
async openFormModal() {
|
||||
async openFormModal(filters) {
|
||||
const input = this.$refs.input;
|
||||
const newDoc = await frappe.getNewDoc(this.getTarget());
|
||||
let defaultValues = {};
|
||||
if (filters) {
|
||||
for (let key of Object.keys(filters)) {
|
||||
defaultValues[key] = filters[key];
|
||||
}
|
||||
}
|
||||
defaultValues.name = input.value !== '__newItem' ? input.value : null;
|
||||
this.$formModal.open(newDoc, {
|
||||
defaultValues: {
|
||||
name: input.value !== '__newItem' ? input.value : null
|
||||
},
|
||||
defaultValues,
|
||||
onClose: () => {
|
||||
// if new doc was not created
|
||||
// then reset the input value
|
||||
|
@ -1,31 +1,33 @@
|
||||
<script>
|
||||
import Base from './Base';
|
||||
export default {
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputTag() {
|
||||
return 'select';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
required: this.docfield.required,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
getInputChildren(h) {
|
||||
return this.docfield.options.map(option =>
|
||||
h('option', {
|
||||
attrs: {
|
||||
key: option,
|
||||
selected: option === this.value
|
||||
},
|
||||
domProps: {
|
||||
textContent: option
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputTag() {
|
||||
return 'select';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
required: this.docfield.required,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
getInputChildren(h) {
|
||||
return this.docfield.options.map(option =>
|
||||
h('option', {
|
||||
attrs: {
|
||||
key: option,
|
||||
value: option,
|
||||
disabled: option.indexOf('...') > -1,
|
||||
selected: option.indexOf('...') > -1 || option === this.value
|
||||
},
|
||||
domProps: {
|
||||
textContent: option
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -4,12 +4,10 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" width="60">
|
||||
<input class="mr-2" type="checkbox" @change="toggleCheckAll">
|
||||
<input class="mr-2" type="checkbox" @change="toggleCheckAll" />
|
||||
<span>#</span>
|
||||
</th>
|
||||
<th scope="col" v-for="column in columns" :key="column.fieldname">
|
||||
{{ column.label }}
|
||||
</th>
|
||||
<th scope="col" v-for="column in columns" :key="column.fieldname">{{ column.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody v-if="rows.length">
|
||||
@ -20,10 +18,12 @@
|
||||
type="checkbox"
|
||||
:checked="checkedRows.includes(i)"
|
||||
@change="e => onCheck(e, i)"
|
||||
>
|
||||
/>
|
||||
<span>{{ i + 1 }}</span>
|
||||
</th>
|
||||
<td v-for="column in columns" :key="column.fieldname"
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="column.fieldname"
|
||||
tabindex="1"
|
||||
:ref="column.fieldname + i"
|
||||
@click="activateFocus(i, column.fieldname)"
|
||||
@ -37,7 +37,10 @@
|
||||
@keydown.down="focusBelowCell(i, column.fieldname)"
|
||||
@keydown.esc="escOnCell(i, column.fieldname)"
|
||||
>
|
||||
<div class="table-cell" :class="{'active': isFocused(i, column.fieldname)}">
|
||||
<div
|
||||
class="table-cell"
|
||||
:class="{'active': isFocused(i, column.fieldname),'p-1': isEditing(i, column.fieldname)}"
|
||||
>
|
||||
<frappe-control
|
||||
v-if="isEditing(i, column.fieldname)"
|
||||
:docfield="getDocfield(column.fieldname)"
|
||||
@ -47,9 +50,11 @@
|
||||
:autofocus="true"
|
||||
@change="onCellChange(i, column.fieldname, $event)"
|
||||
/>
|
||||
<div class="text-truncate" v-else>
|
||||
{{ row[column.fieldname] || ' ' }}
|
||||
</div>
|
||||
<div
|
||||
class="text-truncate"
|
||||
:data-fieldtype="column.fieldtype"
|
||||
v-else
|
||||
>{{ row[column.fieldname] || ' ' }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -57,16 +62,14 @@
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td :colspan="columns.length + 1" class="text-center">
|
||||
<div class="table-cell">
|
||||
No Data
|
||||
</div>
|
||||
<div class="table-cell">No Data</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-actions" v-if="!disabled">
|
||||
<f-button danger @click="removeCheckedRows" v-if="checkedRows.length">Remove</f-button>
|
||||
<f-button light @click="addRow" v-if="!checkedRows.length">Add Row</f-button>
|
||||
<f-button secondary @click="addRow" v-if="!checkedRows.length">Add Row</f-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -96,7 +99,12 @@ export default {
|
||||
enterPressOnCell() {
|
||||
const { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isEditing(index, fieldname)) {
|
||||
this.deactivateEditing();
|
||||
// FIX: enter pressing on a cell with a value throws error.
|
||||
// Problem: input gets undefined on deactivating
|
||||
setTimeout(() => {
|
||||
this.deactivateEditing();
|
||||
}, 300);
|
||||
|
||||
this.activateFocus(index, fieldname);
|
||||
} else {
|
||||
this.activateEditing(index, fieldname);
|
||||
@ -104,7 +112,10 @@ export default {
|
||||
},
|
||||
focusPreviousCell() {
|
||||
let { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
|
||||
if (
|
||||
this.isFocused(index, fieldname) &&
|
||||
!this.isEditing(index, fieldname)
|
||||
) {
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
pos -= 1;
|
||||
if (pos < 0) {
|
||||
@ -120,8 +131,10 @@ export default {
|
||||
},
|
||||
focusNextCell() {
|
||||
let { index, fieldname } = this.currentlyFocused;
|
||||
if (this.isFocused(index, fieldname) && !this.isEditing(index, fieldname)) {
|
||||
|
||||
if (
|
||||
this.isFocused(index, fieldname) &&
|
||||
!this.isEditing(index, fieldname)
|
||||
) {
|
||||
let pos = this._getColumnIndex(fieldname);
|
||||
pos += 1;
|
||||
if (pos > this.columns.length - 1) {
|
||||
@ -204,7 +217,10 @@ export default {
|
||||
};
|
||||
},
|
||||
activateFocus(i, fieldname) {
|
||||
this.deactivateEditing();
|
||||
if (this.isFocused(i, fieldname) && this.isEditing(i, fieldname)) {
|
||||
return;
|
||||
}
|
||||
// this.deactivateEditing();
|
||||
const docfield = this.columns.find(c => c.fieldname === fieldname);
|
||||
this.currentlyFocused = {
|
||||
index: i,
|
||||
@ -224,14 +240,13 @@ export default {
|
||||
this.currentlyFocused = {};
|
||||
}
|
||||
},
|
||||
addRow() {
|
||||
async addRow() {
|
||||
const rows = this.rows.slice();
|
||||
const newRow = {
|
||||
idx: rows.length
|
||||
};
|
||||
const newRow = { idx: rows.length };
|
||||
|
||||
for (let column of this.columns) {
|
||||
newRow[column.fieldname] = null;
|
||||
if (column.defaultValue) newRow[column.fieldname] = column.defaultValue;
|
||||
else newRow[column.fieldname] = null;
|
||||
}
|
||||
|
||||
rows.push(newRow);
|
||||
@ -292,7 +307,7 @@ export default {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
td {
|
||||
padding: 0;
|
||||
padding: 0rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -322,4 +337,9 @@ td {
|
||||
[data-fieldtype='Link'] .input-group-append {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-fieldtype='Currency'],
|
||||
[data-fieldtype='Float'] {
|
||||
text-align: right !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,24 +1,24 @@
|
||||
<script>
|
||||
import Base from './Base';
|
||||
export default {
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputTag() {
|
||||
return 'textarea';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
required: this.docfield.required,
|
||||
rows: 3,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
getDomProps() {
|
||||
return {
|
||||
value: this.value
|
||||
}
|
||||
}
|
||||
extends: Base,
|
||||
methods: {
|
||||
getInputTag() {
|
||||
return 'textarea';
|
||||
},
|
||||
getInputAttrs() {
|
||||
return {
|
||||
id: this.id,
|
||||
required: this.docfield.required,
|
||||
rows: this.docfield.rows || 3,
|
||||
disabled: this.disabled
|
||||
};
|
||||
},
|
||||
getDomProps() {
|
||||
return {
|
||||
value: this.value
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<div class="row pb-4">
|
||||
<frappe-control class="col-4"
|
||||
v-for="docfield in filters"
|
||||
:key="docfield.fieldname"
|
||||
:docfield="docfield"
|
||||
:value="$data.filterValues[docfield.fieldname]"
|
||||
:doc="$data.filterValues"
|
||||
@change="updateValue(docfield.fieldname, $event)"/>
|
||||
<div class="d-flex px-1">
|
||||
<div class="col-3" v-for="docfield in filterFields" :key="docfield.fieldname">
|
||||
<frappe-control
|
||||
v-if="shouldRenderField(docfield)"
|
||||
:docfield="docfield"
|
||||
:value="$data.filterValues[docfield.fieldname]"
|
||||
:onlyInput="true"
|
||||
:doc="filterDoc"
|
||||
@change="updateValue(docfield.fieldname, $event)"
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import FrappeControl from 'frappejs/ui/components/controls/FrappeControl';
|
||||
|
||||
export default {
|
||||
props: ['filters', 'filterDefaults'],
|
||||
props: ['filterFields', 'filterDoc', 'filterDefaults'],
|
||||
data() {
|
||||
const filterValues = {};
|
||||
for (let filter of this.filters) {
|
||||
for (let filter of this.filterFields) {
|
||||
filterValues[filter.fieldname] =
|
||||
this.filterDefaults[filter.fieldname] || null;
|
||||
}
|
||||
@ -32,7 +36,22 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
shouldRenderField(field) {
|
||||
let hidden;
|
||||
try {
|
||||
hidden = Boolean(field.hidden(this.filterDoc));
|
||||
} catch (e) {
|
||||
hidden = Boolean(field.hidden) || false;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateValue(fieldname, value) {
|
||||
this.filterDoc.set(fieldname, value);
|
||||
this.filterValues[fieldname] = value;
|
||||
this.$emit('change', this.filterValues);
|
||||
}
|
||||
|
16
ui/pages/Report/ReportLinks.vue
Normal file
16
ui/pages/Report/ReportLinks.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="page-header pr-4 ml-0 pl-0 py-2 d-flex align-items-center border-bottom bg-white">
|
||||
<div class="text-right ml-2" v-for="link of links" :key="link.label">
|
||||
<f-button secondary @click="link.handler">{{ link.label }}</f-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['links']
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
@ -1,17 +1,29 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<h4 class="pb-2">{{ reportConfig.title }}</h4>
|
||||
<report-filters v-if="filtersExists" :filters="reportConfig.filterFields" :filterDefaults="filters" @change="getReportData"></report-filters>
|
||||
<div class="pt-2" ref="datatable" v-once></div>
|
||||
</div>
|
||||
<not-found v-if="!reportConfig" />
|
||||
<div>
|
||||
<div class="p-4">
|
||||
<div class="row pb-4">
|
||||
<h4 class="col-6 d-flex">{{ reportConfig.title }}</h4>
|
||||
<report-links class="col-6 d-flex pr-0 flex-row-reverse" v-if="linksExists" :links="links"></report-links>
|
||||
</div>
|
||||
<div class="row pb-4">
|
||||
<report-filters
|
||||
class="col-12 pr-0"
|
||||
v-if="filtersExists"
|
||||
:filters="reportConfig.filterFields"
|
||||
:filterDefaults="filters"
|
||||
@change="getReportData"
|
||||
></report-filters>
|
||||
</div>
|
||||
<div class="pt-2 pr-3" ref="datatable" v-once></div>
|
||||
</div>
|
||||
<not-found v-if="!reportConfig" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import DataTable from 'frappe-datatable';
|
||||
import frappe from 'frappejs';
|
||||
import ReportFilters from './ReportFilters';
|
||||
import ReportFilters from 'frappejs/ui/pages/Report/ReportFilters';
|
||||
import ReportLinks from 'frappejs/ui/pages/Report/ReportLinks';
|
||||
import utils from 'frappejs/client/ui/utils';
|
||||
|
||||
export default {
|
||||
@ -20,13 +32,30 @@ export default {
|
||||
computed: {
|
||||
filtersExists() {
|
||||
return (this.reportConfig.filterFields || []).length;
|
||||
},
|
||||
linksExists() {
|
||||
return (this.reportConfig.linkFields || []).length;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
reportName() {
|
||||
//FIX: Report's data forwards to next consecutively changed report
|
||||
this.getReportData(this.filters);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
links: []
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.setLinks();
|
||||
},
|
||||
methods: {
|
||||
async getReportData(filters) {
|
||||
let data = await frappe.call({
|
||||
method: this.reportConfig.method,
|
||||
args: filters
|
||||
method: this.reportConfig.method,
|
||||
args: filters
|
||||
});
|
||||
|
||||
let rows, columns;
|
||||
@ -48,8 +77,8 @@ export default {
|
||||
columns = this.getColumns();
|
||||
}
|
||||
|
||||
for(let column of columns) {
|
||||
column.editable = false;
|
||||
for (let column of columns) {
|
||||
column.editable = false;
|
||||
}
|
||||
|
||||
if (this.datatable) {
|
||||
@ -57,9 +86,26 @@ export default {
|
||||
} else {
|
||||
this.datatable = new DataTable(this.$refs.datatable, {
|
||||
columns: columns,
|
||||
data: rows
|
||||
data: rows,
|
||||
treeView: this.reportConfig.treeView || false,
|
||||
cellHeight: 35
|
||||
});
|
||||
}
|
||||
return [rows, columns];
|
||||
},
|
||||
setLinks() {
|
||||
if (this.linksExists) {
|
||||
let links = [];
|
||||
for (let link of this.reportConfig.linkFields) {
|
||||
links.push({
|
||||
label: link.label,
|
||||
handler: () => {
|
||||
link.action(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.links = links;
|
||||
}
|
||||
},
|
||||
getColumns(data) {
|
||||
const columns = this.reportConfig.getColumns(data);
|
||||
@ -67,9 +113,13 @@ export default {
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ReportFilters
|
||||
ReportFilters,
|
||||
ReportLinks
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.datatable {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
@ -4,34 +4,37 @@ const luxon = require('luxon');
|
||||
const frappe = require('frappejs');
|
||||
|
||||
module.exports = {
|
||||
format(value, field) {
|
||||
if (typeof field === 'string') {
|
||||
field = { fieldtype: field };
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Currency') {
|
||||
value = numberFormat.formatNumber(value);
|
||||
|
||||
} else if (field.fieldtype === 'Text') {
|
||||
// value = markdown.makeHtml(value || '');
|
||||
|
||||
} else if (field.fieldtype === 'Date') {
|
||||
let dateFormat;
|
||||
if (!frappe.SystemSettings) {
|
||||
dateFormat = 'yyyy-MM-dd';
|
||||
} else {
|
||||
dateFormat = frappe.SystemSettings.dateFormat;
|
||||
}
|
||||
|
||||
value = luxon.DateTime.fromISO(value).toFormat(dateFormat);
|
||||
|
||||
} else {
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
} else {
|
||||
value = value + '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
format(value, field) {
|
||||
if (typeof field === 'string') {
|
||||
field = { fieldtype: field };
|
||||
}
|
||||
}
|
||||
if (field.fieldtype === 'Currency') {
|
||||
value = numberFormat.formatNumber(value);
|
||||
} else if (field.fieldtype === 'Text') {
|
||||
// value = markdown.makeHtml(value || '');
|
||||
} else if (field.fieldtype === 'Date') {
|
||||
let dateFormat;
|
||||
if (!frappe.SystemSettings) {
|
||||
dateFormat = 'yyyy-MM-dd';
|
||||
} else {
|
||||
dateFormat = frappe.SystemSettings.dateFormat;
|
||||
}
|
||||
|
||||
value = luxon.DateTime.fromISO(value).toFormat(dateFormat);
|
||||
if (value === 'Invalid DateTime') {
|
||||
value = '';
|
||||
}
|
||||
} else if (field.fieldtype === 'Check') {
|
||||
typeof parseInt(value) === 'number'
|
||||
? (value = parseInt(value))
|
||||
: (value = Boolean(value));
|
||||
} else {
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
} else {
|
||||
value = value + '';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,15 @@
|
||||
const numberFormats = {
|
||||
"#,###.##": { fractionSep: ".", groupSep: ",", precision: 2 },
|
||||
"#.###,##": { fractionSep: ",", groupSep: ".", precision: 2 },
|
||||
"# ###.##": { fractionSep: ".", groupSep: " ", precision: 2 },
|
||||
"# ###,##": { fractionSep: ",", groupSep: " ", precision: 2 },
|
||||
"#'###.##": { fractionSep: ".", groupSep: "'", precision: 2 },
|
||||
"#, ###.##": { fractionSep: ".", groupSep: ", ", precision: 2 },
|
||||
"#,##,###.##": { fractionSep: ".", groupSep: ",", precision: 2 },
|
||||
"#,###.###": { fractionSep: ".", groupSep: ",", precision: 3 },
|
||||
"#.###": { fractionSep: "", groupSep: ".", precision: 0 },
|
||||
"#,###": { fractionSep: "", groupSep: ",", precision: 0 },
|
||||
}
|
||||
'#,###.##': { fractionSep: '.', groupSep: ',', precision: 2 },
|
||||
'#.###,##': { fractionSep: ',', groupSep: '.', precision: 2 },
|
||||
'# ###.##': { fractionSep: '.', groupSep: ' ', precision: 2 },
|
||||
'# ###,##': { fractionSep: ',', groupSep: ' ', precision: 2 },
|
||||
"#'###.##": { fractionSep: '.', groupSep: "'", precision: 2 },
|
||||
'#, ###.##': { fractionSep: '.', groupSep: ', ', precision: 2 },
|
||||
'#,##,###.##': { fractionSep: '.', groupSep: ',', precision: 2 },
|
||||
'#,###.###': { fractionSep: '.', groupSep: ',', precision: 3 },
|
||||
'#.###': { fractionSep: '', groupSep: '.', precision: 0 },
|
||||
'#,###': { fractionSep: '', groupSep: ',', precision: 0 }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// parse a formatted number string
|
||||
@ -53,7 +53,8 @@ module.exports = {
|
||||
|
||||
for (var i = integer.length; i >= 0; i--) {
|
||||
var l = this.removeSeparator(str, info.groupSep).length;
|
||||
if (format == "#,##,###.##" && str.indexOf(",") != -1) { // INR
|
||||
if (format == '#,##,###.##' && str.indexOf(',') != -1) {
|
||||
// INR
|
||||
group_position = 2;
|
||||
l += 1;
|
||||
}
|
||||
@ -64,24 +65,27 @@ module.exports = {
|
||||
str += info.groupSep;
|
||||
}
|
||||
}
|
||||
parts[0] = str.split("").reverse().join("");
|
||||
parts[0] = str
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('');
|
||||
}
|
||||
if (parts[0] + "" == "") {
|
||||
parts[0] = "0";
|
||||
if (parts[0] + '' == '') {
|
||||
parts[0] = '0';
|
||||
}
|
||||
|
||||
// join decimal
|
||||
parts[1] = (parts[1] && info.fractionSep) ? (info.fractionSep + parts[1]) : "";
|
||||
parts[1] = parts[1] && info.fractionSep ? info.fractionSep + parts[1] : '';
|
||||
|
||||
// join
|
||||
return (is_negative ? "-" : "") + parts[0] + parts[1];
|
||||
return (is_negative ? '-' : '') + parts[0] + parts[1];
|
||||
},
|
||||
|
||||
getFormatInfo(format) {
|
||||
let format_info = numberFormats[format];
|
||||
|
||||
if (!format_info) {
|
||||
throw `Unknown number format "${format}"`;
|
||||
throw new Error(`Unknown number format "${format}"`);
|
||||
}
|
||||
|
||||
return format_info;
|
||||
@ -92,13 +96,14 @@ module.exports = {
|
||||
var d = parseInt(precision || 0);
|
||||
var m = Math.pow(10, d);
|
||||
var n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
|
||||
var i = Math.floor(n), f = n - i;
|
||||
var r = ((!precision && f == 0.5) ? ((i % 2 == 0) ? i : i + 1) : Math.round(n));
|
||||
var i = Math.floor(n),
|
||||
f = n - i;
|
||||
var r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
|
||||
r = d ? r / m : r;
|
||||
return is_negative ? -r : r;
|
||||
},
|
||||
|
||||
removeSeparator(text, sep) {
|
||||
return text.replace(new RegExp(sep === "." ? "\\." : sep, "g"), '');
|
||||
return text.replace(new RegExp(sep === '.' ? '\\.' : sep, 'g'), '');
|
||||
}
|
||||
};
|
||||
|
@ -21,7 +21,9 @@ function makeConfig() {
|
||||
|
||||
const whiteListedModules = ['vue'];
|
||||
const allDependencies = Object.assign(frappeDependencies, appDependencies);
|
||||
const externals = Object.keys(allDependencies).filter(d => !whiteListedModules.includes(d));
|
||||
const externals = Object.keys(allDependencies).filter(
|
||||
d => !whiteListedModules.includes(d)
|
||||
);
|
||||
|
||||
getConfig = function getConfig() {
|
||||
const config = {
|
||||
@ -31,7 +33,9 @@ function makeConfig() {
|
||||
externals: isElectron ? externals : undefined,
|
||||
target: isElectron ? 'electron-renderer' : 'web',
|
||||
output: {
|
||||
path: isElectron ? resolveAppDir('./dist/electron') : resolveAppDir('./dist'),
|
||||
path: isElectron
|
||||
? resolveAppDir('./dist/electron')
|
||||
: resolveAppDir('./dist'),
|
||||
filename: '[name].js',
|
||||
// publicPath: appConfig.dev.assetsPublicPath,
|
||||
libraryTarget: isElectron ? 'commonjs2' : undefined
|
||||
@ -46,10 +50,8 @@ function makeConfig() {
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: file => (
|
||||
/node_modules/.test(file) &&
|
||||
!/\.vue\.js/.test(file)
|
||||
)
|
||||
exclude: file =>
|
||||
/node_modules/.test(file) && !/\.vue\.js/.test(file)
|
||||
},
|
||||
{
|
||||
test: /\.node$/,
|
||||
@ -57,48 +59,62 @@ function makeConfig() {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
use: ['vue-style-loader', 'css-loader', {
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
],
|
||||
},
|
||||
}]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
'sass-loader'
|
||||
]
|
||||
use: ['vue-style-loader', 'css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(png|svg|jpg|gif)$/,
|
||||
use: [
|
||||
'file-loader'
|
||||
]
|
||||
test: /\.(png|svg|jpg|woff|woff2|gif)$/,
|
||||
use: ['file-loader']
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json', '.css', '.node'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.esm.js',
|
||||
'deepmerge$': 'deepmerge/dist/umd.js',
|
||||
vue$: 'vue/dist/vue.esm.js',
|
||||
deepmerge$: 'deepmerge/dist/umd.js',
|
||||
'@': appConfig.dev.srcDir ? resolveAppDir(appConfig.dev.srcDir) : null
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(Object.assign({
|
||||
'process.env': appConfig.dev.env,
|
||||
'process.env.NODE_ENV': isProduction ? '"production"' : '"development"',
|
||||
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
|
||||
}, !isProduction ? {
|
||||
'__static': `"${resolveAppDir(appConfig.staticPath).replace(/\\/g, '\\\\')}"`
|
||||
} : {})),
|
||||
new webpack.DefinePlugin(
|
||||
Object.assign(
|
||||
{
|
||||
'process.env': appConfig.dev.env,
|
||||
'process.env.NODE_ENV': isProduction
|
||||
? '"production"'
|
||||
: '"development"',
|
||||
'process.env.ELECTRON': JSON.stringify(process.env.ELECTRON)
|
||||
},
|
||||
!isProduction
|
||||
? {
|
||||
__static: `"${resolveAppDir(appConfig.staticPath).replace(
|
||||
/\\/g,
|
||||
'\\\\'
|
||||
)}"`
|
||||
}
|
||||
: {}
|
||||
)
|
||||
),
|
||||
new VueLoaderPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: resolveAppDir(appConfig.dev.entryHtml),
|
||||
nodeModules: !isProduction
|
||||
? isMonoRepo ? resolveAppDir('../../node_modules') : resolveAppDir('./node_modules')
|
||||
? isMonoRepo
|
||||
? resolveAppDir('../../node_modules')
|
||||
: resolveAppDir('./node_modules')
|
||||
: false
|
||||
}),
|
||||
new CaseSensitivePathsWebpackPlugin(),
|
||||
@ -106,17 +122,23 @@ function makeConfig() {
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new FriendlyErrorsWebpackPlugin({
|
||||
compilationSuccessInfo: {
|
||||
messages: [`FrappeJS server started at http://${appConfig.dev.devServerHost}:${appConfig.dev.devServerPort}`],
|
||||
},
|
||||
messages: [
|
||||
`FrappeJS server started at http://${
|
||||
appConfig.dev.devServerHost
|
||||
}:${appConfig.dev.devServerPort}`
|
||||
]
|
||||
}
|
||||
}),
|
||||
new webpack.ProgressPlugin(),
|
||||
isProduction ? new CopyWebpackPlugin([
|
||||
{
|
||||
from: resolveAppDir(appConfig.staticPath),
|
||||
to: resolveAppDir('./dist/electron/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
]) : null,
|
||||
isProduction
|
||||
? new CopyWebpackPlugin([
|
||||
{
|
||||
from: resolveAppDir(appConfig.staticPath),
|
||||
to: resolveAppDir('./dist/electron/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
])
|
||||
: null
|
||||
// isProduction ? new BabiliWebpackPlugin() : null,
|
||||
// isProduction ? new webpack.LoaderOptionsPlugin({ minimize: true }) : null,
|
||||
].filter(Boolean),
|
||||
@ -143,10 +165,10 @@ function makeConfig() {
|
||||
tls: 'empty',
|
||||
child_process: 'empty'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
};
|
||||
|
||||
getElectronMainConfig = function getElectronMainConfig() {
|
||||
return {
|
||||
@ -179,16 +201,17 @@ function makeConfig() {
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// isProduction && new BabiliWebpackPlugin(),
|
||||
isProduction && new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
})
|
||||
isProduction &&
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
})
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: ['.js', '.json', '.node']
|
||||
},
|
||||
target: 'electron-main'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
makeConfig();
|
||||
|
Loading…
x
Reference in New Issue
Block a user