Merge pull request #37859 from marination/split-invoice-refactor
fix: `split_invoices_based_on_payment_terms`
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c0e3ab3..fc22f53 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1684,13 +1684,42 @@
return data
-def split_invoices_based_on_payment_terms(outstanding_invoices, company):
- invoice_ref_based_on_payment_terms = {}
+def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
+ """Split a list of invoices based on their payment terms."""
+ exc_rates = get_currency_data(outstanding_invoices, company)
+ outstanding_invoices_after_split = []
+ for entry in outstanding_invoices:
+ if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
+ if payment_term_template := frappe.db.get_value(
+ entry.voucher_type, entry.voucher_no, "payment_terms_template"
+ ):
+ split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
+ if not split_rows:
+ continue
+
+ frappe.msgprint(
+ _("Splitting {0} {1} into {2} rows as per Payment Terms").format(
+ _(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
+ ),
+ alert=True,
+ )
+ outstanding_invoices_after_split += split_rows
+ continue
+
+ # If not an invoice or no payment terms template, add as it is
+ outstanding_invoices_after_split.append(entry)
+
+ return outstanding_invoices_after_split
+
+
+def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
+ """Get currency and conversion data for a list of invoices."""
+ exc_rates = frappe._dict()
company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None
)
- exc_rates = frappe._dict()
+
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all(
@@ -1705,72 +1734,54 @@
company_currency=company_currency,
)
- for idx, d in enumerate(outstanding_invoices):
- if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
- payment_term_template = frappe.db.get_value(
- d.voucher_type, d.voucher_no, "payment_terms_template"
+ return exc_rates
+
+
+def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
+ """Split invoice based on its payment schedule table."""
+ split_rows = []
+ allocate_payment_based_on_payment_terms = frappe.db.get_value(
+ "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
+ )
+
+ if not allocate_payment_based_on_payment_terms:
+ return [invoice]
+
+ payment_schedule = frappe.get_all(
+ "Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
+ )
+ for payment_term in payment_schedule:
+ if not payment_term.outstanding > 0.1:
+ continue
+
+ doc_details = exc_rates.get(payment_term.parent, None)
+ is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
+ doc_details.party_account_currency != doc_details.company_currency
+ )
+ payment_term_outstanding = flt(payment_term.outstanding)
+ if not is_multi_currency_acc:
+ payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
+
+ split_rows.append(
+ frappe._dict(
+ {
+ "due_date": invoice.due_date,
+ "currency": invoice.currency,
+ "voucher_no": invoice.voucher_no,
+ "voucher_type": invoice.voucher_type,
+ "posting_date": invoice.posting_date,
+ "invoice_amount": flt(invoice.invoice_amount),
+ "outstanding_amount": payment_term_outstanding
+ if payment_term_outstanding
+ else invoice.outstanding_amount,
+ "payment_term_outstanding": payment_term_outstanding,
+ "payment_amount": payment_term.payment_amount,
+ "payment_term": payment_term.payment_term,
+ }
)
- if payment_term_template:
- allocate_payment_based_on_payment_terms = frappe.get_cached_value(
- "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
- )
- if allocate_payment_based_on_payment_terms:
- payment_schedule = frappe.get_all(
- "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
- )
+ )
- for payment_term in payment_schedule:
- if payment_term.outstanding > 0.1:
- doc_details = exc_rates.get(payment_term.parent, None)
- is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
- doc_details.party_account_currency != doc_details.company_currency
- )
- payment_term_outstanding = flt(payment_term.outstanding)
- if not is_multi_currency_acc:
- payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
-
- invoice_ref_based_on_payment_terms.setdefault(idx, [])
- invoice_ref_based_on_payment_terms[idx].append(
- frappe._dict(
- {
- "due_date": d.due_date,
- "currency": d.currency,
- "voucher_no": d.voucher_no,
- "voucher_type": d.voucher_type,
- "posting_date": d.posting_date,
- "invoice_amount": flt(d.invoice_amount),
- "outstanding_amount": payment_term_outstanding
- if payment_term_outstanding
- else d.outstanding_amount,
- "payment_term_outstanding": payment_term_outstanding,
- "payment_amount": payment_term.payment_amount,
- "payment_term": payment_term.payment_term,
- "account": d.account,
- }
- )
- )
-
- outstanding_invoices_after_split = []
- if invoice_ref_based_on_payment_terms:
- for idx, ref in invoice_ref_based_on_payment_terms.items():
- voucher_no = ref[0]["voucher_no"]
- voucher_type = ref[0]["voucher_type"]
-
- frappe.msgprint(
- _("Spliting {} {} into {} row(s) as per Payment Terms").format(
- voucher_type, voucher_no, len(ref)
- ),
- alert=True,
- )
-
- outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
-
- existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
- index = outstanding_invoices.index(existing_row[0])
- outstanding_invoices.pop(index)
-
- outstanding_invoices_after_split += outstanding_invoices
- return outstanding_invoices_after_split
+ return split_rows
def get_orders_to_be_billed(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index b6b93b6..603f24a 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -6,10 +6,11 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
-from frappe.utils import flt, nowdate
+from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
+ get_outstanding_reference_documents,
get_payment_entry,
get_reference_details,
)
@@ -1249,6 +1250,45 @@
so.reload()
self.assertEqual(so.advance_paid, so.rounded_total)
+ def test_outstanding_invoices_api(self):
+ """
+ Test if `get_outstanding_reference_documents` fetches invoices in the right order.
+ """
+ customer = create_customer("Max Mustermann", "INR")
+ create_payment_terms_template()
+
+ # SI has an earlier due date and SI2 has a later due date
+ si = create_sales_invoice(
+ qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
+ )
+ si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
+ si2.payment_terms_template = "Test Receivable Template"
+ si2.submit()
+
+ args = {
+ "posting_date": nowdate(),
+ "company": "_Test Company",
+ "party_type": "Customer",
+ "payment_type": "Pay",
+ "party": customer,
+ "party_account": "Debtors - _TC",
+ }
+ args.update(
+ {
+ "get_outstanding_invoices": True,
+ "from_posting_date": add_days(nowdate(), -4),
+ "to_posting_date": add_days(nowdate(), 2),
+ }
+ )
+ references = get_outstanding_reference_documents(args)
+
+ self.assertEqual(len(references), 3)
+ self.assertEqual(references[0].voucher_no, si.name)
+ self.assertEqual(references[1].voucher_no, si2.name)
+ self.assertEqual(references[2].voucher_no, si2.name)
+ self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
+ self.assertEqual(references[2].payment_term, "Tax Receivable")
+
def test_receive_payment_from_payable_party_type(self):
pe = create_payment_entry(
party_type="Supplier",
@@ -1342,6 +1382,9 @@
def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
+ """
+ Create a Payment Terms Template with % or amount discount.
+ """
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"