diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json index 290ed11c98e..5ad2479e858 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.json @@ -12,7 +12,8 @@ "against_voucher_no", "amount", "currency", - "event" + "event", + "delinked" ], "fields": [ { @@ -68,12 +69,20 @@ "label": "Company", "options": "Company", "read_only": 1 + }, + { + "default": "0", + "fieldname": "delinked", + "fieldtype": "Check", + "label": "DeLinked", + "read_only": 1 } ], + "grid_page_length": 50, "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-11-05 10:31:28.736671", + "modified": "2025-07-29 11:37:42.678556", "modified_by": "Administrator", "module": "Accounts", "name": "Advance Payment Ledger Entry", @@ -107,7 +116,8 @@ "share": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py index 0ec2d411761..a9392fc9367 100644 --- a/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/advance_payment_ledger_entry/advance_payment_ledger_entry.py @@ -1,9 +1,11 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from erpnext.accounts.utils import update_voucher_outstanding + class AdvancePaymentLedgerEntry(Document): # begin: auto-generated types @@ -19,9 +21,16 @@ class AdvancePaymentLedgerEntry(Document): amount: DF.Currency company: DF.Link | None currency: DF.Link | None + delinked: DF.Check event: DF.Data | None voucher_no: DF.DynamicLink | None voucher_type: DF.Link | None # end: auto-generated types - pass + def on_update(self): + if ( + self.against_voucher_type in ["Purchase Order", "Sales Order"] + and self.flags.update_outstanding == "Yes" + and not frappe.flags.is_reverse_depr_entry + ): + update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d7386902c81..4341aef9a6a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -191,8 +191,6 @@ class JournalEntry(AccountsController): self.validate_cheque_info() self.check_credit_limit() self.make_gl_entries() - self.make_advance_payment_ledger_entries() - self.update_advance_paid() self.update_asset_value() self.update_inter_company_jv() self.update_invoice_discounting() @@ -225,8 +223,6 @@ class JournalEntry(AccountsController): "Advance Payment Ledger Entry", ) self.make_gl_entries(1) - self.make_advance_payment_ledger_entries() - self.update_advance_paid() self.unlink_advance_entry_reference() self.unlink_asset_reference() self.unlink_inter_company_jv() @@ -237,18 +233,6 @@ class JournalEntry(AccountsController): def get_title(self): return self.pay_to_recd_from or self.accounts[0].account - def update_advance_paid(self): - advance_paid = frappe._dict() - advance_payment_doctypes = get_advance_payment_doctypes() - for d in self.get("accounts"): - if d.is_advance: - if d.reference_type in advance_payment_doctypes: - advance_paid.setdefault(d.reference_type, []).append(d.reference_name) - - for voucher_type, order_list in advance_paid.items(): - for voucher_no in list(set(order_list)): - frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid() - def validate_inter_company_accounts(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: doc = frappe.db.get_value( @@ -1094,49 +1078,65 @@ class JournalEntry(AccountsController): self.transaction_exchange_rate = row.exchange_rate break + advance_doctypes = get_advance_payment_doctypes() + for d in self.get("accounts"): if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"): r = [d.user_remark, self.remark] r = [x for x in r if x] remarks = "\n".join(r) + row = { + "account": d.account, + "party_type": d.party_type, + "due_date": self.due_date, + "party": d.party, + "against": d.against_account, + "debit": flt(d.debit, d.precision("debit")), + "credit": flt(d.credit, d.precision("credit")), + "account_currency": d.account_currency, + "debit_in_account_currency": flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ), + "credit_in_account_currency": flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ), + "transaction_currency": self.transaction_currency, + "transaction_exchange_rate": self.transaction_exchange_rate, + "debit_in_transaction_currency": flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ) + if self.transaction_currency == d.account_currency + else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate, + "credit_in_transaction_currency": flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ) + if self.transaction_currency == d.account_currency + else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate, + "against_voucher_type": d.reference_type, + "against_voucher": d.reference_name, + "remarks": remarks, + "voucher_detail_no": d.reference_detail_no, + "cost_center": d.cost_center, + "project": d.project, + "finance_book": self.finance_book, + "advance_voucher_type": d.advance_voucher_type, + "advance_voucher_no": d.advance_voucher_no, + } + + if d.reference_type in advance_doctypes: + row.update( + { + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "advance_voucher_type": d.reference_type, + "advance_voucher_no": d.reference_name, + } + ) + gl_map.append( self.get_gl_dict( - { - "account": d.account, - "party_type": d.party_type, - "due_date": self.due_date, - "party": d.party, - "against": d.against_account, - "debit": flt(d.debit, d.precision("debit")), - "credit": flt(d.credit, d.precision("credit")), - "account_currency": d.account_currency, - "debit_in_account_currency": flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ), - "credit_in_account_currency": flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ), - "transaction_currency": self.transaction_currency, - "transaction_exchange_rate": self.transaction_exchange_rate, - "debit_in_transaction_currency": flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ) - if self.transaction_currency == d.account_currency - else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate, - "credit_in_transaction_currency": flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ) - if self.transaction_currency == d.account_currency - else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate, - "against_voucher_type": d.reference_type, - "against_voucher": d.reference_name, - "remarks": remarks, - "voucher_detail_no": d.reference_detail_no, - "cost_center": d.cost_center, - "project": d.project, - "finance_book": self.finance_book, - }, + row, item=d, ) ) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 00fd7b9d0ab..45c2b4ce764 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -32,6 +32,8 @@ "reference_name", "reference_due_date", "reference_detail_no", + "advance_voucher_type", + "advance_voucher_no", "col_break3", "is_advance", "user_remark", @@ -263,12 +265,28 @@ "label": "Reference Detail No", "no_copy": 1, "search_index": 1 + }, + { + "fieldname": "advance_voucher_type", + "fieldtype": "Link", + "label": "Advance Voucher Type", + "no_copy": 1, + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "advance_voucher_no", + "fieldtype": "Dynamic Link", + "label": "Advance Voucher No", + "no_copy": 1, + "options": "advance_voucher_type", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-05 01:10:50.224840", + "modified": "2025-07-25 04:45:28.117715", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", @@ -279,4 +297,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py index 00c9dcb374b..b801ac8c9a5 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.py @@ -17,8 +17,9 @@ class JournalEntryAccount(Document): account: DF.Link account_currency: DF.Link | None account_type: DF.Data | None + advance_voucher_no: DF.DynamicLink | None + advance_voucher_type: DF.Link | None against_account: DF.Text | None - balance: DF.Currency bank_account: DF.Link | None cost_center: DF.Link | None credit: DF.Currency @@ -31,7 +32,6 @@ class JournalEntryAccount(Document): parentfield: DF.Data parenttype: DF.Data party: DF.DynamicLink | None - party_balance: DF.Currency party_type: DF.Link | None project: DF.Link | None reference_detail_no: DF.Data | None diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 6e8e5e02451..b034e48e3ec 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -117,12 +117,10 @@ class PaymentEntry(AccountsController): def on_submit(self): if self.difference_amount: frappe.throw(_("Difference Amount must be zero")) + self.update_payment_requests() self.make_gl_entries() self.update_outstanding_amounts() self.update_payment_schedule() - self.update_payment_requests() - self.make_advance_payment_ledger_entries() - self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def validate_for_repost(self): @@ -222,13 +220,11 @@ class PaymentEntry(AccountsController): "Advance Payment Ledger Entry", ) super().on_cancel() + self.update_payment_requests(cancel=True) self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) - self.update_payment_requests(cancel=True) - self.make_advance_payment_ledger_entries() - self.update_advance_paid() # advance_paid_status depends on the payment request amount self.set_status() def update_payment_requests(self, cancel=False): @@ -1366,23 +1362,27 @@ class PaymentEntry(AccountsController): dr_or_cr + "_in_transaction_currency": d.allocated_amount if self.transaction_currency == self.party_account_currency else allocated_amount_in_company_currency / self.transaction_exchange_rate, + "advance_voucher_type": d.advance_voucher_type, + "advance_voucher_no": d.advance_voucher_no, }, item=self, ) ) - if self.book_advance_payments_in_separate_party_account: - if d.reference_doctype in advance_payment_doctypes: - # Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine - gle.update( - { - "against_voucher_type": d.reference_doctype, - "against_voucher": d.reference_name, - } - ) - else: - # Do not reference Invoices while Advance is in separate party account - gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name}) + if d.reference_doctype in advance_payment_doctypes: + # advance reference + gle.update( + { + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "advance_voucher_type": d.reference_doctype, + "advance_voucher_no": d.reference_name, + } + ) + + elif self.book_advance_payments_in_separate_party_account: + # Do not reference Invoices while Advance is in separate party account + gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name}) else: gle.update( { @@ -1512,6 +1512,8 @@ class PaymentEntry(AccountsController): { "against_voucher_type": invoice.reference_doctype, "against_voucher": invoice.reference_name, + "advance_voucher_type": invoice.advance_voucher_type, + "advance_voucher_no": invoice.advance_voucher_no, "posting_date": posting_date, } ) @@ -1536,6 +1538,8 @@ class PaymentEntry(AccountsController): { "against_voucher_type": "Payment Entry", "against_voucher": self.name, + "advance_voucher_type": invoice.advance_voucher_type, + "advance_voucher_no": invoice.advance_voucher_no, } ) gle = self.get_gl_dict( @@ -1684,17 +1688,6 @@ class PaymentEntry(AccountsController): return flt(gl_dict.get(field, 0) / (conversion_rate or 1)) - def update_advance_paid(self): - if self.payment_type not in ("Receive", "Pay") or not self.party: - return - - advance_payment_doctypes = get_advance_payment_doctypes() - for d in self.get("references"): - if d.allocated_amount and d.reference_doctype in advance_payment_doctypes: - frappe.get_doc( - d.reference_doctype, d.reference_name, for_update=True - ).set_total_advance_paid() - def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 1d0810852f0..da6c2eefae4 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -52,7 +52,7 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(pe.paid_to_account_type, "Cash") expected_gle = dict( - (d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]] + (d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]] ) self.validate_gl_entries(pe.name, expected_gle) @@ -84,7 +84,7 @@ class TestPaymentEntry(FrappeTestCase): expected_gle = dict( (d[0], d) - for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]] + for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]] ) self.validate_gl_entries(pe.name, expected_gle) 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 57e401275fd..111de2ebd4e 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -22,7 +22,9 @@ "exchange_gain_loss", "account", "payment_request", - "payment_request_outstanding" + "payment_request_outstanding", + "advance_voucher_type", + "advance_voucher_no" ], "fields": [ { @@ -151,12 +153,28 @@ "fieldtype": "Date", "label": "Reconcile Effect On", "read_only": 1 + }, + { + "columns": 2, + "fieldname": "advance_voucher_type", + "fieldtype": "Link", + "label": "Advance Voucher Type", + "options": "DocType", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "advance_voucher_no", + "fieldtype": "Dynamic Link", + "label": "Advance Voucher No", + "options": "advance_voucher_type", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-01-13 15:56:18.895082", + "modified": "2025-07-25 04:32:11.040025", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", @@ -167,4 +185,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} 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 1d869b92715..a5e0b21a9af 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py @@ -16,6 +16,8 @@ class PaymentEntryReference(Document): account: DF.Link | None account_type: DF.Data | None + advance_voucher_no: DF.DynamicLink | None + advance_voucher_type: DF.Link | None allocated_amount: DF.Float bill_no: DF.Data | None due_date: DF.Date | None @@ -26,7 +28,6 @@ class PaymentEntryReference(Document): parentfield: DF.Data parenttype: DF.Data payment_request: DF.Link | None - payment_request_outstanding: DF.Float payment_term: DF.Link | None payment_term_outstanding: DF.Float payment_type: DF.Data | None diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json index 28c9529995d..30a2da24bbb 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.json @@ -197,4 +197,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index bd5ba42b0b6..679eb90f19a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -2206,6 +2206,136 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("payments")), 0) self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200) + def test_partial_advance_payment_with_closed_fiscal_year(self): + """ + Test Advance Payment partial reconciliation before period closing and partial after period closing + """ + default_settings = frappe.db.get_value( + "Company", + self.company, + [ + "book_advance_payments_in_separate_party_account", + "default_advance_paid_account", + "reconciliation_takes_effect_on", + ], + as_dict=True, + ) + first_fy_start_date = frappe.db.get_value("Fiscal Year", {"disabled": 0}, "min(year_start_date)") + prev_fy_start_date = add_years(first_fy_start_date, -1) + prev_fy_end_date = add_days(first_fy_start_date, -1) + + create_fiscal_year( + company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date + ) + + frappe.db.set_value( + "Company", + self.company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_paid_account": self.advance_payable_account, + "reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance", + }, + ) + + self.supplier = "_Test Supplier" + + # Create advance payment of 1000 (previous FY) + pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date) + pe.party_type = "Supplier" + pe.party = self.supplier + pe.payment_type = "Pay" + pe.paid_from = self.cash + pe.paid_to = self.advance_payable_account + pe.save().submit() + + # Create purchase invoice of 600 (previous FY) + pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True) + pi1.posting_date = prev_fy_start_date + pi1.set_posting_time = 1 + pi1.supplier = self.supplier + pi1.credit_to = self.creditors + pi1.save().submit() + + # Reconcile advance payment + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.party = self.supplier + pr.receivable_payable_account = self.creditors + pr.default_advance_account = self.advance_payable_account + pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date + pr.from_payment_date = pr.to_payment_date = pe.posting_date + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name] + payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + # Verify partial reconciliation + pe.reload() + pi1.reload() + + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.references[0].allocated_amount, 600) + self.assertEqual(flt(pe.unallocated_amount), 400) + + self.assertEqual(pi1.outstanding_amount, 0) + self.assertEqual(pi1.status, "Paid") + + # Close accounting period for March (previous FY) + pcv = make_period_closing_voucher( + company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date + ) + pcv.reload() + self.assertEqual(pcv.gle_processing_status, "Completed") + + # Change reconciliation setting to "Reconciliation Date" + frappe.db.set_value( + "Company", + self.company, + "reconciliation_takes_effect_on", + "Reconciliation Date", + ) + + # Create new purchase invoice for 400 in new fiscal year + pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True) + pi2.posting_date = today() + pi2.set_posting_time = 1 + pi2.supplier = self.supplier + pi2.currency = "INR" + pi2.credit_to = self.creditors + pi2.save() + pi2.submit() + + # Allocate 600 from advance payment to purchase invoice + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.party = self.supplier + pr.receivable_payable_account = self.creditors + pr.default_advance_account = self.advance_payable_account + pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date + pr.from_payment_date = pr.to_payment_date = pe.posting_date + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name] + payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pe.reload() + pi2.reload() + + # Assert advance payment is fully allocated + self.assertEqual(len(pe.references), 2) + self.assertEqual(flt(pe.unallocated_amount), 0) + + # Assert new invoice is fully paid + self.assertEqual(pi2.outstanding_amount, 0) + self.assertEqual(pi2.status, "Paid") + + # Verify reconciliation dates are correct based on company setting + self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date)) + self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date)) + + frappe.db.set_value("Company", self.company, default_settings) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index f38c58d6d32..c3f73f0fc12 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -328,7 +328,7 @@ class TestPaymentRequest(FrappeTestCase): self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount self.assertEqual(pe.references[0].allocated_amount, 800) - self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero + self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero self.assertEqual(pe.references[0].payment_request, pr.name) so.load_from_db() diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 800f1647471..aec26b280c4 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -169,6 +169,10 @@ def start_repost(account_repost_doc=str) -> None: frappe.db.delete( "Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} ) + frappe.db.delete( + "Advance Payment Ledger Entry", + filters={"voucher_type": doc.doctype, "voucher_no": doc.name}, + ) if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if not repost_doc.delete_cancelled_entries: diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py index 6b90300a899..6fd1b0f2bf2 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.py @@ -8,7 +8,7 @@ from frappe import _, qb from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry +from erpnext.accounts.utils import _delete_adv_pl_entries, _delete_pl_entries, create_payment_ledger_entry VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"] @@ -16,6 +16,7 @@ VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None): if voucher_type and voucher_no and gle_map: _delete_pl_entries(voucher_type, voucher_no) + _delete_adv_pl_entries(voucher_type, voucher_no) create_payment_ledger_entry(gle_map, cancel=0) diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 021703f6483..256d7870714 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -465,10 +465,26 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) - # Assert 'Advance Paid' so.reload() self.assertEqual(so.advance_paid, 1000) + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + unreconcile.allocations = [x for x in unreconcile.allocations if x.reference_name == si.name] + unreconcile.save().submit() + + # after unreconcilaition advance paid will be reduced + # Assert 'Advance Paid' + so.reload() + self.assertEqual(so.advance_paid, 0) + self.disable_advance_as_liability() def test_unreconcile_advance_from_journal_entry(self): diff --git a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py index f7c826bf3fc..02cad39de8f 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py @@ -12,7 +12,6 @@ from frappe.utils.data import comma_and from erpnext.accounts.utils import ( cancel_exchange_gain_loss_journal, - get_advance_payment_doctypes, unlink_ref_doc_from_payment_entries, update_voucher_outstanding, ) @@ -45,31 +44,12 @@ class UnreconcilePayment(Document): @frappe.whitelist() def get_allocations_from_payment(self): - allocated_references = [] - ple = qb.DocType("Payment Ledger Entry") - allocated_references = ( - qb.from_(ple) - .select( - ple.account, - ple.party_type, - ple.party, - ple.against_voucher_type.as_("reference_doctype"), - ple.against_voucher_no.as_("reference_name"), - Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), - ple.account_currency, - ) - .where( - (ple.docstatus == 1) - & (ple.voucher_type == self.voucher_type) - & (ple.voucher_no == self.voucher_no) - & (ple.voucher_no != ple.against_voucher_no) - ) - .groupby(ple.against_voucher_type, ple.against_voucher_no) - .run(as_dict=True) + return get_linked_payments_for_doc( + company=self.company, + doctype=self.voucher_type, + docname=self.voucher_no, ) - return allocated_references - def add_references(self): allocations = self.get_allocations_from_payment() @@ -82,42 +62,43 @@ class UnreconcilePayment(Document): doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc, self.voucher_no) cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no) + + # update outstanding amounts update_voucher_outstanding( - alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party + alloc.reference_doctype, + alloc.reference_name, + alloc.account, + alloc.party_type, + alloc.party, ) - if doc.doctype in get_advance_payment_doctypes(): - self.make_advance_payment_ledger(alloc) - doc.set_total_advance_paid() frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) - def make_advance_payment_ledger(self, alloc): - if alloc.allocated_amount > 0: - doc = frappe.new_doc("Advance Payment Ledger Entry") - doc.company = self.company - doc.voucher_type = self.voucher_type - doc.voucher_no = self.voucher_no - doc.against_voucher_type = alloc.reference_doctype - doc.against_voucher_no = alloc.reference_name - doc.amount = -1 * alloc.allocated_amount - doc.event = "Unreconcile" - doc.currency = alloc.account_currency - doc.flags.ignore_permissions = 1 - doc.save() - @frappe.whitelist() def doc_has_references(doctype: str | None = None, docname: str | None = None): + count = 0 if doctype in ["Sales Invoice", "Purchase Invoice"]: - return frappe.db.count( + count = frappe.db.count( "Payment Ledger Entry", filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]}, ) else: - return frappe.db.count( + count = frappe.db.count( "Payment Ledger Entry", filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]}, ) + count += frappe.db.count( + "Advance Payment Ledger Entry", + filters={ + "delinked": 0, + "voucher_no": docname, + "voucher_type": doctype, + "event": ["=", "Submit"], + }, + ) + + return count @frappe.whitelist() @@ -139,9 +120,12 @@ def get_linked_payments_for_doc( res = ( qb.from_(ple) .select( + ple.account, + ple.party_type, + ple.party, ple.company, - ple.voucher_type, - ple.voucher_no, + ple.voucher_type.as_("reference_doctype"), + ple.voucher_no.as_("reference_name"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ple.account_currency, ) @@ -163,19 +147,52 @@ def get_linked_payments_for_doc( qb.from_(ple) .select( ple.company, - ple.against_voucher_type.as_("voucher_type"), - ple.against_voucher_no.as_("voucher_no"), + ple.account, + ple.party_type, + ple.party, + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) ) + res = query.run(as_dict=True) + + res += get_linked_advances(company, _dn) + return res + return [] +def get_linked_advances(company, docname): + adv = qb.DocType("Advance Payment Ledger Entry") + criteria = [ + (adv.company == company), + (adv.delinked == 0), + (adv.voucher_no == docname), + (adv.event == "Submit"), + ] + + return ( + qb.from_(adv) + .select( + adv.company, + adv.against_voucher_type.as_("reference_doctype"), + adv.against_voucher_no.as_("reference_name"), + Abs(Sum(adv.amount)).as_("allocated_amount"), + adv.currency, + ) + .where(Criterion.all(criteria)) + .having(qb.Field("allocated_amount") > 0) + .groupby(adv.against_voucher_no) + .run(as_dict=True) + ) + + @frappe.whitelist() def create_unreconcile_doc_for_selection(selections=None): if selections: diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 20476b4164c..a0d08792d29 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -305,6 +305,8 @@ def get_merge_properties(dimensions=None): "project", "finance_book", "voucher_no", + "advance_voucher_type", + "advance_voucher_no", ] if dimensions: merge_properties.extend(dimensions) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 97b1bc9ab83..022dfb889a9 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -471,42 +471,27 @@ def reconcile_against_document( reconciled_entries[(row.voucher_type, row.voucher_no)].append(row) for key, entries in reconciled_entries.items(): - voucher_type = key[0] - voucher_no = key[1] + voucher_type, voucher_no = key - # cancel advance entry doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True - # When Advance is allocated from an Order to an Invoice - # whole ledger must be reposted - repost_whole_ledger = any([x.voucher_detail_no for x in entries]) - if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: - if repost_whole_ledger: - doc.make_gl_entries(cancel=1) - else: - doc.make_advance_gl_entries(cancel=1) - else: - _delete_pl_entries(voucher_type, voucher_no) - + reposting_rows = [] for entry in entries: check_if_advance_entry_modified(entry) validate_allocated_amount(entry) dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions) - # update ref in advance entry if voucher_type == "Journal Entry": - referenced_row, update_advance_paid = update_reference_in_journal_entry( - entry, doc, do_not_save=False - ) + referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) # advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss # amount and account in args # referenced_row is used to deduplicate gain/loss journal - entry.update({"referenced_row": referenced_row}) + entry.update({"referenced_row": referenced_row.name}) doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: - referenced_row, update_advance_paid = update_reference_in_payment_entry( + referenced_row = update_reference_in_payment_entry( entry, doc, do_not_save=True, @@ -515,20 +500,16 @@ def reconcile_against_document( ) if referenced_row.get("outstanding_amount"): referenced_row.outstanding_amount -= flt(entry.allocated_amount) + + reposting_rows.append(referenced_row) + doc.save(ignore_permissions=True) - # re-submit advance entry - doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: - # When Advance is allocated from an Order to an Invoice - # whole ledger must be reposted - if repost_whole_ledger: - doc.make_gl_entries() - else: - # both ledgers must be posted to for `Advance` in separate account feature - # TODO: find a more efficient way post only for the new linked vouchers - doc.make_advance_gl_entries() + for row in reposting_rows: + doc.make_advance_gl_entries(entry=row) else: + _delete_pl_entries(voucher_type, voucher_no) gl_map = doc.build_gl_map() # Make sure there is no overallocation from erpnext.accounts.general_ledger import process_debit_credit_difference @@ -545,11 +526,6 @@ def reconcile_against_document( entry.party_type, entry.party, ) - # update advance paid in Advance Receivable/Payable doctypes - if update_advance_paid: - for t, n in update_advance_paid: - frappe.get_doc(t, n).set_total_advance_paid() - frappe.flags.ignore_party_validation = False @@ -630,12 +606,6 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] - # Update Advance Paid in SO/PO since they might be getting unlinked - update_advance_paid = [] - - if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]: - update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) - rev_dr_or_cr = ( "debit_in_account_currency" if d["dr_or_cr"] == "credit_in_account_currency" @@ -688,6 +658,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): new_row.is_advance = cstr(jv_detail.is_advance) new_row.docstatus = 1 + if jv_detail.get("reference_type") in get_advance_payment_doctypes(): + new_row.advance_voucher_type = jv_detail.get("reference_type") + new_row.advance_voucher_no = jv_detail.get("reference_name") + # will work as update after submit journal_entry.flags.ignore_validate_update_after_submit = True # Ledgers will be reposted by Reconciliation tool @@ -695,7 +669,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): if not do_not_save: journal_entry.save(ignore_permissions=True) - return new_row.name, update_advance_paid + return new_row def update_reference_in_payment_entry( @@ -714,7 +688,8 @@ def update_reference_in_payment_entry( "account": d.account, "dimensions": d.dimensions, } - update_advance_paid = [] + + advance_payment_doctypes = get_advance_payment_doctypes() # Update Reconciliation effect date in reference if payment_entry.book_advance_payments_in_separate_party_account: @@ -726,10 +701,6 @@ def update_reference_in_payment_entry( if d.voucher_detail_no: existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] - # Update Advance Paid in SO/PO since they are getting unlinked - if existing_row.get("reference_doctype") in ["Sales Order", "Purchase Order"]: - update_advance_paid.append((existing_row.reference_doctype, existing_row.reference_name)) - if d.allocated_amount <= existing_row.allocated_amount: existing_row.allocated_amount -= d.allocated_amount @@ -737,7 +708,13 @@ def update_reference_in_payment_entry( new_row.docstatus = 1 for field in list(reference_details): new_row.set(field, reference_details[field]) + + if existing_row.reference_doctype in advance_payment_doctypes: + new_row.advance_voucher_type = existing_row.reference_doctype + new_row.advance_voucher_no = existing_row.reference_name + row = new_row + else: new_row = payment_entry.append("references") new_row.docstatus = 1 @@ -771,7 +748,8 @@ def update_reference_in_payment_entry( payment_entry.flags.ignore_reposting_on_reconciliation = True if not do_not_save: payment_entry.save(ignore_permissions=True) - return row, update_advance_paid + + return row def get_reconciliation_effect_date(against_voucher_type, against_voucher, company, posting_date): @@ -949,6 +927,24 @@ def update_accounting_ledgers_after_reference_removal( ple_update_query = ple_update_query.where(ple.voucher_no == payment_name) ple_update_query.run() + # Advance Payment + adv = qb.DocType("Advance Payment Ledger Entry") + adv_ple = ( + qb.update(adv) + .set(adv.delinked, 1) + .set(adv.modified, now()) + .set(adv.modified_by, frappe.session.user) + .where(adv.delinked == 0) + .where( + ((adv.against_voucher_type == ref_type) & (adv.against_voucher_no == ref_no)) + | ((adv.voucher_type == ref_type) & (adv.voucher_no == ref_no)) + ) + ) + if payment_name: + adv_ple = adv_ple.where(adv.voucher_no == payment_name) + + adv_ple.run() + def remove_ref_from_advance_section(ref_doc: object = None): # TODO: this might need some testing @@ -985,6 +981,8 @@ def remove_ref_doc_link_from_jv( qb.update(jea) .set(jea.reference_type, None) .set(jea.reference_name, None) + .set(jea.advance_voucher_type, None) + .set(jea.advance_voucher_no, None) .set(jea.modified, now()) .set(jea.modified_by, frappe.session.user) .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no)) @@ -1495,6 +1493,11 @@ def _delete_pl_entries(voucher_type, voucher_no): qb.from_(ple).delete().where((ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)).run() +def _delete_adv_pl_entries(voucher_type, voucher_no): + adv = qb.DocType("Advance Payment Ledger Entry") + qb.from_(adv).delete().where((adv.voucher_type == voucher_type) & (adv.voucher_no == voucher_no)).run() + + def _delete_gl_entries(voucher_type, voucher_no): gle = qb.DocType("GL Entry") qb.from_(gle).delete().where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)).run() @@ -1814,6 +1817,11 @@ def get_payment_ledger_entries(gl_entries, cancel=0): dr_or_cr *= -1 dr_or_cr_account_currency *= -1 + against_voucher_type = ( + gle.against_voucher_type if gle.against_voucher_type else gle.voucher_type + ) + against_voucher_no = gle.against_voucher if gle.against_voucher else gle.voucher_no + ple = frappe._dict( doctype="Payment Ledger Entry", posting_date=gle.posting_date, @@ -1828,14 +1836,12 @@ def get_payment_ledger_entries(gl_entries, cancel=0): voucher_type=gle.voucher_type, voucher_no=gle.voucher_no, voucher_detail_no=gle.voucher_detail_no, - against_voucher_type=gle.against_voucher_type - if gle.against_voucher_type - else gle.voucher_type, - against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no, + against_voucher_type=against_voucher_type, + against_voucher_no=against_voucher_no, account_currency=gle.account_currency, amount=dr_or_cr, amount_in_account_currency=dr_or_cr_account_currency, - delinked=True if cancel else False, + delinked=cancel, remarks=gle.remarks, ) @@ -1844,10 +1850,40 @@ def get_payment_ledger_entries(gl_entries, cancel=0): for dimension in dimensions_and_defaults[0]: ple[dimension.fieldname] = gle.get(dimension.fieldname) + if gle.advance_voucher_no: + # create advance entry + adv = get_advance_ledger_entry( + gle, against_voucher_type, against_voucher_no, dr_or_cr_account_currency, cancel + ) + + ple_map.append(adv) + ple_map.append(ple) + return ple_map +def get_advance_ledger_entry(gle, against_voucher_type, against_voucher_no, amount, cancel): + event = ( + "Submit" + if (against_voucher_type == gle.voucher_type and against_voucher_no == gle.voucher_no) + else "Adjustment" + ) + return frappe._dict( + doctype="Advance Payment Ledger Entry", + company=gle.company, + voucher_type=gle.voucher_type, + voucher_no=gle.voucher_no, + voucher_detail_no=gle.voucher_detail_no, + against_voucher_type=gle.advance_voucher_type, + against_voucher_no=gle.advance_voucher_no, + amount=amount, + currency=gle.account_currency, + event=event, + delinked=cancel, + ) + + def create_payment_ledger_entry( gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False ): @@ -1868,6 +1904,14 @@ def create_payment_ledger_entry( def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): + if not voucher_type or not voucher_no: + return + + if voucher_type in ["Purchase Order", "Sales Order"]: + ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no) + ref_doc.set_total_advance_paid() + return + ple = frappe.qb.DocType("Payment Ledger Entry") vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})] common_filter = [] @@ -1910,7 +1954,27 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa def delink_original_entry(pl_entry, partial_cancel=False): - if pl_entry: + if not pl_entry: + return + + if pl_entry.doctype == "Advance Payment Ledger Entry": + adv = qb.DocType("Advance Payment Ledger Entry") + + ( + qb.update(adv) + .set(adv.delinked, 1) + .set(adv.event, "Cancel") + .set(adv.modified, now()) + .set(adv.modified_by, frappe.session.user) + .where(adv.voucher_type == pl_entry.voucher_type) + .where(adv.voucher_no == pl_entry.voucher_no) + .where(adv.against_voucher_type == pl_entry.against_voucher_type) + .where(adv.against_voucher_no == pl_entry.against_voucher_no) + .where(adv.event == pl_entry.event) + .run() + ) + + else: ple = qb.DocType("Payment Ledger Entry") query = ( qb.update(ple) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index e29e695f48c..6419a4ebac2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -505,6 +505,7 @@ class PurchaseOrder(BuyingController): self.ignore_linked_doctypes = ( "GL Entry", "Payment Ledger Entry", + "Advance Payment Ledger Entry", "Unreconcile Payment", "Unreconcile Payment Entries", ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e0e53adeebd..c2fd009fc4a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -397,7 +397,6 @@ class AccountsController(TransactionBase): def on_trash(self): from erpnext.accounts.utils import delete_exchange_gain_loss_journal - self._remove_advance_payment_ledger_entries() self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() self.remove_serial_and_batch_bundle() @@ -426,6 +425,8 @@ class AccountsController(TransactionBase): (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) ).run() + self._remove_advance_payment_ledger_entries() + def remove_serial_and_batch_bundle(self): bundles = frappe.get_all( "Serial and Batch Bundle", @@ -2233,54 +2234,30 @@ class AccountsController(TransactionBase): def calculate_total_advance_from_ledger(self): adv = frappe.qb.DocType("Advance Payment Ledger Entry") - advance = ( - frappe.qb.from_(adv) - .select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount")) - .where( - (adv.against_voucher_type == self.doctype) - & (adv.against_voucher_no == self.name) - & (adv.company == self.company) - ) + return ( + qb.from_(adv) + .select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency")) + .where(adv.company == self.company) + .where(adv.delinked == 0) + .where(adv.against_voucher_type == self.doctype) + .where(adv.against_voucher_no == self.name) .run(as_dict=True) ) - return advance def set_total_advance_paid(self): advance = self.calculate_total_advance_from_ledger() - advance_paid, order_total = None, None + advance_paid = 0 if advance: advance = advance[0] advance_paid = flt(advance.amount, self.precision("advance_paid")) - formatted_advance_paid = fmt_money( - advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency - ) - if advance.account_currency: frappe.db.set_value( self.doctype, self.name, "party_account_currency", advance.account_currency ) - if advance.account_currency == self.currency: - order_total = self.get("rounded_total") or self.grand_total - precision = "rounded_total" if self.get("rounded_total") else "grand_total" - else: - order_total = self.get("base_rounded_total") or self.base_grand_total - precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total" - - formatted_order_total = fmt_money( - order_total, precision=self.precision(precision), currency=advance.account_currency - ) - - if self.currency == self.company_currency and advance_paid > order_total: - frappe.throw( - _( - "Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})" - ).format(formatted_advance_paid, self.name, formatted_order_total) - ) - - self.db_set("advance_paid", advance_paid) + self.db_set("advance_paid", advance_paid) @property def company_abbr(self): @@ -2924,64 +2901,6 @@ class AccountsController(TransactionBase): def get_advance_payment_doctypes(self) -> list: return _get_advance_payment_doctypes() - def make_advance_payment_ledger_for_journal(self): - advance_payment_doctypes = self.get_advance_payment_doctypes() - advance_doctype_references = [ - x for x in self.accounts if x.reference_type in advance_payment_doctypes - ] - - for x in advance_doctype_references: - # Looking for payments - dr_or_cr = ( - "credit_in_account_currency" - if x.account_type == "Receivable" - else "debit_in_account_currency" - ) - - amount = x.get(dr_or_cr) - if amount > 0: - doc = frappe.new_doc("Advance Payment Ledger Entry") - doc.company = self.company - doc.voucher_type = self.doctype - doc.voucher_no = self.name - doc.against_voucher_type = x.reference_type - doc.against_voucher_no = x.reference_name - doc.amount = amount if self.docstatus == 1 else -1 * amount - doc.event = "Submit" if self.docstatus == 1 else "Cancel" - doc.currency = x.account_currency - doc.flags.ignore_permissions = 1 - doc.save() - - def make_advance_payment_ledger_for_payment(self): - advance_payment_doctypes = self.get_advance_payment_doctypes() - advance_doctype_references = [ - x for x in self.references if x.reference_doctype in advance_payment_doctypes - ] - currency = ( - self.paid_from_account_currency - if self.payment_type == "Receive" - else self.paid_to_account_currency - ) - for x in advance_doctype_references: - doc = frappe.new_doc("Advance Payment Ledger Entry") - doc.company = self.company - doc.voucher_type = self.doctype - doc.voucher_no = self.name - doc.against_voucher_type = x.reference_doctype - doc.against_voucher_no = x.reference_name - doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount - doc.currency = currency - doc.event = "Submit" if self.docstatus == 1 else "Cancel" - doc.flags.ignore_permissions = 1 - doc.save() - - def make_advance_payment_ledger_entries(self): - if self.docstatus != 0: - if self.doctype == "Journal Entry": - self.make_advance_payment_ledger_for_journal() - elif self.doctype == "Payment Entry": - self.make_advance_payment_ledger_for_payment() - def set_transaction_currency_and_rate_in_gl_map(self, gl_entries): for x in gl_entries: x["transaction_currency"] = self.currency diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7bd30f81bc3..d87f595a500 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -361,7 +361,7 @@ erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16 -erpnext.patches.v15_0.create_advance_payment_ledger_records +erpnext.patches.v15_0.create_advance_payment_ledger_records #2025-07-04 # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 @@ -412,6 +412,7 @@ erpnext.patches.v15_0.drop_sle_indexes erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies erpnext.patches.v15_0.set_company_on_pos_inv_merge_log +erpnext.patches.v15_0.update_payment_ledger_entries_against_advance_doctypes erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice diff --git a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py index 8d247885cab..e155eaea1d1 100644 --- a/erpnext/patches/v15_0/create_advance_payment_ledger_records.py +++ b/erpnext/patches/v15_0/create_advance_payment_ledger_records.py @@ -1,63 +1,28 @@ import frappe -from frappe import qb -from frappe.query_builder.custom import ConstantColumn +from frappe.model.naming import _generate_random_string +from frappe.query_builder import Case +from frappe.utils import now_datetime +from erpnext.accounts.utils import get_advance_payment_doctypes -def get_advance_doctypes() -> list: - return frappe.get_hooks("advance_payment_doctypes") +DOCTYPE = "Advance Payment Ledger Entry" - -def get_payments_with_so_po_reference() -> list: - advance_payment_entries = [] - advance_doctypes = get_advance_doctypes() - per = qb.DocType("Payment Entry Reference") - payments_with_reference = ( - qb.from_(per) - .select(per.parent) - .distinct() - .where(per.reference_doctype.isin(advance_doctypes) & per.docstatus.eq(1)) - .run() - ) - if payments_with_reference: - pe = qb.DocType("Payment Entry") - advance_payment_entries = ( - qb.from_(pe) - .select(ConstantColumn("Payment Entry").as_("doctype")) - .select(pe.name) - .where(pe.name.isin(payments_with_reference) & pe.docstatus.eq(1)) - .run(as_dict=True) - ) - - return advance_payment_entries - - -def get_journals_with_so_po_reference() -> list: - advance_journal_entries = [] - advance_doctypes = get_advance_doctypes() - jea = qb.DocType("Journal Entry Account") - journals_with_reference = ( - qb.from_(jea) - .select(jea.parent) - .distinct() - .where(jea.reference_type.isin(advance_doctypes) & jea.docstatus.eq(1)) - .run() - ) - if journals_with_reference: - je = qb.DocType("Journal Entry") - advance_journal_entries = ( - qb.from_(je) - .select(ConstantColumn("Journal Entry").as_("doctype")) - .select(je.name) - .where(je.name.isin(journals_with_reference) & je.docstatus.eq(1)) - .run(as_dict=True) - ) - - return advance_journal_entries - - -def make_advance_ledger_entries(vouchers: list): - for x in vouchers: - frappe.get_doc(x.doctype, x.name).make_advance_payment_ledger_entries() +FIELDS = [ + "name", + "creation", + "modified", + "owner", + "modified_by", + "company", + "voucher_type", + "voucher_no", + "against_voucher_type", + "against_voucher_no", + "amount", + "currency", + "event", + "delinked", +] def execute(): @@ -65,9 +30,102 @@ def execute(): Description: Create Advance Payment Ledger Entry for all Payments made against Sales / Purchase Orders """ - frappe.db.truncate("Advance Payment Ledger Entry") - payment_entries = get_payments_with_so_po_reference() - make_advance_ledger_entries(payment_entries) + frappe.db.truncate(DOCTYPE) + advance_doctpyes = get_advance_payment_doctypes() - journals = get_journals_with_so_po_reference() - make_advance_ledger_entries(journals) + if not advance_doctpyes: + return + + make_advance_ledger_entries_for_payment_entries(advance_doctpyes) + make_advance_ledger_entries_for_journal_entries(advance_doctpyes) + + +def make_advance_ledger_entries_for_payment_entries(advance_doctpyes) -> list: + pe = frappe.qb.DocType("Payment Entry") + per = frappe.qb.DocType("Payment Entry Reference") + + entries = ( + frappe.qb.from_(per) + .inner_join(pe) + .on(pe.name == per.parent) + .select( + pe.company, + per.parenttype.as_("voucher_type"), + per.parent.as_("voucher_no"), + per.reference_doctype.as_("against_voucher_type"), + per.reference_name.as_("against_voucher_no"), + per.allocated_amount.as_("amount"), + Case() + .when(pe.payment_type == "Receive", pe.paid_from_account_currency) + .else_(pe.paid_to_account_currency) + .as_("currency"), + ) + .where(per.reference_doctype.isin(advance_doctpyes) & per.docstatus.eq(1)) + .run(as_dict=True) + ) + + if not entries: + return + + bulk_insert_advance_entries(entries) + + +def make_advance_ledger_entries_for_journal_entries(advance_doctpyes) -> list: + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + + entries = ( + frappe.qb.from_(jea) + .inner_join(je) + .on(je.name == jea.parent) + .select( + je.company, + jea.parenttype.as_("voucher_type"), + jea.parent.as_("voucher_no"), + jea.reference_type.as_("against_voucher_type"), + jea.reference_name.as_("against_voucher_no"), + Case() + .when(jea.account_type == "Receivable", jea.credit_in_account_currency) + .else_(jea.debit_in_account_currency) + .as_("amount"), + jea.account_currency.as_("currency"), + ) + .where(jea.reference_type.isin(advance_doctpyes) & jea.docstatus.eq(1)) + .run(as_dict=True) + ) + + if not entries: + return + + bulk_insert_advance_entries(entries) + + +def bulk_insert_advance_entries(entries): + details = [] + user = frappe.session.user + now = now_datetime() + for entry in entries: + if entry.amount < 0: + continue + details.append(get_values(user, now, entry)) + + frappe.db.bulk_insert(DOCTYPE, fields=FIELDS, values=details) + + +def get_values(user, now, entry): + return ( + _generate_random_string(10), + now, + now, + user, + user, + entry.company, + entry.voucher_type, + entry.voucher_no, + entry.against_voucher_type, + entry.against_voucher_no, + entry.amount * -1, + entry.currency, + "Submit", + 0, + ) diff --git a/erpnext/patches/v15_0/update_payment_ledger_entries_against_advance_doctypes.py b/erpnext/patches/v15_0/update_payment_ledger_entries_against_advance_doctypes.py new file mode 100644 index 00000000000..f5a20803c6e --- /dev/null +++ b/erpnext/patches/v15_0/update_payment_ledger_entries_against_advance_doctypes.py @@ -0,0 +1,25 @@ +import frappe + +from erpnext.accounts.utils import get_advance_payment_doctypes + +DOCTYPE = "Payment Ledger Entry" + + +def execute(): + """ + Description: + Set against_voucher as entry for Payment Ledger Entry against advance vouchers. + """ + advance_payment_doctypes = get_advance_payment_doctypes() + + if not advance_payment_doctypes: + return + ple = frappe.qb.DocType(DOCTYPE) + + ( + frappe.qb.update(ple) + .set(ple.against_voucher_type, ple.voucher_type) + .set(ple.against_voucher_no, ple.voucher_no) + .where(ple.against_voucher_type.isin(advance_payment_doctypes)) + .run() + ) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 072b541753d..4ccbf0106d7 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -42,8 +42,8 @@ erpnext.accounts.unreconcile_payment = { selection_map = selections.map(function (elem) { return { company: elem.company, - voucher_type: elem.voucher_type, - voucher_no: elem.voucher_no, + voucher_type: elem.reference_doctype, + voucher_no: elem.reference_name, against_voucher_type: frm.doc.doctype, against_voucher_no: frm.doc.name, }; @@ -54,8 +54,8 @@ erpnext.accounts.unreconcile_payment = { company: elem.company, voucher_type: frm.doc.doctype, voucher_no: frm.doc.name, - against_voucher_type: elem.voucher_type, - against_voucher_no: elem.voucher_no, + against_voucher_type: elem.reference_doctype, + against_voucher_no: elem.reference_name, }; }); } @@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = { let child_table_fields = [ { label: __("Voucher Type"), - fieldname: "voucher_type", + fieldname: "reference_doctype", fieldtype: "Link", options: "DocType", in_list_view: 1, @@ -77,9 +77,9 @@ erpnext.accounts.unreconcile_payment = { }, { label: __("Voucher No"), - fieldname: "voucher_no", + fieldname: "reference_name", fieldtype: "Dynamic Link", - options: "voucher_type", + options: "reference_doctype", in_list_view: 1, read_only: 1, }, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b89e14b5bcf..b6a979dbc61 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -444,6 +444,7 @@ class SalesOrder(SellingController): "GL Entry", "Stock Ledger Entry", "Payment Ledger Entry", + "Advance Payment Ledger Entry", "Unreconcile Payment", "Unreconcile Payment Entries", )