mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-30 23:18:02 +00:00
feat: new banking module (#54720)
* feat: initial SPA setup for banking * wip: bring over new banking module * feat: added Espresso design tokens * feat: button styles * fix: add all ink colors * wip: espresso design system changes * feat: button and badge espresso components * fix: button styling for reconcile * feat: Espresso progress bar * feat: Espresso toggle switch * feat: Espresso tabs design * fix: vertical tab support * fix: button sizing across modals * feat: Espresso style table layout * feat: Espresso tooltip * feat: Espresso elevations and checkbox * feat: Dialog with Espresso styles * feat: Espresso textarea * fix: input styles * fix: colors on bank picker * fix: breadcrumb styling * fix: bank picker styling * feat: create doctypes and fields for bank reconciliation * feat: APIs for banking * fix: use date format parser * fix: font styling to match Espresso * wip: settings modal * feat: settings dialog component * fix: icons and invalid requests * feat: preferences tab * fix: adjust icon stroke width to 1.5 * feat: rule configuration in settings * fix: remove sheet component * feat: alert and error banner component * feat: dropdown in Espresso * feat: popover and select in Espresso * fix: cleanup more styles * fix: match size of link fields * feat: command styling * fix: remove unused style tokens * fix: styles for global date picker dropdown * fix: styles for match and reconcile * feat: table Espresso component * feat: remove all other design tokens * fix: remove unused tokens * fix: form elements * fix: remove unused styles and fix filters in bank transaction list * feat: fetch bank rec doctypes for filtering * fix: record payment modal * feat: support for dark mode switching * fix: move bank logos to public folder * feat: add support for RTL * feat: support for RTL * chore: send layout direction in dev boot * fix: make checkbox work in RTL * feat: dark mode support * fix: dark mode style * feat: bank logos in dark mode * feat: dark mode bank logos * chore: use dark mode bank logos everywhere * chore: move rule evaluation to controller * chore: add tests for bank transaction rules * fix: move deps to fix actions errors * fix: move tw-animate-css to deps * fix: remove shadcn * fix: do not open modal if no transactions selected * fix: add translation strings * feat: add banner on existing bank reconciliation tool * feat: bank statement import * fix: translations and layout directions * fix: validation for transaction matching rule * fix: styles * fix: show conflicting transactions in alert * fix: show help text for new banking module forms * feat: show total debits and credits * fix: dark mode colors in automatic config * feat: add keyboard shortcuts help * feat: added keyboard shortcut for settings * fix: decrease size of progress bar * chore: bump packages * feat: add tests for statement import * fix: settings dialog * fix: show banner on small screens * fix: show banner when no bank account set
This commit is contained in:
249
banking/src/lib/numbers.ts
Normal file
249
banking/src/lib/numbers.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { in_list } from "./checks";
|
||||
import { getCurrencyNumberFormat, getCurrencyProperty, getCurrencySymbol } from "./currency";
|
||||
import { getSystemDefault } from "./frappe";
|
||||
import _ from "@/lib/translate";
|
||||
|
||||
export const formatCurrency = (value?: number, currency: string = '', decimals: number = 2) => {
|
||||
|
||||
if (!value) {
|
||||
value = 0
|
||||
}
|
||||
|
||||
if (!currency) {
|
||||
currency = getSystemDefault('currency') ?? ''
|
||||
}
|
||||
const format = get_number_format(currency);
|
||||
const symbol = getCurrencySymbol(currency);
|
||||
|
||||
const show_symbol_on_right = getCurrencyProperty(currency, 'symbol_on_right') ?? false;
|
||||
|
||||
if (decimals === undefined) {
|
||||
decimals = getSystemDefault('currency_precision') || null;
|
||||
}
|
||||
|
||||
if (symbol) {
|
||||
if (show_symbol_on_right) {
|
||||
return format_number(value, format, decimals) + " " + _(symbol);
|
||||
}
|
||||
return _(symbol) + " " + format_number(value, format, decimals);
|
||||
} else {
|
||||
return format_number(value, format, decimals);
|
||||
}
|
||||
}
|
||||
|
||||
const replace_all = (str: string, search: string, replace: string) => {
|
||||
return str.split(search).join(replace);
|
||||
};
|
||||
|
||||
const number_format_info = {
|
||||
"#,###.##": { decimal_str: ".", group_sep: "," },
|
||||
"#.###,##": { decimal_str: ",", group_sep: "." },
|
||||
"# ###.##": { decimal_str: ".", group_sep: " " },
|
||||
"# ###,##": { decimal_str: ",", group_sep: " " },
|
||||
"#'###.##": { decimal_str: ".", group_sep: "'" },
|
||||
"#, ###.##": { decimal_str: ".", group_sep: ", " },
|
||||
"#,##,###.##": { decimal_str: ".", group_sep: "," },
|
||||
"#,###.###": { decimal_str: ".", group_sep: "," },
|
||||
"#.###": { decimal_str: "", group_sep: "." },
|
||||
"#,###": { decimal_str: "", group_sep: "," },
|
||||
};
|
||||
|
||||
const format_number = (v?: number, format?: string, decimals?: number | null) => {
|
||||
if (!format) {
|
||||
format = get_number_format();
|
||||
if (decimals == null) decimals = cint(getSystemDefault("float_precision")) || 3;
|
||||
}
|
||||
|
||||
const info = get_number_format_info(format);
|
||||
|
||||
// Fix the decimal first, toFixed will auto fill trailing zero.
|
||||
if (decimals == null) decimals = info.precision;
|
||||
|
||||
v = flt(v, decimals, format);
|
||||
|
||||
let is_negative = false;
|
||||
if (v < 0) is_negative = true;
|
||||
v = Math.abs(v);
|
||||
|
||||
const val = v.toFixed(decimals)
|
||||
|
||||
const part = val.split(".");
|
||||
|
||||
// get group position and parts
|
||||
let group_position = info.group_sep ? 3 : 0;
|
||||
|
||||
if (group_position) {
|
||||
const integer = part[0];
|
||||
let str = "";
|
||||
for (let i = integer.length; i >= 0; i--) {
|
||||
let l = replace_all(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;
|
||||
}
|
||||
}
|
||||
part[0] = str.split("").reverse().join("");
|
||||
}
|
||||
if (part[0] + "" == "") {
|
||||
part[0] = "0";
|
||||
}
|
||||
|
||||
// join decimal
|
||||
part[1] = part[1] && info.decimal_str ? info.decimal_str + part[1] : "";
|
||||
|
||||
// join
|
||||
return (is_negative ? "-" : "") + part[0] + part[1];
|
||||
};
|
||||
|
||||
function get_number_format_info(format: string) {
|
||||
let info: { decimal_str: string, group_sep: string, precision?: number } = number_format_info[format as keyof typeof number_format_info];
|
||||
|
||||
if (!info) {
|
||||
info = { decimal_str: ".", group_sep: "," };
|
||||
}
|
||||
|
||||
// get the precision from the number format
|
||||
info.precision = format.split(info.decimal_str).slice(1)[0].length;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
function get_number_format(currency?: string): string {
|
||||
return (
|
||||
(cint(getSystemDefault("use_number_format_from_currency")) &&
|
||||
currency &&
|
||||
getCurrencyNumberFormat(currency)) ||
|
||||
getSystemDefault("number_format") ||
|
||||
"#,###.##"
|
||||
)
|
||||
}
|
||||
|
||||
export const flt = (value?: number | string | null, decimals?: number, number_format?: string, rounding_method?: string) => {
|
||||
if (value === undefined || value === null || value === "") return 0
|
||||
|
||||
if (typeof value !== "number") {
|
||||
value = value + "";
|
||||
|
||||
// strip currency symbol if exists
|
||||
if (value.indexOf(" ") != -1) {
|
||||
// using slice(1).join(" ") because space could also be a group separator
|
||||
const parts = value.split(" ");
|
||||
value = isNaN(parseFloat(parts[0])) ? parts.slice(parts.length - 1).join(" ") : value;
|
||||
}
|
||||
|
||||
value = strip_number_groups(value, number_format);
|
||||
|
||||
value = parseFloat(value as string);
|
||||
if (isNaN(value)) value = 0;
|
||||
}
|
||||
|
||||
if (decimals != null) return _round(value, decimals, rounding_method);
|
||||
return value;
|
||||
}
|
||||
|
||||
function strip_number_groups(v: string, number_format?: string) {
|
||||
if (!number_format) number_format = get_number_format();
|
||||
const info = get_number_format_info(number_format);
|
||||
|
||||
// strip groups (,)
|
||||
const group_regex = new RegExp(info.group_sep === "." ? "\\." : info.group_sep, "g");
|
||||
v = v.replace(group_regex, "");
|
||||
|
||||
// replace decimal separator with (.)
|
||||
if (info.decimal_str !== "." && info.decimal_str !== "") {
|
||||
const decimal_regex = new RegExp(info.decimal_str, "g");
|
||||
v = v.replace(decimal_regex, ".");
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
const _round = (num: number, precision: number, rounding_method?: string) => {
|
||||
|
||||
rounding_method = rounding_method || getSystemDefault('rounding_method') || "Banker's Rounding (legacy)";
|
||||
|
||||
const is_negative = num < 0 ? true : false;
|
||||
|
||||
if (rounding_method == "Banker's Rounding (legacy)") {
|
||||
const d = cint(precision);
|
||||
const m = Math.pow(10, d);
|
||||
const n = +(d ? Math.abs(num) * m : Math.abs(num)).toFixed(8); // Avoid rounding errors
|
||||
const i = Math.floor(n),
|
||||
f = n - i;
|
||||
let r = !precision && f == 0.5 ? (i % 2 == 0 ? i : i + 1) : Math.round(n);
|
||||
r = d ? r / m : r;
|
||||
return is_negative ? -r : r;
|
||||
} else if (rounding_method == "Banker's Rounding") {
|
||||
if (num == 0) return 0.0;
|
||||
precision = cint(precision);
|
||||
|
||||
const multiplier = Math.pow(10, precision);
|
||||
num = Math.abs(num) * multiplier;
|
||||
|
||||
const floor_num = Math.floor(num);
|
||||
const decimal_part = num - floor_num;
|
||||
|
||||
// For explanation of this method read python flt implementation notes.
|
||||
const epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0);
|
||||
|
||||
if (Math.abs(decimal_part - 0.5) < epsilon) {
|
||||
num = floor_num % 2 == 0 ? floor_num : floor_num + 1;
|
||||
} else {
|
||||
num = Math.round(num);
|
||||
}
|
||||
num = num / multiplier;
|
||||
return is_negative ? -num : num;
|
||||
} else if (rounding_method == "Commercial Rounding") {
|
||||
if (num == 0) return 0.0;
|
||||
|
||||
const digits = cint(precision);
|
||||
const multiplier = Math.pow(10, digits);
|
||||
|
||||
num = num * multiplier;
|
||||
|
||||
// For explanation of this method read python flt implementation notes.
|
||||
let epsilon = 2.0 ** (Math.log2(Math.abs(num)) - 52.0);
|
||||
if (is_negative) {
|
||||
epsilon = -1 * epsilon;
|
||||
}
|
||||
|
||||
num = Math.round(num + epsilon);
|
||||
return num / multiplier;
|
||||
} else {
|
||||
throw new Error(`Unknown rounding method ${rounding_method}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const cint = (v: boolean | string | number, def?: boolean | string | number) => {
|
||||
if (v === true) return 1;
|
||||
if (v === false) return 0;
|
||||
v = v + "";
|
||||
if (v !== "0") v = lstrip(v, ["0"]);
|
||||
v = parseInt(v); // eslint-ignore-line
|
||||
if (isNaN(v)) v = def === undefined ? 0 : def;
|
||||
return v as number;
|
||||
};
|
||||
|
||||
export const lstrip = (s: string, chars?: string[]) => {
|
||||
if (!chars) chars = ["\n", "\t", " "];
|
||||
// strip left
|
||||
let first_char = s.substring(0, 1);
|
||||
while (chars.includes(first_char)) {
|
||||
s = s.substring(1);
|
||||
first_char = s.substring(0, 1);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
export const getCurrencyFormatInfo = (currency?: string) => {
|
||||
const format = get_number_format(currency);
|
||||
return get_number_format_info(format);
|
||||
};
|
||||
Reference in New Issue
Block a user