2
0
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:
Alan 2022-01-11 11:32:59 +05:30 committed by GitHub
commit 3f1b4605a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 475 additions and 311 deletions

View File

@ -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();
}
};

View File

@ -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);

View File

@ -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;
},
};

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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;
});
}

View File

@ -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',
],

View File

@ -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",

View File

@ -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
View File

@ -0,0 +1,3 @@
export const DEFAULT_INTERNAL_PRECISION = 11;
export const DEFAULT_DISPLAY_PRECISION = 2;
export const DEFAULT_LOCALE = 'en-IN';

View File

@ -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);
}

View File

@ -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,
};

View File

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

View File

@ -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"