mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-13 01:34:10 +00:00
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> Co-authored-by: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
This commit is contained in:
@@ -38,7 +38,10 @@
|
|||||||
"column_break_3czf",
|
"column_break_3czf",
|
||||||
"bank_party_name",
|
"bank_party_name",
|
||||||
"bank_party_account_number",
|
"bank_party_account_number",
|
||||||
"bank_party_iban"
|
"bank_party_iban",
|
||||||
|
"extended_bank_statement_section",
|
||||||
|
"included_fee",
|
||||||
|
"excluded_fee"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -233,12 +236,32 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_oufv",
|
"fieldname": "column_break_oufv",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "extended_bank_statement_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Extended Bank Statement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "included_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Included Fee",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "currency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||||
|
"fieldname": "excluded_fee",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Excluded Fee",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "currency"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-23 17:32:58.514807",
|
"modified": "2025-12-07 20:49:18.600757",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Transaction",
|
"name": "Bank Transaction",
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class BankTransaction(Document):
|
|||||||
date: DF.Date | None
|
date: DF.Date | None
|
||||||
deposit: DF.Currency
|
deposit: DF.Currency
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
|
excluded_fee: DF.Currency
|
||||||
|
included_fee: DF.Currency
|
||||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||||
party: DF.DynamicLink | None
|
party: DF.DynamicLink | None
|
||||||
party_type: DF.Link | None
|
party_type: DF.Link | None
|
||||||
@@ -45,9 +47,11 @@ class BankTransaction(Document):
|
|||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def before_validate(self):
|
def before_validate(self):
|
||||||
|
self.handle_excluded_fee()
|
||||||
self.update_allocated_amount()
|
self.update_allocated_amount()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_included_fee()
|
||||||
self.validate_duplicate_references()
|
self.validate_duplicate_references()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
|
||||||
@@ -307,6 +311,40 @@ class BankTransaction(Document):
|
|||||||
|
|
||||||
self.party_type, self.party = result
|
self.party_type, self.party = result
|
||||||
|
|
||||||
|
def validate_included_fee(self):
|
||||||
|
"""
|
||||||
|
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||||
|
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.included_fee and self.withdrawal:
|
||||||
|
if self.included_fee > self.withdrawal:
|
||||||
|
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||||
|
|
||||||
|
def handle_excluded_fee(self):
|
||||||
|
# Include the excluded fee on validate to handle all further processing the same
|
||||||
|
excluded_fee = flt(self.excluded_fee)
|
||||||
|
if excluded_fee <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||||
|
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||||
|
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||||
|
|
||||||
|
# Enforce directionality
|
||||||
|
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||||
|
)
|
||||||
|
|
||||||
|
if flt(self.deposit) > 0:
|
||||||
|
self.deposit = flt(self.deposit) - excluded_fee
|
||||||
|
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||||
|
elif flt(self.withdrawal) >= 0:
|
||||||
|
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||||
|
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||||
|
self.excluded_fee = 0
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_doctypes_for_bank_reconciliation():
|
def get_doctypes_for_bank_reconciliation():
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestBankTransactionFees(FrappeTestCase):
|
||||||
|
def test_included_fee_throws(self):
|
||||||
|
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||||
|
withdrawal itself."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.withdrawal = 100
|
||||||
|
bt.included_fee = 101
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||||
|
|
||||||
|
def test_included_fee_allows_equal(self):
|
||||||
|
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||||
|
amount (only the fee was deducted from the account)."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.withdrawal = 100
|
||||||
|
bt.included_fee = 100
|
||||||
|
|
||||||
|
bt.validate_included_fee()
|
||||||
|
|
||||||
|
def test_included_fee_allows_for_deposit(self):
|
||||||
|
"""For deposits, a fee may be recorded separately without limiting the
|
||||||
|
received amount."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 10
|
||||||
|
bt.included_fee = 999
|
||||||
|
|
||||||
|
bt.validate_included_fee()
|
||||||
|
|
||||||
|
def test_excluded_fee_noop_when_zero(self):
|
||||||
|
"""When there is no excluded fee to apply, the amounts should remain
|
||||||
|
unchanged."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 100
|
||||||
|
bt.withdrawal = 0
|
||||||
|
bt.included_fee = 5
|
||||||
|
bt.excluded_fee = 0
|
||||||
|
|
||||||
|
bt.handle_excluded_fee()
|
||||||
|
|
||||||
|
self.assertEqual(bt.deposit, 100)
|
||||||
|
self.assertEqual(bt.withdrawal, 0)
|
||||||
|
self.assertEqual(bt.included_fee, 5)
|
||||||
|
self.assertEqual(bt.excluded_fee, 0)
|
||||||
|
|
||||||
|
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||||
|
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||||
|
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 10
|
||||||
|
bt.excluded_fee = 11
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||||
|
|
||||||
|
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||||
|
"""A transaction must be either incoming or outgoing when applying a
|
||||||
|
fee, not both."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 10
|
||||||
|
bt.withdrawal = 10
|
||||||
|
bt.excluded_fee = 1
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||||
|
|
||||||
|
def test_excluded_fee_deducts_from_deposit(self):
|
||||||
|
"""When a fee is deducted from an incoming payment, the net received
|
||||||
|
amount decreases and the fee is tracked as included."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 100
|
||||||
|
bt.withdrawal = 0
|
||||||
|
bt.included_fee = 2
|
||||||
|
bt.excluded_fee = 5
|
||||||
|
|
||||||
|
bt.handle_excluded_fee()
|
||||||
|
|
||||||
|
self.assertEqual(bt.deposit, 95)
|
||||||
|
self.assertEqual(bt.withdrawal, 0)
|
||||||
|
self.assertEqual(bt.included_fee, 7)
|
||||||
|
self.assertEqual(bt.excluded_fee, 0)
|
||||||
|
|
||||||
|
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||||
|
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||||
|
while still tracking the fee."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 5
|
||||||
|
bt.withdrawal = 0
|
||||||
|
bt.included_fee = 0
|
||||||
|
bt.excluded_fee = 5
|
||||||
|
|
||||||
|
bt.handle_excluded_fee()
|
||||||
|
|
||||||
|
self.assertEqual(bt.deposit, 0)
|
||||||
|
self.assertEqual(bt.withdrawal, 0)
|
||||||
|
self.assertEqual(bt.included_fee, 5)
|
||||||
|
self.assertEqual(bt.excluded_fee, 0)
|
||||||
|
|
||||||
|
def test_excluded_fee_increases_outgoing_payment(self):
|
||||||
|
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||||
|
the total money leaving increases and the fee is tracked."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 0
|
||||||
|
bt.withdrawal = 100
|
||||||
|
bt.included_fee = 2
|
||||||
|
bt.excluded_fee = 5
|
||||||
|
|
||||||
|
bt.handle_excluded_fee()
|
||||||
|
|
||||||
|
self.assertEqual(bt.deposit, 0)
|
||||||
|
self.assertEqual(bt.withdrawal, 105)
|
||||||
|
self.assertEqual(bt.included_fee, 7)
|
||||||
|
self.assertEqual(bt.excluded_fee, 0)
|
||||||
|
|
||||||
|
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||||
|
"""If only an excluded fee is provided, it should be treated as an
|
||||||
|
outgoing payment and the fee is then tracked as included."""
|
||||||
|
bt = frappe.new_doc("Bank Transaction")
|
||||||
|
bt.deposit = 0
|
||||||
|
bt.withdrawal = 0
|
||||||
|
bt.included_fee = 0
|
||||||
|
bt.excluded_fee = 5
|
||||||
|
|
||||||
|
bt.handle_excluded_fee()
|
||||||
|
|
||||||
|
self.assertEqual(bt.deposit, 0)
|
||||||
|
self.assertEqual(bt.withdrawal, 5)
|
||||||
|
self.assertEqual(bt.included_fee, 5)
|
||||||
|
self.assertEqual(bt.excluded_fee, 0)
|
||||||
Reference in New Issue
Block a user