Add methods=["POST"] to 50 whitelisted functions that create or modify
documents (get_doc followed by insert/save/submit), so they can no
longer be invoked via GET requests.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Extend the boundary rule to callers: non-decorated code that built or
annotated with ItemDetailsCtx now uses frappe._dict directly, and drops
the now-unused import. asset_capitalization keeps ItemDetailsCtx for its
own normalize_ctx_input-decorated functions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
make_depreciation_entry posts a Journal Entry per schedule row in a loop; the except only stored the error, so the next row's je.save()/submit() ran on the Postgres-poisoned txn (InFailedSqlTransaction). Savepoint per iteration + rollback(save_point=) before storing the error; the final raise of the collected error is unchanged. No-op on MariaDB.
get_latest_location_and_custodian ordered by transaction_date DESC only; equal-dated movements left the current location/custodian engine-dependent. Add asm.name DESC tiebreaker so both engines pick the same movement.
The "Is Fully Depreciated" field was hidden on the Asset form (hidden: 1),
so it could never be set for manually entered existing assets.
Make it visible based on context:
- Existing Asset with Calculate Depreciation off -> visible and editable
- Calculate Depreciation on -> visible but read-only and forced unchecked
(it is only meaningful for manually entered assets)
The unchecked value is enforced in the form script (immediate feedback on
toggle and on load) and in server-side validate() so it can never be saved
as checked while depreciation is being calculated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A deeper re-audit (with an adversarial skeptic) of the queries left raw
in the prior commit found more that have exact ORM equivalents:
- scalar SUM/MAX -> frappe.qb + Sum/Max .run()[0][0]
- SUM ... GROUP BY -> frappe.qb .groupby().select(Sum().as_()) run(as_dict)
- name IN (values) -> get_all(filters={'f': ['in', ...]})
- sql_list(select col) -> get_all(pluck='col')
- bulk UPDATE ... = NULL/value -> frappe.db.set_value(filters, field, val)
- positional as_list reads -> get_all(..., as_list=True) (+ sorted())
Note: get_value(dt, filters, 'sum(x)') and get_all(fields=['sum(x)'])
are rejected by frappe ('SQL functions are not allowed as strings'), so
aggregates go through frappe.qb. get_all(as_list=True) returns a tuple
(not a list), so consumers that mutate use sorted().
All affected test modules pass on MariaDB.
Conservative cleanup of frappe.throw/msgprint messages per the message style
guide; meaning, severity, and .format() arguments are unchanged:
- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break gettext
extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms
- drop no-op _() wrapping runtime-built strings
Part of #53976.
The Fixed Asset Register report had no test file. Add tests for asset value
(net purchase amount, reduced by opening accumulated depreciation), the
status (In Location) and asset category filters, and group-by-asset-category
value totals.
The repo-wide query audit fixed runtime/source queries, but test files carry their
own raw SQL helpers that were never swept and only fail when the suite runs on
Postgres. Port the staging branch's already-green fixes for them:
- timestamp(posting_date, posting_time) (raw + qb Timestamp) -> posting_datetime /
CombineDatetime (test_stock_ledger_entry, test_stock_balance, test_utils)
- HAVING <select-alias> -> qb .having(<expr>) (test_asset_capitalization, test_purchase_order)
- capital-cased identifiers ("Status", "Name") -> lowercase (test_delivery_note,
test_purchase_order, test_employee)
- raw GL/SLE select helpers -> frappe.get_all / qb, with order-independent
comparisons where account ordering is collation-dependent across engines
(test_purchase_invoice, test_sales_invoice, test_payment_entry, test_asset,
test_purchase_receipt, test_payment_request, test_repost_accounting_ledger,
test_journal_entry)
All changes are test-only and behaviour-identical on MariaDB (lowercase column names
resolve the same; posting_datetime == timestamp(posting_date, posting_time); HAVING on
the expression is the same computation). Verified: the heavy modules pass on both
MariaDB and Postgres, and MariaDB output is unchanged.
- cancel_movement_entries: filter the parent Asset Movement's docstatus via a qb join
(as the original SQL did), instead of the child Asset Movement Item.docstatus. Behaviour
is identical in normal flows (child docstatus is synced) but this is exactly faithful.
- get_maintenance_log: add a both-engine test for this previously-untested whitelisted
endpoint. Confirms the frappe v16 dict aggregate field spec ({"COUNT": ...}) runs and
returns correct per-status counts (no runtime crash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert the remaining raw frappe.db.sql in the Assets module to frappe.qb / ORM so
the queries run on PostgreSQL as well as MariaDB. Faithful 1:1 conversions -- no
MariaDB behaviour change:
- asset.py (gl-entry / bom-cost fetches), asset_maintenance.py (team members),
asset_movement.py (latest location/custodian), location.py (get_children)
- fixed_asset_register.py: the depreciation-amount aggregate groups by asset.name
(the primary key) selecting only asset.name + Sum(gle.debit), which is valid under
Postgres strict GROUP BY (PK functional dependency)
Tests: existing asset (61), asset_maintenance, asset_movement and location suites
pass on both engines; adds a test for the previously-untested Fixed Asset Register
report (covers the GROUP BY aggregate on both engines).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update erpnext's own importers (asset depreciation, invoice discounting and the
JE tests) to import the builders from mapper.py directly. Drop
make_inter_company_journal_entry and make_reverse_journal_entry from the
backward-compat re-export in journal_entry.py -- they are not part of the
custom-app call surface; only the payment-entry builders remain re-exported.
Relocate get_voucher_details, check_expense_account and get_debit_field_precision
from StockController to BaseStockGLComposer, where they are only used (by compose()
and AssetCapitalizationGLComposer). Call sites flipped from doc.X to self.X.
Inventory-account resolution (get_inventory_account_map/_dict, etc.) stays on the
controller: it is a doc-contract method called as doc.X from non-stock-composer code
(PI controller/composer, accounts/utils, repost_accounting_ledger), so it cannot fold
into BaseStockGLComposer. make_gl_entries / make_gl_entries_on_cancel / add_gl_entry
likewise stay (contract entry points).
Behaviour-preserving: ledger snapshots, subcontracting receipt and asset
capitalization suites stay green.
frappe-modifying-but-not-comitting anchors on the method definition, so the
suppression must sit on the def line (matching the convention used elsewhere
in the codebase); inner-line comments did not suppress it.
Both patterns are unchanged from develop but newly appear in the diff
because the refactoring relocated them:
- purchase_order/mapper.make_purchase_invoice_from_portal: portal flow
needs commit before redirect (matches develop behaviour)
- asset_repair.on_cancel: ignore_linked_doctypes is a runtime cancel flag,
not a persisted field
Resolve 4 conflicts from Phase 7 service/mapper extraction vs upstream:
- asset.py: take extraction; repoint dangling make_asset_movement JS to mapper
- job_card: port upstream field_no_map(naming_series) into mapper.make_subcontracting_po
- sales_order: port upstream rows-index fix into mapper.make_delivery_note
- sales_invoice (Phase 7): take service delegations; port upstream SQL->QB/ORM
changes for get_warehouse, get_all_mode_of_payments, get_discounting_status,
clear_unallocated_mode_of_payments, and set_pos_fields(POS DN skip) into services
Repoint all JS method strings and Python imports for mapper functions
across 18 doctypes from the doctype module to its mapper module, and
remove the now-unused re-export shims from each doctype file (keeping
only names used internally).
Semgrep rule frappe-modifying-but-not-comitting-other-method flags
setting self.ignore_linked_doctypes inside make_gl_entries() instead
of in the calling on_cancel method. Follows the same pattern used by
AssetCapitalization.
Move the get_gl_dict/add_gl_entry logic from AccountsController/StockController
into free functions in accounts/services/gl_entry_builder.py with doc as first arg.
BaseGLComposer gains get_gl_dict and add_gl_entry methods that delegate to the free
functions — GL composers now call self.get_gl_dict/self.add_gl_entry directly
without going through the doc. AccountsController and StockController keep thin
shims for backward compatibility with unrefactored callers.
Also move update_gl_dict_with_regional_fields and update_gl_dict_with_app_based_fields
to gl_entry_builder.py, re-exporting them from accounts_controller.py to avoid a
circular import.
Extracts get_gl_entries logic from SubcontractingReceipt,
AssetCapitalization, and AssetRepair into dedicated GL composer classes
under each doctype's services/ package. Each composer follows the
established BaseGLComposer / BaseStockGLComposer pattern, and the
original get_gl_entries becomes a 3-line shim.
- SubcontractingReceiptGLComposer(BaseStockGLComposer): moves
make_item_gl_entries and make_item_gl_entries_for_lcv
- AssetCapitalizationGLComposer(BaseStockGLComposer): moves
get_gl_entries_for_consumed_{stock,asset,service}_items and
get_gl_entries_for_target_item; inventory_account_map/sle_map/precision
become composer instance attributes
- AssetRepairGLComposer(BaseGLComposer): moves
get_gl_entries_for_repair_cost and get_gl_entries_for_consumed_items
(AR inherits AccountsController, not StockController)
All 29 GL snapshot tests and existing doctype test suites (32 SCR,
5 AC, 18 AR) pass.