From 3263f2023c0a6ceabfbdd1eeb5202843d4532b76 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Sep 2023 09:36:18 +0530 Subject: [PATCH 01/12] test: assert ledger entries on partial reconciliation with `Advance as Liability`, partial reconciliation should not post duplicate ledger entries for same reference --- .../purchase_invoice/test_purchase_invoice.py | 4 +- .../sales_invoice/test_sales_invoice.py | 95 ++++++++++++++++++- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 171cc0ccdf..322fc9f16b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1769,10 +1769,10 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Check GL Entry against payment doctype expected_gle = [ - ["Advances Paid - _TC", 0.0, 500, nowdate()], + ["Advances Paid - _TC", 500.0, 0.0, nowdate()], + ["Advances Paid - _TC", 0.0, 500.0, nowdate()], ["Cash - _TC", 0.0, 500, nowdate()], ["Creditors - _TC", 500, 0.0, nowdate()], - ["Creditors - _TC", 500, 0.0, nowdate()], ] check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 017bfa9654..840a31942a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3371,21 +3371,21 @@ class TestSalesInvoice(FrappeTestCase): def test_advance_entries_as_liability(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry - account = create_account( + advance_account = create_account( parent_account="Current Liabilities - _TC", account_name="Advances Received", company="_Test Company", account_type="Receivable", ) - set_advance_flag(company="_Test Company", flag=1, default_account=account) + set_advance_flag(company="_Test Company", flag=1, default_account=advance_account) pe = create_payment_entry( company="_Test Company", payment_type="Receive", party_type="Customer", party="_Test Customer", - paid_from="Debtors - _TC", + paid_from=advance_account, paid_to="Cash - _TC", paid_amount=1000, ) @@ -3411,9 +3411,9 @@ class TestSalesInvoice(FrappeTestCase): # Check GL Entry against payment doctype expected_gle = [ + ["Advances Received - _TC", 0.0, 1000.0, nowdate()], ["Advances Received - _TC", 500, 0.0, nowdate()], ["Cash - _TC", 1000, 0.0, nowdate()], - ["Debtors - _TC", 0.0, 1000, nowdate()], ["Debtors - _TC", 0.0, 500, nowdate()], ] @@ -3450,6 +3450,93 @@ class TestSalesInvoice(FrappeTestCase): si.items[0].rate = 10 si.save() + def test_partial_allocation_on_advance_as_liability(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + company = "_Test Company" + customer = "_Test Customer" + debtors_acc = "Debtors - _TC" + advance_account = create_account( + parent_account="Current Liabilities - _TC", + account_name="Advances Received", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=advance_account) + + pe = create_payment_entry( + company=company, + payment_type="Receive", + party_type="Customer", + party=customer, + paid_from=advance_account, + paid_to="Cash - _TC", + paid_amount=1000, + ) + pe.submit() + + si = create_sales_invoice( + company=company, + customer=customer, + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + ) + si.base_grand_total = 1000 + si.grand_total = 1000 + si.set_advances() + for advance in si.advances: + advance.allocated_amount = 200 if advance.reference_name == pe.name else 0 + si.save() + si.submit() + + self.assertEqual(si.advances[0].allocated_amount, 200) + + # Check GL Entry against partial from advance + expected_gle = [ + [advance_account, 0.0, 1000.0, nowdate()], + [advance_account, 200.0, 0.0, nowdate()], + ["Cash - _TC", 1000.0, 0.0, nowdate()], + [debtors_acc, 0.0, 200.0, nowdate()], + ] + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + si.reload() + self.assertEqual(si.outstanding_amount, 800.0) + + pr = frappe.get_doc("Payment Reconciliation") + pr.company = company + pr.party_type = "Customer" + pr.party = customer + pr.receivable_payable_account = debtors_acc + pr.default_advance_account = advance_account + pr.get_unreconciled_entries() + + # allocate some more of the same advance + # self.assertEqual(len(pr.invoices), 1) + # self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices if x.get("invoice_number") == si.name] + payments = [x.as_dict() for x in pr.payments if x.get("reference_name") == pe.name] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 300 + pr.reconcile() + + si.reload() + self.assertEqual(si.outstanding_amount, 500.0) + + # Check GL Entry against multi partial allocations from advance + expected_gle = [ + [advance_account, 0.0, 1000.0, nowdate()], + [advance_account, 200.0, 0.0, nowdate()], + [advance_account, 300.0, 0.0, nowdate()], + ["Cash - _TC", 1000.0, 0.0, nowdate()], + [debtors_acc, 0.0, 200.0, nowdate()], + [debtors_acc, 0.0, 300.0, nowdate()], + ] + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + set_advance_flag(company="_Test Company", flag=0, default_account="") + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 78ab11f9914725266eb8cf8b98dd81a5f544d79d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Sep 2023 13:05:04 +0530 Subject: [PATCH 02/12] refactor: post to GL and Payment Ledger on advance as liability --- erpnext/accounts/utils.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d32a03931..d133307d39 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -472,7 +472,9 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # cancel advance entry doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True - _delete_pl_entries(voucher_type, voucher_no) + + if not (voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account): + _delete_pl_entries(voucher_type, voucher_no) for entry in entries: check_if_advance_entry_modified(entry) @@ -494,16 +496,19 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) - gl_map = doc.build_gl_map() - create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) + + if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: + # both ledgers must be posted to for `Advance as Liability` + doc.make_gl_entries() + else: + gl_map = doc.build_gl_map() + create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) # Only update outstanding for newly linked vouchers for entry in entries: update_voucher_outstanding( entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party ) - if voucher_type == "Payment Entry": - doc.make_advance_gl_entries(entry.against_voucher_type, entry.against_voucher) frappe.flags.ignore_party_validation = False From 58114e7b24788f0244b89401235a6a20721b9603 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Nov 2023 16:50:22 +0530 Subject: [PATCH 03/12] refactor: use different GL build logic for advance as liability --- .../doctype/payment_entry/payment_entry.py | 112 ++++++++++-------- erpnext/accounts/utils.py | 2 +- 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0344e3de9f..63c5fa92d3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1055,64 +1055,76 @@ class PaymentEntry(AccountsController): item=self, ) - for d in self.get("references"): - # re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse - dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" - cost_center = self.cost_center - if d.reference_doctype == "Sales Invoice" and not cost_center: - cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") - - gle = party_gl_dict.copy() - - allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d) - - if self.book_advance_payments_in_separate_party_account: - against_voucher_type = "Payment Entry" - against_voucher = self.name - else: - against_voucher_type = d.reference_doctype - against_voucher = d.reference_name - - reverse_dr_or_cr = 0 - if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]: - is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return") - payable_party_types = get_party_types_from_account_type("Payable") - receivable_party_types = get_party_types_from_account_type("Receivable") - if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"): - reverse_dr_or_cr = 1 - elif ( - is_return and self.party_type in payable_party_types and (self.payment_type == "Receive") - ): - reverse_dr_or_cr = 1 - - if is_return and not reverse_dr_or_cr: - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - gle.update( - { - dr_or_cr: abs(allocated_amount_in_company_currency), - dr_or_cr + "_in_account_currency": abs(d.allocated_amount), - "against_voucher_type": against_voucher_type, - "against_voucher": against_voucher, - "cost_center": cost_center, - } - ) - gl_entries.append(gle) - dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" - if self.unallocated_amount: - exchange_rate = self.get_exchange_rate() - base_unallocated_amount = self.unallocated_amount * exchange_rate + if self.book_advance_payments_in_separate_party_account: + if self.payment_type == "Receive": + amount = self.base_paid_amount + else: + amount = self.base_received_amount gle = party_gl_dict.copy() gle.update( { - dr_or_cr + "_in_account_currency": self.unallocated_amount, - dr_or_cr: base_unallocated_amount, + dr_or_cr: amount, + # TODO: handle multi currency payments + dr_or_cr + "_in_account_currency": amount, + "against_voucher_type": "Payment Entry", + "against_voucher": self.name, + "cost_center": self.cost_center, } ) - gl_entries.append(gle) + else: + for d in self.get("references"): + # re-defining dr_or_cr for every reference in order to avoid the last value affecting calculation of reverse + dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" + cost_center = self.cost_center + if d.reference_doctype == "Sales Invoice" and not cost_center: + cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center") + + gle = party_gl_dict.copy() + + allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d) + reverse_dr_or_cr = 0 + + if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]: + is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return") + payable_party_types = get_party_types_from_account_type("Payable") + receivable_party_types = get_party_types_from_account_type("Receivable") + if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"): + reverse_dr_or_cr = 1 + elif ( + is_return and self.party_type in payable_party_types and (self.payment_type == "Receive") + ): + reverse_dr_or_cr = 1 + + if is_return and not reverse_dr_or_cr: + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + gle.update( + { + dr_or_cr: abs(allocated_amount_in_company_currency), + dr_or_cr + "_in_account_currency": abs(d.allocated_amount), + "against_voucher_type": d.reference_doctype, + "against_voucher": d.reference_name, + "cost_center": cost_center, + } + ) + gl_entries.append(gle) + + if self.unallocated_amount: + exchange_rate = self.get_exchange_rate() + base_unallocated_amount = self.unallocated_amount * exchange_rate + + gle = party_gl_dict.copy() + gle.update( + { + dr_or_cr + "_in_account_currency": self.unallocated_amount, + dr_or_cr: base_unallocated_amount, + } + ) + + gl_entries.append(gle) def make_advance_gl_entries(self, against_voucher_type=None, against_voucher=None, cancel=0): if self.book_advance_payments_in_separate_party_account: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d133307d39..e2632d3de4 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -499,7 +499,7 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: # both ledgers must be posted to for `Advance as Liability` - doc.make_gl_entries() + doc.make_advance_gl_entries() else: gl_map = doc.build_gl_map() create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) From 5fc19dab54a2672ec131dbf97974b2bde277aa76 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Nov 2023 17:13:43 +0530 Subject: [PATCH 04/12] refactor: 'make_advance_gl_entries' method make_advance_gl_entries -> add_advance_gl_entries -> add_advance_gl_for_reference 'make_advance_gl_entries' - main method thats builds and post GL entries for all or one specific reference based on parameters 'add_advance_gl_entries' - build GL map for all or one specific reference. Return an array of dict. 'add_advance_gl_for_reference' - utility function to build gl entries. returns dict. --- .../doctype/payment_entry/payment_entry.py | 66 +++++++------------ 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 63c5fa92d3..c732c0a1c5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1126,53 +1126,31 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) - def make_advance_gl_entries(self, against_voucher_type=None, against_voucher=None, cancel=0): + def make_advance_gl_entries( + self, entry: object | dict = None, cancel: bool = 0, update_outstanding: str = "Yes" + ): + gl_entries = [] + self.add_advance_gl_entries(gl_entries, entry) + + if cancel: + make_reverse_gl_entries(gl_entries, partial_cancel=True) + else: + make_gl_entries(gl_entries, update_outstanding=update_outstanding) + + def add_advance_gl_entries(self, gl_entries: list, entry: object | dict | None): + """ + If 'entry' is passed, GL enties only for that reference is added. + """ if self.book_advance_payments_in_separate_party_account: - gl_entries = [] - for d in self.get("references"): - if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"): - if not (against_voucher_type and against_voucher) or ( - d.reference_doctype == against_voucher_type and d.reference_name == against_voucher - ): - self.make_invoice_liability_entry(gl_entries, d) + references = [x for x in self.get("references")] + if entry: + references = [x for x in self.get("references") if x.name == entry.name] - if cancel: - for entry in gl_entries: - frappe.db.set_value( - "GL Entry", - { - "voucher_no": self.name, - "voucher_type": self.doctype, - "voucher_detail_no": entry.voucher_detail_no, - "against_voucher_type": entry.against_voucher_type, - "against_voucher": entry.against_voucher, - }, - "is_cancelled", - 1, - ) + for ref in references: + if ref.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"): + self.add_advance_gl_for_reference(gl_entries, ref) - make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True) - return - - # same reference added to payment entry - for gl_entry in gl_entries.copy(): - if frappe.db.exists( - "GL Entry", - { - "account": gl_entry.account, - "voucher_type": gl_entry.voucher_type, - "voucher_no": gl_entry.voucher_no, - "voucher_detail_no": gl_entry.voucher_detail_no, - "debit": gl_entry.debit, - "credit": gl_entry.credit, - "is_cancelled": 0, - }, - ): - gl_entries.remove(gl_entry) - - make_gl_entries(gl_entries) - - def make_invoice_liability_entry(self, gl_entries, invoice): + def add_advance_gl_for_reference(self, gl_entries, invoice): args_dict = { "party_type": self.party_type, "party": self.party, From ecb533c4d1971d5963f9d8ff4b038fa4b62cf516 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Nov 2023 17:04:07 +0530 Subject: [PATCH 05/12] refactor: return the newly added reference upon reconciliation --- erpnext/accounts/utils.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index e2632d3de4..0dea1eb9c7 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -473,6 +473,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True + # For payments with `Advance` in separate account feature enabled, only new ledger entries are posted for each reference. + # No need to cancel/delete payment ledger entries if not (voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account): _delete_pl_entries(voucher_type, voucher_no) @@ -489,7 +491,7 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n entry.update({"referenced_row": referenced_row}) doc.make_exchange_gain_loss_journal([entry]) else: - update_reference_in_payment_entry( + referenced_row = update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe ) @@ -498,8 +500,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: - # both ledgers must be posted to for `Advance as Liability` - doc.make_advance_gl_entries() + # both ledgers must be posted to for `Advance` in separate account feature + doc.make_advance_gl_entries(referenced_row, update_outstanding="No") else: gl_map = doc.build_gl_map() create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1) @@ -681,11 +683,12 @@ def update_reference_in_payment_entry( new_row.docstatus = 1 for field in list(reference_details): new_row.set(field, reference_details[field]) - + row = new_row else: new_row = payment_entry.append("references") new_row.docstatus = 1 new_row.update(reference_details) + row = new_row payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.clear_unallocated_reference_document_rows() @@ -700,6 +703,7 @@ def update_reference_in_payment_entry( if not do_not_save: payment_entry.save(ignore_permissions=True) + return row def cancel_exchange_gain_loss_journal( @@ -876,7 +880,13 @@ def remove_ref_doc_link_from_pe( try: pe_doc = frappe.get_doc("Payment Entry", pe) pe_doc.set_amounts() - pe_doc.make_advance_gl_entries(against_voucher_type=ref_type, against_voucher=ref_no, cancel=1) + + # Call cancel on only removed reference + references = [ + x for x in pe_doc.references if x.reference_doctype == ref_type and x.reference_name == ref_no + ] + [pe_doc.make_advance_gl_entries(x, cancel=1) for x in references] + pe_doc.clear_unallocated_reference_document_rows() pe_doc.validate_payment_type_with_outstanding() except Exception as e: From 2633d7dca33b50d7900f1b80fb5b6514a1cc9d81 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Nov 2023 17:05:29 +0530 Subject: [PATCH 06/12] refactor: 'partial' flag to only cancel unlinked ledger entries --- erpnext/accounts/general_ledger.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 70a8470614..030a41e5cd 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -597,7 +597,30 @@ def make_reverse_gl_entries( is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries) validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"]) - if not partial_cancel: + if partial_cancel: + # Partial cancel is only used by `Advance` in separate account feature. + # Only cancel GL entries for unlinked reference using `voucher_detail_no` + gle = frappe.qb.DocType("GL Entry") + for x in gl_entries: + query = ( + frappe.qb.update(gle) + .set(gle.is_cancelled, True) + .set(gle.modified, now()) + .set(gle.modified_by, frappe.session.user) + .where( + (gle.company == x.company) + & (gle.account == x.account) + & (gle.party_type == x.party_type) + & (gle.party == x.party) + & (gle.voucher_type == x.voucher_type) + & (gle.voucher_no == x.voucher_no) + & (gle.against_voucher_type == x.against_voucher_type) + & (gle.against_voucher == x.against_voucher) + & (gle.voucher_detail_no == x.voucher_detail_no) + ) + ) + query.run() + else: set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: From 3e6306348ac564976fc8b0d3ef7ec12a8d9961cc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Nov 2023 20:04:47 +0530 Subject: [PATCH 07/12] refactor: redefine dr_or_cr for unallocated amount --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c732c0a1c5..e2e655befb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1113,6 +1113,7 @@ class PaymentEntry(AccountsController): gl_entries.append(gle) if self.unallocated_amount: + dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" exchange_rate = self.get_exchange_rate() base_unallocated_amount = self.unallocated_amount * exchange_rate From 2add802d0dbe61c536b5753f1d0f84b0896306cb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 29 Nov 2023 08:59:14 +0530 Subject: [PATCH 08/12] refactor(test): advance allocation on purchase invoice --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 322fc9f16b..e43ea6ecbe 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1747,6 +1747,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): paid_to="Creditors - _TC", paid_amount=500, ) + pe.save() # save trigger is needed for set_liability_account() to be executed pe.submit() pi = make_purchase_invoice( From 0255e09285d62eb9bfe80f56970e0dcaef4df17f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 29 Nov 2023 10:57:28 +0530 Subject: [PATCH 09/12] test: ledger pre and post reconciliation on advance as liability --- .../payment_entry/test_payment_entry.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index f4b0c55313..e77f4632b2 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -8,6 +8,7 @@ from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, nowdate +from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.payment_entry.payment_entry import ( InvalidPaymentEntry, get_outstanding_reference_documents, @@ -1358,6 +1359,140 @@ class TestPaymentEntry(FrappeTestCase): ] self.check_gl_entries() + def test_ledger_entries_for_advance_as_liability(self): + from erpnext.accounts.doctype.account.test_account import create_account + + company = "_Test Company" + + advance_account = create_account( + parent_account="Current Assets - _TC", + account_name="Advances Received", + company=company, + account_type="Liability", + ) + + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_received_account": advance_account, + }, + ) + # Advance Payment + pe = create_payment_entry( + party_type="Customer", + party="_Test Customer", + payment_type="Receive", + paid_from="Debtors - _TC", + paid_to="_Test Cash - _TC", + ) + pe.save() # use save() to trigger set_liability_account() + pe.submit() + + # Normal Invoice + si = create_sales_invoice(qty=10, rate=100, customer="_Test Customer") + + pre_reconciliation_gle = [ + {"account": advance_account, "debit": 0.0, "credit": 1000.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + pre_reconciliation_ple = [ + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -1000.0, + } + ] + + self.voucher_no = pe.name + self.expected_gle = pre_reconciliation_gle + self.expected_ple = pre_reconciliation_ple + self.check_gl_entries() + self.check_pl_entries() + + # Partially reconcile advance against invoice + pr = frappe.get_doc("Payment Reconciliation") + pr.company = company + pr.party_type = "Customer" + pr.party = "_Test Customer" + pr.receivable_payable_account = si.debit_to + pr.default_advance_account = advance_account + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + 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})) + pr.allocation[0].allocated_amount = 400 + pr.reconcile() + + # assert General and Payment Ledger entries post partial reconciliation + self.expected_gle = [ + {"account": si.debit_to, "debit": 0.0, "credit": 400.0}, + {"account": advance_account, "debit": 400.0, "credit": 0.0}, + {"account": advance_account, "debit": 0.0, "credit": 1000.0}, + {"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0}, + ] + self.expected_ple = [ + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -1000.0, + }, + { + "account": si.debit_to, + "voucher_no": pe.name, + "against_voucher_no": si.name, + "amount": -400.0, + }, + { + "account": advance_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": 400.0, + }, + ] + self.check_gl_entries() + self.check_pl_entries() + + # Unreconcile + unrecon = ( + frappe.get_doc( + { + "doctype": "Unreconcile Payment", + "company": company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + "allocations": [{"reference_doctype": si.doctype, "reference_name": si.name}], + } + ) + .save() + .submit() + ) + + self.voucher_no = pe.name + self.expected_gle = pre_reconciliation_gle + self.expected_ple = pre_reconciliation_ple + self.check_gl_entries() + self.check_pl_entries() + + def check_pl_entries(self): + ple = frappe.qb.DocType("Payment Ledger Entry") + pl_entries = ( + frappe.qb.from_(ple) + .select(ple.account, ple.voucher_no, ple.against_voucher_no, ple.amount) + .where((ple.voucher_no == self.voucher_no) & (ple.delinked == 0)) + .orderby(ple.creation) + ).run(as_dict=True) + for row in range(len(self.expected_ple)): + for field in ["account", "voucher_no", "against_voucher_no", "amount"]: + self.assertEqual(self.expected_ple[row][field], pl_entries[row][field]) + def check_gl_entries(self): gle = frappe.qb.DocType("GL Entry") gl_entries = ( From eecf9cd1d88987cfe6903c678216ba5af1e5d231 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 11:43:57 +0530 Subject: [PATCH 10/12] fix(test): use correct account type for testing --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index e77f4632b2..5b8a46b2c2 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1368,7 +1368,7 @@ class TestPaymentEntry(FrappeTestCase): parent_account="Current Assets - _TC", account_name="Advances Received", company=company, - account_type="Liability", + account_type="Receivable", ) frappe.db.set_value( From 080aa304077548a39678aabc3a33bcf3fb243a02 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 12:09:37 +0530 Subject: [PATCH 11/12] refactor(test): filter on document names --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 5b8a46b2c2..8a03dd7278 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1419,6 +1419,8 @@ class TestPaymentEntry(FrappeTestCase): pr.party = "_Test Customer" pr.receivable_payable_account = si.debit_to pr.default_advance_account = advance_account + pr.payment_name = pe.name + pr.invoice_name = si.name pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) From 961bdf0d247fc58f7d091459bb4d0059bd6ea791 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Dec 2023 12:13:00 +0530 Subject: [PATCH 12/12] refactor: handle forex payment advance entries --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e2e655befb..f1064ad535 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1057,17 +1057,19 @@ class PaymentEntry(AccountsController): dr_or_cr = "credit" if self.payment_type == "Receive" else "debit" if self.book_advance_payments_in_separate_party_account: + gle = party_gl_dict.copy() + if self.payment_type == "Receive": amount = self.base_paid_amount else: amount = self.base_received_amount - gle = party_gl_dict.copy() + exchange_rate = self.get_exchange_rate() + amount_in_account_currency = amount * exchange_rate gle.update( { dr_or_cr: amount, - # TODO: handle multi currency payments - dr_or_cr + "_in_account_currency": amount, + dr_or_cr + "_in_account_currency": amount_in_account_currency, "against_voucher_type": "Payment Entry", "against_voucher": self.name, "cost_center": self.cost_center,