Production engineering guide for erp upgrade planning covering patterns, implementation strategies, and operational best practices.
ERP upgrades are not just updates—they are architectural reconfigurations that realign business logic, data models, integrations, and user workflows. This page details the steps, decisions, and pitfalls in planning an ERP upgrade, from assessing current systems to validating post-upgrade stability. It applies to any enterprise system that manages finance, supply chain, manufacturing, and human resources—especially where the upgrade involves significant schema changes, workflow rewrites, and integration rework.
Before any upgrade begins, capture the full state of the existing ERP environment. This includes identifying all customizations, integrations, and dependencies that may break or require rework.
Use a standardized inventory script to collect configuration and metadata across the system:
./inventory-erp.sh --output-format=json --include-customizations --include-integrations
This command outputs a JSON file with keys such as:
{
"system": {
"name": "AcmeERP",
"version": "17.2.3",
"build": "20231015",
"instance": "prod-01"
},
"customizations": [
{
"type": "codeunit",
"id": 50100,
"name": "Order Processing Logic",
"source": "/src/codeunits/OrderProcessingCodeunit.al",
"version": "2.1.0",
"dependencies": ["SalesOrder", "Inventory"]
},
{
"type": "page",
"id": 10000,
"name": "Purchase Requisition List",
"modified": "2023-08-01T12:00:00Z",
"customized": true,
"extensions": ["PO-Add-Notes", "PO-Approval-Workflow"]
}
],
"integrations": [
{
"name": "Sales to D365 Commerce",
"endpoint": "https://commerce.acme.com/api/v1/sync-sales",
"protocol": "HTTP",
"auth": "OAuth2",
"last_sync": "2023-10-17T08:30:00Z",
"status": "healthy"
}
]
}
A key failure point: custom code is not versioned. When the upgrade copies code from a dev environment, it often misses the source path or fails to record version and dependencies—leading to undetected regression in production.
The upgrade scope determines what changes are included and how they are sequenced. Use a declarative migration plan file that defines which components are upgraded and how.
# upgrade-plan.yaml
version: 1.0
target: 19.5
strategy: "phased"
phases:
- name: "Pre-upgrade"
tasks:
- name: "Backup database"
command: "pg_dump -Fc -Z 9 -f /backups/erp-20231017.dump acme_erp"
dependencies: []
- name: "Export customizations"
command: "tar -czf /exports/customizations-20231017.tar.gz /src/codeunits /src/pages"
dependencies: ["Backup database"]
- name: "Core Upgrade"
tasks:
- name: "Apply ERP patch 19.5"
command: "upgrade-erp --patch 19.5 --dry-run"
dependencies: ["Pre-upgrade"]
- name: "Run schema migration scripts"
command: "psql -d acme_erp -f /migrations/19.5-schema.sql"
dependencies: ["Apply ERP patch 19.5"]
- name: "Post-upgrade"
tasks:
- name: "Rebuild index on large tables"
command: "psql -d acme_erp -c 'REINDEX TABLE sales_order, inventory_item'"
dependencies: ["Run schema migration scripts"]
- name: "Validate integration endpoints"
command: "curl -X GET https://commerce.acme.com/api/v1/sync-sales/health"
dependencies: ["Core Upgrade"]
The strategy field controls whether the upgrade is:
in-place (direct upgrade of the current instance),phased (upgrade in stages with a dual environment),parallel (run old and new ERP systems simultaneously, then cutover).The most common failure: a phased strategy assumes the new system is a drop-in replacement, but the schema changes break data flow. The migration script must include a schema compatibility layer that maps old column names to new ones during the transition.
Testing must go beyond unit and integration tests. The upgrade must be validated against realistic, representative data.
Generate test data using a known dataset that includes:
Use a test data generator that outputs a standard format:
# Generate test data for ERP upgrade validation
./generate-test-data.py \
--size=large \
--output-format=parquet \
--output-dir=/test-data/erp-20231017 \
--include-attachments \
--include-logs
This command creates a directory with:
sales_orders.parquetinventory_items.parquetemployees.csvlogs/attachments/The test data must be loaded into the test ERP instance using a standard data import pipeline:
# Import test data into ERP
./import-erp-data.sh \
--source /test-data/erp-20231017 \
--target-instance test-erp-01 \
--transform-file transform-rules.json
The transform-rules.json file defines how to map old data to new schema:
{
"tables": {
"sales_order": {
"column_map": {
"order_id": "id",
"customer_number": "customer_id",
"total_amount": "net_amount"
},
"transform": {
"net_amount": "round(gross_amount * (1 - discount_rate), 2)",
"status": "case when order_date < '2023-01-01' then 'legacy' else 'new' end"
},
"default_values": {
"created_by": "system",
"version": "19.5"
}
}
}
}
A silent failure: data migration scripts assume column order in the input CSV, but the parquet file is columnar, so the loader reads the wrong column. The error appears as missing order_id values, but the data was correctly formatted—just misaligned.
Cutover is the transition from old ERP to new ERP. The plan must include:
Define the cutover plan in a cutover.yaml file:
# cutover.yaml
cutover_window: "2023-11-01T00:00:00Z"
duration: "4 hours"
rollback_strategy: "snapshot-rollback"
sync_strategy: "delta"
steps:
- time: "2023-11-01T00:00:00Z"
task: "Stop all incoming data feeds"
dependencies: []
- time: "2023-11-01T00:30:00Z"
task: "Final data sync: last 30 minutes of transactions"
command: "sync-erp-data --from prod-01 --to test-erp-01 --since '2023-11-01T00:00:00Z'"
dependencies: ["Stop all incoming data feeds"]
- time: "2023-11-01T01:00:00Z"
task: "Promote test-erp-01 to production"
command: "kubectl rollout restart deployment/erp-app --namespace=prod"
dependencies: ["Final data sync"]
- time: "2023-11-01T01:30:00Z"
task: "Verify system health"
command: "curl -s http://erp.acme.com/api/health | jq '.status == \"healthy\"'"
dependencies: ["Promote test-erp-01 to production"]
- time: "2023-11-01T02:00:00Z"
task: "Switch DNS to new ERP"
command: "dns-update --zone acme.com --record erp.acme.com --target 10.20.30.40"
dependencies: ["Verify system health"]
Key edge: delta sync is not idempotent. The sync-erp-data command assumes that each transaction has a sequence_id, but if the source system fails to assign a sequence ID during the final sync, the new ERP may process the same transaction twice.
After cutover, the system must be monitored for stability and correctness. Use a standard set of validation checks that run on a recurring schedule.
# Run post-upgrade validation suite
./validate-erp-upgrade.sh \
--target-host erp.acme.com \
--check-list /validation/checks.json \
--output-format=html \
--output-file /reports/upgrade-validation-20231101.html
The checks.json file defines the validation criteria:
{
"checks": [
{
"name": "Sales Order Creation",
"type": "api",
"endpoint": "/api/v1/sales-order",
"method": "POST",
"expected_status": 201,
"validate_response": {
"path": "data",
"schema": {
"required": ["id", "status", "created_at"],
"properties": {
"id": { "type": "integer" },
"status": { "enum": ["draft", "confirmed", "shipped", "delivered"] }
}
},
"timeout": 5
},
"run_after": "cutover"
},
{
"name": "Inventory Reconciliation",
"type": "sql",
"query": "SELECT count(*) FROM inventory_item WHERE last_updated < '2023-11-01T00:00:00Z'",
"expected_count": 20000,
"run_after": "cutover"
}
]
}
The system generates an HTML report that includes:
A common failure: the validation suite runs but does not fail when a check returns a non-200 status. The validate_response schema is validated, but the expected_status is ignored. The system logs a 400 error, but the validation tool does not mark the check as failed.
If the upgrade fails after cutover, the rollback plan must be executable, repeatable, and tested.
A rollback is not just “revert to old version.” It must include:
The rollback process is defined in a rollback.yaml file:
# rollback.yaml
version: 1.0
target: 17.2.3
steps:
- name: "Restore database from snapshot"
command: "pg_restore -d acme_erp -v -1 /backups/erp-20231017.dump"
dependencies: []
- name: "Switch application to old ERP"
command: "kubectl patch deployment/erp-app -p '{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"version\":\"17.2.3\"}}}}}'"
dependencies: ["Restore database from snapshot"]
- name: "Restart integration endpoints"
command: "systemctl restart integration-orchestrator"
dependencies: ["Switch application to old ERP"]
- name: "Verify rollback"
command: "curl -s http://erp.acme.com/api/health | jq '.version == \"17.2.3\"'"
dependencies: ["Restart integration endpoints"]
The most overlooked edge: the rollback process does not include a validation step for the restored database. The pg_restore command runs, but the data is not consistent. A VACUUM and ANALYZE must run after restore:
psql -d acme_erp -c "VACUUM ANALYZE"
Without this, queries on the restored database are slow and inconsistent, and users see missing data.
sequence_id.These failures are not rare—they are inevitable. The key to a successful ERP upgrade is not just executing the plan, but validating and hardening each step with real data, real commands, and real checks.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy