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"), ''); + } +};