mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-13 09:10:36 +00:00
Merge pull request #56089 from mihir-kandoi/pg-ar-future-payments
fix(accounts): Journal Entry future payments mis-allocated to one invoice in Accounts Receivable
This commit is contained in:
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, query_builder, scrub
|
from frappe import _, qb, query_builder, scrub
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.functions import Date, Max, Substring, Sum
|
from frappe.query_builder.functions import Date, Substring, Sum
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -691,13 +691,11 @@ class ReceivablePayableReport:
|
|||||||
.inner_join(jea)
|
.inner_join(jea)
|
||||||
.on(jea.parent == je.name)
|
.on(jea.parent == je.name)
|
||||||
.select(
|
.select(
|
||||||
# Sum() below makes this an implicit aggregate (no GROUP BY); the non-aggregated columns
|
jea.reference_name.as_("invoice_no"),
|
||||||
# are arbitrary per the single group on MySQL -> Max() keeps it valid on postgres.
|
jea.party,
|
||||||
Max(jea.reference_name).as_("invoice_no"),
|
jea.party_type,
|
||||||
Max(jea.party).as_("party"),
|
je.posting_date.as_("future_date"),
|
||||||
Max(jea.party_type).as_("party_type"),
|
je.cheque_no.as_("future_ref"),
|
||||||
Max(je.posting_date).as_("future_date"),
|
|
||||||
Max(je.cheque_no).as_("future_ref"),
|
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(je.docstatus < 2)
|
(je.docstatus < 2)
|
||||||
@@ -727,6 +725,14 @@ class ReceivablePayableReport:
|
|||||||
future_amount.as_("future_amount"),
|
future_amount.as_("future_amount"),
|
||||||
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
|
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
|
||||||
)
|
)
|
||||||
|
# One row per (future-payment JE, invoice, party): group by the JE name (primary key, so the
|
||||||
|
# JE-level posting_date/cheque_no are deterministic) plus the per-reference dimensions, summing
|
||||||
|
# amounts across JE Account rows that hit the same invoice. Without this GROUP BY the implicit
|
||||||
|
# single-group aggregate collapsed every future JE payment into one row keyed by an arbitrary
|
||||||
|
# invoice, mis-allocating the whole sum.
|
||||||
|
query = query.groupby(
|
||||||
|
je.name, jea.reference_name, jea.party, jea.party_type, je.posting_date, je.cheque_no
|
||||||
|
)
|
||||||
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
|
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
|
||||||
query = query.having(future_amount > 0)
|
query = query.having(future_amount > 0)
|
||||||
return query.run(as_dict=True)
|
return query.run(as_dict=True)
|
||||||
|
|||||||
@@ -699,6 +699,61 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
|
|||||||
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_future_payments_from_journal_entry(self):
|
||||||
|
# A single future-dated Journal Entry paying two different invoices must surface as one
|
||||||
|
# future-payment row PER invoice, not collapse the whole sum onto one arbitrary invoice
|
||||||
|
# (regression: the implicit single-group aggregate filed all future JE payments under one key).
|
||||||
|
si_a = self.create_sales_invoice(no_payment_schedule=True)
|
||||||
|
si_b = self.create_sales_invoice(no_payment_schedule=True)
|
||||||
|
|
||||||
|
je = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Journal Entry",
|
||||||
|
"voucher_type": "Journal Entry",
|
||||||
|
"company": self.company,
|
||||||
|
"posting_date": add_days(today(), 1),
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"reference_type": "Sales Invoice",
|
||||||
|
"reference_name": si_a.name,
|
||||||
|
"credit_in_account_currency": 50,
|
||||||
|
"credit": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"reference_type": "Sales Invoice",
|
||||||
|
"reference_name": si_b.name,
|
||||||
|
"credit_in_account_currency": 50,
|
||||||
|
"credit": 50,
|
||||||
|
},
|
||||||
|
{"account": self.cash, "debit_in_account_currency": 100, "debit": 100},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
je.insert().submit()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range": "30, 60, 90, 120",
|
||||||
|
"show_future_payments": True,
|
||||||
|
}
|
||||||
|
report = execute(filters)[1]
|
||||||
|
rows_a = [row for row in report if row.voucher_no == si_a.name]
|
||||||
|
rows_b = [row for row in report if row.voucher_no == si_b.name]
|
||||||
|
|
||||||
|
# exactly one report row per invoice, each keeping its own future payment; the bug collapsed
|
||||||
|
# both into a single row and allocated the whole 100 to one arbitrary invoice
|
||||||
|
self.assertEqual(len(rows_a), 1)
|
||||||
|
self.assertEqual(len(rows_b), 1)
|
||||||
|
self.assertEqual(rows_a[0].future_amount, 50.0)
|
||||||
|
self.assertEqual(rows_b[0].future_amount, 50.0)
|
||||||
|
|
||||||
def test_sales_person(self):
|
def test_sales_person(self):
|
||||||
sales_person = frappe.get_doc(
|
sales_person = frappe.get_doc(
|
||||||
{"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}
|
{"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}
|
||||||
|
|||||||
Reference in New Issue
Block a user