From 9d9b83362af97442986c54a5bf63a0c4fa756c14 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 31 Jan 2024 20:40:26 +0530 Subject: [PATCH 1/5] fix: incorrect advance paid in Sales/Purchase Order --- .../doctype/payment_entry/payment_entry.py | 20 +++++++++++++++---- erpnext/accounts/utils.py | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 77efe78368b..1a89c45e001 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1271,7 +1271,13 @@ class PaymentEntry(AccountsController): references = [x for x in self.get("references") if x.name == entry.name] for ref in references: - if ref.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Journal Entry"): + if ref.reference_doctype in ( + "Sales Invoice", + "Purchase Invoice", + "Journal Entry", + "Sales Order", + "Purchase Order", + ): self.add_advance_gl_for_reference(gl_entries, ref) def add_advance_gl_for_reference(self, gl_entries, invoice): @@ -1285,9 +1291,10 @@ class PaymentEntry(AccountsController): "voucher_detail_no": invoice.name, } - posting_date = frappe.db.get_value( - invoice.reference_doctype, invoice.reference_name, "posting_date" - ) + 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 @@ -2197,6 +2204,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre else: outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid")) + if reference_doctype in ["Sales Order", "Purchase Order"]: + party_type = "Customer" if reference_doctype == "Sales Order" else "Supplier" + party_field = "customer" if reference_doctype == "Sales Order" else "supplier" + party = ref_doc.get(party_field) + account = get_party_account(party_type, party, ref_doc.company) else: # Get the exchange rate based on the posting date of the ref doc. exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d4cb57b2143..b436d022983 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -522,7 +522,8 @@ def reconcile_against_document( if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: # both ledgers must be posted to for `Advance` in separate account feature - doc.make_advance_gl_entries(referenced_row, update_outstanding="No") + doc.make_advance_gl_entries(cancel=1) + 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 158112896e5a8ead224b397e079d6784a475f49d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 Feb 2024 12:26:58 +0530 Subject: [PATCH 2/5] refactor(test): reference details will have account --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 8a03dd7278a..47cdb1b975f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1084,6 +1084,7 @@ class TestPaymentEntry(FrappeTestCase): ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency) expected_response = { + "account": pe.paid_from, "total_amount": 5000.0, "outstanding_amount": 5000.0, "exchange_rate": 1.0, From cb2529cec806ab1e31e1dcec1385115d26766749 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 12 Feb 2024 14:05:27 +0530 Subject: [PATCH 3/5] refactor(test): use get_party_account for reference details section --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 4 +++- 1 file changed, 3 insertions(+), 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 47cdb1b975f..5a014b85c99 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1070,6 +1070,8 @@ class TestPaymentEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, pe_draft.submit) def test_details_update_on_reference_table(self): + from erpnext.accounts.party import get_party_account + so = make_sales_order( customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True ) @@ -1084,7 +1086,7 @@ class TestPaymentEntry(FrappeTestCase): ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency) expected_response = { - "account": pe.paid_from, + "account": get_party_account("Customer", so.customer, so.company), "total_amount": 5000.0, "outstanding_amount": 5000.0, "exchange_rate": 1.0, From d9a0494fc33a4f5438d8ff49a92af66a1757a8ea Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Mar 2024 10:28:43 +0530 Subject: [PATCH 4/5] fix: advance paid amount and ledger entries against SO/PO --- .../doctype/payment_entry/payment_entry.py | 2 +- erpnext/accounts/utils.py | 28 ++++++++++++------- erpnext/controllers/accounts_controller.py | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1a89c45e001..7970a3e0678 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1299,7 +1299,7 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - dr_or_cr = "credit" if invoice.reference_doctype == "Sales Invoice" else "debit" + dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit" args_dict["account"] = invoice.account args_dict[dr_or_cr] = invoice.allocated_amount args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b436d022983..0755f2e9e84 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -490,7 +490,9 @@ def reconcile_against_document( # 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): + if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: + doc.make_advance_gl_entries(cancel=1) + else: _delete_pl_entries(voucher_type, voucher_no) for entry in entries: @@ -501,14 +503,16 @@ def reconcile_against_document( # update ref in advance entry if voucher_type == "Journal Entry": - referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) + referenced_row, update_advance_paid = 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}) doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: - referenced_row = update_reference_in_payment_entry( + referenced_row, update_advance_paid = update_reference_in_payment_entry( entry, doc, do_not_save=True, @@ -522,7 +526,7 @@ def reconcile_against_document( if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account: # both ledgers must be posted to for `Advance` in separate account feature - doc.make_advance_gl_entries(cancel=1) + # TODO: find a more efficient way post only for the new linked vouchers doc.make_advance_gl_entries() else: gl_map = doc.build_gl_map() @@ -533,6 +537,10 @@ def reconcile_against_document( update_voucher_outstanding( entry.against_voucher_type, entry.against_voucher, entry.account, 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 @@ -622,11 +630,12 @@ 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 = [] advance_payment_doctypes = frappe.get_hooks( "advance_payment_receivable_doctypes" ) + frappe.get_hooks("advance_payment_payable_doctypes") if jv_detail.get("reference_type") in advance_payment_doctypes: - frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid() + update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: # adjust the unreconciled balance @@ -675,7 +684,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 + return new_row.name, update_advance_paid def update_reference_in_payment_entry( @@ -694,6 +703,7 @@ def update_reference_in_payment_entry( "account": d.account, "dimensions": d.dimensions, } + update_advance_paid = [] if d.voucher_detail_no: existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] @@ -703,9 +713,7 @@ def update_reference_in_payment_entry( "advance_payment_receivable_doctypes" ) + frappe.get_hooks("advance_payment_payable_doctypes") if existing_row.get("reference_doctype") in advance_payment_doctypes: - frappe.get_doc( - existing_row.reference_doctype, existing_row.reference_name - ).set_total_advance_paid() + 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 @@ -735,7 +743,7 @@ def update_reference_in_payment_entry( if not do_not_save: payment_entry.save(ignore_permissions=True) - return row + return row, update_advance_paid def cancel_exchange_gain_loss_journal( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3db19672d8..aa3d3e07f03 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1864,7 +1864,7 @@ class AccountsController(TransactionBase): (ple.against_voucher_type == self.doctype) & (ple.against_voucher_no == self.name) & (ple.party == party) - & (ple.docstatus == 1) + & (ple.delinked == 0) & (ple.company == self.company) ) .run(as_dict=True) From e52c4c8f22d97c62391f9c4d76a513b5347290ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Mar 2024 15:13:05 +0530 Subject: [PATCH 5/5] refactor(test): make sure party has USD account 1. Don't reset 'party_account_currency' of SO/PO upon Payment Entry cancellation. This happens when there are no payments against a SO/PO --- .../purchase_order/test_purchase_order.py | 85 ++++++++++++++++++- erpnext/controllers/accounts_controller.py | 5 +- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d262783ae9a..c667ee821bd 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -762,11 +762,94 @@ class TestPurchaseOrder(FrappeTestCase): pe_doc = frappe.get_doc("Payment Entry", pe.name) pe_doc.cancel() + def create_account(self, account_name, company, currency, parent): + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": company} + ): + account = frappe.get_doc( + { + "doctype": "Account", + "account_name": account_name, + "parent_account": parent, + "company": company, + "account_currency": currency, + "is_group": 0, + "account_type": "Payable", + } + ).insert() + else: + account = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": company}, + fieldname="name", + pluck=True, + ) + + return account + + def test_advance_payment_with_separate_party_account_enabled(self): + """ + Test "Advance Paid" on Purchase Order, when "Book Advance Payments in Separate Party Account" is enabled and + the payment entry linked to the Order is allocated to Purchase Invoice. + """ + supplier = "_Test Supplier" + company = "_Test Company" + + # Setup default 'Advance Paid' account + account = self.create_account( + "Advance Paid", company, "INR", "Application of Funds (Assets) - _TC" + ) + company_doc = frappe.get_doc("Company", company) + company_doc.book_advance_payments_in_separate_party_account = True + company_doc.default_advance_paid_account = account.name + company_doc.save() + + po_doc = create_purchase_order(supplier=supplier) + + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + + pe = get_payment_entry("Purchase Order", po_doc.name) + pe.save().submit() + + po_doc.reload() + self.assertEqual(po_doc.advance_paid, 5000) + + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice + + pi = make_purchase_invoice(po_doc.name) + pi.append( + "advances", + { + "reference_type": pe.doctype, + "reference_name": pe.name, + "reference_row": pe.references[0].name, + "advance_amount": 5000, + "allocated_amount": 5000, + }, + ) + pi.save().submit() + pe.reload() + po_doc.reload() + self.assertEqual(po_doc.advance_paid, 0) + + company_doc.book_advance_payments_in_separate_party_account = False + company_doc.save() + @change_settings("Accounts Settings", {"unlink_advance_payment_on_cancelation_of_order": 1}) def test_advance_paid_upon_payment_entry_cancellation(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1) + supplier = "_Test Supplier USD" + company = "_Test Company" + + # Setup default USD payable account for Supplier + account = self.create_account("Creditors USD", company, "USD", "Accounts Payable - _TC") + supplier_doc = frappe.get_doc("Supplier", supplier) + if not [x for x in supplier_doc.accounts if x.company == company]: + supplier_doc.append("accounts", {"company": company, "account": account.name}) + supplier_doc.save() + + po_doc = create_purchase_order(supplier=supplier, currency="USD", do_not_submit=1) po_doc.conversion_rate = 80 po_doc.submit() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index aa3d3e07f03..c543dfc2cd0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1880,7 +1880,10 @@ class AccountsController(TransactionBase): advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency ) - frappe.db.set_value(self.doctype, self.name, "party_account_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