diff --git a/client/desk/index.js b/client/desk/index.js
index 74287dfa..44d2546b 100644
--- a/client/desk/index.js
+++ b/client/desk/index.js
@@ -51,7 +51,9 @@ module.exports = class Desk {
frappe.router.add('new/:doctype', async (params) => {
let doc = await frappe.get_new_doc(params.doctype);
- frappe.router.set_route('edit', doc.doctype, doc.name);
+ // unset the name, its local
+ await frappe.router.set_route('edit', doc.doctype, doc.name);
+ await doc.set('name', '');
});
}
diff --git a/client/view/controls/base.js b/client/view/controls/base.js
index cc3fd30d..1a4578b0 100644
--- a/client/view/controls/base.js
+++ b/client/view/controls/base.js
@@ -15,11 +15,6 @@ class BaseControl {
bind(doc) {
this.doc = doc;
-
- this.doc.add_handler(this.fieldname, () => {
- this.set_doc_value();
- });
-
this.set_doc_value();
}
@@ -56,6 +51,7 @@ class BaseControl {
make_input() {
this.input = frappe.ui.add('input', 'form-control', this.form_group);
+ this.input.setAttribute('autocomplete', 'off');
}
set_input_name() {
@@ -70,16 +66,24 @@ class BaseControl {
}
set_input_value(value) {
+ this.input.value = this.format(value);
+ }
+
+ format(value) {
if (value === undefined || value === null) {
value = '';
}
- this.input.value = value;
+ return value;
}
- async get_input_value() {
+ async get_parsed_value() {
return await this.parse(this.input.value);
}
+ get_input_value() {
+ return this.input.value;
+ }
+
async parse(value) {
return value;
}
@@ -93,9 +97,11 @@ class BaseControl {
}
async handle_change(e) {
- let value = await this.get_input_value();
+ let value = await this.parse(this.get_input_value());
value = await this.validate(value);
- await this.doc.set(this.fieldname, value);
+ if (this.doc[this.fieldname] !== value) {
+ await this.doc.set(this.fieldname, value);
+ }
}
disable() {
diff --git a/client/view/controls/currency.js b/client/view/controls/currency.js
new file mode 100644
index 00000000..1b1cfe9f
--- /dev/null
+++ b/client/view/controls/currency.js
@@ -0,0 +1,13 @@
+const FloatControl = require('./float');
+const frappe = require('frappejs');
+
+class CurrencyControl extends FloatControl {
+ parse(value) {
+ return frappe.parse_number(value);
+ }
+ format(value) {
+ return frappe.format_number(value);
+ }
+};
+
+module.exports = CurrencyControl;
\ No newline at end of file
diff --git a/client/view/controls/float.js b/client/view/controls/float.js
new file mode 100644
index 00000000..b6943299
--- /dev/null
+++ b/client/view/controls/float.js
@@ -0,0 +1,11 @@
+const BaseControl = require('./base');
+
+class FloatControl extends BaseControl {
+ make() {
+ super.make();
+ this.input.setAttribute('type', 'text');
+ this.input.classList.add('text-right');
+ }
+};
+
+module.exports = FloatControl;
\ No newline at end of file
diff --git a/client/view/controls/index.js b/client/view/controls/index.js
index fe4b4f97..88ea9d01 100644
--- a/client/view/controls/index.js
+++ b/client/view/controls/index.js
@@ -2,10 +2,11 @@ const control_classes = {
Data: require('./data'),
Text: require('./text'),
Select: require('./select'),
- Link: require('./link')
+ Link: require('./link'),
+ Float: require('./float'),
+ Currency: require('./currency')
}
-
module.exports = {
get_control_class(fieldtype) {
return control_classes[fieldtype];
diff --git a/client/view/form.js b/client/view/form.js
index 21179c36..23f40a64 100644
--- a/client/view/form.js
+++ b/client/view/form.js
@@ -80,6 +80,14 @@ module.exports = class BaseForm {
for (let control of this.controls_list) {
control.bind(this.doc);
}
+
+ // refresh value in control
+ this.doc.add_handler('change', (params) => {
+ let control = this.controls[params.fieldname];
+ if (control && control.get_input_value() !== control.format(params.fieldname)) {
+ control.set_doc_value();
+ }
+ });
}
async submit() {
diff --git a/client/view/list.js b/client/view/list.js
index 9fef6bba..4b0b277e 100644
--- a/client/view/list.js
+++ b/client/view/list.js
@@ -119,7 +119,7 @@ module.exports = class BaseList {
}
get_row_html(data) {
- return `${data.name}`;
}
get_row(i) {
diff --git a/model/document.js b/model/document.js
index a35c7783..c5673201 100644
--- a/model/document.js
+++ b/model/document.js
@@ -22,13 +22,14 @@ module.exports = class BaseDocument {
this.handlers[key].push(method || key);
}
- get(key) {
- return this[key];
+ get(fieldname) {
+ return this[fieldname];
}
- set(key, value) {
- this.validate_field(key, value);
- this[key] = value;
+ // set value and trigger change
+ async set(fieldname, value) {
+ this[fieldname] = await this.validate_field(fieldname, value);
+ await this.trigger('change', {doc: this, fieldname: fieldname, value: value});
}
set_name() {
@@ -69,11 +70,12 @@ module.exports = class BaseDocument {
}
}
- validate_field (key, value) {
+ async validate_field (key, value) {
let df = this.meta.get_field(key);
if (df.fieldtype=='Select') {
- this.meta.validate_select(df, value);
+ return this.meta.validate_select(df, value);
}
+ return value;
}
get_valid_dict() {
@@ -110,9 +112,11 @@ module.exports = class BaseDocument {
this.set_name();
this.set_standard_values();
this.set_keywords();
- await this.trigger('validate', 'before_insert');
+ await this.trigger('validate');
+ await this.trigger('before_insert');
await frappe.db.insert(this.doctype, this.get_valid_dict());
- await this.trigger('after_insert', 'after_save');
+ await this.trigger('after_insert');
+ await this.trigger('after_save');
}
async delete() {
@@ -121,15 +125,13 @@ module.exports = class BaseDocument {
await this.trigger('after_delete');
}
- async trigger() {
- for(var key of arguments) {
- if (this.handlers[key]) {
- for (let method of this.handlers[key]) {
- if (typeof method === 'string') {
- await this[method]();
- } else {
- await method(this);
- }
+ async trigger(key, params) {
+ if (this.handlers[key]) {
+ for (let method of this.handlers[key]) {
+ if (typeof method === 'string') {
+ await this[method](params);
+ } else {
+ await method(params);
}
}
}
@@ -138,9 +140,11 @@ module.exports = class BaseDocument {
async update() {
this.set_standard_values();
this.set_keywords();
- await this.trigger('validate', 'before_update');
+ await this.trigger('validate');
+ await this.trigger('before_update');
await frappe.db.update(this.doctype, this.get_valid_dict());
- await this.trigger('after_update', 'after_save');
+ await this.trigger('after_update');
+ await this.trigger('after_save');
return this;
}
};
\ No newline at end of file
diff --git a/model/meta.js b/model/meta.js
index f81a9d5a..8f08a4d8 100644
--- a/model/meta.js
+++ b/model/meta.js
@@ -85,10 +85,10 @@ module.exports = class BaseMeta extends BaseDocument {
if (!options.includes(value)) {
throw new frappe.errors.ValueError(`${value} must be one of ${options.join(", ")}`);
}
+ return value;
}
async trigger(key, event = {}) {
-
Object.assign(event, {
doc: this,
name: key
diff --git a/models/doctype/todo/todo_client.js b/models/doctype/todo/todo_client.js
index ba1c56f5..258d6a29 100644
--- a/models/doctype/todo/todo_client.js
+++ b/models/doctype/todo/todo_client.js
@@ -6,7 +6,8 @@ class ToDoList extends BaseList {
return ['name', 'subject', 'status'];
}
get_row_html(data) {
- return `${data.subject}`;
+ let symbol = data.status=="Closed" ? "✔" : "";
+ return `${symbol} ${data.subject}`;
}
}
diff --git a/tests/test_utils.js b/tests/test_utils.js
new file mode 100644
index 00000000..39c30ec7
--- /dev/null
+++ b/tests/test_utils.js
@@ -0,0 +1,35 @@
+const utils = require('frappejs/utils');
+const assert = require('assert');
+
+describe('Number Formatting', () => {
+ it('should format numbers', () => {
+ assert.equal(utils.format_number(100), '100.00');
+ assert.equal(utils.format_number(1000), '1,000.00');
+ assert.equal(utils.format_number(10000), '10,000.00');
+ assert.equal(utils.format_number(100000), '100,000.00');
+ assert.equal(utils.format_number(1000000), '1,000,000.00');
+ assert.equal(utils.format_number(100.1234), '100.12');
+ assert.equal(utils.format_number(1000.1234), '1,000.12');
+ });
+
+ it('should parse numbers', () => {
+ assert.equal(utils.parse_number('100.00'), 100);
+ assert.equal(utils.parse_number('1,000.00'), 1000);
+ assert.equal(utils.parse_number('10,000.00'), 10000);
+ assert.equal(utils.parse_number('100,000.00'), 100000);
+ assert.equal(utils.parse_number('1,000,000.00'), 1000000);
+ assert.equal(utils.parse_number('100.1234'), 100.1234);
+ assert.equal(utils.parse_number('1,000.1234'), 1000.1234);
+ });
+
+ it('should format lakhs and crores', () => {
+ assert.equal(utils.format_number(100, '#,##,###.##'), '100.00');
+ assert.equal(utils.format_number(1000, '#,##,###.##'), '1,000.00');
+ assert.equal(utils.format_number(10000, '#,##,###.##'), '10,000.00');
+ assert.equal(utils.format_number(100000, '#,##,###.##'), '1,00,000.00');
+ assert.equal(utils.format_number(1000000, '#,##,###.##'), '10,00,000.00');
+ assert.equal(utils.format_number(10000000, '#,##,###.##'), '1,00,00,000.00');
+ assert.equal(utils.format_number(100.1234, '#,##,###.##'), '100.12');
+ assert.equal(utils.format_number(1000.1234, '#,##,###.##'), '1,000.12');
+ });
+});
\ No newline at end of file
diff --git a/utils/index.js b/utils/index.js
index 803c4abb..d49da504 100644
--- a/utils/index.js
+++ b/utils/index.js
@@ -1,4 +1,8 @@
-module.exports = {
+let utils = {};
+
+Object.assign(utils, require('./number_format'));
+
+Object.assign(utils, {
slug(text) {
return text.toLowerCase().replace(/ /g, '_');
},
@@ -17,4 +21,6 @@ module.exports = {
setTimeout(resolve, seconds * 1000);
});
}
-}
+});
+
+module.exports = utils;
\ No newline at end of file
diff --git a/utils/number_format.js b/utils/number_format.js
new file mode 100644
index 00000000..746bd60f
--- /dev/null
+++ b/utils/number_format.js
@@ -0,0 +1,104 @@
+const number_formats = {
+ "#,###.##": { fraction_sep: ".", group_sep: ",", precision: 2 },
+ "#.###,##": { fraction_sep: ",", group_sep: ".", precision: 2 },
+ "# ###.##": { fraction_sep: ".", group_sep: " ", precision: 2 },
+ "# ###,##": { fraction_sep: ",", group_sep: " ", precision: 2 },
+ "#'###.##": { fraction_sep: ".", group_sep: "'", precision: 2 },
+ "#, ###.##": { fraction_sep: ".", group_sep: ", ", precision: 2 },
+ "#,##,###.##": { fraction_sep: ".", group_sep: ",", precision: 2 },
+ "#,###.###": { fraction_sep: ".", group_sep: ",", precision: 3 },
+ "#.###": { fraction_sep: "", group_sep: ".", precision: 0 },
+ "#,###": { fraction_sep: "", group_sep: ",", precision: 0 },
+}
+
+module.exports = {
+ // parse a formatted number string
+ // from "4,555,000.34" -> 4555000.34
+ parse_number(number, format='#,###.##') {
+ if (!number) {
+ return 0;
+ }
+ if (typeof number === 'number') {
+ return number;
+ }
+ const info = this.get_format_info(format);
+ return parseFloat(this.remove_separator(number, info.group_sep));
+ },
+
+ format_number(number, format = '#,###.##', precision = null) {
+ if (!number) {
+ number = 0;
+ }
+ let info = this.get_format_info(format);
+ if (precision) {
+ info.precision = precision;
+ }
+ let is_negative = false;
+
+ number = this.parse_number(number);
+ if (number < 0) {
+ is_negative = true;
+ }
+ number = Math.abs(number);
+ number = number.toFixed(info.precision);
+
+ var parts = number.split('.');
+
+ // get group position and parts
+ var group_position = info.group_sep ? 3 : 0;
+
+ if (group_position) {
+ var integer = parts[0];
+ var str = '';
+ var offset = integer.length % group_position;
+ for (var i = integer.length; i >= 0; i--) {
+ var l = this.remove_separator(str, info.group_sep).length;
+ if (format == "#,##,###.##" && str.indexOf(",") != -1) { // INR
+ group_position = 2;
+ l += 1;
+ }
+
+ str += integer.charAt(i);
+
+ if (l && !((l + 1) % group_position) && i != 0) {
+ str += info.group_sep;
+ }
+ }
+ parts[0] = str.split("").reverse().join("");
+ }
+ if (parts[0] + "" == "") {
+ parts[0] = "0";
+ }
+
+ // join decimal
+ parts[1] = (parts[1] && info.fraction_sep) ? (info.fraction_sep + parts[1]) : "";
+
+ // join
+ return (is_negative ? "-" : "") + parts[0] + parts[1];
+ },
+
+ get_format_info(format) {
+ let format_info = number_formats[format];
+
+ if (!format_info) {
+ throw `Unknown number format "${format}"`;
+ }
+
+ return format_info;
+ },
+
+ round(num, precision) {
+ var is_negative = num < 0 ? true : false;
+ 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));
+ r = d ? r / m : r;
+ return is_negative ? -r : r;
+ },
+
+ remove_separator(text, sep) {
+ return text.replace(new RegExp(sep === "." ? "\\." : sep, "g"), '');
+ }
+};