mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-23 00:28:30 +00:00
Co-authored-by: Karm Soni <karmdsoni8159@gmail.com> Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
-> Resolves dunning automatically
|
-> Resolves dunning automatically
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
@@ -156,40 +157,66 @@ class Dunning(AccountsController):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def resolve_dunning(doc, state):
|
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||||
"""
|
if (
|
||||||
Check if all payments have been made and resolve dunning, if yes. Called
|
doc.doctype != "Sales Invoice"
|
||||||
when a Payment Entry is submitted.
|
or doc.is_return
|
||||||
"""
|
or previous_outstanding_amount == doc.outstanding_amount
|
||||||
for reference in doc.references:
|
):
|
||||||
# Consider partial and full payments:
|
return
|
||||||
# Submitting full payment: outstanding_amount will be 0
|
|
||||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
|
||||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
|
||||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
|
||||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
|
||||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
|
||||||
|
|
||||||
if reference.reference_doctype == "Sales Invoice" and (
|
to_resolve = doc.outstanding_amount < previous_outstanding_amount
|
||||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
state = "Unresolved" if to_resolve else "Resolved"
|
||||||
):
|
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
|
||||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
if not dunnings:
|
||||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
return
|
||||||
|
|
||||||
for dunning in dunnings:
|
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
|
||||||
resolve = True
|
invoices = set()
|
||||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
payment_schedule_ids = set()
|
||||||
for overdue_payment in dunning.overdue_payments:
|
|
||||||
outstanding_inv = frappe.get_value(
|
|
||||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
|
||||||
)
|
|
||||||
outstanding_ps = frappe.get_value(
|
|
||||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
|
||||||
)
|
|
||||||
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
|
|
||||||
|
|
||||||
dunning.status = "Resolved" if resolve else "Unresolved"
|
for dunning in dunnings:
|
||||||
dunning.save()
|
for overdue_payment in dunning.overdue_payments:
|
||||||
|
invoices.add(overdue_payment.sales_invoice)
|
||||||
|
if overdue_payment.payment_schedule:
|
||||||
|
payment_schedule_ids.add(overdue_payment.payment_schedule)
|
||||||
|
|
||||||
|
invoice_outstanding_amounts = dict(
|
||||||
|
frappe.get_all(
|
||||||
|
"Sales Invoice",
|
||||||
|
filters={"name": ["in", list(invoices)]},
|
||||||
|
fields=["name", "outstanding_amount"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ps_outstanding_amounts = (
|
||||||
|
dict(
|
||||||
|
frappe.get_all(
|
||||||
|
"Payment Schedule",
|
||||||
|
filters={"name": ["in", list(payment_schedule_ids)]},
|
||||||
|
fields=["name", "outstanding"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if payment_schedule_ids
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
for dunning in dunnings:
|
||||||
|
has_outstanding = False
|
||||||
|
for overdue_payment in dunning.overdue_payments:
|
||||||
|
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
|
||||||
|
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
|
||||||
|
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
|
||||||
|
if has_outstanding:
|
||||||
|
break
|
||||||
|
|
||||||
|
new_status = "Resolved" if not has_outstanding else "Unresolved"
|
||||||
|
|
||||||
|
if dunning.status != new_status:
|
||||||
|
dunning.status = new_status
|
||||||
|
dunning.save()
|
||||||
|
|
||||||
|
|
||||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||||
|
|||||||
@@ -139,6 +139,64 @@ class TestDunning(FrappeTestCase):
|
|||||||
self.assertEqual(sales_invoice.status, "Overdue")
|
self.assertEqual(sales_invoice.status, "Overdue")
|
||||||
self.assertEqual(dunning.status, "Unresolved")
|
self.assertEqual(dunning.status, "Unresolved")
|
||||||
|
|
||||||
|
def test_dunning_resolution_from_credit_note(self):
|
||||||
|
"""
|
||||||
|
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||||
|
"""
|
||||||
|
sales_invoice = create_sales_invoice_against_cost_center(
|
||||||
|
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||||
|
)
|
||||||
|
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||||
|
dunning.submit()
|
||||||
|
|
||||||
|
self.assertEqual(dunning.status, "Unresolved")
|
||||||
|
|
||||||
|
credit_note = frappe.copy_doc(sales_invoice)
|
||||||
|
credit_note.is_return = 1
|
||||||
|
credit_note.return_against = sales_invoice.name
|
||||||
|
credit_note.update_outstanding_for_self = 0
|
||||||
|
|
||||||
|
for item in credit_note.items:
|
||||||
|
item.qty = -item.qty
|
||||||
|
|
||||||
|
credit_note.save()
|
||||||
|
credit_note.submit()
|
||||||
|
|
||||||
|
dunning.reload()
|
||||||
|
self.assertEqual(dunning.status, "Resolved")
|
||||||
|
|
||||||
|
credit_note.cancel()
|
||||||
|
dunning.reload()
|
||||||
|
self.assertEqual(dunning.status, "Unresolved")
|
||||||
|
|
||||||
|
def test_dunning_not_affected_by_standalone_credit_note(self):
|
||||||
|
"""
|
||||||
|
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
|
||||||
|
"""
|
||||||
|
sales_invoice = create_sales_invoice_against_cost_center(
|
||||||
|
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||||
|
)
|
||||||
|
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||||
|
dunning.submit()
|
||||||
|
|
||||||
|
self.assertEqual(dunning.status, "Unresolved")
|
||||||
|
|
||||||
|
credit_note = frappe.copy_doc(sales_invoice)
|
||||||
|
credit_note.is_return = 1
|
||||||
|
credit_note.return_against = sales_invoice.name
|
||||||
|
credit_note.update_outstanding_for_self = 1
|
||||||
|
|
||||||
|
for item in credit_note.items:
|
||||||
|
item.qty = -item.qty
|
||||||
|
|
||||||
|
credit_note.save()
|
||||||
|
|
||||||
|
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
|
||||||
|
credit_note.submit()
|
||||||
|
|
||||||
|
dunning.reload()
|
||||||
|
self.assertEqual(dunning.status, "Unresolved")
|
||||||
|
|
||||||
|
|
||||||
def create_dunning(overdue_days, dunning_type_name=None):
|
def create_dunning(overdue_days, dunning_type_name=None):
|
||||||
posting_date = add_days(today(), -1 * overdue_days)
|
posting_date = add_days(today(), -1 * overdue_days)
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
frappe.throw(_("Difference Amount must be zero"))
|
frappe.throw(_("Difference Amount must be zero"))
|
||||||
self.update_payment_requests()
|
self.update_payment_requests()
|
||||||
|
self.update_payment_schedule()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_payment_schedule()
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def validate_for_repost(self):
|
def validate_for_repost(self):
|
||||||
@@ -221,10 +221,10 @@ class PaymentEntry(AccountsController):
|
|||||||
)
|
)
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
self.update_payment_requests(cancel=True)
|
self.update_payment_requests(cancel=True)
|
||||||
|
self.update_payment_schedule(cancel=1)
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.delink_advance_entry_references()
|
self.delink_advance_entry_references()
|
||||||
self.update_payment_schedule(cancel=1)
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def update_payment_requests(self, cancel=False):
|
def update_payment_requests(self, cancel=False):
|
||||||
|
|||||||
@@ -1906,6 +1906,8 @@ def create_payment_ledger_entry(
|
|||||||
|
|
||||||
|
|
||||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||||
|
from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
|
||||||
|
|
||||||
if not voucher_type or not voucher_no:
|
if not voucher_type or not voucher_no:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1938,6 +1940,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
|||||||
):
|
):
|
||||||
outstanding = voucher_outstanding[0]
|
outstanding = voucher_outstanding[0]
|
||||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||||
|
previous_outstanding_amount = ref_doc.outstanding_amount
|
||||||
outstanding_amount = flt(
|
outstanding_amount = flt(
|
||||||
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
|
||||||
)
|
)
|
||||||
@@ -1951,6 +1954,7 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
|
|||||||
outstanding_amount,
|
outstanding_amount,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
update_linked_dunnings(ref_doc, previous_outstanding_amount)
|
||||||
ref_doc.set_status(update=True)
|
ref_doc.set_status(update=True)
|
||||||
ref_doc.notify_update()
|
ref_doc.notify_update()
|
||||||
|
|
||||||
|
|||||||
@@ -363,7 +363,9 @@ doc_events = {
|
|||||||
"erpnext.regional.create_transaction_log",
|
"erpnext.regional.create_transaction_log",
|
||||||
"erpnext.regional.italy.utils.sales_invoice_on_submit",
|
"erpnext.regional.italy.utils.sales_invoice_on_submit",
|
||||||
],
|
],
|
||||||
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
|
"on_cancel": [
|
||||||
|
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
|
||||||
|
],
|
||||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||||
},
|
},
|
||||||
"Purchase Invoice": {
|
"Purchase Invoice": {
|
||||||
@@ -375,9 +377,7 @@ doc_events = {
|
|||||||
"Payment Entry": {
|
"Payment Entry": {
|
||||||
"on_submit": [
|
"on_submit": [
|
||||||
"erpnext.regional.create_transaction_log",
|
"erpnext.regional.create_transaction_log",
|
||||||
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
|
|
||||||
],
|
],
|
||||||
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
|
||||||
"on_trash": "erpnext.regional.check_deletion_permission",
|
"on_trash": "erpnext.regional.check_deletion_permission",
|
||||||
},
|
},
|
||||||
"Address": {
|
"Address": {
|
||||||
|
|||||||
@@ -674,13 +674,13 @@ def prepare_data_for_internal_transfer():
|
|||||||
company = "_Test Company with perpetual inventory"
|
company = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
customer = create_internal_customer(
|
customer = create_internal_customer(
|
||||||
"_Test Internal Customer 3",
|
"_Test Internal Customer 2",
|
||||||
company,
|
company,
|
||||||
company,
|
company,
|
||||||
)
|
)
|
||||||
|
|
||||||
supplier = create_internal_supplier(
|
supplier = create_internal_supplier(
|
||||||
"_Test Internal Supplier 3",
|
"_Test Internal Supplier 2",
|
||||||
company,
|
company,
|
||||||
company,
|
company,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user