refactor(postgres): port Accounts Receivable report query to the query builder

Most queries are straight raw-SQL -> query-builder ports. One rider:
get_future_payments_from_journal_entry sums future amounts with no GROUP BY,
so its non-aggregated identity columns (invoice_no/party/future_date/future_ref)
are wrapped in Max() to satisfy postgres strict GROUP BY. The summed amount is
unchanged; the attributed invoice/party label stays within MariaDB's existing
arbitrary-row indeterminacy for that already-aggregated single-row query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mihir Kandoi
2026-06-17 16:49:48 +05:30
parent 8527e78820
commit 30568d36d0

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.query_builder.functions import Date, Max, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -427,32 +427,21 @@ class ReceivablePayableReport:
self.delivery_notes = frappe._dict()
# delivery note link inside sales invoice
# nosemgrep
si_against_dn = frappe.db.sql(
"""
select parent, delivery_note
from `tabSales Invoice Item`
where docstatus=1 and parent in (%s)
"""
% (",".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
si_against_dn = frappe.get_all(
"Sales Invoice Item",
filters={"docstatus": 1, "parent": ["in", list(self.invoices)]},
fields=["parent", "delivery_note"],
)
for d in si_against_dn:
if d.delivery_note:
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
# nosemgrep
dn_against_si = frappe.db.sql(
"""
select distinct parent, against_sales_invoice
from `tabDelivery Note Item`
where against_sales_invoice in (%s)
"""
% (",".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
dn_against_si = frappe.get_all(
"Delivery Note Item",
filters={"against_sales_invoice": ["in", list(self.invoices)]},
fields=["parent", "against_sales_invoice"],
distinct=True,
)
for d in dn_against_si:
@@ -476,14 +465,10 @@ class ReceivablePayableReport:
# Get Sales Team
if self.filters.show_sales_person:
# nosemgrep
sales_team = frappe.db.sql(
"""
select parent, sales_person
from `tabSales Team`
where parenttype = 'Sales Invoice'
""",
as_dict=1,
sales_team = frappe.get_all(
"Sales Team",
filters={"parenttype": "Sales Invoice"},
fields=["parent", "sales_person"],
)
for d in sales_team:
self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append(
@@ -548,22 +533,31 @@ class ReceivablePayableReport:
def get_payment_terms(self, row):
# build payment_terms for row
# nosemgrep
payment_terms_details = frappe.db.sql(
f"""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
""",
row.voucher_no,
as_dict=1,
si = frappe.qb.DocType(row.voucher_type)
ps = frappe.qb.DocType("Payment Schedule")
payment_terms_details = (
frappe.qb.from_(si)
.inner_join(ps)
.on(si.name == ps.parent)
.select(
si.name,
si.party_account_currency,
si.currency,
si.conversion_rate,
si.total_advance,
ps.due_date,
ps.payment_term,
ps.payment_amount,
ps.base_payment_amount,
ps.description,
ps.paid_amount,
ps.base_paid_amount,
ps.discounted_amount,
)
.where((ps.parenttype == row.voucher_type) & (si.name == row.voucher_no) & (si.is_return == 0))
.orderby(ps.paid_amount, order=frappe.qb.desc)
.orderby(ps.due_date)
.run(as_dict=1)
)
original_row = frappe._dict(row)
@@ -661,7 +655,6 @@ class ReceivablePayableReport:
def get_future_payments_from_payment_entry(self):
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
return (
frappe.qb.from_(pe)
@@ -674,11 +667,14 @@ class ReceivablePayableReport:
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
ifelse(
# CASE is portable; MySQL's IF() does not exist on postgres
query_builder.Case()
.when(
pe.payment_type == "Receive",
pe.source_exchange_rate * pe_ref.allocated_amount,
pe.target_exchange_rate * pe_ref.allocated_amount,
).as_("future_amount_in_base_currency"),
)
.else_(pe.target_exchange_rate * pe_ref.allocated_amount)
.as_("future_amount_in_base_currency"),
)
.where(
(pe.docstatus < 2)
@@ -695,11 +691,13 @@ class ReceivablePayableReport:
.inner_join(jea)
.on(jea.parent == je.name)
.select(
jea.reference_name.as_("invoice_no"),
jea.party,
jea.party_type,
je.posting_date.as_("future_date"),
je.cheque_no.as_("future_ref"),
# Sum() below makes this an implicit aggregate (no GROUP BY); the non-aggregated columns
# are arbitrary per the single group on MySQL -> Max() keeps it valid on postgres.
Max(jea.reference_name).as_("invoice_no"),
Max(jea.party).as_("party"),
Max(jea.party_type).as_("party_type"),
Max(je.posting_date).as_("future_date"),
Max(je.cheque_no).as_("future_ref"),
)
.where(
(je.docstatus < 2)
@@ -712,30 +710,25 @@ class ReceivablePayableReport:
if self.filters.get("party"):
if self.account_type == "Payable":
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
future_amount = Sum(jea.debit_in_account_currency - jea.credit_in_account_currency)
future_amount_in_base_currency = Sum(jea.debit - jea.credit)
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
future_amount = Sum(jea.credit_in_account_currency - jea.debit_in_account_currency)
future_amount_in_base_currency = Sum(jea.credit - jea.debit)
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
"future_amount_in_base_currency"
)
)
query = query.select(
Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
).as_("future_amount")
future_amount_in_base_currency = Sum(jea.debit if self.account_type == "Payable" else jea.credit)
future_amount = Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
)
query = query.having(qb.Field("future_amount") > 0)
query = query.select(
future_amount.as_("future_amount"),
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
)
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
query = query.having(future_amount > 0)
return query.run(as_dict=True)
def allocate_future_payments(self, row):
@@ -891,16 +884,19 @@ class ReceivablePayableReport:
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
# nosemgrep
records = frappe.db.sql(
"""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype in ('Customer', 'Sales Invoice')
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""",
(lft, rgt),
as_dict=1,
steam = frappe.qb.DocType("Sales Team")
sp = frappe.qb.DocType("Sales Person")
records = (
frappe.qb.from_(steam)
.select(steam.parent, steam.parenttype)
.distinct()
.where(
steam.parenttype.isin(["Customer", "Sales Invoice"])
& steam.sales_person.isin(
frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))
)
)
.run(as_dict=1)
)
self.sales_person_records = frappe._dict()