mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 07:28:39 +00:00
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user