Skip to content
Draft
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
8 changes: 8 additions & 0 deletions banking/custom_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ def get_custom_fields():
allow_on_submit=1,
no_copy=1,
),
dict(
fieldname="on_hold_until",
label=_("On Hold Until"),
fieldtype="Date",
insert_after="reserved_voucher",
allow_on_submit=1,
no_copy=1,
),
],
"Journal Entry": [
dict(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Cast, Coalesce, Sum
from frappe.utils import flt, sbool
from frappe.utils import add_days, cint, flt, nowdate, sbool
from pypika import Order
from pypika.terms import ExistsCriterion

Expand Down Expand Up @@ -119,6 +119,10 @@ def get_bank_transactions(
["docstatus", "=", 1],
["status", "not in", ["Reconciled", "Cancelled"]],
]
or_filters = [
["on_hold_until", "is", "not set"],
["on_hold_until", "<", nowdate()],
]

if bank_account:
filters.append(["bank_account", "=", bank_account])
Expand Down Expand Up @@ -164,13 +168,61 @@ def get_bank_transactions(
"Bank Transaction",
fields=fields,
filters=filters,
or_filters=or_filters,
order_by=order_by,
)
enrich_transactions_with_deposit_fee_for_reconciliation(transactions)

return transactions


@frappe.whitelist()
def set_bank_transaction_on_hold(bank_transaction_name: str, on_hold_until: str) -> None:
"""Hide a submitted Bank Transaction from reconciliation until the given date."""
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
frappe.has_permission("Bank Transaction", "write", transaction, throw=True)
transaction.db_set("on_hold_until", on_hold_until, update_modified=True)


@frappe.whitelist()
def request_invoice(bank_transaction_name: str, recipient_type: str, recipient: str) -> None:
"""Request an invoice and optionally put its Bank Transaction on hold."""
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
frappe.has_permission("Bank Transaction", "write", transaction, throw=True)

recipient_email = get_invoice_request_recipient_email(recipient_type, recipient)
if not recipient_email:
frappe.throw(_("The selected {0} has no email address.").format(_(recipient_type)))

frappe.sendmail(
recipients=[recipient_email],
subject=_("Invoice request for Bank Transaction {0}").format(transaction.name),
message=_("Please provide the invoice for Bank Transaction {0}.").format(transaction.name),
reference_doctype=transaction.doctype,
reference_name=transaction.name,
)

if frappe.db.get_single_value("Banking Settings", "automatically_set_on_hold_after_invoice_request"):
threshold = cint(
frappe.db.get_single_value("Banking Settings", "on_hold_threshold_after_invoice_request")
)
transaction.db_set("on_hold_until", add_days(nowdate(), threshold), update_modified=True)


def get_invoice_request_recipient_email(recipient_type: str, recipient: str) -> str | None:
"""Return the primary email address for a supported invoice-request recipient."""
field_map = {
"User": ("User", "email"),
"Contact": ("Contact", "email_id"),
"Employee": ("Employee", "company_email"),
}
if recipient_type not in field_map:
frappe.throw(_("Recipient type must be User, Contact, or Employee."))

doctype, fieldname = field_map[recipient_type]
return frappe.db.get_value(doctype, recipient, fieldname)


def enrich_transactions_with_deposit_fee_for_reconciliation(transactions: list) -> None:
"""Expose deposit included fees that unpaid-invoice reconciliation can book.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
create_payment_entry_bts,
get_bank_transactions,
get_linked_payments,
request_invoice,
set_bank_transaction_on_hold,
)

test_dependencies = ["Warehouse", "Item", "Account", "Cost Center", "UOM", "Company"]
Expand Down Expand Up @@ -1688,6 +1690,34 @@ def test_get_bank_transactions_hides_fee_without_bank_fee_account(self):
row = next(transaction for transaction in transactions if transaction.name == bt.name)
self.assertEqual(row.included_fee_for_reconciliation, 0)

def test_on_hold_transaction_is_hidden_until_the_following_day(self):
bt = create_bank_transaction(deposit=100, bank_account=self.bank_account)

set_bank_transaction_on_hold(bt.name, getdate())

transactions = get_bank_transactions(bank_account=self.bank_account)
self.assertNotIn(bt.name, [transaction.name for transaction in transactions])

frappe.db.set_value("Bank Transaction", bt.name, "on_hold_until", add_days(getdate(), -1))
transactions = get_bank_transactions(bank_account=self.bank_account)
self.assertIn(bt.name, [transaction.name for transaction in transactions])

def test_request_invoice_sends_email_and_sets_on_hold(self):
bt = create_bank_transaction(deposit=100, bank_account=self.bank_account)
frappe.db.set_single_value("Banking Settings", "automatically_set_on_hold_after_invoice_request", 1)
frappe.db.set_single_value("Banking Settings", "on_hold_threshold_after_invoice_request", 3)

with patch(
"banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.frappe.sendmail"
) as sendmail:
request_invoice(bt.name, "User", "Administrator")

sendmail.assert_called_once()
self.assertEqual(sendmail.call_args.kwargs["recipients"], ["Administrator"])
self.assertEqual(
frappe.db.get_value("Bank Transaction", bt.name, "on_hold_until"), add_days(getdate(), 3)
)

def _create_draft_journal_entry(self, bt, **kwargs):
defaults = {
"bank_transaction_name": bt.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"fintech_license_key",
"bank_reconciliation_tab",
"enable_automatic_journal_entries_for_bank_fees",
"invoice_request_section",
"automatically_set_on_hold_after_invoice_request",
"on_hold_threshold_after_invoice_request",
"advanced_section",
"reference_fields",
"voucher_matching_defaults"
Expand Down Expand Up @@ -123,6 +126,25 @@
"fieldname": "enable_automatic_journal_entries_for_bank_fees",
"fieldtype": "Check",
"label": "Enable Automatic Journal Entries for Bank Fees"
},
{
"fieldname": "invoice_request_section",
"fieldtype": "Section Break",
"label": "Invoice Requests"
},
{
"default": "0",
"fieldname": "automatically_set_on_hold_after_invoice_request",
"fieldtype": "Check",
"label": "Automatically Set On Hold after Invoice Request"
},
{
"default": "7",
"depends_on": "automatically_set_on_hold_after_invoice_request",
"fieldname": "on_hold_threshold_after_invoice_request",
"fieldtype": "Int",
"label": "On Hold Threshold (after Invoice Request)",
"non_negative": 1
}
],
"grid_page_length": 50,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ banking.bank_reconciliation.ActionsPanelManager = class ActionsPanelManager {
});
},
},
{
tab_name: "on_hold",
tab_label: __("On Hold"),
make_tab: () => {
return new banking.bank_reconciliation.OnHoldTab({
actions_panel: this,
transaction: this.transaction,
panel_manager: this.panel_manager,
});
},
},
];

if (!is_reserved) {
Expand Down Expand Up @@ -139,10 +150,10 @@ banking.bank_reconciliation.ActionsPanelManager = class ActionsPanelManager {
message: with_new_voucher
? __("Bank Transaction {0} partially reconciled.", [
this.transaction.name,
])
])
: __("Bank Transaction {0} partially matched.", [
this.transaction.name,
]),
]),
indicator: "blue",
});
this.panel_manager.refresh_transaction(unallocated_amount);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
frappe.provide("banking.bank_reconciliation");

banking.bank_reconciliation.OnHoldTab = class OnHoldTab {
constructor(opts) {
$.extend(this, opts);
this.make();
}

make() {
this.panel_manager.actions_tab = "on_hold-tab";
this.field_group = new frappe.ui.FieldGroup({
fields: this.get_fields(),
body: this.actions_panel.$tab_content,
card_layout: true,
doc: this.transaction,
});
this.field_group.make();
}

get_fields() {
return [
{
label: __("On Hold Until"),
fieldname: "on_hold_until",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Set On Hold"),
fieldname: "set_on_hold",
fieldtype: "Button",
primary: true,
click: () => this.set_on_hold(),
},
{ fieldtype: "Section Break", label: __("Request Invoice") },
{
label: __("Recipient Type"),
fieldname: "recipient_type",
fieldtype: "Select",
options: "User\nContact\nEmployee",
default: "Employee",
reqd: 1,
},
{
label: __("Recipient"),
fieldname: "recipient",
fieldtype: "Dynamic Link",
options: "recipient_type",
reqd: 1,
},
{
label: __("Request Invoice"),
fieldname: "request_invoice",
fieldtype: "Button",
click: () => this.request_invoice(),
},
];
}

set_on_hold() {
const on_hold_until = this.field_group.get_value("on_hold_until");
if (on_hold_until) {
this.call("set_bank_transaction_on_hold", { on_hold_until });
}
}

request_invoice() {
const recipient_type = this.field_group.get_value("recipient_type");
const recipient = this.field_group.get_value("recipient");
if (recipient_type && recipient) {
this.call("request_invoice", { recipient_type, recipient });
}
}

call(method, args) {
frappe.call({
method: `banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.${method}`,
args: { bank_transaction_name: this.transaction.name, ...args },
freeze: true,
callback: (response) => {
if (!response.exc) {
this.panel_manager.reload_transactions();
}
},
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./actions_panel/actions_panel_manager";
import "./actions_panel/create_tab";
import "./actions_panel/details_tab";
import "./actions_panel/match_tab";
import "./actions_panel/on_hold_tab";
import "./actions_panel/reconcile_conversion_dialog";

import "./summary_number_card";
Loading