From 2d0dadd9acc370b9559f8d3e70578b6aa29cdf0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 16 Sep 2021 16:42:51 +0200 Subject: [PATCH] feat: rework dunning backend --- erpnext/accounts/doctype/dunning/dunning.py | 51 ++++++------- .../doctype/sales_invoice/sales_invoice.py | 75 ++++++++++--------- 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index b4df0a5270..56d49df4be 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -15,25 +15,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.get_cached_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() @@ -113,20 +122,6 @@ def resolve_dunning(doc, state): 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 2075d57a35..0aa6eab862 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2510,55 +2510,58 @@ def get_mode_of_payment_info(mode_of_payment, company): @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 postprocess_dunning(source, target): + from erpnext.accounts.doctype.dunning.dunning import 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]} - ) + 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()) - 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.outstanding_amount, - target.rate_of_interest, - target.dunning_fee, - target.overdue_days, + letter_text = get_dunning_letter_text( + dunning_type=dunning_type.name, + doc=target.as_dict(), + language=source.language ) - 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, - { + 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') + + 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, - set_missing_values, + target_doc=target_doc, + postprocess=postprocess_dunning, + ignore_permissions=ignore_permissions ) - return doclist + def check_if_return_invoice_linked_with_payment_entry(self):