get_data() grouped only by Purchase Order Item while selecting Purchase
Order parent columns. MariaDB allows this loose GROUP BY; Postgres rejects
it with "column ... must appear in the GROUP BY clause".
Add the Purchase Order PK (po.name) to the GROUP BY. po.name is 1:1 with
the already-grouped po_item.name, so groups are unchanged and the result
is identical on MariaDB.
Adds a test (no test file existed) that runs the report and asserts the PO
is listed, exercising the GROUP BY on both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_po_entries() grouped only by (Purchase Order, material_request_item)
while selecting other Purchase Order Item columns. MariaDB allows this
loose GROUP BY (arbitrary-picking the extra columns); Postgres rejects it
with "column ... must appear in the GROUP BY clause".
Add the Purchase Order Item PK (child.name) to the GROUP BY so the
selected child columns are functionally determined by a grouped key.
Behaviour note: this is not a MariaDB no-op. When one PO has multiple
items sharing the same/blank material_request_item, MariaDB collapsed
them into one arbitrary row; now there is one row per PO line. The
downstream report already keys rows by purchase_order, so totals are
unaffected and the per-line breakdown is more correct.
Adds a test that runs the report and asserts the PO is listed, exercising
the GROUP BY on both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_query_bom_items / _build_base_bom_items_query / _add_*_item_columns selected
non-grouped columns (idx, item_name, image, project, item-default fields, BOM
Item attributes) alongside `group by item_code` -> arbitrary pick on MariaDB,
GroupingError on Postgres. Wrap them in Max() (Min() for idx, preserving the
original ordering). Every wrapped column is functionally dependent on the
grouped item_code (item attributes / the single BOM's project / one Item
Default per item+company), so Max()/Min() returns exactly the value MySQL
picked arbitrarily -> MariaDB output unchanged.
This was previously shipped in #56008 and reverted with that batch; re-applied
in isolation here. Verified: test_work_order 85/85 on BOTH MariaDB (no change)
and Postgres (was 85/85 failing on this query).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_items_from_manufacture_stock_entry aggregated Stock Entry Detail rows by
item_code while selecting item_name/description/warehouses/etc. (and an orderby
on the non-grouped idx) -> arbitrary pick on MariaDB, GroupingError on Postgres.
Wrap the non-grouped columns in Max() (Min(idx) for the orderby), preserving the
one-row-per-item shape the disassembly expects; an item plays one role with one
uom/warehouse across the WO's manufacture entries, so MariaDB output is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_secondary_items_from_job_card selected item_name/description/stock_uom/
bom_secondary_item alongside `group by item_code, secondary_item_type` (and an
orderby on the non-grouped idx) -> arbitrary pick on MariaDB, GroupingError on
Postgres. Wrap the non-grouped columns in Max() (Min(idx) for the orderby);
they are item attributes / the secondary-item BOM link, constant per group, so
MariaDB output is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validate_expense_account SLE existence check -> frappe.db.get_all(limit=1);
get_items_for_stock_reco's two comma-join SELECTs -> frappe.qb inner_joins, with
the correlated Warehouse-subtree EXISTS replaced by a precomputed
warehouses_in_tree subquery + isin and ifnull(disabled,0)=0 -> disabled==0|isnull.
The Item-Default query's `group by i.name` is dropped (sound: validate_item_defaults
enforces one Item Default per (item, company), so the company-filtered query already
returns one row per item; the downstream (item_code, warehouse) de-dup is unchanged).
Same result on MariaDB; valid under Postgres.
Tests: get_items_for_stock_reco Bin branch (stocked item) and Item-Default branch
(default_warehouse, no stock).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_items_to_be_repost selected posting_date/posting_time/creation/posting_datetime
alongside `group_by item_code, warehouse` with no aggregation -> arbitrary pick on
MariaDB, GroupingError on Postgres. Wrap the four columns in `Min()` (earliest row
per item+warehouse, the correct repost-start point; a single voucher's SLEs share
posting_date/time per group -> MariaDB-identical). This is reached by every stock
transaction submit/cancel via repost_future_sle_and_gle, so it unblocks the whole
transaction-heavy stock suite on Postgres (e.g. test_purchase_receipt 105/105).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the `update tabSerial No set purchase_rate ... where name in (...)` to
frappe.qb.update(isin). Also fix the #39 Postgres bug in
set_landed_cost_voucher_amount: `.select(Sum(applicable_charges), cost_center)`
selected a non-grouped column with no GROUP BY (GroupingError on PG) -> wrap it
in `Max(cost_center)` (deterministic representative; per (receipt_document,
receipt_item) the matching LCV items share a cost_center -> MariaDB-identical).
Covered by the existing landed-cost tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw `select pr_detail, qty from Purchase Invoice Item` (summed in
Python) with a frappe.qb GROUP BY Sum(qty) per pr_detail, matching the sibling
get_returned_qty_map. Same result on MariaDB; valid under Postgres. Covered by
the existing make_purchase_invoice tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_already_received_qty (sum over Purchase Receipt Item, parent != self.name)
and the two Purchase-Invoice-against-receipt existence checks (implicit
comma-joins -> child-table get_all on Purchase Invoice Item, docstatus=1).
Also fixes a pre-existing `self.submit_rv` -> `submit_rv` typo in the (dead)
check_next_docstatus that staging carried forward. Same result on MariaDB;
valid under Postgres.
Tests: get_already_received_qty (parent-exclusion sum) and check_next_docstatus
(blocks on a submitted Purchase Invoice; also locks the typo fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The authorized-user backdated-entry guard used MariaDB-only
`MAX(timestamp(posting_date, posting_time))`, invalid on Postgres. Convert to
`Max(posting_datetime)` (the precomputed column) via frappe.qb. Same result on
MariaDB; now valid under Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw nearest-ancestor warehouse-account SELECT with frappe.get_all;
`account is not null and ifnull(account,'')!=''` -> filter ["account","is","set"]
(IS NOT NULL AND != ''), order_by lft desc, limit 1, pluck. Same result on
MariaDB; valid under Postgres. Covered by the existing get_warehouse_account tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert get_stock_value_from_bin (comma-join + internal ifnull/warehouse-subtree
fragments -> inner_join + qb subquery), get_latest_stock_qty, get_latest_stock_balance,
get_avg_purchase_rate and get_incoming_outgoing_rate_for_cancel (Case/Abs) to
frappe.qb / get_all. Same result on MariaDB; valid under Postgres.
Tests: get_latest_stock_qty and get_stock_value_from_bin against received stock.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the repost item/warehouse UNION, get_balance_qty_from_sle,
get_reserved_qty (UNION of correlated subqueries -> two qb Sum branches with an
inner_join to Sales Order Item, added in Python; qty!=0 guards the divide and
mirrors the original `qty>=delivered_qty` which on MariaDB excluded x/0 NULL
rows), get_indented_qty, get_planned_qty and set_stock_balance_as_per_serial_no
to frappe.qb / get_all / db.count. Same result on MariaDB; valid under Postgres.
Tests (new test_stock_balance.py): get_reserved_qty SO-item + packed-bundle
branches and get_indented_qty, all without delivery so they avoid the unrelated
#39 SLE-repost path and pass on Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
validate_qty_against_so: the already-indented (Material Request Item) and
Sales-Order-qty (Sales Order Item) sum lookups -> frappe.get_all({SUM}).
check_modified_date: raw `select modified` + MariaDB-only `TIMEDIFF` ->
frappe.db.get_value + a get_datetime() comparison. The TIMEDIFF removal also
fixes a real Postgres bug: update_status() (Stop/Reopen/Cancel) ran TIMEDIFF,
which errors on PG (`function timediff does not exist`); this greens 7
previously-failing status-change tests on Postgres.
Same result on MariaDB. Tests: concurrent-modification guard (pass + throw
branches) and the over-request-against-SO throw (both converted SUM queries +
the boundary). mapper.py is intentionally left untouched (no raw SQL; its
staging copy predates develop's RFQ cost_center field-map).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_default_contact / get_default_address: raw correlated-subquery SELECTs over
Dynamic Link -> frappe.qb with a LEFT join (preserving the original
correlated-subquery semantics: a Dynamic Link whose parent Contact/Address is
missing still returns, with a NULL flag). Same result on MariaDB; valid under
Postgres.
Tests: pin the converted query output (real linked Contact/Address) and lock
the LEFT-join choice with an orphaned-Dynamic-Link case (fails under an inner
join).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
update_billed_amount_based_on_so: raw "select sum(amount) ... where
dn_detail=%s and docstatus=1" -> frappe.get_all(fields=[{SUM: amount}]); the
bare aggregate needs no GROUP BY and the NULL-sum still resolves to 0. Same
result on MariaDB; valid under Postgres. Covered by the existing billing tests
in test_delivery_note.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cancel_packing_slips: raw "SELECT name FROM `tabPacking Slip` WHERE
delivery_note=%s AND docstatus=1" -> frappe.get_all(pluck="name") with
pluck-aware iteration. Same result on MariaDB; valid under Postgres.
Covered by test_cancel_packing_slips_cancels_submitted_slips in
test_delivery_note.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
set_actual_qty Bin lookup -> frappe.db.get_value; validate_proj_cust raw
"customer=%s OR ifnull(customer,'')=''" -> get_all or_filters with
[customer, is, not set] (correct PG empty-string/NULL handling); the two
check_next_docstatus implicit comma-joins -> get_all on the child table
(Sales Invoice Item / Installation Note Item, docstatus=1). Same result on
MariaDB; valid under Postgres.
Tests: validate_proj_cust mismatch + no-customer (the or_filters branch), and
check_next_docstatus blocking cancel when a submitted Sales Invoice draws from
the DN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw SELECT with get_match_cond in item_details with
frappe.qb.get_query(ignore_permissions=False) plus a Delivery Note Item
subquery; get_query applies the permission match conditions. Same result on
MariaDB; valid under Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the get_all(limit=1, pluck) existence checks with frappe.db.exists and
the no-own-valuation-method Stock Ledger Entry EXISTS with a frappe.qb
subquery (null-or-empty valuation_method preserved). Same result on MariaDB;
valid under Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the on_trash Stock Ledger Entry serial-match SELECT and the
update_maintenance_status expiry SELECT with frappe.get_all (or_filters for
the amc/warranty expiry OR). Same result on MariaDB; valid under Postgres.
Tests: maintenance-status expiry transition, the not-in exclusion (with a
negative-control candidate), and NULL-status handling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw f-string UPDATE (name + production_item match) that links a
Quality Inspection back to its Job Card with frappe.qb.update. Same result on
MariaDB; valid under Postgres.
Tests: the Job Card reference update and its production_item scoping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw UPDATE ... modified=NOW() in update_item_price with
frappe.qb.update (now() for modified). Same result on MariaDB; valid under
Postgres.
Tests: currency/buying/selling/modified propagation and price-list scoping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw UNION of forward/two-way alternative-item matches with two
frappe.get_all calls, order-preserving dedup (dict.fromkeys) and Python
pagination. Each leg is bounded to start+page_len rows so the per-keystroke
search round trip stays small (the original bounded with LIMIT/OFFSET);
ItemAlternative forbids duplicate (item_code, alternative_item_code) pairs, so
each leg is internally distinct and that bound is exact. Same result on
MariaDB; valid under Postgres.
Tests: both-direction dedup, txt filtering, pagination, bounded-and-exact
page-walk reconstruction, and case-insensitive (ILIKE-on-Postgres) matching.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw lft/rgt SELECT with frappe.get_all(pluck="name"). Same result
on MariaDB; valid under Postgres.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw EXISTS subquery (items within an item-group subtree) with
frappe.qb (item_group.isin(subquery)). Same result on MariaDB; valid under
Postgres' stricter SQL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
make_timesheet(simulate=True) logs at now_datetime(); when the suite runs late
in the day under the site timezone the 2h log crosses midnight, so its to_time
falls outside the report's `to_time <= end-of-day` bound and the submitted
timesheet is (correctly) excluded — making test_submitted_timesheet_in_summary
fail in an evening window (observed at 22:51 IST in CI). Pin the log to a fixed
mid-day window on today so the assertion is time-of-day independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>