diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 69debbec5c7..5f191e4800a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -21,6 +21,7 @@ "party_name", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", + "advance_reconciliation_takes_effect_on", "column_break_11", "bank_account", "party_bank_account", @@ -782,6 +783,16 @@ "options": "No\nYes", "print_hide": 1, "search_index": 1 + }, + { + "default": "Oldest Of Invoice Or Advance", + "fetch_from": "company.reconciliation_takes_effect_on", + "fieldname": "advance_reconciliation_takes_effect_on", + "fieldtype": "Select", + "hidden": 1, + "label": "Advance Reconciliation Takes Effect On", + "no_copy": 1, + "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" } ], "index_web_pages_for_search": 1, @@ -795,7 +806,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-11-07 11:19:19.320883", + "modified": "2025-01-13 16:03:47.169699", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7e3d8a5833b..d714df0927b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1401,16 +1401,26 @@ class PaymentEntry(AccountsController): "voucher_detail_no": invoice.name, } - if self.reconcile_on_advance_payment_date: - posting_date = self.posting_date + if invoice.reconcile_effect_on: + posting_date = invoice.reconcile_effect_on else: - date_field = "posting_date" - if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: - date_field = "transaction_date" - posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field) - - if getdate(posting_date) < getdate(self.posting_date): + # For backwards compatibility + # Supporting reposting on payment entries reconciled before select field introduction + if self.advance_reconciliation_takes_effect_on == "Advance Payment Date": posting_date = self.posting_date + elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + date_field = "posting_date" + if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: + date_field = "transaction_date" + posting_date = frappe.db.get_value( + invoice.reference_doctype, invoice.reference_name, date_field + ) + + if getdate(posting_date) < getdate(self.posting_date): + posting_date = self.posting_date + elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date": + posting_date = nowdate() + frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) dr_or_cr, account = self.get_dr_and_account_for_advances(invoice) args_dict["account"] = account diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 361f516b830..57e401275fd 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -13,6 +13,7 @@ "payment_term_outstanding", "account_type", "payment_type", + "reconcile_effect_on", "column_break_4", "total_amount", "outstanding_amount", @@ -144,12 +145,18 @@ "is_virtual": 1, "label": "Payment Request Outstanding", "read_only": 1 + }, + { + "fieldname": "reconcile_effect_on", + "fieldtype": "Date", + "label": "Reconcile Effect On", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-09-16 18:11:50.019343", + "modified": "2025-01-13 15:56:18.895082", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py index 2ac92ba4a84..1d869b92715 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -30,6 +30,7 @@ class PaymentEntryReference(Document): payment_term: DF.Link | None payment_term_outstanding: DF.Float payment_type: DF.Data | None + reconcile_effect_on: DF.Date | None reference_doctype: DF.Link reference_name: DF.DynamicLink total_amount: DF.Float diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 3f0fb29d671..061bbf556fc 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -6,6 +6,7 @@ import frappe from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_years, flt, getdate, nowdate, today +from frappe.utils.data import getdate as convert_to_date from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -1671,7 +1672,7 @@ class TestPaymentReconciliation(FrappeTestCase): { "book_advance_payments_in_separate_party_account": 1, "default_advance_paid_account": self.advance_payable_account, - "reconcile_on_advance_payment_date": 1, + "reconciliation_takes_effect_on": "Advance Payment Date", }, ) @@ -1720,7 +1721,7 @@ class TestPaymentReconciliation(FrappeTestCase): { "book_advance_payments_in_separate_party_account": 1, "default_advance_received_account": self.advance_receivable_account, - "reconcile_on_advance_payment_date": 0, + "reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance", }, ) amount = 200.0 @@ -1829,7 +1830,7 @@ class TestPaymentReconciliation(FrappeTestCase): { "book_advance_payments_in_separate_party_account": 1, "default_advance_paid_account": self.advance_payable_account, - "reconcile_on_advance_payment_date": 0, + "reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance", }, ) amount = 200.0 @@ -2048,6 +2049,102 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.get("invoices"), []) self.assertEqual(pr.get("payments"), []) + def test_advance_reconciliation_effect_on_same_date(self): + frappe.db.set_value( + "Company", + self.company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_received_account": self.advance_receivable_account, + "reconciliation_takes_effect_on": "Reconciliation Date", + }, + ) + inv_date = convert_to_date(add_days(nowdate(), -1)) + adv_date = convert_to_date(add_days(nowdate(), -2)) + + si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=200) + pe = self.create_payment_entry(posting_date=adv_date, amount=80).save().submit() + + pr = self.create_payment_reconciliation() + pr.from_invoice_date = add_days(nowdate(), -1) + pr.to_invoice_date = nowdate() + pr.from_payment_date = add_days(nowdate(), -2) + pr.to_payment_date = nowdate() + pr.default_advance_account = self.advance_receivable_account + + # reconcile multiple payments against invoice + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + + pr.reconcile() + + si.reload() + self.assertEqual(si.status, "Partly Paid") + # check PR tool output post reconciliation + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120) + self.assertEqual(pr.get("payments"), []) + + # Assert Ledger Entries + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name}, + fields=["account", "posting_date", "voucher_no", "against_voucher", "debit", "credit"], + order_by="account, against_voucher, debit", + ) + + expected_gl = [ + { + "account": self.advance_receivable_account, + "posting_date": adv_date, + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": 0.0, + "credit": 80.0, + }, + { + "account": self.advance_receivable_account, + "posting_date": convert_to_date(nowdate()), + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": 80.0, + "credit": 0.0, + }, + { + "account": self.debit_to, + "posting_date": convert_to_date(nowdate()), + "voucher_no": pe.name, + "against_voucher": si.name, + "debit": 0.0, + "credit": 80.0, + }, + { + "account": self.bank, + "posting_date": adv_date, + "voucher_no": pe.name, + "against_voucher": None, + "debit": 80.0, + "credit": 0.0, + }, + ] + + self.assertEqual(expected_gl, gl_entries) + + # cancel PE + pe.reload() + pe.cancel() + pr.get_unreconciled_entries() + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 1) + self.assertEqual(len(pr.get("payments")), 0) + self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 51b4ed248ce..ae90819b409 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -712,6 +712,23 @@ def update_reference_in_payment_entry( } update_advance_paid = [] + # Update Reconciliation effect date in reference + if payment_entry.book_advance_payments_in_separate_party_account: + if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date": + reconcile_on = payment_entry.posting_date + elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + date_field = "posting_date" + if d.against_voucher_type in ["Sales Order", "Purchase Order"]: + date_field = "transaction_date" + reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field) + + if getdate(reconcile_on) < getdate(payment_entry.posting_date): + reconcile_on = payment_entry.posting_date + elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date": + reconcile_on = nowdate() + + reference_details.update({"reconcile_effect_on": reconcile_on}) + if d.voucher_detail_no: existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cfc8be90b4b..a16f87de0dc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -387,3 +387,4 @@ erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions erpnext.patches.v15_0.enable_allow_existing_serial_no erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts erpnext.patches.v15_0.update_asset_status_to_work_in_progress +erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect diff --git a/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py b/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py new file mode 100644 index 00000000000..883921cfdf8 --- /dev/null +++ b/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py @@ -0,0 +1,18 @@ +import frappe + + +def execute(): + """ + A New select field 'reconciliation_takes_effect_on' has been added to control Advance Payment Reconciliation dates. + Migrate old checkbox configuration to new select field on 'Company' and 'Payment Entry' + """ + companies = frappe.db.get_all("Company", fields=["name", "reconciliation_takes_effect_on"]) + for x in companies: + new_value = ( + "Advance Payment Date" if x.reconcile_on_advance_payment_date else "Oldest Of Invoice Or Advance" + ) + frappe.db.set_value("Company", x.name, "reconciliation_takes_effect_on", new_value) + + frappe.db.sql( + """update `tabPayment Entry` set advance_reconciliation_takes_effect_on = if(reconcile_on_advance_payment_date = 0, 'Oldest Of Invoice Or Advance', 'Advance Payment Date')""" + ) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 271b440fbda..ae473e15917 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -75,6 +75,7 @@ "advance_payments_section", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", + "reconciliation_takes_effect_on", "column_break_fwcf", "default_advance_received_account", "default_advance_paid_account", @@ -796,6 +797,7 @@ "description": "If Enabled - Reconciliation happens on the Advance Payment posting date
\nIf Disabled - Reconciliation happens on oldest of 2 Dates: Invoice Date or the Advance Payment posting date
\n", "fieldname": "reconcile_on_advance_payment_date", "fieldtype": "Check", + "hidden": 1, "label": "Reconcile on Advance Payment Date" }, { @@ -841,6 +843,13 @@ { "fieldname": "column_break_dcdl", "fieldtype": "Column Break" + }, + { + "default": "Oldest Of Invoice Or Advance", + "fieldname": "reconciliation_takes_effect_on", + "fieldtype": "Select", + "label": "Reconciliation Takes Effect On", + "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" } ], "icon": "fa fa-building", @@ -848,7 +857,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-12-02 15:37:32.723176", + "modified": "2025-01-09 20:12:25.471544", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 27005d99301..8ae843abc24 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -87,6 +87,9 @@ class Company(NestedSet): payment_terms: DF.Link | None phone_no: DF.Data | None reconcile_on_advance_payment_date: DF.Check + reconciliation_takes_effect_on: DF.Literal[ + "Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date" + ] registration_details: DF.Code | None rgt: DF.Int round_off_account: DF.Link | None