ERP Localization Challenges
Enterprise Resource Planning (ERP) systems are critical for scaling businesses, but their out-of-the-box configurations rarely match the operational realities of East African Small and Medium Enterprises (SMEs). In Kenya and East Africa, businesses must navigate complex fiscal policies (such as the Kenya Revenue Authority's eTIMS system for electronic invoicing) and rely on mobile money (M-Pesa) as their primary payment mechanism.
Customizing Odoo—an open-source ERP—requires developers to build custom modules that handle local tax compliance, coordinate transactions, and optimize Point of Sale (POS) experiences.
This guide provides a technical walkthrough of creating an Odoo module that integrates localized invoice generation and complies with East African regulatory requirements.
Building a Custom Odoo Module
In Odoo, customizations are structured as modular applications. We define these modules using Python for the backend models and business logic, and XML for the user interfaces and reports.
Let's look at a custom Python model for Odoo 17/18 that extends the standard invoice model (account.move) to include eTIMS validation fields and calculate custom VAT formatting:
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import UserError
import requests
import json
class AccountMove(models.Model):
_inherit = 'account.move'
etims_signature = fields.Char(string='eTIMS Signature', readonly=True, copy=False)
etims_status = fields.Selection([
('draft', 'Not Sent'),
('submitted', 'Submitted'),
('failed', 'Failed')
], string='eTIMS Status', default='draft', readonly=True, copy=False)
kra_pin = fields.Char(string='KRA PIN', related='partner_id.vat', readonly=True)
def action_post(self):
# Extend the standard invoice validation flow
res = super(AccountMove, self).action_post()
for move in self:
if move.move_type in ['out_invoice', 'out_refund']:
move.submit_to_etims()
return res
def submit_to_etims(self):
self.ensure_one()
# Mock payload construction for KRA eTIMS API
payload = {
"invoiceNumber": self.name,
"customerId": self.kra_pin or "PIN_NOT_PROVIDED",
"invoiceDate": self.invoice_date.isoformat() if self.invoice_date else fields.Date.today().isoformat(),
"amount": self.amount_total,
"taxAmount": self.amount_tax,
"items": [{
"itemName": line.product_id.name,
"qty": line.quantity,
"price": line.price_unit,
"taxCode": "A" if line.tax_ids else "E"
} for line in self.invoice_line_ids]
}
# Send request to eTIMS validation proxy
try:
# Local configuration parameter for API endpoint
api_url = self.env['ir.config_parameter'].sudo().get_param('etims.api_url')
headers = {'Content-Type': 'application/json'}
response = requests.post(api_url, data=json.dumps(payload), headers=headers, timeout=10)
if response.status_code == 200:
data = response.json()
self.write({
'etims_signature': data.get('signature'),
'etims_status': 'submitted'
})
else:
self.write({'etims_status': 'failed'})
# Log error for administrative review
_logger.error("eTIMS Submission failed: %s", response.text)
except Exception as e:
self.write({'etims_status': 'failed'})
raise UserError("Could not connect to eTIMS API: %s" % str(e))XML Layout for custom fields
To render these new fields on the invoice form view, we write an XML inheritance layout:
account.move.form.inherit.etims
account.move
Localized Workflows and POS Optimization
Beyond basic backend modifications, optimizing Point of Sale (POS) environments for rapid checkout is critical. In low-bandwidth settings, local caching of tax structures and invoice queuing is essential.
By extending Odoo's OWL (Odoo Web Library) frontend framework, developers can capture transactions offline and synchronize them with the backend when connection is restored. This ensures that retail businesses do not halt operations during internet outages, maintaining transaction flows across regional store networks.
