diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 65ada530df5..8c940208292 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -18,24 +18,34 @@ from erpnext.controllers.accounts_controller import AccountsController class Dunning(AccountsController): + def validate(self): - self.validate_overdue_days() - self.validate_amount() + self.validate_overdue_payments() + self.validate_totals() + if not self.income_account: self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account') - def validate_overdue_days(self): - self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 + def validate_overdue_payments(self): + for row in self.overdue_payments: + row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 + interest_per_year = flt(row.outstanding) * flt(self.rate_of_interest) / 100 + row.interest_amount = (interest_per_year * cint(row.overdue_days)) / 365 - def validate_amount(self): - amounts = calculate_interest_and_amount( - self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) - if self.interest_amount != amounts.get('interest_amount'): - self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) - if self.dunning_amount != amounts.get('dunning_amount'): - self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount')) - if self.grand_total != amounts.get('grand_total'): - self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total')) + def validate_totals(self): + total_outstanding = sum(row.outstanding for row in self.overdue_payments) + total_interest = sum(row.interest_amount for row in self.overdue_payments) + dunning_amount = flt(total_interest) + flt(self.dunning_fee) + grand_total = flt(total_outstanding) + flt(dunning_amount) + + if self.total_outstanding != total_outstanding: + self.total_outstanding = flt(total_outstanding, self.precision('total_outstanding')) + if self.total_interest != total_interest: + self.total_interest = flt(total_interest, self.precision('total_interest')) + if self.dunning_amount != dunning_amount: + self.dunning_amount = flt(dunning_amount, self.precision('dunning_amount')) + if self.grand_total != grand_total: + self.grand_total = flt(grand_total, self.precision('grand_total')) def on_submit(self): self.make_gl_entries() @@ -95,18 +105,6 @@ def resolve_dunning(doc, state): for dunning in dunnings: frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') -def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): - interest_amount = 0 - grand_total = flt(outstanding_amount) + flt(dunning_fee) - if rate_of_interest: - interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 - interest_amount = (interest_per_year * cint(overdue_days)) / 365 - grand_total += flt(interest_amount) - dunning_amount = flt(interest_amount) + flt(dunning_fee) - return { - 'interest_amount': interest_amount, - 'grand_total': grand_total, - 'dunning_amount': dunning_amount} @frappe.whitelist() def get_dunning_letter_text(dunning_type, doc, language=None): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ec249c24194..beba21f9ff4 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2026,42 +2026,58 @@ def get_mode_of_payment_info(mode_of_payment, company): (company, mode_of_payment), as_dict=1) @frappe.whitelist() -def create_dunning(source_name, target_doc=None): +def create_dunning(source_name, target_doc=None, ignore_permissions=False): from frappe.model.mapper import get_mapped_doc - from erpnext.accounts.doctype.dunning.dunning import ( - calculate_interest_and_amount, - get_dunning_letter_text, - ) - def set_missing_values(source, target): - target.sales_invoice = source_name - target.outstanding_amount = source.outstanding_amount - overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days - target.overdue_days = overdue_days - if frappe.db.exists('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}): - dunning_type = frappe.get_doc('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}) + def postprocess_dunning(source, target): + from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text + + dunning_type = frappe.db.exists('Dunning Type', {'is_default': 1}) + if dunning_type: + dunning_type = frappe.get_doc("Dunning Type", dunning_type) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict()) + letter_text = get_dunning_letter_text( + dunning_type=dunning_type.name, + doc=target.as_dict(), + language=source.language + ) + if letter_text: target.body_text = letter_text.get('body_text') target.closing_text = letter_text.get('closing_text') target.language = letter_text.get('language') - amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount, - target.rate_of_interest, target.dunning_fee, target.overdue_days) - target.interest_amount = amounts.get('interest_amount') - target.dunning_amount = amounts.get('dunning_amount') - target.grand_total = amounts.get('grand_total') - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Dunning", - } - }, target_doc, set_missing_values) - return doclist + def postprocess_overdue_payment(source, target, source_parent): + target.overdue_days = (getdate(nowdate()) - getdate(source.due_date)).days + + return get_mapped_doc( + from_doctype="Sales Invoice", + from_docname=source_name, + table_maps={ + "Sales Invoice": { + "doctype": "Dunning", + "field_map": { + "customer_address": "customer_address", + "parent": "sales_invoice" + }, + }, + "Payment Schedule": { + "doctype": "Overdue Payment", + "field_map": { + "name": "payment_schedule", + "parent": "sales_invoice" + }, + "condition": lambda doc: doc.outstanding > 0, + "postprocess": postprocess_overdue_payment + } + }, + target_doc=target_doc, + postprocess=postprocess_dunning, + ignore_permissions=ignore_permissions + ) + def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices,