mirror of
https://github.com/frappe/books.git
synced 2025-02-11 08:28:47 +00:00
Merge pull request #153 from 18alantom/make-money
refactor: use dedicated class to handle currency
This commit is contained in:
commit
3f1b4605a2
@ -13,9 +13,10 @@ module.exports = class Database extends Observable {
|
||||
|
||||
connect() {
|
||||
this.knex = Knex(this.connectionParams);
|
||||
this.knex.on('query-error', error => {
|
||||
this.knex.on('query-error', (error) => {
|
||||
error.type = this.getError(error);
|
||||
});
|
||||
this.executePostDbConnect();
|
||||
}
|
||||
|
||||
close() {
|
||||
@ -41,15 +42,25 @@ module.exports = class Database extends Observable {
|
||||
|
||||
async initializeSingles() {
|
||||
let singleDoctypes = frappe
|
||||
.getModels(model => model.isSingle)
|
||||
.map(model => model.name);
|
||||
.getModels((model) => model.isSingle)
|
||||
.map((model) => model.name);
|
||||
|
||||
for (let doctype of singleDoctypes) {
|
||||
if (await this.singleExists(doctype)) {
|
||||
const singleValues = await this.getSingleFieldsToInsert(doctype);
|
||||
singleValues.forEach(({ fieldname, value }) => {
|
||||
let singleValue = frappe.newDoc({
|
||||
doctype: 'SingleValue',
|
||||
parent: doctype,
|
||||
fieldname,
|
||||
value,
|
||||
});
|
||||
singleValue.insert();
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let meta = frappe.getMeta(doctype);
|
||||
if (meta.fields.every(df => df.default == null)) {
|
||||
if (meta.fields.every((df) => df.default == null)) {
|
||||
continue;
|
||||
}
|
||||
let defaultValues = meta.fields.reduce((doc, df) => {
|
||||
@ -70,6 +81,26 @@ module.exports = class Database extends Observable {
|
||||
return res.count > 0;
|
||||
}
|
||||
|
||||
async getSingleFieldsToInsert(doctype) {
|
||||
const existingFields = (
|
||||
await frappe.db
|
||||
.knex('SingleValue')
|
||||
.where({ parent: doctype })
|
||||
.select('fieldname')
|
||||
).map(({ fieldname }) => fieldname);
|
||||
|
||||
return frappe
|
||||
.getMeta(doctype)
|
||||
.fields.map(({ fieldname, default: value }) => ({
|
||||
fieldname,
|
||||
value,
|
||||
}))
|
||||
.filter(
|
||||
({ fieldname, value }) =>
|
||||
!existingFields.includes(fieldname) && value !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
tableExists(table) {
|
||||
return this.knex.schema.hasTable(table);
|
||||
}
|
||||
@ -80,7 +111,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
runCreateTableQuery(doctype, fields) {
|
||||
return this.knex.schema.createTable(doctype, table => {
|
||||
return this.knex.schema.createTable(doctype, (table) => {
|
||||
for (let field of fields) {
|
||||
this.buildColumnForTable(table, field);
|
||||
}
|
||||
@ -93,7 +124,7 @@ module.exports = class Database extends Observable {
|
||||
let newForeignKeys = await this.getNewForeignKeys(doctype);
|
||||
|
||||
return this.knex.schema
|
||||
.table(doctype, table => {
|
||||
.table(doctype, (table) => {
|
||||
if (diff.added.length) {
|
||||
for (let field of diff.added) {
|
||||
this.buildColumnForTable(table, field);
|
||||
@ -132,7 +163,10 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
// required
|
||||
if (!!field.required && !(field.required instanceof Function)) {
|
||||
if (
|
||||
(!!field.required && !(field.required instanceof Function)) ||
|
||||
field.fieldtype === 'Currency'
|
||||
) {
|
||||
column.notNullable();
|
||||
}
|
||||
|
||||
@ -162,7 +196,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
}
|
||||
|
||||
const validFieldNames = validFields.map(field => field.fieldname);
|
||||
const validFieldNames = validFields.map((field) => field.fieldname);
|
||||
for (let column of tableColumns) {
|
||||
if (!validFieldNames.includes(column)) {
|
||||
diff.removed.push(column);
|
||||
@ -235,7 +269,7 @@ module.exports = class Database extends Observable {
|
||||
fields: ['*'],
|
||||
filters: { parent: doc.name },
|
||||
orderBy: 'idx',
|
||||
order: 'asc'
|
||||
order: 'asc',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -246,7 +280,7 @@ module.exports = class Database extends Observable {
|
||||
fields: ['fieldname', 'value'],
|
||||
filters: { parent: doctype },
|
||||
orderBy: 'fieldname',
|
||||
order: 'asc'
|
||||
order: 'asc',
|
||||
});
|
||||
let doc = {};
|
||||
for (let row of values) {
|
||||
@ -255,15 +289,90 @@ module.exports = class Database extends Observable {
|
||||
return doc;
|
||||
}
|
||||
|
||||
getOne(doctype, name, fields = '*') {
|
||||
/**
|
||||
* Get list of values from the singles table.
|
||||
* @param {...string | Object} fieldnames list of fieldnames to get the values of
|
||||
* @returns {Array<Object>} array of {parent, value, fieldname}.
|
||||
* @example
|
||||
* Database.getSingleValues('internalPrecision');
|
||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
||||
* @example
|
||||
* Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'});
|
||||
* // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }]
|
||||
*/
|
||||
async getSingleValues(...fieldnames) {
|
||||
fieldnames = fieldnames.map((fieldname) => {
|
||||
if (typeof fieldname === 'string') {
|
||||
return { fieldname };
|
||||
}
|
||||
return fieldname;
|
||||
});
|
||||
|
||||
let builder = frappe.db.knex('SingleValue');
|
||||
builder = builder.where(fieldnames[0]);
|
||||
|
||||
fieldnames.slice(1).forEach(({ fieldname, parent }) => {
|
||||
if (typeof parent === 'undefined') {
|
||||
builder = builder.orWhere({ fieldname });
|
||||
} else {
|
||||
builder = builder.orWhere({ fieldname, parent });
|
||||
}
|
||||
});
|
||||
|
||||
const values = await builder.select('fieldname', 'value', 'parent');
|
||||
|
||||
return values.map((value) => {
|
||||
const fields = frappe.getMeta(value.parent).fields;
|
||||
return this.getDocFormattedDoc(fields, values);
|
||||
});
|
||||
}
|
||||
|
||||
async getOne(doctype, name, fields = '*') {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
|
||||
return this.knex
|
||||
const doc = await this.knex
|
||||
.select(fields)
|
||||
.from(baseDoctype)
|
||||
.where('name', name)
|
||||
.first();
|
||||
|
||||
if (!doc) {
|
||||
return doc;
|
||||
}
|
||||
|
||||
return this.getDocFormattedDoc(meta.fields, doc);
|
||||
}
|
||||
|
||||
getDocFormattedDoc(fields, doc) {
|
||||
// format for usage, not going into the db
|
||||
const docFields = Object.keys(doc);
|
||||
const filteredFields = fields.filter(({ fieldname }) =>
|
||||
docFields.includes(fieldname)
|
||||
);
|
||||
|
||||
const formattedValues = filteredFields.reduce((d, field) => {
|
||||
const { fieldname } = field;
|
||||
d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]);
|
||||
return d;
|
||||
}, {});
|
||||
|
||||
return Object.assign(doc, formattedValues);
|
||||
}
|
||||
|
||||
getDocFormattedValues(field, value) {
|
||||
// format for usage, not going into the db
|
||||
try {
|
||||
if (field.fieldtype === 'Currency') {
|
||||
return frappe.pesa(value);
|
||||
}
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${
|
||||
field.fieldname
|
||||
}', label: '${field.label}'`;
|
||||
throw err;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
triggerChange(doctype, name) {
|
||||
@ -358,8 +467,8 @@ module.exports = class Database extends Observable {
|
||||
|
||||
updateOne(doctype, doc) {
|
||||
let validFields = this.getValidFields(doctype);
|
||||
let fieldsToUpdate = Object.keys(doc).filter(f => f !== 'name');
|
||||
let fields = validFields.filter(df =>
|
||||
let fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name');
|
||||
let fields = validFields.filter((df) =>
|
||||
fieldsToUpdate.includes(df.fieldname)
|
||||
);
|
||||
let formattedDoc = this.getFormattedDoc(fields, doc);
|
||||
@ -396,7 +505,7 @@ module.exports = class Database extends Observable {
|
||||
doctype: 'SingleValue',
|
||||
parent: doctype,
|
||||
fieldname: field.fieldname,
|
||||
value: value
|
||||
value: value,
|
||||
});
|
||||
await singleValue.insert();
|
||||
}
|
||||
@ -404,9 +513,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
deleteSingleValues(name) {
|
||||
return this.knex('SingleValue')
|
||||
.where('parent', name)
|
||||
.delete();
|
||||
return this.knex('SingleValue').where('parent', name).delete();
|
||||
}
|
||||
|
||||
async rename(doctype, oldName, newName) {
|
||||
@ -438,8 +545,9 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
getFormattedDoc(fields, doc) {
|
||||
// format for storage, going into the db
|
||||
let formattedDoc = {};
|
||||
fields.map(field => {
|
||||
fields.map((field) => {
|
||||
let value = doc[field.fieldname];
|
||||
formattedDoc[field.fieldname] = this.getFormattedValue(field, value);
|
||||
});
|
||||
@ -447,6 +555,25 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
getFormattedValue(field, value) {
|
||||
// format for storage, going into the db
|
||||
const type = typeof value;
|
||||
if (field.fieldtype === 'Currency') {
|
||||
let currency = value;
|
||||
|
||||
if (type === 'number' || type === 'string') {
|
||||
currency = frappe.pesa(value);
|
||||
}
|
||||
|
||||
const currencyValue = currency.store;
|
||||
if (typeof currencyValue !== 'string') {
|
||||
throw new Error(
|
||||
`invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'`
|
||||
);
|
||||
}
|
||||
|
||||
return currencyValue;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
if (field.fieldtype === 'Date') {
|
||||
// date
|
||||
@ -507,9 +634,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
|
||||
deleteChildren(parenttype, parent) {
|
||||
return this.knex(parenttype)
|
||||
.where('parent', parent)
|
||||
.delete();
|
||||
return this.knex(parenttype).where('parent', parent).delete();
|
||||
}
|
||||
|
||||
async exists(doctype, name) {
|
||||
@ -533,14 +658,14 @@ module.exports = class Database extends Observable {
|
||||
start: 0,
|
||||
limit: 1,
|
||||
orderBy: 'name',
|
||||
order: 'asc'
|
||||
order: 'asc',
|
||||
});
|
||||
return row.length ? row[0][fieldname] : null;
|
||||
}
|
||||
|
||||
async setValue(doctype, name, fieldname, value) {
|
||||
return await this.setValues(doctype, name, {
|
||||
[fieldname]: value
|
||||
[fieldname]: value,
|
||||
});
|
||||
}
|
||||
|
||||
@ -557,7 +682,7 @@ module.exports = class Database extends Observable {
|
||||
return value;
|
||||
}
|
||||
|
||||
getAll({
|
||||
async getAll({
|
||||
doctype,
|
||||
fields,
|
||||
filters,
|
||||
@ -565,7 +690,7 @@ module.exports = class Database extends Observable {
|
||||
limit,
|
||||
groupBy,
|
||||
orderBy = 'creation',
|
||||
order = 'desc'
|
||||
order = 'desc',
|
||||
} = {}) {
|
||||
let meta = frappe.getMeta(doctype);
|
||||
let baseDoctype = meta.getBaseDocType();
|
||||
@ -600,7 +725,8 @@ module.exports = class Database extends Observable {
|
||||
builder.limit(limit);
|
||||
}
|
||||
|
||||
return builder;
|
||||
const docs = await builder;
|
||||
return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc));
|
||||
}
|
||||
|
||||
applyFiltersToBuilder(builder, filters) {
|
||||
@ -643,7 +769,7 @@ module.exports = class Database extends Observable {
|
||||
}
|
||||
}
|
||||
|
||||
filtersArray.map(filter => {
|
||||
filtersArray.map((filter) => {
|
||||
const [field, operator, comparisonValue] = filter;
|
||||
if (operator === '=') {
|
||||
builder.where(field, comparisonValue);
|
||||
@ -689,4 +815,8 @@ module.exports = class Database extends Observable {
|
||||
initTypeMap() {
|
||||
this.typeMap = {};
|
||||
}
|
||||
|
||||
executePostDbConnect() {
|
||||
frappe.initializeMoneyMaker();
|
||||
}
|
||||
};
|
||||
|
@ -61,7 +61,7 @@ class SqliteDatabase extends Database {
|
||||
// prettier-ignore
|
||||
this.typeMap = {
|
||||
'AutoComplete': 'text',
|
||||
'Currency': 'float',
|
||||
'Currency': 'text',
|
||||
'Int': 'integer',
|
||||
'Float': 'float',
|
||||
'Percent': 'float',
|
||||
@ -107,6 +107,7 @@ class SqliteDatabase extends Database {
|
||||
async prestigeTheTable(tableName, tableRows) {
|
||||
// Alter table hacx for sqlite in case of schema change.
|
||||
const tempName = `__${tableName}`;
|
||||
await this.knex.schema.dropTableIfExists(tempName);
|
||||
await this.knex.raw('PRAGMA foreign_keys=OFF');
|
||||
await this.createTable(tableName, tempName);
|
||||
await this.knex.batchInsert(tempName, tableRows);
|
||||
|
@ -1,17 +1,15 @@
|
||||
const utils = require('../utils');
|
||||
const numberFormat = require('../utils/numberFormat');
|
||||
const format = require('../utils/format');
|
||||
const errors = require('./errors');
|
||||
const BaseDocument = require('frappejs/model/document');
|
||||
const BaseMeta = require('frappejs/model/meta');
|
||||
|
||||
module.exports = {
|
||||
initLibs(frappe) {
|
||||
Object.assign(frappe, utils);
|
||||
Object.assign(frappe, numberFormat);
|
||||
Object.assign(frappe, format);
|
||||
frappe.errors = errors;
|
||||
frappe.BaseDocument = BaseDocument;
|
||||
frappe.BaseMeta = BaseMeta;
|
||||
}
|
||||
}
|
||||
initLibs(frappe) {
|
||||
Object.assign(frappe, utils);
|
||||
Object.assign(frappe, format);
|
||||
frappe.errors = errors;
|
||||
frappe.BaseDocument = BaseDocument;
|
||||
frappe.BaseMeta = BaseMeta;
|
||||
},
|
||||
};
|
||||
|
46
index.js
46
index.js
@ -1,5 +1,10 @@
|
||||
const Observable = require('./utils/observable');
|
||||
const utils = require('./utils');
|
||||
const { getMoneyMaker } = require('pesa');
|
||||
const {
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
} = require('./utils/consts');
|
||||
|
||||
module.exports = {
|
||||
initializeAndRegister(customModels = {}, force = false) {
|
||||
@ -11,6 +16,47 @@ module.exports = {
|
||||
this.registerModels(customModels);
|
||||
},
|
||||
|
||||
async initializeMoneyMaker(currency) {
|
||||
currency ??= 'XXX';
|
||||
|
||||
// to be called after db initialization
|
||||
const values =
|
||||
(await frappe.db?.getSingleValues(
|
||||
{
|
||||
fieldname: 'internalPrecision',
|
||||
parent: 'SystemSettings',
|
||||
},
|
||||
{
|
||||
fieldname: 'displayPrecision',
|
||||
parent: 'SystemSettings',
|
||||
}
|
||||
)) ?? [];
|
||||
|
||||
let { internalPrecision: precision, displayPrecision: display } =
|
||||
values.reduce((acc, { fieldname, value }) => {
|
||||
acc[fieldname] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (typeof precision === 'undefined') {
|
||||
precision = DEFAULT_INTERNAL_PRECISION;
|
||||
}
|
||||
|
||||
if (typeof precision === 'string') {
|
||||
precision = parseInt(precision);
|
||||
}
|
||||
|
||||
if (typeof display === 'undefined') {
|
||||
display = DEFAULT_DISPLAY_PRECISION;
|
||||
}
|
||||
|
||||
if (typeof display === 'string') {
|
||||
display = parseInt(display);
|
||||
}
|
||||
|
||||
this.pesa = getMoneyMaker({ currency, precision, display });
|
||||
},
|
||||
|
||||
init(force) {
|
||||
if (this._initialized && !force) return;
|
||||
this.initConfig();
|
||||
|
@ -1,7 +1,8 @@
|
||||
const frappe = require('frappejs');
|
||||
const Observable = require('frappejs/utils/observable');
|
||||
const naming = require('./naming');
|
||||
const { round } = require('frappejs/utils/numberFormat');
|
||||
const { isPesa } = require('../utils/index');
|
||||
const { DEFAULT_INTERNAL_PRECISION } = require('../utils/consts');
|
||||
|
||||
module.exports = class BaseDocument extends Observable {
|
||||
constructor(data) {
|
||||
@ -106,17 +107,16 @@ module.exports = class BaseDocument extends Observable {
|
||||
setDefaults() {
|
||||
for (let field of this.meta.fields) {
|
||||
if (this[field.fieldname] == null) {
|
||||
let defaultValue = null;
|
||||
let defaultValue = getPreDefaultValues(field.fieldtype);
|
||||
|
||||
if (field.fieldtype === 'Table') {
|
||||
defaultValue = [];
|
||||
if (typeof field.default === 'function') {
|
||||
defaultValue = field.default(this);
|
||||
} else if (field.default !== undefined) {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
if (field.default) {
|
||||
if (typeof field.default === 'function') {
|
||||
defaultValue = field.default(this);
|
||||
} else {
|
||||
defaultValue = field.default;
|
||||
}
|
||||
|
||||
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
|
||||
defaultValue = frappe.pesa(defaultValue);
|
||||
}
|
||||
|
||||
this[field.fieldname] = defaultValue;
|
||||
@ -136,8 +136,10 @@ module.exports = class BaseDocument extends Observable {
|
||||
}
|
||||
if (['Int', 'Check'].includes(field.fieldtype)) {
|
||||
value = parseInt(value, 10);
|
||||
} else if (['Float', 'Currency'].includes(field.fieldtype)) {
|
||||
} else if (field.fieldtype === 'Float') {
|
||||
value = parseFloat(value);
|
||||
} else if (field.fieldtype === 'Currency' && !isPesa(value)) {
|
||||
value = frappe.pesa(value);
|
||||
}
|
||||
this[field.fieldname] = value;
|
||||
}
|
||||
@ -169,23 +171,25 @@ module.exports = class BaseDocument extends Observable {
|
||||
_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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
const childDoc = new BaseDocument(data);
|
||||
childDoc.setDefaults();
|
||||
return childDoc;
|
||||
}
|
||||
|
||||
validateInsert() {
|
||||
@ -508,7 +512,7 @@ module.exports = class BaseDocument extends Observable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['Float', 'Currency'].includes(field.fieldtype)) {
|
||||
if ('Float' === field.fieldtype) {
|
||||
value = this.round(value, field);
|
||||
}
|
||||
|
||||
@ -526,7 +530,7 @@ module.exports = class BaseDocument extends Observable {
|
||||
roundFloats() {
|
||||
let fields = this.meta
|
||||
.getValidFields()
|
||||
.filter((df) => ['Float', 'Currency', 'Table'].includes(df.fieldtype));
|
||||
.filter((df) => ['Float', 'Table'].includes(df.fieldtype));
|
||||
|
||||
for (let df of fields) {
|
||||
let value = this[df.fieldname];
|
||||
@ -658,10 +662,26 @@ module.exports = class BaseDocument extends Observable {
|
||||
}
|
||||
|
||||
// helper functions
|
||||
getSum(tablefield, childfield) {
|
||||
return (this[tablefield] || [])
|
||||
.map((d) => parseFloat(d[childfield], 10) || 0)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
getSum(tablefield, childfield, convertToFloat = true) {
|
||||
const sum = (this[tablefield] || [])
|
||||
.map((d) => {
|
||||
const value = d[childfield] ?? 0;
|
||||
if (!isPesa(value)) {
|
||||
try {
|
||||
return frappe.pesa(value);
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
.reduce((a, b) => a.add(b), frappe.pesa(0));
|
||||
|
||||
if (convertToFloat) {
|
||||
return sum.float;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
getFrom(doctype, name, fieldname) {
|
||||
@ -673,11 +693,9 @@ module.exports = class BaseDocument extends Observable {
|
||||
if (typeof df === 'string') {
|
||||
df = this.meta.getField(df);
|
||||
}
|
||||
let systemPrecision = frappe.SystemSettings.floatPrecision;
|
||||
let defaultPrecision = systemPrecision != null ? systemPrecision : 2;
|
||||
let precision =
|
||||
df && df.precision != null ? df.precision : defaultPrecision;
|
||||
return round(value, precision);
|
||||
const precision =
|
||||
frappe.SystemSettings.internalPrecision ?? DEFAULT_INTERNAL_PRECISION;
|
||||
return frappe.pesa(value).clip(precision).float;
|
||||
}
|
||||
|
||||
isNew() {
|
||||
@ -691,3 +709,17 @@ module.exports = class BaseDocument extends Observable {
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
function getPreDefaultValues(fieldtype) {
|
||||
switch (fieldtype) {
|
||||
case 'Table':
|
||||
return [];
|
||||
case 'Currency':
|
||||
return frappe.pesa(0.0);
|
||||
case 'Int':
|
||||
case 'Float':
|
||||
return 0;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -48,13 +48,6 @@ module.exports = class BaseMeta extends BaseDocument {
|
||||
df.required = 1;
|
||||
}
|
||||
|
||||
// attach default precision to Float and Currency
|
||||
if (['Float', 'Currency'].includes(df.fieldtype)) {
|
||||
let defaultPrecision = frappe.SystemSettings
|
||||
? frappe.SystemSettings.floatPrecision
|
||||
: 2;
|
||||
df.precision = df.precision || defaultPrecision;
|
||||
}
|
||||
return df;
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
const { DateTime } = require('luxon');
|
||||
const { _ } = require('frappejs/utils');
|
||||
const {
|
||||
DEFAULT_DISPLAY_PRECISION,
|
||||
DEFAULT_INTERNAL_PRECISION,
|
||||
DEFAULT_LOCALE,
|
||||
} = require('../../../utils/consts');
|
||||
|
||||
let dateFormatOptions = (() => {
|
||||
let formats = [
|
||||
@ -37,20 +42,51 @@ module.exports = {
|
||||
options: dateFormatOptions,
|
||||
default: 'MMM d, y',
|
||||
required: 1,
|
||||
description: _('Sets the app-wide date display format.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'floatPrecision',
|
||||
label: 'Precision',
|
||||
fieldtype: 'Select',
|
||||
options: ['2', '3', '4', '5'],
|
||||
default: '2',
|
||||
fieldname: 'locale',
|
||||
label: 'Locale',
|
||||
fieldtype: 'Data',
|
||||
default: DEFAULT_LOCALE,
|
||||
description: _('Set the local code, this is used for number formatting.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'displayPrecision',
|
||||
label: 'Display Precision',
|
||||
fieldtype: 'Int',
|
||||
default: DEFAULT_DISPLAY_PRECISION,
|
||||
required: 1,
|
||||
minValue: 0,
|
||||
maxValue: 9,
|
||||
validate(value, doc) {
|
||||
if (value >= 0 && value <= 9) {
|
||||
return;
|
||||
}
|
||||
throw new frappe.errors.ValidationError(
|
||||
_('Display Precision should have a value between 0 and 9.')
|
||||
);
|
||||
},
|
||||
description: _('Sets how many digits are shown after the decimal point.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'internalPrecision',
|
||||
label: 'Internal Precision',
|
||||
fieldtype: 'Int',
|
||||
minValue: 0,
|
||||
default: DEFAULT_INTERNAL_PRECISION,
|
||||
description: _(
|
||||
'Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.'
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: 'hideGetStarted',
|
||||
label: 'Hide Get Started',
|
||||
fieldtype: 'Check',
|
||||
default: 0,
|
||||
description: _(
|
||||
'Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app.'
|
||||
),
|
||||
},
|
||||
{
|
||||
fieldname: 'autoUpdate',
|
||||
@ -58,13 +94,14 @@ module.exports = {
|
||||
fieldtype: 'Check',
|
||||
default: 1,
|
||||
description: _(
|
||||
'Automatically check for updates and download them if available. The update will be applied after you restart the app.'
|
||||
'Automatically checks for updates and download them if available. The update will be applied after you restart the app.'
|
||||
),
|
||||
},
|
||||
],
|
||||
quickEditFields: [
|
||||
'dateFormat',
|
||||
'floatPrecision',
|
||||
'locale',
|
||||
'displayPrecision',
|
||||
'hideGetStarted',
|
||||
'autoUpdate',
|
||||
],
|
||||
|
@ -32,6 +32,7 @@
|
||||
"multer": "^1.4.3",
|
||||
"node-fetch": "^3.0.0",
|
||||
"nunjucks": "^3.2.3",
|
||||
"pesa": "^1.1.3",
|
||||
"postcss": "^8.3.11",
|
||||
"postcss-loader": "^6.2.0",
|
||||
"sass-loader": "^12.3.0",
|
||||
|
@ -1,35 +0,0 @@
|
||||
const numberFormat = require('frappejs/utils/numberFormat');
|
||||
const assert = require('assert');
|
||||
|
||||
describe('Number Formatting', () => {
|
||||
it('should format numbers', () => {
|
||||
assert.equal(numberFormat.formatNumber(100), '100.00');
|
||||
assert.equal(numberFormat.formatNumber(1000), '1,000.00');
|
||||
assert.equal(numberFormat.formatNumber(10000), '10,000.00');
|
||||
assert.equal(numberFormat.formatNumber(100000), '100,000.00');
|
||||
assert.equal(numberFormat.formatNumber(1000000), '1,000,000.00');
|
||||
assert.equal(numberFormat.formatNumber(100.1234), '100.12');
|
||||
assert.equal(numberFormat.formatNumber(1000.1234), '1,000.12');
|
||||
});
|
||||
|
||||
it('should parse numbers', () => {
|
||||
assert.equal(numberFormat.parseNumber('100.00'), 100);
|
||||
assert.equal(numberFormat.parseNumber('1,000.00'), 1000);
|
||||
assert.equal(numberFormat.parseNumber('10,000.00'), 10000);
|
||||
assert.equal(numberFormat.parseNumber('100,000.00'), 100000);
|
||||
assert.equal(numberFormat.parseNumber('1,000,000.00'), 1000000);
|
||||
assert.equal(numberFormat.parseNumber('100.1234'), 100.1234);
|
||||
assert.equal(numberFormat.parseNumber('1,000.1234'), 1000.1234);
|
||||
});
|
||||
|
||||
it('should format lakhs and crores', () => {
|
||||
assert.equal(numberFormat.formatNumber(100, '#,##,###.##'), '100.00');
|
||||
assert.equal(numberFormat.formatNumber(1000, '#,##,###.##'), '1,000.00');
|
||||
assert.equal(numberFormat.formatNumber(10000, '#,##,###.##'), '10,000.00');
|
||||
assert.equal(numberFormat.formatNumber(100000, '#,##,###.##'), '1,00,000.00');
|
||||
assert.equal(numberFormat.formatNumber(1000000, '#,##,###.##'), '10,00,000.00');
|
||||
assert.equal(numberFormat.formatNumber(10000000, '#,##,###.##'), '1,00,00,000.00');
|
||||
assert.equal(numberFormat.formatNumber(100.1234, '#,##,###.##'), '100.12');
|
||||
assert.equal(numberFormat.formatNumber(1000.1234, '#,##,###.##'), '1,000.12');
|
||||
});
|
||||
});
|
3
utils/consts.js
Normal file
3
utils/consts.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const DEFAULT_INTERNAL_PRECISION = 11;
|
||||
export const DEFAULT_DISPLAY_PRECISION = 2;
|
||||
export const DEFAULT_LOCALE = 'en-IN';
|
@ -1,6 +1,6 @@
|
||||
const numberFormat = require('./numberFormat');
|
||||
const luxon = require('luxon');
|
||||
const frappe = require('frappejs');
|
||||
const { DEFAULT_DISPLAY_PRECISION, DEFAULT_LOCALE } = require('./consts');
|
||||
|
||||
module.exports = {
|
||||
format(value, df, doc) {
|
||||
@ -13,7 +13,8 @@ module.exports = {
|
||||
}
|
||||
|
||||
if (df.fieldtype === 'Currency') {
|
||||
value = formatCurrency(value, df, doc);
|
||||
const currency = getCurrency(df, doc);
|
||||
value = formatCurrency(value, currency);
|
||||
} else if (df.fieldtype === 'Date') {
|
||||
let dateFormat;
|
||||
if (!frappe.SystemSettings) {
|
||||
@ -46,23 +47,71 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
formatCurrency,
|
||||
formatNumber,
|
||||
};
|
||||
|
||||
function formatCurrency(value, df, doc) {
|
||||
let currency = df.currency || '';
|
||||
if (doc && df.getCurrency) {
|
||||
if (doc.meta && doc.meta.isChild) {
|
||||
currency = df.getCurrency(doc, doc.parentdoc);
|
||||
} else {
|
||||
currency = df.getCurrency(doc);
|
||||
}
|
||||
function formatCurrency(value, currency) {
|
||||
let valueString;
|
||||
try {
|
||||
valueString = formatNumber(value);
|
||||
} catch (err) {
|
||||
err.message += ` value: '${value}', type: ${typeof value}`;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!currency) {
|
||||
currency = frappe.AccountingSettings.currency;
|
||||
const currencySymbol = frappe.currencySymbols[currency];
|
||||
if (currencySymbol) {
|
||||
return currencySymbol + ' ' + valueString;
|
||||
}
|
||||
|
||||
let currencySymbol = frappe.currencySymbols[currency] || '';
|
||||
return currencySymbol + ' ' + numberFormat.formatNumber(value);
|
||||
return valueString;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const currencyFormatter = getNumberFormatter();
|
||||
if (typeof value === 'number') {
|
||||
return currencyFormatter.format(value);
|
||||
}
|
||||
|
||||
if (value.round) {
|
||||
return currencyFormatter.format(value.round());
|
||||
}
|
||||
|
||||
const formattedCurrency = currencyFormatter.format(value);
|
||||
if (formattedCurrency === 'NaN') {
|
||||
throw Error(
|
||||
`invalid value passed to formatCurrency: '${value}' of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
return formattedCurrency;
|
||||
}
|
||||
|
||||
function getNumberFormatter() {
|
||||
if (frappe.currencyFormatter) {
|
||||
return frappe.currencyFormatter;
|
||||
}
|
||||
|
||||
const locale = frappe.SystemSettings.locale ?? DEFAULT_LOCALE;
|
||||
const display =
|
||||
frappe.SystemSettings.displayPrecision ?? DEFAULT_DISPLAY_PRECISION;
|
||||
|
||||
return (frappe.currencyFormatter = Intl.NumberFormat(locale, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: display,
|
||||
}));
|
||||
}
|
||||
|
||||
function getCurrency(df, doc) {
|
||||
if (!(doc && df.getCurrency)) {
|
||||
return df.currency || frappe.AccountingSettings.currency || '';
|
||||
}
|
||||
|
||||
if (doc.meta && doc.meta.isChild) {
|
||||
return df.getCurrency(doc, doc.parentdoc);
|
||||
}
|
||||
|
||||
return df.getCurrency(doc);
|
||||
}
|
||||
|
135
utils/index.js
135
utils/index.js
@ -1,68 +1,76 @@
|
||||
Array.prototype.equals = function( array ) {
|
||||
return this.length == array.length &&
|
||||
this.every( function(item,i) { return item == array[i] } );
|
||||
}
|
||||
const { pesa } = require('pesa');
|
||||
|
||||
Array.prototype.equals = function (array) {
|
||||
return (
|
||||
this.length == array.length &&
|
||||
this.every(function (item, i) {
|
||||
return item == array[i];
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function slug(str) {
|
||||
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) {
|
||||
return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
|
||||
}).replace(/\s+/g, '');
|
||||
return str
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
|
||||
return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
function getRandomString() {
|
||||
return Math.random().toString(36).substr(3);
|
||||
return Math.random().toString(36).substr(3);
|
||||
}
|
||||
|
||||
async function sleep(seconds) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, seconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
function _(text, args) {
|
||||
// should return translated text
|
||||
return stringReplace(text, args);
|
||||
// should return translated text
|
||||
return stringReplace(text, args);
|
||||
}
|
||||
|
||||
function stringReplace(str, args) {
|
||||
if (!Array.isArray(args)) {
|
||||
args = [args];
|
||||
if (!Array.isArray(args)) {
|
||||
args = [args];
|
||||
}
|
||||
|
||||
if (str == undefined) return str;
|
||||
|
||||
let unkeyed_index = 0;
|
||||
return str.replace(/\{(\w*)\}/g, (match, key) => {
|
||||
if (key === '') {
|
||||
key = unkeyed_index;
|
||||
unkeyed_index++;
|
||||
}
|
||||
|
||||
if(str==undefined) return str;
|
||||
|
||||
let unkeyed_index = 0;
|
||||
return str.replace(/\{(\w*)\}/g, (match, key) => {
|
||||
if (key === '') {
|
||||
key = unkeyed_index;
|
||||
unkeyed_index++
|
||||
}
|
||||
if (key == +key) {
|
||||
return args[key] !== undefined
|
||||
? args[key]
|
||||
: match;
|
||||
}
|
||||
});
|
||||
if (key == +key) {
|
||||
return args[key] !== undefined ? args[key] : match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getQueryString(params) {
|
||||
if (!params) return '';
|
||||
let parts = [];
|
||||
for (let key in params) {
|
||||
if (key!=null && params[key]!=null) {
|
||||
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))
|
||||
}
|
||||
if (!params) return '';
|
||||
let parts = [];
|
||||
for (let key in params) {
|
||||
if (key != null && params[key] != null) {
|
||||
parts.push(
|
||||
encodeURIComponent(key) + '=' + encodeURIComponent(params[key])
|
||||
);
|
||||
}
|
||||
return parts.join('&');
|
||||
}
|
||||
return parts.join('&');
|
||||
}
|
||||
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => Promise.resolve(fn(req, res, next))
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
// handle error
|
||||
res.status(err.statusCode || 500).send({error: err.message});
|
||||
});
|
||||
return (req, res, next) =>
|
||||
Promise.resolve(fn(req, res, next)).catch((err) => {
|
||||
console.log(err);
|
||||
// handle error
|
||||
res.status(err.statusCode || 500).send({ error: err.message });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -70,15 +78,15 @@ function asyncHandler(fn) {
|
||||
* @param {Number} n
|
||||
*/
|
||||
function range(n) {
|
||||
return Array.from(Array(4)).map((d, i) => i)
|
||||
return Array.from(Array(4)).map((d, i) => i);
|
||||
}
|
||||
|
||||
function unique(list, key = it => it) {
|
||||
var seen = {};
|
||||
return list.filter(item => {
|
||||
var k = key(item);
|
||||
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
|
||||
});
|
||||
function unique(list, key = (it) => it) {
|
||||
var seen = {};
|
||||
return list.filter((item) => {
|
||||
var k = key(item);
|
||||
return seen.hasOwnProperty(k) ? false : (seen[k] = true);
|
||||
});
|
||||
}
|
||||
|
||||
function getDuplicates(array) {
|
||||
@ -96,15 +104,20 @@ function getDuplicates(array) {
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_,
|
||||
slug,
|
||||
getRandomString,
|
||||
sleep,
|
||||
stringReplace,
|
||||
getQueryString,
|
||||
asyncHandler,
|
||||
range,
|
||||
unique,
|
||||
getDuplicates
|
||||
function isPesa(value) {
|
||||
return value instanceof pesa().constructor;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
_,
|
||||
slug,
|
||||
getRandomString,
|
||||
sleep,
|
||||
stringReplace,
|
||||
getQueryString,
|
||||
asyncHandler,
|
||||
range,
|
||||
unique,
|
||||
getDuplicates,
|
||||
isPesa,
|
||||
};
|
||||
|
@ -1,109 +0,0 @@
|
||||
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 }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// parse a formatted number string
|
||||
// from "4,555,000.34" -> 4555000.34
|
||||
parseNumber(number, format = '#,###.##') {
|
||||
if (!number) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof number === 'number') {
|
||||
return number;
|
||||
}
|
||||
const info = this.getFormatInfo(format);
|
||||
return parseFloat(this.removeSeparator(number, info.groupSep));
|
||||
},
|
||||
|
||||
formatNumber(number, format = '#,###.##', precision = null) {
|
||||
if (!number) {
|
||||
number = 0;
|
||||
}
|
||||
let info = this.getFormatInfo(format);
|
||||
if (precision) {
|
||||
info.precision = precision;
|
||||
}
|
||||
let is_negative = false;
|
||||
|
||||
number = this.parseNumber(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.groupSep ? 3 : 0;
|
||||
|
||||
if (group_position) {
|
||||
var integer = parts[0];
|
||||
var str = '';
|
||||
|
||||
for (var i = integer.length; i >= 0; i--) {
|
||||
var l = this.removeSeparator(str, info.groupSep).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.groupSep;
|
||||
}
|
||||
}
|
||||
parts[0] = str
|
||||
.split('')
|
||||
.reverse()
|
||||
.join('');
|
||||
}
|
||||
if (parts[0] + '' == '') {
|
||||
parts[0] = '0';
|
||||
}
|
||||
|
||||
// join decimal
|
||||
parts[1] = parts[1] && info.fractionSep ? info.fractionSep + parts[1] : '';
|
||||
|
||||
// join
|
||||
return (is_negative ? '-' : '') + parts[0] + parts[1];
|
||||
},
|
||||
|
||||
getFormatInfo(format) {
|
||||
let format_info = numberFormats[format];
|
||||
|
||||
if (!format_info) {
|
||||
throw new Error(`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;
|
||||
},
|
||||
|
||||
removeSeparator(text, sep) {
|
||||
return text.replace(new RegExp(sep === '.' ? '\\.' : sep, 'g'), '');
|
||||
}
|
||||
};
|
@ -3417,6 +3417,11 @@ performance-now@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
pesa@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/pesa/-/pesa-1.1.3.tgz#cddd43b02a1db55cd6fb7b220257d33a268588a4"
|
||||
integrity sha512-WcgR2zb5h8h+k9JQb+xkLsYkdMuoxqKgqWm5uTcbi3EGNg3r0tfzcvIBpRYLtZ6TtICbyCRZxWi0WXCh5jSw0A==
|
||||
|
||||
pg-connection-string@2.5.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34"
|
||||
|
Loading…
x
Reference in New Issue
Block a user