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:
Nikhil Kothari
2026-05-09 23:14:58 +05:30
committed by GitHub
parent 332026fe5e
commit 6de5367f12
262 changed files with 39467 additions and 14 deletions

View File

@@ -0,0 +1,12 @@
frappe.defaults = {
get_user_default: function (key) {
let defaults = frappe.boot.user.defaults;
let d = defaults[key];
if (!d) {
key = key.replace(/ /g, "_").toLowerCase();
d = defaults[key];
}
if (Array.isArray(d)) d = d[0];
return d;
},
};

View File

@@ -0,0 +1,3 @@
import "./namespace";
import "./sync";
import "./defaults";

View File

@@ -0,0 +1,22 @@
if (!window.frappe) window.frappe = {};
frappe.provide = function (namespace) {
// docs: create a namespace //
var nsl = namespace.split(".");
var parent = window;
for (var i = 0; i < nsl.length; i++) {
var n = nsl[i];
if (!parent[n]) {
parent[n] = {};
}
parent = parent[n];
}
return parent;
};
frappe.provide("locals");
frappe.provide("frappe.flags");
frappe.provide("frappe.settings");
frappe.provide("locals.DocType");
frappe.provide("frappe.model");
frappe.provide("frappe.defaults");

View File

@@ -0,0 +1,187 @@
import isPlainObject from "lodash.isplainobject";
Object.assign(frappe.model, {
docinfo: {},
sync: function (r) {
/* docs:
extract docs, docinfo (attachments, comments, assignments)
from incoming request and set in `locals` and `frappe.model.docinfo`
*/
var isPlain;
if (!r.docs && !r.docinfo) r = { docs: r };
isPlain = isPlainObject(r.docs);
if (isPlain) r.docs = [r.docs];
if (r.docs) {
for (var i = 0, l = r.docs.length; i < l; i++) {
var d = r.docs[i];
if (locals[d.doctype] && locals[d.doctype][d.name]) {
// update values
frappe.model.update_in_locals(d);
} else {
frappe.model.add_to_locals(d);
}
d.__last_sync_on = new Date();
}
}
frappe.model.sync_docinfo(r);
return r.docs;
},
sync_docinfo: (r) => {
// set docinfo (comments, assign, attachments)
if (r.docinfo) {
const { doctype, name } = r.docinfo;
if (!frappe.model.docinfo[doctype]) {
frappe.model.docinfo[doctype] = {};
}
frappe.model.docinfo[doctype][name] = r.docinfo;
// copy values to frappe.boot.user_info
Object.assign(frappe.boot.user_info, r.docinfo.user_info);
}
return r.docs;
},
add_to_locals: function (doc) {
if (!locals[doc.doctype]) locals[doc.doctype] = {};
if (!doc.name && doc.__islocal) {
// get name (local if required)
if (!doc.parentfield) frappe.model.clear_doc(doc);
doc.name = frappe.model.get_new_name(doc.doctype);
if (!doc.parentfield) frappe.provide("frappe.model.docinfo." + doc.doctype + "." + doc.name);
}
locals[doc.doctype][doc.name] = doc;
// let meta = frappe.get_meta(doc.doctype);
// let is_table = meta ? meta.istable : doc.parentfield;
// // add child docs to locals
// if (!is_table) {
// for (var i in doc) {
// var value = doc[i];
// if (isArray(value)) {
// for (var x = 0, y = value.length; x < y; x++) {
// var d = value[x];
// if (typeof d == "object" && !d.parent) d.parent = doc.name;
// frappe.model.add_to_locals(d);
// }
// }
// }
// }
},
update_in_locals: function (doc) {
// update values in the existing local doc instead of replacing
let local_doc = locals[doc.doctype][doc.name];
let clear_keys = function (source, target) {
Object.keys(target).map((key) => {
if (source[key] == undefined) delete target[key];
});
};
for (let fieldname in doc) {
let df = frappe.meta.get_field(doc.doctype, fieldname);
if (df && frappe.model.table_fields.includes(df.fieldtype)) {
// table
if (!(doc[fieldname] instanceof Array)) {
doc[fieldname] = [];
}
if (!(local_doc[fieldname] instanceof Array)) {
local_doc[fieldname] = [];
}
// child table, override each row and append new rows if required
for (let i = 0; i < doc[fieldname].length; i++) {
let d = doc[fieldname][i];
let local_d = local_doc[fieldname][i];
if (local_d) {
// deleted and added again
if (!locals[d.doctype]) locals[d.doctype] = {};
if (!d.name) {
// incoming row is new, find a new name
d.name = frappe.model.get_new_name(doc.doctype);
}
// if incoming row is not registered, register it
if (!locals[d.doctype][d.name]) {
// detach old key
delete locals[d.doctype][local_d.name];
// re-attach with new name
locals[d.doctype][d.name] = local_d;
}
// row exists, just copy the values
Object.assign(local_d, d);
clear_keys(d, local_d);
} else {
local_doc[fieldname].push(d);
if (!d.parent) d.parent = doc.name;
frappe.model.add_to_locals(d);
}
}
// remove extra rows
if (local_doc[fieldname].length > doc[fieldname].length) {
for (let i = doc[fieldname].length; i < local_doc[fieldname].length; i++) {
// clear from local
let d = local_doc[fieldname][i];
if (locals[d.doctype] && locals[d.doctype][d.name]) {
delete locals[d.doctype][d.name];
}
}
local_doc[fieldname].length = doc[fieldname].length;
}
} else {
// literal
local_doc[fieldname] = doc[fieldname];
}
}
// clear keys on parent
clear_keys(doc, local_doc);
},
remove_from_locals: function (doctype, name) {
let clear_doc = function (doctype, name) {
var doc = locals[doctype] && locals[doctype][name];
if (!doc) return;
var parent = null;
if (doc.parenttype) {
parent = doc.parent;
var parenttype = doc.parenttype,
parentfield = doc.parentfield;
}
delete locals[doctype][name];
if (parent) {
var parent_doc = locals[parenttype][parent];
var newlist = [],
idx = 1;
$.each(parent_doc[parentfield], function (i, d) {
if (d.name != name) {
newlist.push(d);
d.idx = idx;
idx++;
}
parent_doc[parentfield] = newlist;
});
}
};
clear_doc(doctype, name);
},
});