Every stalled ERP project we have been asked to rescue traces back to one of two mistakes. Either someone tried to make the company work the way the demo worked, or someone customized so aggressively that the system can no longer be upgraded. Both end in the same place: a team quietly running the business out of spreadsheets beside a system nobody trusts.
The 80/20
Out of the box, any credible ERP handles the universal 80%: sales orders, inventory moves, invoices, the chart of accounts, purchase approvals. Do not customize that part. It is the same in every company for a reason, and every line you change there is a line you will pay to re-test twice a year.
The other 20% is the reason you are competitive. It is the pricing rule nobody else has, the approval path your auditor insists on, the stock reservation policy that keeps your largest customer supplied, and the report your operations lead rebuilds by hand every Friday because the standard one does not quite fit.
That 20% is exactly what an extension is for.
What a safe extension looks like
The specifics differ by platform — AL extensions on Dynamics 365 Business Central, SuiteScript on NetSuite, Python modules on Odoo, DocTypes and server scripts on ERPNext — but the rules are the same everywhere:
- It extends rather than modifies. Nothing in the vendor's own objects is edited in place.
- Business logic lives in one layer and presentation in another, so a UI change never forces a logic change.
- Permissions and record-level access are part of the module from the start, not a separate configuration exercise somebody forgets.
- It is tested against the target version before anyone touches production, and re-tested against the next version before you upgrade.
Here is the shape of it on Odoo, as an example — inheriting the model rather than editing it, and calling through to the standard behaviour:
class SaleOrder(models.Model):
_inherit = "sale.order"
priority_tier = fields.Selection(
[("std", "Standard"), ("exp", "Expedited")],
default="std",
)
def action_confirm(self):
# your rule, expressed once, in the framework's grain
for order in self.filtered(lambda o: o.priority_tier == "exp"):
order._reserve_priority_stock()
return super().action_confirm()
The equivalent on Business Central is an event subscriber in an AL extension; on NetSuite it is a user event script. Different syntax, identical discipline: the vendor's code stays the vendor's, and the upgrade path stays open.
The test that tells you whether you got it right
Ask what happens at the next version upgrade. If the honest answer involves a developer manually reconciling your changes against the vendor's, the customization was built in the wrong place, and every future upgrade will carry that cost again.
If the answer is that the extensions are re-tested and the upgrade proceeds, then customization has done its job. Upgrades stop being something to dread, and your team stops doing by hand what the system should be doing for them.
That, in the end, is what an ERP is for. It is also why we write the extension rather than asking you to bend a working process to fit the software.