Merge pull request #38484 from frappe/mergify/bp/version-15-hotfix/pr-38393

refactor: GL entries build logic for `Advance in Separate Party Account` option. (backport #38393)
This commit is contained in:
ruthra kumar 2023-12-01 13:26:11 +05:30 committed by GitHub
commit bf44e9ed64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 362 additions and 106 deletions

View File

@ -1055,112 +1055,105 @@ 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:
gle = party_gl_dict.copy()
if self.payment_type == "Receive":
amount = self.base_paid_amount
else:
amount = self.base_received_amount
exchange_rate = self.get_exchange_rate()
amount_in_account_currency = amount * exchange_rate
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
dr_or_cr: 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,
}
)
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")
def make_advance_gl_entries(self, against_voucher_type=None, against_voucher=None, cancel=0):
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)
gle = party_gl_dict.copy()
if cancel:
for entry in gl_entries:
frappe.db.set_value(
"GL Entry",
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(
{
"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,
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:
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
gle = party_gl_dict.copy()
gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
dr_or_cr: base_unallocated_amount,
}
)
make_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True)
return
gl_entries.append(gle)
# 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)
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)
make_gl_entries(gl_entries)
if cancel:
make_reverse_gl_entries(gl_entries, partial_cancel=True)
else:
make_gl_entries(gl_entries, update_outstanding=update_outstanding)
def make_invoice_liability_entry(self, gl_entries, invoice):
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:
references = [x for x in self.get("references")]
if entry:
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"):
self.add_advance_gl_for_reference(gl_entries, ref)
def add_advance_gl_for_reference(self, gl_entries, invoice):
args_dict = {
"party_type": self.party_type,
"party": self.party,

View File

@ -8,6 +8,7 @@ from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
get_payment_entry,
@ -1318,6 +1319,142 @@ 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="Receivable",
)
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.payment_name = pe.name
pr.invoice_name = si.name
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 = (

View File

@ -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(
@ -1769,10 +1770,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")

View File

@ -3377,21 +3377,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,
)
@ -3417,9 +3417,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()],
]
@ -3456,6 +3456,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(

View File

@ -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:

View File

@ -472,7 +472,11 @@ 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)
# 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)
for entry in entries:
check_if_advance_entry_modified(entry)
@ -487,23 +491,26 @@ 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
)
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` 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)
# 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
@ -671,11 +678,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()
@ -688,6 +696,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(
@ -864,7 +873,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: