Separates all make_*/create_* document-creation functions from the
SalesOrder controller into a dedicated mapper.py for better separation
of concerns. Re-exports from sales_order.py preserve backward compat.
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.
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.
The free functions (get_gl_dict, add_gl_entry, get_voucher_subtype, etc.) live
in the same module as BaseGLComposer — they are all about building GL entries,
so there is no reason to split them across two files. Removes gl_entry_builder.py
and updates all import references to base_gl_composer.
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.
Move validate_conversion_rate, validate_taxes_and_charges, validate_account_head,
validate_cost_center, validate_inclusive_tax, set_balance_in_account_currency,
set_child_tax_template_and_map, add_taxes_from_tax_template, merge_taxes,
get_tax_rate, get_default_taxes_and_charges, and get_taxes_and_charges out of
accounts_controller into a dedicated accounts/services/taxes.py module.
Re-export all symbols from accounts_controller for backward compatibility.
Moves all advance-related query and management logic out of the 4500-line
AccountsController into a dedicated module-level service:
- get_advance_journal_entries, get_advance_payment_entries,
get_advance_payment_entries_for_regional, get_common_query
- set_advances, get_advance_entries, validate_advance_entries,
set_advance_gain_or_loss, calculate_total_advance_from_ledger,
set_total_advance_paid, set_advance_payment_status,
delink_advance_entries, create_advance_and_reconcile
AccountsController methods become thin shims; module-level functions in
accounts_controller.py are replaced with re-exports for backward
compatibility. payment_reconciliation.py updated to import directly from
the new service.
All 29 GL snapshots, 121 SI tests, 53 PE tests, and 37 payment
reconciliation tests pass.
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.
purchase_receipt/services/gl_composer.py → PurchaseReceiptGLComposer(BaseStockGLComposer).
compose() orchestrates the four builder steps: _make_item_gl_entries,
_make_tax_gl_entries, set_gl_entry_for_purchase_expense (stays on doc),
update_regional_gl_entries (module-level).
_make_item_gl_entries preserves the original closure structure (six inner
functions: make_item_asset_inward_gl_entry, make_stock_received_but_not_billed_entry,
make_landed_cost_gl_entries, make_amount_difference_entry,
make_sub_contracting_gl_entries, make_divisional_loss_gl_entry); all doc
calls go through self.doc. _make_tax_gl_entries is a direct port.
Helpers that stay on the document: add_provisional_gl_entry (public —
PI composer calls it via purchase_receipt_doc.add_provisional_gl_entry),
add_gl_entry, get_item_account_wise_lcv_entries, update_assets,
is_landed_cost_booked_for_any_item.
PurchaseReceipt.get_gl_entries is now a 3-line shim; make_item_gl_entries
and make_tax_gl_entries removed from the class.
Verified: 29 GL snapshots byte-identical on test-erpnext-v17;
101 PR tests green on test-site-ai.
Extends the Phase-0 characterization suite with 3 PR scenarios:
pr_basic, pr_with_taxes, pr_return — all using _Test Company with
perpetual inventory (TCP1) so stock-received GL entries are produced.
Also refreshes se_material_issue.json (cumulative stock on test-erpnext-v17
shifted the outgoing valuation rate). 29 snapshots total, all green.
Stock Entry
stock_entry/services/gl_composer.py → StockEntryGLComposer(BaseStockGLComposer)
compose() calls super().compose() for the base warehouse↔expense GL pairs,
then adds additional-cost entries (_build_additional_cost_per_item_account +
_append_additional_cost_gl_entries) and LCV adjustments (_append_lcv_gl_entries).
get_item_account_wise_lcv_entries stays on StockController (called via self.doc).
StockEntry.get_gl_entries is now a 3-line shim.
Removed private helpers from StockEntry; dropped unused process_gl_map and
get_account_currency imports.
Stock Reconciliation
stock_reconciliation/services/gl_composer.py → StockReconciliationGLComposer(BaseStockGLComposer)
compose() guards cost_center and delegates to
super().compose(inventory_account_map, doc.expense_account, doc.cost_center).
StockReconciliation.get_gl_entries is now a 3-line shim.
Verified: 26 GL snapshots byte-identical on test-erpnext-v17;
89 SE tests and 33/34 SR tests green on test-site-ai
(1 pre-existing SR failure in test_serial_no_status_with_backdated_stock_reco,
unrelated to GL — IndexError in serial bundle setup).
Extends the Phase-0 characterization suite with 4 scenarios:
se_material_receipt, se_material_issue, se_material_transfer, sr_basic.
All use _Test Company with perpetual inventory (TCP1) so stock accounting
GL entries are produced. 26 snapshots total, all green on test-erpnext-v17.
Moves the StockController.get_gl_entries body into
erpnext/stock/services/base_stock_gl_composer.py → BaseStockGLComposer(BaseGLComposer).
compose(inventory_account_map, default_expense_account, default_cost_center) contains
all warehouse↔expense-account GL pair building and the internal-transfer rounding-diff
block; all helpers (get_inventory_account_dict, get_stock_ledger_details, etc.) remain
on self.doc and are called via doc.<method>.
StockController.get_gl_entries becomes a 3-line shim. Delivery Note, Stock Entry, and
Stock Reconciliation continue to work unchanged — DN inherits the shim directly; SE and
SR override and call super(), which now delegates to the composer.
Verified: 22 GL snapshots byte-identical on test-erpnext-v17.
Extends the Phase-0 characterization suite with 2 DN scenarios (basic
delivery and return) using _Test Company with perpetual inventory so
stock accounting GL entries are produced. Uses stock_entry_utils.make_stock_entry
directly (avoids importing test_delivery_note and its conflicting test-record deps).
Run: bench --site test-erpnext-v17 run-tests --module erpnext.accounts.test_gl_characterization
Move the Journal Entry GL assembly into a new JournalEntryGLComposer(
BaseGLComposer); compose() projects the accounts child rows into GL dicts,
mirroring the former build_gl_map, which is now a thin shim delegating to
the composer. Drop the now-unused get_advance_payment_doctypes import.
Extend the Phase-0 GL safety net with three representative Journal Entry
scenarios (basic two-line, multi-currency, against a Sales Invoice with
party and reference) ahead of moving JE onto the composer.
Move the Payment Entry GL row builders (party, bank, deductions, tax)
onto a new PaymentEntryGLComposer(BaseGLComposer); compose() mirrors the
former build_gl_map, which is now a thin shim delegating to the composer.
The builders operate on self.doc and shared helpers stay on the document.
Advance-posting builders are left on the controller; they post in a
separate pass and move with the advances service in a later phase.
Extend the Phase-0 GL safety net with five representative Payment Entry
scenarios (receive against SI, pay against PI, with deductions, with
taxes, multi-currency) ahead of moving PE onto the composer.
Move make_item_gl_entries, make_stock_adjustment_entry,
get_provisional_accounts, make_provisional_gl_entry, and
update_net_purchase_amount_for_linked_assets from PurchaseInvoice onto
PurchaseInvoiceGLComposer, completing the full GL builder migration.
purchase_invoice.py no longer contains any GL row-building logic;
PurchaseInvoiceGLComposer is the single authoritative source for all
PI GL entries, mirroring the SalesInvoiceGLComposer pattern.
All 12 GL characterization snapshots pass.
Move make_supplier_gl_entry, add_supplier_gl_entry, make_tax_gl_entries,
make_internal_transfer_gl_entries, make_gl_entries_for_tax_withholding,
make_payment_gl_entries, make_write_off_gl_entry, and
make_gle_for_rounding_adjustment from PurchaseInvoice onto
PurchaseInvoiceGLComposer.
compose() now calls self.X for all moved builders; the make_item cluster
(make_item_gl_entries, make_provisional_gl_entry, get_provisional_accounts,
update_net_purchase_amount_for_linked_assets, make_stock_adjustment_entry)
still lives on doc pending batch-2 migration.
All 12 GL characterization snapshots pass.
Phase 3 of the accounts/controller refactor. Adds PurchaseInvoiceGLComposer;
PI's get_gl_entries body moves into compose() and the method becomes a thin
shim. Row-builder methods still live on the document (invoked via self.doc)
and migrate onto the composer next.
After comparing the SI and PI compose() flows, BaseGLComposer is kept minimal:
the two differ in step order, builders, and per-doctype regional function, so a
shared template is not warranted. No behavior change (Phase 0 snapshots and PI
GL tests stay green).
Relocates all 11 Sales Invoice-specific GL entry builders from the document
onto SalesInvoiceGLComposer, operating on self.doc. The perpetual-inventory
super().get_gl_entries() call becomes super(SalesInvoice, doc).get_gl_entries().
Shared bucket-A helpers (get_gl_dict, make_discount_gl_entries, etc.) remain on
AccountsController for now, invoked via self.doc, until all doctypes use a
composer. No behavior change: Phase 0 snapshots and the SI tests covering
perpetual inventory, POS, write-off, returns, fixed assets, internal transfer
and loyalty all stay green.
Phase 2 (pilot) of the accounts/controller refactor. Adds BaseGLComposer
and SalesInvoiceGLComposer; Sales Invoice's get_gl_entries body moves into
compose() and the method becomes a thin shim. Row-builder methods still live
on the document (invoked via self.doc) and migrate onto the composer next.
No behavior change (Phase 0 GL snapshots remain byte-identical).
make_reverse_gl_entries passed adv_adj as the company argument to
check_freezing_date, so the freeze-date check silently no-op'd on
cancellation (no company matched). Pass company explicitly so
cancellations respect the freezing date like submissions do.
Adds a regression test covering cancellation after the freeze date.
Phase 1 of the accounts/controller refactor. Moves the six pure
list-level validators (validate_disabled_accounts, validate_accounting_period,
validate_cwip_accounts, check_freezing_date, validate_against_pcv,
validate_allowed_dimensions) out of general_ledger.py into the new
erpnext/accounts/services/gl_validator.py. general_ledger.py imports and
calls them at the existing sites; no behavior change (Phase 0 GL snapshots
remain byte-identical).
The debit/credit balance trio stays in general_ledger.py for now since
get_debit_credit_difference mutates entries and is interleaved with the
round-off repair.
Phased plan to decompose accounts_controller.py and the sales_invoice.py
monolith into composed services. Documents the frozen GL-layer design
(GLComposer / gl_validator / general_ledger sink), method bucketing, and
the 8-phase rollout.