Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions banking/custom/bank_transaction.js
Comment thread
0xD0M1M0 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ frappe.ui.form.on("Bank Transaction", {
frm.trigger("set_included_fee_headline");
frm.trigger("set_foreign_currency_included_fee_headline");
}

refresh_rule_creation_menu(frm);
},

set_included_fee_headline(frm) {
Expand Down Expand Up @@ -53,6 +55,25 @@ frappe.ui.form.on("Bank Transaction", {
},
});

function refresh_rule_creation_menu(frm) {
frappe.require("rule_creation_dialog.bundle.js", () => {
if (frm._rule_creation_menu_link) {
frm._rule_creation_menu_link.closest("li").remove();
frm._rule_creation_menu_link = null;
}
if (
!frm.doc.bank_account ||
!frappe.model.can_create("Bank Reconciliation Rule")
) {
return;
}
frm._rule_creation_menu_link = frm.page.add_menu_item(
__("Create Reconciliation Rule"),
() => banking.bank_reconciliation.show_rule_creation_dialog(frm.doc)
);
});
}

function has_deposit_with_included_fee(frm) {
return (
get_rounded_amount(frm, "deposit") > 0 &&
Expand Down
Comment thread
0xD0M1M0 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@
// For license information, please see license.txt

frappe.ui.form.on("Bank Reconciliation Rule", {
onload: function (frm) {
frm.trigger("render_bt_filters");
},
refresh(frm) {
frm.trigger("set_target_account_query");
frappe.require("bank_reconciliation_rule_stats.bundle.js", () => {
if (!frm.stats_manager) {
frm.stats_manager =
new banking.bank_reconciliation.BankReconciliationRuleStatsManager(
frm
);
}
const stats = frm.stats_manager;

frm.trigger("set_target_account_query");
frm.remove_custom_button(__("Open Matches"));
const filters_json =
frm.doc.filters && frm.doc.filters !== "[]" ? frm.doc.filters : "[]";
let has_filters = false;
try {
has_filters = JSON.parse(filters_json).length > 0;
} catch (e) {
has_filters = false;
}
if (frm.doc.bank_account && has_filters) {
frm.add_custom_button(__("Open Matches"), () => {
stats.open_bank_transaction_list();
});
}

if (stats.needs_filter_rerender()) {
frm.trigger("render_bt_filters");
} else {
stats.fetch_and_show();
}
});
},
bank_account(frm) {
frm.trigger("set_target_account_query");
frm.stats_manager?.schedule_fetch();
},
async set_target_account_query(frm) {
if (!frm.doc.bank_account) {
Expand Down Expand Up @@ -49,30 +77,63 @@ frappe.ui.form.on("Bank Reconciliation Rule", {
}));
},
render_bt_filters(frm) {
const stats = frm.stats_manager;
stats.teardown();

const parent = frm.fields_dict.filter_area.$wrapper;
parent.empty();

const filters =
frm.doc.filters && frm.doc.filters !== "[]"
? JSON.parse(frm.doc.filters)
: [];
let filters = [];
try {
if (frm.doc.filters && frm.doc.filters !== "[]") {
const parsed = JSON.parse(frm.doc.filters);
filters = Array.isArray(parsed) ? parsed : [];
}
} catch (e) {
filters = [];
}

frappe.model.with_doctype("Bank Transaction", () => {
const filter_group = new frappe.ui.FilterGroup({
parent: parent,
doctype: "Bank Transaction",
on_change: () => {
frm.set_value("filters", JSON.stringify(filter_group.get_filters()));
stats.schedule_fetch();
},
});

filter_group.add_filters_to_filter_group(filters);
stats.filter_group = filter_group;

const after_filters_ready = () => {
if (frm.doc.docstatus !== 0) {
parent.find(".filter-action-buttons").remove();
parent.find(".divider").remove();
parent.find(".remove-filter").remove();
parent.find(".form-control").prop("disabled", true);
}

stats.rendered_for = frm.docname;
stats.mount(parent);
};

if (frm.doc.docstatus === 1) {
parent.find(".filter-action-buttons").remove();
parent.find(".divider").remove();
parent.find(".remove-filter").remove();
parent.find(".form-control").prop("disabled", true);
if (filters.length) {
filter_group.toggle_empty_filters(false);
frappe
.run_serially(
filters.map(
(f) => () => filter_group.add_filter(f[0], f[1], f[2], f[3])
)
)
.then(() => {
filter_group.update_filters();
after_filters_ready();
})
.catch(() => {
after_filters_ready();
});
} else {
after_filters_ready();
}
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,113 @@
# For license information, please see license.txt

import json
from typing import Any

import frappe
from frappe import _
from frappe.exceptions import ValidationError
from frappe.model.db_query import DatabaseQuery
from frappe.model.document import Document
from frappe.utils import add_days, getdate, today
from frappe.utils.data import get_filter

from banking.exceptions import CurrencyMismatchError

# Same cap as List View (`count_upper_bound`): count at most N rows, show "N-1+" when hit.
COUNT_UPPER_BOUND = 1001


class NoFiltersError(ValidationError):
pass


def _normalize_filter_rows(raw_filters: list) -> list[list[Any]]:
normalized: list[list[Any]] = []
for row in raw_filters:
if not isinstance(row, list | tuple) or len(row) < 4:
frappe.throw(_("Invalid filter"))
doctype = row[0]
if doctype != "Bank Transaction":
frappe.throw(_("Invalid filter: only Bank Transaction fields are supported"))
fd = get_filter(doctype, row)
normalized.append([fd.doctype, fd.fieldname, fd.operator, fd.value])
return normalized
Comment thread
greptile-apps[bot] marked this conversation as resolved.


def _bank_transaction_stats_filters(
user_filters: list,
bank_account: str,
min_date: str,
) -> list[list[Any]]:
filters = _normalize_filter_rows(user_filters)
filters.extend(
[
["Bank Transaction", "bank_account", "=", bank_account],
["Bank Transaction", "docstatus", "=", 1],
["Bank Transaction", "date", ">=", min_date],
]
)
return filters


def _approx_bank_transaction_count(filters: list) -> int:
"""Fast capped count, same approach as List View (LIMIT then count rows)."""
return len(
DatabaseQuery("Bank Transaction").execute(
filters=filters,
limit=COUNT_UPPER_BOUND,
order_by=None,
pluck="name",
)
)


@frappe.whitelist()
def get_bank_transaction_match_stats(
bank_account: str,
filters: str | None = None,
bank_reconciliation_rule: str | None = None,
) -> dict[str, Any]:
"""Approximate count of submitted Bank Transactions matching the rule filters.

Uses a capped count (like List View) so large tables stay fast.
Windows use calendar days: last 30 days and last 365 days (\"12 months\"), relative to *today*.
"""
if not bank_account:
frappe.throw(_("Bank Account is required"))

frappe.has_permission("Bank Transaction", ptype="read", throw=True)

if bank_reconciliation_rule and frappe.db.exists("Bank Reconciliation Rule", bank_reconciliation_rule):
rule = frappe.get_doc("Bank Reconciliation Rule", bank_reconciliation_rule)
if rule.bank_account != bank_account:
frappe.throw(_("Bank Account does not match this Bank Reconciliation Rule"))

try:
user_filters = json.loads(filters or "[]")
except json.JSONDecodeError:
frappe.throw(_("Invalid filters"))

if not isinstance(user_filters, list) or not user_filters:
frappe.throw(_("Please define at least one filter"))

as_of = getdate(today())
# Match UI wording: transactions with *date* on or after (today - 30) / (today - 365).
min_30 = add_days(as_of, -30)
min_365 = add_days(as_of, -365)

return {
"last_30_days": _approx_bank_transaction_count(
_bank_transaction_stats_filters(user_filters, bank_account, min_30)
),
"last_12_months": _approx_bank_transaction_count(
_bank_transaction_stats_filters(user_filters, bank_account, min_365)
),
"as_of": as_of,
"count_upper_bound": COUNT_UPPER_BOUND,
}


class BankReconciliationRule(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
Expand Down Expand Up @@ -47,5 +141,11 @@ def validate_account_currencies(self):
)

def validate_filters(self):
if not self.filters or not json.loads(self.filters):
if not self.filters:
frappe.throw(_("Please define at least one filter!"), NoFiltersError)
try:
parsed = json.loads(self.filters)
except json.JSONDecodeError:
frappe.throw(_("Invalid filters"), NoFiltersError)
if not parsed:
frappe.throw(_("Please define at least one filter!"), NoFiltersError)
Loading
Loading