Back to articles
DevelopmentMay 15, 20263 min read... views

Customizing Odoo ERP for East African SMEs: Development and Integration

Technical walkthrough of developing custom Odoo modules tailored for East African business models, covering local taxation models, and POS optimization.

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.

SME Business retail shop 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:

python
# -*- 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:

xml


    
        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.

Share this article