Remove per-doctype branching from the shared stock GL composer so each
voucher owns its own behavior:
- Move the Stock Reconciliation voucher-detail synthesis out of
BaseStockGLComposer.get_voucher_details into a
StockReconciliationGLComposer.get_voucher_details override.
- Replace the hardcoded doctype allow-list in check_expense_account with
an overridable class attribute enforce_pl_expense_account (default
True). Vouchers that post the difference to a balance-sheet account
(Stock Entry, Stock Reconciliation, Delivery Note) set it False.
- Add DeliveryNoteGLComposer to own the P&L-exempt rule and wire
DeliveryNote.get_gl_entries to it.
No change to GL output; behavior is relocated, not altered.
* fix: added permission checks on various whitelisted functions
* fix: permission checks on `get_party_account` and using "select" `ptype`
* test(`BootStrapTestData`): assign `Accounts User` role to User `test@example.com`
update_current_stock() in delivery_note.py used to call
frappe.db.get_value("Bin", ...) separately for every row in items and
every row in packed_items - so a delivery note with 200 items and 200
packed items made 400 separate database calls on every save.
now it groups item codes by warehouse and fetches bin data with one
query per distinct warehouse, then assigns actual_qty/projected_qty to
each row from that result - same values as before, far fewer database
calls, and no cross-product over-fetch across warehouses.
get_item_warehouse_projected_qty ran an uncached frappe.get_doc per Bin
row to walk the warehouse parent chain, re-fetching the same ancestors
for every item sharing a warehouse. Preload the warehouse parent map
once and walk it in memory instead.
based_wise_columns_query() and group_wise_column() in trends.py built
column labels as raw strings, so "Item", "Item Name", "Customer",
"Supplier", "Territory", "Currency", etc. never went through _() and
stayed in English regardless of the user's language, unlike the
period and total columns right next to them which were already
wrapped correctly.
Add regression coverage for the new abbreviation-rename propagation:
a simple item_code rename, item_name derived from a template whose
item_name differs from its item_code, and a manually customized
item_name getting rebuilt rather than left stale.
Item Attribute abbreviations only got baked into a variant's item_code
and item_name at creation time (make_variant_item_code returns early
once item_code is set). Renaming an abbreviation afterwards left every
existing variant stuck with the stale code, silently out of sync with
its own attribute.
Detect abbreviation renames on Item Attribute save, find every variant
using the affected value, and rebuild+rename its item_code via
frappe.rename_doc so linked records follow along. item_name is rebuilt
in lockstep from the template's item_name, even if it had since been
customized, since both fields are meant to be derived from the same
abbreviation.
On postgres, outward batch valuation row-locked the item's ENTIRE SLE / Serial and Batch Entry history (a separate plain SELECT ... FOR UPDATE per site, since FOR UPDATE is invalid with GROUP BY there). That writes a lock marker (xmax + WAL) on every historical tuple per outward movement - write amplification that grows with history forever - and locks nothing at all when the history is empty (negative-stock edge: two concurrent outwards don't serialize).
Replace all four history-wide postgres lock statements with one frappe.db.transaction_advisory_lock(("batch-valuation", item_code, warehouse)) at the top of BatchNoValuation.calculate_avg_rate's outward branch - every valuation read (bundle + the three deprecated paths) is downstream of it. Released at commit/rollback, participates in deadlock detection, and serializes regardless of history size, so the empty-history edge is closed by construction. Batch qty updates iterate in sorted order so concurrent vouchers lock Batch rows in the same sequence.
MariaDB is unchanged: it keeps the original grouped FOR UPDATE row locks (and its gap locks). Requires frappe#40621.
Test: outward delivery of a batched item must leave the xact advisory lock visible in pg_locks for the submitting transaction.
Postgres has no gap locks, so the lock-then-read pattern (plain SELECT ... FOR UPDATE before a grouped read) only serializes on rows that already exist. Two sites had reachable races where the lock set is empty or disjoint:
- Pick list: two pick lists against the same SO item submitted concurrently lock only docstatus=1 rows, so with no previously-submitted picks their lock sets are disjoint and both pass validate_picked_qty (over-pick; picked_qty last-writer-wins). Gate on the referenced Sales Order Item / Packed Item rows, which always exist.
- Stock reservation: the first concurrent reservations for an (item, warehouse) find no SRE rows to lock, so both pass and reserved qty can exceed actual. Gate on the Bin row, which exists once there is stock.
MariaDB is unchanged (its gap locks already serialize both; the gates are postgres-only). Also: ORDER BY on the small-set postgres lock selects for deterministic lock order, and the repost pre-lock in get_future_stock_vouchers selects a constant instead of shipping every matching SLE name to the client.
A full audit of the loose-GROUP-BY fixes found four recurring mistakes in the fixes themselves: incoherent Max/Min pairs over coupled columns, NULL-skipping Max on discriminators, Sum(x)*Max(y) fabricated arithmetic, and wrong-bound picks. Add them to the compatibility catalog (new 3.1) and to the Greptile review instructions so future PRs get flagged.
The Budget Variance Report chart plotted the actual expense one month
earlier than the table (e.g. July actual shown under June).
build_comparison_chart_data() collected budget columns using
fieldname.startswith("budget_"). The dimension column "budget_against"
also matches that prefix, so it was added as an extra leading entry to
budget_fields and labels, while actual_fields had no such leading entry.
This shifted every actual value one position ahead of its label.
Skip the "budget_against" dimension column so budget/actual values and
labels stay aligned per month.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>