Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Advance Payment(Liability) ledger entries and deduplication #37172

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 70 additions & 82 deletions erpnext/accounts/doctype/payment_entry/payment_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,8 @@ def make_gl_entries(self, cancel=0, adv_adj=0):
else:
self.make_exchange_gain_loss_journal()

self.make_advance_gl_entries(cancel=cancel)
if self.book_advance_payments_in_separate_party_account:
self.make_advance_gl_entries(cancel=cancel)

def add_party_gl_entries(self, gl_entries):
if self.party_account:
Expand All @@ -1055,55 +1056,67 @@ def add_party_gl_entries(self, gl_entries):
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")
if self.book_advance_payments_in_separate_party_account:
dr_or_cr = (
"credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
)

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,
dr_or_cr: self.base_paid_amount
if self.payment_type == "Receive"
else self.base_received_amount,
dr_or_cr + "_in_account_currency": self.paid_amount
if self.payment_type == "Receive"
else self.received_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)

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

gle = party_gl_dict.copy()
gle.update(
{
Expand All @@ -1114,53 +1127,28 @@ def add_party_gl_entries(self, gl_entries):

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

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,
)
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_reverse_gl_entries(gl_entries=gl_entries, partial_cancel=True)
return
if cancel:
make_reverse_gl_entries(gl_entries, partial_cancel=True)
else:
make_gl_entries(gl_entries, update_outstanding=update_outstanding)

# 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 add_advance_gl_entries(self, gl_entries: list, entry: object | dict):
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]

make_gl_entries(gl_entries)
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 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1769,10 +1769,10 @@ def test_advance_entries_as_asset(self):

# 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")
Expand Down
95 changes: 91 additions & 4 deletions erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -3371,21 +3371,21 @@ def test_sales_invoice_with_payable_tax_account(self):
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,
)
Expand All @@ -3411,9 +3411,9 @@ def test_advance_entries_as_liability(self):

# 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()],
]

Expand Down Expand Up @@ -3450,6 +3450,93 @@ def test_sales_return_negative_rate(self):
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(
Expand Down
26 changes: 25 additions & 1 deletion erpnext/accounts/general_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,31 @@ 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:
Expand Down
Loading
Loading