AccountLookup
Account details, disabled/frozen/group status
PearlThoughts conducted a proof-of-concept migration extracting core ERPNext accounting modules into idiomatic Go. The approach validated the patterns that became ModernizeSpec’s specification design.
The migration follows the Strangler Fig Pattern with hexagonal architecture (ports and adapters):
+---------------------------------------+| Business Logic || (validation, calculation, rules) |+----------+--------------+-------------+| Ports | Ports | Ports || (in) | (out) | (out) |+-----+----+-------+------+------+------+ | | |+-----v----+ +----v------+ +---v-------+| HTTP/ | | Database | | External || gRPC | | Adapter | | Services |+----------+ +-----------+ +-----------+Dependencies on Stock, Selling, HR, and other modules are abstracted through Go interfaces (ports) and mocked in tests. This allows extracting modules one at a time without building the entire system.
Python source: mode_of_payment.py (66 lines)
| Go File | Purpose | Lines |
|---|---|---|
model.go | ModeOfPayment struct, ModeOfPaymentAccount child table, PaymentType enum | 43 |
validation.go | 3 validation methods + orchestrating Validate() | 130 |
validation_test.go | 19 table-driven tests, 85.3% coverage | 345 |
AccountLookup interface (port)POSChecker interface (port)| Aspect | Python | Go | Status |
|---|---|---|---|
| Duplicate company detection | frappe.throw() in validate_repeating_companies() | ValidationError in ValidateRepeatingCompanies() | Verified (19 tests) |
| Account-company match | frappe.db.get_value() + frappe.throw() | AccountLookup interface + ValidationError | Verified |
| POS profile constraint | frappe.db.sql() + frappe.throw() | POSChecker interface + ValidationError | Verified |
| Test coverage | 0 tests in Python | 19 tests in Go | Go exceeds Python |
Python source: taxes_and_totals.py (1,334 lines)
| Go File | Purpose | Lines |
|---|---|---|
model.go | Tax-related structs, currency types | 188 |
calculator.go | Indian GST calculation, multi-currency, threshold rules | 442 |
calculator_test.go | 24 table-driven tests, 90.2% coverage | 610 |
| Rule | Description |
|---|---|
| 5 charge types | Actual, OnNetTotal, OnPreviousRowAmount, OnPreviousRowTotal, OnItemQuantity |
| Per-item tax overrides | JSON item_tax_rate field parsing and application |
| Multi-currency | USD to INR at configurable exchange rates |
| GST split | CGST/SGST calculation for Indian tax compliance |
| Cascading taxes | Tax-on-tax with fixed shipping and deductions |
| Precision replication | Flt() and Round() replicating frappe.utils.flt() behavior |
This iteration proved that complex financial calculation logic can be extracted with full behavioral parity, including edge cases around floating-point precision that are notoriously difficult to replicate across languages.
Python source: general_ledger.py (878 lines) — the most complex iteration.
| Go File | Purpose | Lines |
|---|---|---|
model.go | GLEntry (30+ fields), PaymentLedgerEntry, Account, GLMap | 224 |
ports.go | 8 port interfaces for all external dependencies | 180 |
errors.go | 7 typed error definitions | 141 |
engine.go | GL posting engine with dual-entry validation | 634 |
engine_test.go | 25 unit tests | 818 |
integration_test.go | 7 integration tests with realistic ERPNext data | 611 |
AccountLookup
Account details, disabled/frozen/group status
CompanySettings
Round-off accounts, frozen dates, cost centers
AccountingPeriodChecker
Closed period validation
FiscalYearLookup
Fiscal year from posting date
GLEntryStore
Save/retrieve/cancel GL entries
PaymentLedgerStore
AR/AP payment ledger entries
BudgetValidator
Budget enforcement
AccountingDimensionProvider
Dimension offsetting entries
MakeGLEntries() — the main entry point that all financial transactions in ERPNext call — plus:
| Metric | Value |
|---|---|
| Python source analyzed | ~2,278 lines from 3 files |
| Go code produced | ~4,366 lines (implementation + tests) |
| Total tests | 68 (all passing) |
| External dependencies | Zero (stdlib only) |
| Test-to-implementation ratio | ~1.15:1 |
| Code expansion factor | ~1.9x (Go requires more explicit code than Python) |
| Iteration | Python Source | Go Output | Tests | Calendar Time |
|---|---|---|---|---|
| Mode of Payment | 66 lines | 518 lines | 19 | ~1 day |
| Tax Calculator | 1,334 lines | 1,240 lines | 24 | ~1 day |
| GL Entry Engine | 878 lines | 2,608 lines | 32 | ~1 day |
Rate: approximately 760 lines of Python source processed per day per person (experienced engineer with AI tooling).
| Dimension | Migrated | Remaining |
|---|---|---|
| Accounts doctypes | 3 | ~289 |
| Other module doctypes | 0 | ~229 |
| Shared controllers | 1 partial (taxes_and_totals) | 15 |
| API endpoints | 0 | 768 |
| Reports | 0 | ~177 |
| Frontend (JS) | 0 | 73,932 lines |
| Framework (Frappe) | 0 | Entire runtime |
The 3 iterations migrated architecturally central components — Mode of Payment (master data), Tax Calculator (shared controller), and General Ledger (accounting core). These represent the most complex per-line code in ERPNext.
Every aspect of this experiment maps to a ModernizeSpec concept:
| Experiment Concept | ModernizeSpec Equivalent |
|---|---|
| Identifying extraction targets | extraction-plan.json phases and sequencing |
| Measuring complexity per module | complexity.json tiers and hotspots |
| Mapping bounded contexts | domains.json context definitions |
| Tracking what is migrated | migration-state.json per-context progress |
| Parity test results | parity-tests.json test inventory |
| Port interfaces as boundaries | domains.json coupling scores |
The specification formalizes what was ad-hoc during the experiment, making the approach repeatable for any legacy system.