Un-deprecate the `disabled` checkbox: it is now editable (also after
submit) and parks a bundle version without ceding its active slot, so
re-enabling restores it without re-activation.
- `get_active_product_bundle` (the single resolution entry point) skips
disabled bundles, so every consumer stops treating the item as a bundle
while it is disabled
- the version pickers on transaction item rows and the buying "Get Items
from Product Bundle" dialog filter out disabled bundles
- an explicitly selected disabled version blocks the transaction with a
validation error instead of silently re-packing another version
- Product Bundle Balance report excludes disabled bundles
- list view indicator: Disabled (grey) / Active (green), falling back to
docstatus for drafts, cancelled and inactive submitted versions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Avoids basename collision with the core SLE engine erpnext/stock/stock_ledger.py
(the service even imports make_sl_entries from it). File now maps 1:1 to its class,
StockLedgerService.
The preview feature serves both accounts and stock vouchers (SI/PI/PE + DN/PR/SE)
and its show_*_preview entry points live in controllers/stock_controller, so the
cohesive GL+SLE preview module belongs in controllers/, not stock/services/. Pure
move + import-path update; GL and stock previews stay together (shared get_columns/
get_data formatters; read-side, kept out of the write-path services).
Verified: ledger snapshots green; module resolves at new path.
Merge the stock exceptions into the existing app-wide erpnext/exceptions.py (under a
'# stock' section) instead of a separate erpnext/stock/exceptions.py, matching the
established convention. stock_controller still re-exports them for backward
compatibility; services import from erpnext.exceptions.
Verified: ledger snapshots, quality inspection suite, stock_entry batch-expiry stay green.
Re-audited the kept delegators for true external callers. Two had none:
- has_landed_cost_amount: no caller anywhere (the landed_cost_voucher.py free
function is what the composers use) — pure dead delegator, removed.
- validate_internal_transfer: only StockController.validate() called it; inline that
one hook to StockInternalTransferService(self).validate_internal_transfer() and
remove the delegator.
All other kept delegators have real external/subclass/run_method callers and remain
as the stock extension contract.
Verified: ledger snapshots + DN/PR internal-transfer suites stay green.
#8 ledger_preview: wrap the submit-in-memory dry run in a savepoint inside
get_accounting_ledger_preview / get_stock_ledger_preview and roll back to it in a
finally, so the preview never persists entries regardless of caller (previously
only the whitelisted show_*_preview wrappers' full rollback made it safe).
#9 exceptions: move BatchExpiredError and the QualityInspection* errors into a new
erpnext/stock/exceptions.py and re-export them from stock_controller for backward
compatibility (job_card and tests still import from the controller; identity is
preserved). Services now import from the neutral module instead of back from the
controller they were extracted out of.
#10 quality inspection: extract the duplicated doctype->inspection-field map into a
single INSPECTION_FIELDNAME_MAP constant in the service, consumed by both
validate_inspection and check_item_quality_inspection.
Verified: ledger snapshots, quality inspection suite, stock_entry batch-expiry test
stay green; preview smoke-tested to persist nothing and not roll back the caller.
* feat: create sales invoice from pick list
* test: cover sales invoice creation from pick list
* fix: require update stock for pick list invoices
* fix: return SI should not bother with pick list
These clusters are really other doctypes' logic parked on StockController, so they
move next to the doctype that owns them rather than into a stock service:
- set_landed_cost_voucher_amount / get_item_account_wise_lcv_entries /
has_landed_cost_amount -> landed_cost_voucher.py (free functions). Controller keeps
thin delegators (called as doc.X from 4 GL composers, buying_controller and the LCV
doctype).
- validate_putaway_capacity -> putaway_rule.py (free function, next to
get_available_putaway_capacity it already used). Controller keeps a delegator
(validate hook + Stock Entry/Reconciliation); prepare_over_receipt_message becomes a
private helper there.
Drops now-unused Sum/defaultdict imports from stock_controller.
Behaviour-preserving: ledger snapshots, putaway and landed-cost suites stay green.
Move internal-transfer warehouse/currency/packed-item/over-receipt-qty validation
into erpnext/stock/services/internal_transfer.py as a delegating service. This is
the stock-side counterpart to accounts/services/internal_transfer.py (party/rate/
pricing). validate_internal_transfer keeps a controller delegator (validate hook);
the other 7 methods are internal-only. The is_internal_transfer() predicate is
already consolidated on AccountsController.
Behaviour-preserving: ledger snapshots + DN/PR internal-transfer suites stay green.
Move quality-inspection validation (validate_inspection + validate_qi_presence/
submission/rejection) into erpnext/stock/services/quality_inspection.py as a
delegating service. validate_inspection keeps a controller delegator (called from
validate() and 3 other doctypes); the three row-level helpers are internal-only.
The whitelisted module fns check_item_quality_inspection / make_quality_inspections
stay in stock_controller (stable endpoint paths).
Behaviour-preserving: ledger snapshots + quality inspection suite stay green.
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.
Move SLE building and reposting (get_sl_entries, update_inventory_dimensions,
get_stock_ledger_details, get_items_and_warehouses, make_sl_entries,
repost_future_sle_and_gle) into erpnext/stock/services/stock_ledger.py as a
delegating service. All six keep thin controller delegators (each has external
callers). The repost helper *functions* stay module-level in stock_controller
(imported widely); the service calls them. Also drop import orphaned by this and
the prior bundle extraction.
Behaviour-preserving: ledger characterization snapshots and the repost item
valuation suite stay green.
Move serial & batch bundle handling (creation, validation, return bundles,
teardown) out of StockController into erpnext/stock/services/serial_batch_bundle.py
as a delegating service. The controller keeps thin delegators for the 10 methods
reached from other doctypes or run_method; the 12 internal-only helpers live in
the service. make_bundle_for_material_transfer stays a module fn in stock_controller
(imported by stock/serial_batch_bundle.py).
Behaviour-preserving: ledger characterization snapshots and the full Serial and
Batch Bundle test suite stay green.
Remove the doctype-branching make_precision_loss_gl_entry from
exchange_gain_loss.py (and its accounts_controller wrapper); add a
dedicated method to each of SalesInvoiceGLComposer and
PurchaseInvoiceGLComposer. The SI variant now passes 'Sales Invoice'
as the round-off voucher type (output-equivalent) and the throwaway
return value no longer shadows the gettext _ helper.
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).
accounts/services/child_item_update.py:
- ChildItemUpdater class with update() entry point encapsulating all
the logic from the old update_child_qty_rate free function; nested
closures (check_doc_permissions, validate_workflow_conditions,
validate_quantity_and_rate, validate_fg_item_for_subcontracting)
become private methods on the class
- update_child_qty_rate kept as @frappe.whitelist() thin wrapper;
re-exported from accounts_controller.py so the JS whitelist path
"erpnext.controllers.accounts_controller.update_child_qty_rate"
and test imports continue to work
- Free functions: set_order_defaults, validate_child_on_delete,
update_bin_on_delete, validate_and_delete_children, get_allow_zero_qty,
get_child_item_change_state, is_child_item_unchanged,
update_child_item_rate_and_discount, update_child_item_uom_and_weight,
check_if_child_table_updated
accounts_controller.py drops from ~2356 to ~1796 lines.
- accounts/services/party_validation.py: PartyValidator class with
single validate() entry point covering party frozen/disabled check,
party accounts, currency, party account currency, address/contact,
and company-linked addresses. AccountsController.get_party() kept
as a shim (called by advances and payment_schedule services).
- accounts/services/internal_transfer.py: InternalTransferService
class with validate() (reference + transaction rate + pricing/tax
disablers), set_account() for unrealized P&L, is_internal_transfer(),
process_common_party_accounting(), and get_common_party_link().
Shims retained on AccountsController for the three methods called
by selling/buying/stock controllers and GL composers.
accounts_controller.py drops from ~2722 to ~2356 lines.
Introduce PaymentScheduleService and BillingValidationService classes so
call sites read PaymentScheduleService(doc).set_payment_schedule() instead
of the opaque self.set_payment_schedule() shim. Removes 15 shim methods
from AccountsController and updates all 11 call sites across the codebase.
Replaces the shim+free-function pattern with a TaxService class so
callers like TaxService(self).set_taxes() make the source location
explicit. Class lives in taxes.py above the existing free functions.
Deletes the intermediate tax_service.py. Updates AccountsController,
sales_invoice, pos_invoice, subscription, and both GL composers to
call TaxService directly.
Move billing validation, payment schedule, and exchange gain/loss logic from
AccountsController into dedicated service modules under accounts/services/.
AccountsController retains thin shim methods that delegate to the services.