verify child-stock aggregation, transfer sourcing from child warehouses,
that a for warehouse outside the group is rejected, and that a for
warehouse is required when raw materials are fetched.
add an optional raw material group warehouse on production plan. when set,
raw material availability is checked across its child warehouses (bin rows
aggregated), while material is still received into for warehouse. for
warehouse is restricted to a child of the group and required when raw
materials are fetched; a group warehouse can never reach a material request
line. when the group is left blank, availability falls back to for warehouse
and the previous flow.
the whitelisted endpoint typed doc as str | frappe._dict | Document, so a
json request body (a plain dict) failed pydantic type validation. widen to
str | dict | Document, matching the convention used elsewhere in erpnext.
The stock_entry_handler modules were moved to services; update the
retention, expired-batch and subcontract call paths in the client
so the whitelisted methods resolve again.
The type-hint refactor rejected the doc dict sent by the form
controller; broaden the annotation to match ctx and fix the
cts= kwarg typo in transaction_base.
Tests that create USD documents dated today() (e.g. Sales Order in
test_advance_payment_ledger_entry, USD BOM in test_routing) rely on
get_exchange_rate() finding a USD->INR Currency Exchange record. The
only seeded records are dated 2016, so the lookup misses and falls back
to an external API that is blocked in CI, returning 0. That surfaces as
"Exchange Rate is mandatory" on Sales Order validation and a
ZeroDivisionError in BOM.get_routing (hour_rate / conversion_rate).
Whether it passes depends on which shard incidentally committed the
2016 records first, making it an order-dependent flake that unrelated
PRs trip by shifting test distribution.
Seed today()-dated USD<->INR rates once in BootStrapTestData so the
lookup resolves deterministically without the external API. Rates
mirror the latest Currency Exchange test_records to keep cost
calculations unchanged.
The routing field handler only fetched operations from the routing when
the operations table was empty. When a new BOM version is created (via
"New Version"), operations are copied from the source BOM, so selecting a
different routing left the old operations in place - both in the form and
after saving.
Drop the `!frm.doc.operations.length` guard from the routing handler so
that (re)selecting a routing always refetches the operations from that
routing via the existing get_routing method, which clears and repopulates
the operations table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continues the AccountsController service decomposition (Phase 5).
- Add accounts/services/deferred_accounting.py with DeferredAccountingService
owning the deferred revenue/expense validations (income/expense account
defaulting and service start/end date checks).
- Move the document-schedule orchestration (validate_all_documents_schedule
and the invoice/non-invoice variants) into PaymentScheduleService, where
they already delegated, removing the controller-to-service round trip.
- Update the three validate() call sites; keep
validate_auto_repeat_subscription_dates on the controller (still called by
buying/selling controllers).
No behavior change. accounts_controller.py 1818 -> 1745 lines.
* fix(stock): fall back to current date/time for serial and batch bundle posting datetime
Pick List has no posting_date/posting_time fields, so creating or updating a
Serial and Batch Bundle from a Pick List row crashed with
"TypeError: combine() argument 1 must be datetime.date, not None". Fall back
to today/now when the parent voucher doesn't carry its own posting date.
Fixes#56951
* fix(stock): accept a plain dict for add_serial_batch_ledgers' doc and child_row
The whitelisted add_serial_batch_ledgers only converted child_row into an
attribute-accessible frappe._dict when it arrived as a JSON string, and doc's
type hint only allowed Document | str. Frappe's JSON API delivers both as
plain dicts (see frappe.app.make_form_dict, which parses the request body
with orjson and only wraps the top-level dict, not nested values), so every
real request was rejected before the handler body ever ran: first with a
FrappeTypeError on doc, and once that's fixed, with an AttributeError on
child_row.serial_and_batch_bundle. parse_json already wraps a plain dict in
frappe._dict (and leaves a real Document instance untouched), so routing
child_row through it unconditionally fixes both.
* feat(manufacturing): create material request for raw materials from work order
allow raising a material transfer request directly from a work order,
mirroring the existing job card flow, so stores can fulfil it into wip
before the actual stock entry happens
* test(manufacturing): cover work order material request flow
verify the material request created from a work order carries the
right bom/purpose onto the resulting stock entry, and that the work
order status still moves to in process on a partial material-request
transfer
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.