Techbot

Contact

Developer Guide: The Complete Guide to QWeb & PDF Report Generation in Odoo

Developer Guide: The Complete Guide to QWeb & PDF Report Generation in Odoo

Properties
1-7 / 7
Name Property Type Postcode Tags Bedrooms Living Area (m²) Expected Price Selling Price Date Availability
Big Villa 2 2 2 1.00 0.00
Trailer home Residential 54321 1 10 100,000.00 0.00 Aug 10
Big Villa Residential 12345 6 100 1,600,000.00 1,500,000.00 Dec 8
2030 VILLA Villa 00000 2 0 20,000.00 0.00
New Villa Villa 00000 2 0 20.00 0.00 Aug 5
hi Villa 2 0 500.00 0.00
Villa Villa 0000 5 2 -500.00 0.00 Jul 23
function triggerTryIt() { const btn = document.getElementById('tryItBtn'); const container = document.getElementById('appContainer'); if (!btn || !container) return; btn.style.backgroundColor = '#fdfdfd'; btn.style.color = '#714B67'; btn.style.boxShadow = '0 4px 12px rgba(113, 75, 103, 0.15)'; btn.style.border = '1px solid #714B67'; container.style.border = '2px solid #714B67'; container.style.boxShadow = '0 0 15px rgba(113, 75, 103, 0.25)'; setTimeout(() => { btn.style.backgroundColor = '#714B67'; btn.style.color = '#ffffff'; btn.style.boxShadow = 'none'; btn.style.border = '1px solid #714B67'; container.style.border = '1px solid #dee2e6'; container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)'; }, 1000); } function setDevice(type, userClicked = false) { const container = document.getElementById('appContainer'); const buttons = document.querySelectorAll('.device-btn'); buttons.forEach(btn => { btn.classList.remove('active'); btn.style.backgroundColor = 'transparent'; btn.style.color = '#495057'; }); if (type === 'phone') { if (userClicked) container.style.width = '380px'; const b = document.getElementById('btnPhone'); if(b) { b.classList.add('active'); b.style.backgroundColor = '#714B67'; b.style.color = 'white'; } } else if (type === 'tab') { if (userClicked) container.style.width = '640px'; const b = document.getElementById('btnTab'); if(b) { b.classList.add('active'); b.style.backgroundColor = '#714B67'; b.style.color = 'white'; } } else { if (userClicked) container.style.width = '100%'; const b = document.getElementById('btnLaptop'); if(b) { b.classList.add('active'); b.style.backgroundColor = '#714B67'; b.style.color = 'white'; } } } window.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('tryItBtn'); if (btn) { btn.addEventListener('click', triggerTryIt); } const container = document.getElementById('appContainer'); if (container) { setDevice('phone', false); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const currentWidth = entry.contentRect.width; if (currentWidth >= 360 && currentWidth 540 && currentWidth 720) { setDevice('laptop', false); } } }); resizeObserver.observe(container); } }); let state = { currentStart: 1, currentEnd: 7, totalItems: 7, activeView: 'list' }; function updateUI() { const pageIndicator = document.getElementById('pageIndicator'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); if (pageIndicator) pageIndicator.textContent = `${state.currentStart}-${state.currentEnd} / ${state.totalItems}`; if (prevBtn) prevBtn.style.opacity = state.currentStart === 1 ? '0.5' : '1'; if (nextBtn) nextBtn.style.opacity = state.currentEnd >= state.totalItems ? '0.5' : '1'; } document.getElementById('nextBtn')?.addEventListener('click', () => { if (state.currentEnd { if (state.currentStart > 1) { state.currentStart -= 1; state.currentEnd -= 1; updateUI(); } }); function switchView(type) { document.querySelectorAll('.view-btn').forEach(b => { b.classList.remove('active'); b.style.backgroundColor = 'transparent'; b.style.color = '#495057'; }); const activeViewBtn = document.querySelector(`[data-view="${type}"]`); if(activeViewBtn) { activeViewBtn.classList.add('active'); activeViewBtn.style.backgroundColor = '#e9ecef'; activeViewBtn.style.color = '#000'; } state.activeView = type; const listView = document.getElementById('listView'); const kanbanView = document.getElementById('kanbanView'); if (type === 'kanban') { if (listView) listView.style.display = 'none'; if (kanbanView) kanbanView.style.display = 'flex'; } else { if (kanbanView) kanbanView.style.display = 'none'; if (listView) listView.style.display = 'block'; } } function toggleSelectAll(masterCheckbox) { const checkboxes = document.querySelectorAll('.row-checkbox'); checkboxes.forEach(cb => cb.checked = masterCheckbox.checked); updateSelectionState(); } function updateSelectionState() { const checkboxes = document.querySelectorAll('.row-checkbox'); const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length; const centerArea = document.getElementById('centerControlArea'); if (!centerArea) return; if (checkedCount > 0) { centerArea.innerHTML = `
${checkedCount} selected
`; } else { centerArea.innerHTML = ` `; const master = document.getElementById('selectAll'); if (master) master.checked = false; } } function printSelectedProperties() { const checkedBoxes = document.querySelectorAll('.row-checkbox:checked'); if (checkedBoxes.length === 0) return; let printContent = ` Property_Offers_Report body { font-family: Arial, sans-serif; padding: 40px; color: #212529; } .header-info { margin-bottom: 30px; } .company-title { font-weight: bold; font-size: 16px; } .company-address { font-size: 13px; color: #555; line-height: 1.4; margin-top: 5px; } .logo-box { font-weight: bold; font-size: 15px; margin-bottom: 8px; } .property-document { page-break-after: always; margin-bottom: 50px; border-bottom: 1px solid #ccc; padding-bottom: 40px; } .property-title { font-size: 26px; font-weight: bold; margin-bottom: 12px; } .property-detail { font-size: 14px; margin-bottom: 6px; } .offers-msg { font-size: 14px; color: #444; margin-top: 15px; } .page-footer { font-size: 12px; color: #777; margin-top: 30px; } `; checkedBoxes.forEach((cb, index) => { const propertyName = cb.value; const expectedPrice = cb.getAttribute('data-price'); printContent += `
📷 Your logo
YourCompany
8000 Marina Blvd, Suite 300
Brisbane CA 94005
United Arab Emirates
${propertyName}
Expected Price: ${expectedPrice}
Status: New
No offers have been received yet for this property.
`; }); printContent += ``; const existingIframe = document.getElementById('print-iframe'); if (existingIframe) { existingIframe.remove(); } const iframe = document.createElement('iframe'); iframe.id = 'print-iframe'; iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0'; iframe.style.border = '0'; document.body.appendChild(iframe); const frameDoc = iframe.contentWindow || iframe.contentDocument.document || iframe.contentDocument; frameDoc.document.open(); frameDoc.document.write(printContent); frameDoc.document.close(); setTimeout(() => { try { iframe.contentWindow.focus(); iframe.contentWindow.print(); } catch (e) { const fallbackWindow = window.open('', '_blank'); if (fallbackWindow) { fallbackWindow.document.write(printContent); fallbackWindow.document.close(); fallbackWindow.print(); } } }, 300); } function openRecord(name) { alert(`Opening Odoo Form View for: "${name}"`); } function createNewRecord() { const name = prompt("Enter new property name:"); if (name) { alert(`Creating new record "${name}" via Odoo model controller.`); } } function addCard(groupName) { const name = prompt(`Add property to ${groupName}:`); if (name) { const list = document.getElementById(`cards-${groupName}`); if (list) { const card = document.createElement('div'); card.className = 'kanban-card'; card.style.cssText = 'background: #ffffff; border: 1px solid #ced4da; border-radius: 4px; padding: 10px 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease;'; card.onmouseover = function() { this.style.borderColor='#714B67'; this.style.boxShadow='0 2px 5px rgba(0,0,0,0.08)'; }; card.onmouseout = function() { this.style.borderColor='#ced4da'; this.style.boxShadow='0 1px 2px rgba(0,0,0,0.02)'; }; card.innerHTML = `
${name}
Expected Price: 0.00
`; card.onclick = () => openRecord(name); list.appendChild(card); } } } function expandColumn(groupName) { alert(`Expanding collapsed group column: ${groupName}`); } function filterContent() { const searchInput = document.getElementById('searchInput'); if (!searchInput) return; const query = searchInput.value.toLowerCase(); document.querySelectorAll('#listView tbody tr').forEach(row => { row.style.display = row.textContent.toLowerCase().includes(query) ? '' : 'none'; }); document.querySelectorAll('.kanban-card').forEach(card => { card.style.display = card.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); }

About This Guide

This guide provides a straightforward overview of how to create printable and downloadable PDF documents (like patient discharge summaries, medical cards, and prescription slips) in Odoo. It breaks down the process of setting up report layouts, adding custom business details, and placing a “Print” button directly inside your app.

What You Will Learn

  • Report Setup: How to organize your report files and automatically add a “Print” option to your app’s menu.
  • Designing the Layout: How to use basic structure and styling to create clean documents complete with your company logo, headers, footers, and page numbers.
  • Smart Content & Flexibility: How to display information conditionally (such as showing a default message if notes are missing) and reuse common design blocks across different reports.
  • Special Features: How to add scannable barcodes or QR codes, preview reports in your web browser, and prepare documents for multi-language translation.

This Guide expands on QWeb’s primary use cases: creating downloadable and printable PDF reports. PDF reports allow Odoo modules to summarize and present business data such as patient discharge summaries, prescription cards, or doctor appointment lists in clean, formatted layouts complete with company headers and footers.

Understanding File Structure

The architecture of a PDF report consists of two main components:

  1. QWeb XML Template (report/*.xml): Defines the HTML/QWeb visual layout of the report.
  2. Report Action (report/*_reports.xml): Defines the ir.actions.report record that registers the report in Odoo and adds it to the Print menu of the model view.

Both files are stored inside a dedicated report/ folder at the top level of your module’s directory.

Complete Directory Layout

Plaintext

custom_hospital_management/

├── __init__.py

├── __manifest__.py

├── models/

│   ├── __init__.py

│   └── patient.py

├── report/

│   ├── patient_templates.xml      <– QWeb HTML report layouts & sub-templates

│   └── patient_reports.xml        <– ir.actions.report definitions

├── security/

│   └── ir.model.access.csv

└── views/

    ├── patient_views.xml

 

    └── menu_views.xml

Manifest Sequence Rule: Always add your report files to the data list in your __manifest__.py file. Remember that files listed in a manifest are loaded sequentially by Odoo!

Python

# File location: custom_hospital_management/__manifest__.py

{

    ‘name’: ‘Hospital Management’,

    ‘version’: ‘1.0.0’,

    ‘summary’: ‘Manage patient records, appointments, and medical histories’,

    ‘category’: ‘Healthcare’,

    ‘author’: ‘Your Company Name’,

    ‘license’: ‘LGPL-3’,

    ‘depends’: [‘base’],

    ‘data’: [

        ‘security/ir.model.access.csv’,    # 1. Security loads first

        ‘views/patient_views.xml’,         # 2. Views load second

        ‘views/menu_views.xml’,            # 3. Menus load third

        

        # 4. Report Actions & Templates

        ‘report/patient_reports.xml’,      # Action definition

        ‘report/patient_templates.xml’,    # QWeb layout templates

    ],

    ‘installable’: True,

    ‘application’: True,

}

Setting Up Report Test Data

Before writing report code, you need sample data in your database to test your visual layout incrementally. For our hospital module, make sure you have created at least:

  • 3 Patients: 1 in Draft state, 1 in Confirmed state, and 1 in Done state.
  • Ensure at least one patient has filled-in Medical History Notes and another has empty notes so you can test display conditions.

Creating a Minimal QWeb Template

A minimal viable report layout uses basic HTML, Bootstrap styling classes, and QWeb control statements (t- directives).

XML

<!– File location: custom_hospital_management/report/patient_templates.xml –>

<?xml version=”1.0″ encoding=”utf-8″?>

<odoo>

    <template id=”report_patient_card_template”>

        <t t-call=”web.html_container”>

            <t t-foreach=”docs” t-as=”patient”>

                <t t-call=”web.external_layout”>

                    <div class=”page”>

                        <h2>

                            <span t-field=”patient.name”/>

                        </h2>

                        <div class=”mb-3″>

                            <strong>Age: </strong>

                            <span t-field=”patient.age”/>

                        </div>

                        <table class=”table”>

                            <thead>

                                <tr>

                                    <th>Gender</th>

                                    <th>Status</th>

                                </tr>

                            </thead>

                            <tbody>

                                <tr>

                                    <td><span t-field=”patient.gender”/></td>

                                    <td><span t-field=”patient.state”/></td>

                                </tr>

                            </tbody>

                        </table>

                    </div>

                </t>

            </t>

        </t>

    </template>

 

</odoo>

Directives Explained Simply

  • web.html_container: Prepares the standard web HTML canvas and injects default stylesheets/fonts.
  • web.external_layout: Automatically adds your company logo, header, footer, page numbers, and address to the PDF.
  • t-foreach=”docs” t-as=”patient”: Loops through all selected patient records (equivalent to for patient in docs: in Python).
  • class=”table”: Utilizes built-in Twitter Bootstrap CSS formatting to keep design clean without manual CSS styling.

Registering the Report Action (ir.actions.report)

Now that the QWeb template exists, tell Odoo to register it as an official printable report and link it to the patient model’s Print menu.

XML

<!– File location: custom_hospital_management/report/patient_reports.xml –>

<?xml version=”1.0″ encoding=”utf-8″?>

<odoo>

    <record id=”action_report_patient_card” model=”ir.actions.report”>

        <field name=”name”>Patient Medical Card</field>

        <field name=”model”>hospital.patient</field>

        <field name=”report_type”>qweb-pdf</field>

        <field name=”report_name”>custom_hospital_management.report_patient_card_template</field>

        <field name=”report_file”>custom_hospital_management.report_patient_card_template</field>

        <field name=”print_report_name”>’Patient Card – %s’ % (object.name)</field>

        <field name=”binding_model_id” ref=”model_hospital_patient”/>

        <field name=”binding_type”>report</field>

    </record>

 

</odoo>

Action Configuration Breakdown

  • model: Specifies the technical model name (hospital.patient).
  • report_type: Set to qweb-pdf to generate a downloadable PDF (or qweb-html for web previews).
  • report_name: Points directly to <your_module_name>.<template_id>.
  • binding_model_id: Automatically inserts this report into the Print dropdown menu on the patient form and list views (ref=”model_hospital_patient”).
  • print_report_name: Sets the default dynamic file name when saving the PDF locally.

Guided Exercises & Solutions

Exercise 1: Conditional Empty Notes Logic (t-if & t-else)

Add logic so that if a patient has no medical notes recorded, the report prints a helpful notice instead of leaving a blank space.

XML

<!– Solution: Replace the contents inside <div class=”page”> with this –>

<div class=”page”>

    <h2>

        <span t-field=”patient.name”/>

    </h2>

    <div>

        <strong>Age: </strong><span t-field=”patient.age”/> | 

        <strong>Gender: </strong><span t-field=”patient.gender”/>

    </div>

 

    <!– Check if note field contains text –>

    <t t-if=”patient.note”>

        <div class=”mt-3″>

            <strong>Medical History &amp; Notes:</strong>

            <p t-field=”patient.note”/>

        </div>

    </t>

    <t t-else=””>

        <div class=”mt-3 text-muted”>

            <em>No medical history or notes recorded for this patient yet.</em>

        </div>

    </t>

 

</div>

 

Modular Sub-Templates

Sub-templates help break long reports into smaller, readable pieces and allow you to reuse common layout chunks across multiple reports.

Step 1: Extract a Common Table into a Sub-Template

Create a standalone sub-template containing the patient metrics table:

XML

<!– File location: custom_hospital_management/report/patient_templates.xml –>

<odoo>

    <!– Sub-template Definition –>

    <template id=”patient_details_table”>

        <table class=”table table-bordered text-center”>

            <thead class=”table-light”>

                <tr>

                    <th>Patient Name</th>

                    <th>Age</th>

                    <th>Gender</th>

                    <th>Status</th>

                    <th>Is Minor</th>

                </tr>

            </thead>

            <tbody>

                <tr>

                    <td><span t-field=”patient.name”/></td>

                    <td><span t-field=”patient.age”/></td>

                    <td><span t-field=”patient.gender”/></td>

                    <td><span t-field=”patient.state”/></td>

                    <td><span t-field=”patient.is_minor”/></td>

                </tr>

            </tbody>

        </table>

    </template>

 

    <!– Main Template calling the sub-template –>

    <template id=”report_patient_card_template”>

        <t t-call=”web.html_container”>

            <t t-foreach=”docs” t-as=”patient”>

                <t t-call=”web.external_layout”>

                    <div class=”page”>

                        <h3>Patient Information Summary</h3>

                        <!– Call the external sub-template –>

                        <t t-call=”custom_hospital_management.patient_details_table”/>

                    </div>

                </t>

            </t>

        </t>

    </template>

 

</odoo>

 

Exercise 2: Create a Doctor Report for res.users

Create a report linked to the User settings form (res.users) that prints out all patients assigned to a system user.

Because res.users is a core Odoo model, set binding_model_id to ref=”base.model_res_users”.

XML

<!– File location: custom_hospital_management/report/patient_reports.xml –>

<record id=”action_report_doctor_patients” model=”ir.actions.report”>

    <field name=”name”>Assigned Patient Overview</field>

    <field name=”model”>res.users</field>

    <field name=”report_type”>qweb-pdf</field>

    <field name=”report_name”>custom_hospital_management.report_doctor_patients_template</field>

    <field name=”report_file”>custom_hospital_management.report_doctor_patients_template</field>

    <field name=”binding_model_id” ref=”base.model_res_users”/>

    <field name=”binding_type”>report</field>

 

</record>

QWeb Report Inheritance (xpath)

Report templates inherit and modify parent templates using <xpath> expressions identical to Odoo view inheritance.

To extend a template from another module (or modify an existing report), set inherit_id to <module>.<parent_template_id>:

XML

<!– Extend the Patient Card Report –>

<odoo>

    <template id=”report_patient_card_inherit” inherit_id=”custom_hospital_management.report_patient_card_template”>

        <!– Target the heading tag and insert a verified notice below it –>

        <xpath expr=”//h3″ position=”after”>

            <div class=”alert alert-info” role=”alert”>

                <span>Official Hospital Record – Confirmed by Medical Staff</span>

            </div>

        </xpath>

    </template>

 

</odoo>

Additional QWeb Report Features

  • your QWeb HTML tags. Clean text structures allow Odoo’s translation tools to extract and translate report text automatically.
  • Web Browser Previews: You can test your layouts directly inside your web browser without downloading PDF files by switching report_type from qweb-pdf to qweb-html.
  • Generating Barcodes & QR Codes: Standard barcode image generators are built directly into Odoo reports:
  • XML

<!– Render a Code128 Barcode based on patient name –>

 

<img t-att-src=”‘/report/barcode/?barcode_type=%s&value=%s&width=%s&height=%s’ % (‘Code128’, patient.name, 600, 100)” style=”width:300px;height:50px;”/>

Explore the Real Estate App

Properties
1-7 / 7
Name Property Type Postcode Tags Bedrooms Living Area (m²) Expected Price Selling Price Date Availability
Big Villa 2 221.000.00
Trailer home Residential54321 110100,000.000.00Aug 10
Big Villa Residential12345 61001,600,000.001,500,000.00Dec 8
2030 VILLA Villa00000 2020,000.000.00
New Villa Villa00000 2020.000.00Aug 5
hi Villa 20500.000.00
Villa Villa0000 52-500.000.00Jul 23
None (1)
Residential (2)
Trailer home
Expected Price: 100,000.00
Big Villa
Expected Price: 1,600,000.00
Villa (4)
2030 VILLA
Expected Price: 20,000.00
New Villa
Expected Price: 20.00
hi
Expected Scale: 500.00
Villa
Expected Price: -500.00
.preview-wrapper { width: 100%; max-width: 970px; margin: 0 auto; background: #ffffff; padding: 10px; box-sizing: border-box; } .device-selector { display: flex; justify-content: center; gap: 6px; margin-bottom: 10px; background: #f1f3f5; padding: 4px; border-radius: 4px; width: fit-content; margin-left: auto; margin-right: auto; } .device-btn { background: transparent; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #495057; } .device-btn.active { background-color: #714B67; color: white; } .device-btn svg { width: 16px; height: 16px; fill: currentColor; } .app-container { width: 100%; max-width: 100%; margin: 0 auto; background: #ffffff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #dee2e6; border-radius: 4px; overflow: hidden; transition: width 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; } .navbar { background-color: #714B67; color: white; display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; font-size: 14px; } .navbar-left { display: flex; gap: 20px; align-items: center; } .navbar a { color: white; text-decoration: none; } .control-panel { background: #ffffff; border-bottom: 1px solid #dee2e6; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } .control-panel-left { display: flex; gap: 10px; align-items: center; } .control-panel-right { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #495057; background: #ffffff; padding: 4px 8px; border-radius: 4px; } .pagination-counter { font-weight: 500; color: #212529; min-width: 50px; text-align: right; } .pager-buttons { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .pager-btn { background: transparent; border: none; padding: 2px 6px; cursor: pointer; color: #495057; font-size: 12px; } .pager-btn:hover { background-color: #f8f9fa; } .pager-btn:not(:last-child) { border-right: 1px solid #ced4da; } .view-switchers { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .view-btn { background: transparent; border: none; padding: 4px 6px; cursor: pointer; color: #495057; display: flex; align-items: center; justify-content: center; } .view-btn:hover { background-color: #f8f9fa; } .view-btn.active { background-color: #e9ecef; color: #000; } .view-btn:not(:last-child) { border-right: 1px solid #ced4da; } .btn-primary { background-color: #714B67; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; } .search-bar { border: 1px solid #ced4da; padding: 6px 12px; border-radius: 4px; width: 220px; max-width: 100%; } .content-container { padding: 16px; width: 100%; box-sizing: border-box; } .table-container { width: 100%; overflow-x: auto; } table { width: 100%; background: white; border-collapse: collapse; white-space: nowrap; } th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; } th { background-color: #fff; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .kanban-board { display: none; gap: 16px; align-items: flex-start; overflow-x: auto; padding-bottom: 10px; } .kanban-column-vertical { writing-mode: vertical-lr; transform: rotate(180deg); background: #e9ecef; border: 1px solid #ced4da; padding: 12px 8px; font-size: 13px; font-weight: 600; color: #495057; border-radius: 4px; cursor: pointer; text-align: center; height: fit-content; } .kanban-column { background: #ffffff; border: 1px solid #dee2e6; border-radius: 4px; width: 320px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); display: flex; flex-direction: column; flex-shrink: 0; } .kanban-header { padding: 10px 14px; font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; background: #fdfdfe; border-top-left-radius: 4px; border-top-right-radius: 4px; } .add-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: #6c757d; } .kanban-cards-list { padding: 10px; display: flex; flex-direction: column; gap: 8px; } .kanban-card { background: #ffffff; dir: ltr; border: 1px solid #ced4da; border-radius: 4px; padding: 10px 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease; } .kanban-card:hover { border-color: #714B67; box-shadow: 0 2px 5px rgba(0,0,0,0.08); } .card-title { font-weight: 600; font-size: 14px; color: #212529; margin-bottom: 4px; } .card-text { font-size: 12px; color: #495057; } function triggerTryIt() { const btn = document.getElementById('tryItBtn'); const container = document.getElementById('appContainer'); if (!btn || !container) return; // Change button style to matte white with shadow and purple text btn.style.backgroundColor = '#fdfdfd'; btn.style.color = '#714B67'; btn.style.boxShadow = '0 4px 12px rgba(113, 75, 103, 0.15)'; btn.style.border = '1px solid #714B67'; // Highlight app screen border temporarily container.style.border = '2px solid #714B67'; container.style.boxShadow = '0 0 15px rgba(113, 75, 103, 0.25)'; // Revert back after 1 second setTimeout(() => { btn.style.backgroundColor = '#714B67'; btn.style.color = '#ffffff'; btn.style.boxShadow = 'none'; btn.style.border = '1px solid #714B67'; container.style.border = '1px solid #dee2e6'; container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)'; }, 1000); } function setDevice(type, userClicked = false) { const container = document.getElementById('appContainer'); const buttons = document.querySelectorAll('.device-btn'); buttons.forEach(btn => btn.classList.remove('active')); if (type === 'phone') { if (userClicked) container.style.width = '380px'; document.getElementById('btnPhone')?.classList.add('active'); } else if (type === 'tab') { if (userClicked) container.style.width = '640px'; document.getElementById('btnTab')?.classList.add('active'); } else { if (userClicked) container.style.width = '100%'; document.getElementById('btnLaptop')?.classList.add('active'); } } window.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('tryItBtn'); if (btn) { btn.addEventListener('click', triggerTryIt); } const container = document.getElementById('appContainer'); if (container) { setDevice('phone', false); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const currentWidth = entry.contentRect.width; if (currentWidth >= 360 && currentWidth 540 && currentWidth 720) { setDevice('laptop', false); } } }); resizeObserver.observe(container); } }); let state = { currentStart: 1, currentEnd: 7, totalItems: 7, activeView: 'list' }; function updateUI() { const pageIndicator = document.getElementById('pageIndicator'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); if (pageIndicator) pageIndicator.textContent = `${state.currentStart}-${state.currentEnd} / ${state.totalItems}`; if (prevBtn) prevBtn.style.opacity = state.currentStart === 1 ? '0.5' : '1'; if (nextBtn) nextBtn.style.opacity = state.currentEnd >= state.totalItems ? '0.5' : '1'; } document.getElementById('nextBtn')?.addEventListener('click', () => { if (state.currentEnd { if (state.currentStart > 1) { state.currentStart -= 1; state.currentEnd -= 1; updateUI(); } }); function switchView(type) { document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); document.querySelector(`[data-view="${type}"]`)?.classList.add('active'); state.activeView = type; const listView = document.getElementById('listView'); const kanbanView = document.getElementById('kanbanView'); if (type === 'kanban') { if (listView) listView.style.display = 'none'; if (kanbanView) kanbanView.style.display = 'flex'; } else { if (kanbanView) kanbanView.style.display = 'none'; if (listView) listView.style.display = 'block'; } } function openRecord(name) { alert(`Opening Odoo Form View for: "${name}"`); } function createNewRecord() { const name = prompt("Enter new property name:"); if (name) { alert(`Creating new record "${name}" via Odoo model controller.`); } } function addCard(groupName) { const name = prompt(`Add property to ${groupName}:`); if (name) { const list = document.getElementById(`cards-${groupName}`); if (list) { const card = document.createElement('div'); card.className = 'kanban-card'; card.innerHTML = `
${name}
Expected Price: 0.00
`; card.onclick = () => openRecord(name); list.appendChild(card); } } } function expandColumn(groupName) { alert(`Expanding collapsed group column: ${groupName}`); } function filterContent() { const searchInput = document.getElementById('searchInput'); if (!searchInput) return; const query = searchInput.value.toLowerCase(); document.querySelectorAll('#listView tbody tr').forEach(row => { row.style.display = row.textContent.toLowerCase().includes(query) ? '' : 'none'; }); document.querySelectorAll('.kanban-card').forEach(card => { card.style.display = card.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); }

Odoo Developer Guide: Data Security & Access Controls in Odoo

Odoo Developer Guide: Data Security & Access Controls in Odoo

Explore the Real Estate App

How to Test It
Use the profile switcher at the top to see Odoo’s security in action:

  • Real Estate Agent View (Restricted): The property list automatically filters using row-level rules to show only assigned or unassigned listings, and the delete action is locked out because standard agents lack deletion rights.
  • Real Estate Manager View (Full Access): Managers bypass these restriction filters to view every property in the company pipeline and retain full administrative control, including the ability to delete records.
1-7 / 7
Name Property Type Postcode Tags Bedrooms Living Area (m²) Expected Price Selling Price Date Availability
Big Villa 2 221.000.00
Trailer home Residential54321 110100,000.000.00Aug 10
Big Villa Residential12345 61001,600,000.001,500,000.00Dec 8
2030 VILLA Villa00000 2020,000.000.00
New Villa Villa00000 2020.000.00Aug 5
hi Villa 20500.000.00
Villa Villa0000 52-500.000.00Jul 23
None (1)
Residential (2)
Trailer home
Expected Price: 100,000.00
Big Villa
Expected Price: 1,600,000.00
Villa (4)
2030 VILLA
Expected Price: 20,000.00
New Villa
Expected Price: 20.00
hi
Expected Scale: 500.00
Villa
Expected Price: -500.00
.preview-wrapper { width: 100%; max-width: 970px; margin: 0 auto; background: #ffffff; padding: 10px; box-sizing: border-box; } .device-selector { display: flex; justify-content: center; gap: 6px; margin-bottom: 10px; background: #f1f3f5; padding: 4px; border-radius: 4px; width: fit-content; margin-left: auto; margin-right: auto; } .device-btn { background: transparent; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #495057; } .device-btn.active { background-color: #714B67; color: white; } .device-btn svg { width: 16px; height: 16px; fill: currentColor; } .app-container { width: 100%; max-width: 100%; margin: 0 auto; background: #ffffff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #dee2e6; border-radius: 4px; overflow: hidden; transition: width 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; } .navbar { background-color: #714B67; color: white; display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; font-size: 14px; } .navbar-left { display: flex; gap: 20px; align-items: center; } .navbar a { color: white; text-decoration: none; } .control-panel { background: #ffffff; border-bottom: 1px solid #dee2e6; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } .control-panel-left { display: flex; gap: 10px; align-items: center; } .control-panel-middle { display: flex; align-items: center; } .control-panel-right { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #495057; background: #ffffff; padding: 4px 8px; border-radius: 4px; } .pagination-counter { font-weight: 500; color: #212529; min-width: 50px; text-align: right; } .pager-buttons { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .pager-btn { background: transparent; border: none; padding: 2px 6px; cursor: pointer; color: #495057; font-size: 12px; } .pager-btn:hover { background-color: #f8f9fa; } .pager-btn:not(:last-child) { border-right: 1px solid #ced4da; } .view-switchers { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .view-btn { background: transparent; border: none; padding: 4px 6px; cursor: pointer; color: #495057; display: flex; align-items: center; justify-content: center; } .view-btn:hover { background-color: #f8f9fa; } .view-btn.active { background-color: #e9ecef; color: #000; } .view-btn:not(:last-child) { border-right: 1px solid #ced4da; } .btn-primary { background-color: #714B67; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; } .search-bar { border: 1px solid #ced4da; padding: 6px 12px; border-radius: 4px; width: 220px; max-width: 100%; } .content-container { padding: 16px; width: 100%; box-sizing: border-box; } .table-container { width: 100%; overflow-x: auto; } .odoo-table { width: 100%; background: white; border-collapse: collapse; white-space: nowrap; } .odoo-table th, .odoo-table td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #dee2e6; font-size: 13px; } .odoo-table th { background-color: #fff; font-weight: 600; color: #495057; } .odoo-table tbody tr.selected-row { background-color: #e8f4f8 !important; } .odoo-table tbody tr:hover { background-color: #f1f3f5; } .dropdown-menu.show { display: block !important; } .dropdown-item:hover { background-color: #f1f3f5; color: #16181b; } .kanban-board { display: none; gap: 16px; align-items: flex-start; overflow-x: auto; padding-bottom: 10px; } .kanban-column-vertical { writing-mode: vertical-lr; transform: rotate(180deg); background: #e9ecef; border: 1px solid #ced4da; padding: 12px 8px; font-size: 13px; font-weight: 600; color: #495057; border-radius: 4px; cursor: pointer; text-align: center; height: fit-content; } .kanban-column { background: #ffffff; border: 1px solid #dee2e6; border-radius: 4px; width: 320px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); display: flex; flex-direction: column; flex-shrink: 0; } .kanban-header { padding: 10px 14px; font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; background: #fdfdfe; border-top-left-radius: 4px; border-top-right-radius: 4px; } .add-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: #6c757d; } .kanban-cards-list { padding: 10px; display: flex; flex-direction: column; gap: 8px; } .kanban-card { background: #ffffff; border: 1px solid #ced4da; border-radius: 4px; padding: 10px 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease; } .kanban-card:hover { border-color: #714B67; box-shadow: 0 2px 5px rgba(0,0,0,0.08); } .card-title { font-weight: 600; font-size: 14px; color: #212529; margin-bottom: 4px; } .card-text { font-size: 12px; color: #495057; } function triggerTryIt() { const btn = document.getElementById('tryItBtn'); const container = document.getElementById('appContainer'); if (!btn || !container) return; btn.style.backgroundColor = '#fdfdfd'; btn.style.color = '#714B67'; btn.style.boxShadow = '0 4px 12px rgba(113, 75, 103, 0.15)'; btn.style.border = '1px solid #714B67'; container.style.border = '2px solid #714B67'; container.style.boxShadow = '0 0 15px rgba(113, 75, 103, 0.25)'; setTimeout(() => { btn.style.backgroundColor = '#714B67'; btn.style.color = '#ffffff'; btn.style.boxShadow = 'none'; btn.style.border = '1px solid #714B67'; container.style.border = '1px solid #dee2e6'; container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)'; }, 1000); } function setDevice(type, userClicked = false) { const container = document.getElementById('appContainer'); const buttons = document.querySelectorAll('.device-btn'); buttons.forEach(btn => btn.classList.remove('active')); if (type === 'phone') { if (userClicked) container.style.width = '380px'; document.getElementById('btnPhone')?.classList.add('active'); } else if (type === 'tab') { if (userClicked) container.style.width = '640px'; document.getElementById('btnTab')?.classList.add('active'); } else { if (userClicked) container.style.width = '100%'; document.getElementById('btnLaptop')?.classList.add('active'); } } let state = { currentStart: 1, currentEnd: 7, totalItems: 7, activeView: 'list', currentProfile: 'admin' }; function updateUI() { const pageIndicator = document.getElementById('pageIndicator'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); if (pageIndicator) pageIndicator.textContent = `${state.currentStart}-${state.currentEnd} / ${state.totalItems}`; if (prevBtn) prevBtn.style.opacity = state.currentStart === 1 ? '0.5' : '1'; if (nextBtn) nextBtn.style.opacity = state.currentEnd >= state.totalItems ? '0.5' : '1'; } document.getElementById('nextBtn')?.addEventListener('click', () => { if (state.currentEnd { if (state.currentStart > 1) { state.currentStart -= 1; state.currentEnd -= 1; updateUI(); } }); function switchView(type) { document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); document.querySelector(`[data-view="${type}"]`)?.classList.add('active'); state.activeView = type; const listView = document.getElementById('listView'); const kanbanView = document.getElementById('kanbanView'); if (type === 'kanban') { if (listView) listView.style.display = 'none'; if (kanbanView) kanbanView.style.display = 'flex'; } else { if (kanbanView) kanbanView.style.display = 'none'; if (listView) listView.style.display = 'block'; } } function openRecord(name) { alert(`Opening Odoo Form View for: "${name}"`); } function createNewRecord() { const name = prompt("Enter new property name:"); if (name) { alert(`Creating new record "${name}" via Odoo model controller.`); } } function addCard(groupName) { const name = prompt(`Add property to ${groupName}:`); if (name) { const list = document.getElementById(`cards-${groupName}`); if (list) { const card = document.createElement('div'); card.className = 'kanban-card'; card.innerHTML = `
${name}
Expected Price: 0.00
`; card.onclick = () => openRecord(name); list.appendChild(card); } } } function expandColumn(groupName) { alert(`Expanding collapsed group column: ${groupName}`); } function filterContent() { const searchInput = document.getElementById('searchInput'); if (!searchInput) return; const query = searchInput.value.toLowerCase(); document.querySelectorAll('#listView tbody tr').forEach(row => { row.style.display = row.textContent.toLowerCase().includes(query) ? '' : 'none'; }); document.querySelectorAll('.kanban-card').forEach(card => { card.style.display = card.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); } function populateActionsMenu() { const actionsMenu = document.getElementById('actionsMenu'); if (!actionsMenu) return; if (state.currentProfile === 'admin') { actionsMenu.innerHTML = ` Duplicate Archive Unarchive Export Delete `; } else { // Test Agent gets only delete, archive, and unarchive actionsMenu.innerHTML = ` Duplicate Archive Unarchive `; } bindActionItems(); } function bindActionItems() { document.querySelectorAll('.dropdown-item').forEach(item => { item.replaceWith(item.cloneNode(true)); }); document.querySelectorAll('.dropdown-item').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const actionType = e.target.getAttribute('data-action'); const selectedIds = Array.from(document.querySelectorAll('.row-checkbox:checked')) .map(chk => chk.closest('tr').getAttribute('data-id')); alert(`Action "${actionType}" triggered for IDs: ${selectedIds.join(', ')}`); const actionsMenu = document.getElementById('actionsMenu'); if (actionsMenu) actionsMenu.classList.remove('show'); }); }); } // Interactive Selection Logic & Action Menus function bindSelectionEvents() { const selectAllCheckbox = document.getElementById('selectAllCheckbox'); const rowCheckboxes = document.querySelectorAll('.row-checkbox'); const searchInput = document.getElementById('searchInput'); const selectionToolbar = document.getElementById('selectionToolbar'); const selectedCountSpan = document.getElementById('selectedCount'); const clearSelectionBtn = document.getElementById('clearSelection'); const actionsDropdownBtn = document.getElementById('actionsDropdownBtn'); const actionsMenu = document.getElementById('actionsMenu'); function updateSelectionState() { const currentCheckboxes = document.querySelectorAll('.row-checkbox'); const checkedRows = document.querySelectorAll('.row-checkbox:checked'); const count = checkedRows.length; currentCheckboxes.forEach(chk => { const tr = chk.closest('tr'); if (chk.checked) { tr.classList.add('selected-row'); } else { tr.classList.remove('selected-row'); } }); if (count > 0) { if (searchInput) searchInput.style.display = 'none'; if (selectionToolbar) selectionToolbar.style.display = 'flex'; if (selectedCountSpan) selectedCountSpan.textContent = count; } else { if (searchInput) searchInput.style.display = 'block'; if (selectionToolbar) selectionToolbar.style.display = 'none'; if (actionsMenu) actionsMenu.classList.remove('show'); } } rowCheckboxes.forEach(checkbox => { checkbox.replaceWith(checkbox.cloneNode(true)); }); document.querySelectorAll('.row-checkbox').forEach(checkbox => { checkbox.addEventListener('change', updateSelectionState); }); if (selectAllCheckbox) { selectAllCheckbox.replaceWith(selectAllCheckbox.cloneNode(true)); const newSelectAll = document.getElementById('selectAllCheckbox'); newSelectAll.addEventListener('change', (e) => { document.querySelectorAll('.row-checkbox').forEach(checkbox => { checkbox.checked = e.target.checked; }); updateSelectionState(); }); } if (clearSelectionBtn) { clearSelectionBtn.replaceWith(clearSelectionBtn.cloneNode(true)); const newClearBtn = document.getElementById('clearSelection'); newClearBtn.addEventListener('click', () => { document.querySelectorAll('.row-checkbox').forEach(checkbox => { checkbox.checked = false; }); if (selectAllCheckbox) selectAllCheckbox.checked = false; updateSelectionState(); }); } if (actionsDropdownBtn && actionsMenu) { actionsDropdownBtn.replaceWith(actionsDropdownBtn.cloneNode(true)); const newActionBtn = document.getElementById('actionsDropdownBtn'); newActionBtn.addEventListener('click', (e) => { e.stopPropagation(); actionsMenu.classList.toggle('show'); }); } populateActionsMenu(); } window.addEventListener('click', () => { const actionsMenu = document.getElementById('actionsMenu'); if (actionsMenu && actionsMenu.classList.contains('show')) { actionsMenu.classList.remove('show'); } }); // Profile Switcher Logic integration const profileBtn = document.getElementById('userProfileBtn'); const dropdown = document.getElementById('profileDropdown'); const currentUserName = document.getElementById('currentUserName'); const currentUserAvatar = document.getElementById('currentUserAvatar'); const breadcrumbTitle = document.getElementById('breadcrumbTitle'); const tableBodyContent = document.getElementById('tableBodyContent'); profileBtn.addEventListener('click', (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block'; }); window.addEventListener('click', () => { dropdown.style.display = 'none'; }); document.querySelectorAll('.profile-switch-option').forEach(option => { option.addEventListener('click', function() { const profileType = this.getAttribute('data-profile'); state.currentProfile = profileType; if (profileType === 'admin') { currentUserName.textContent = 'Mitchell Admin'; currentUserAvatar.textContent = 'M'; currentUserAvatar.style.backgroundColor = '#714B67'; breadcrumbTitle.textContent = 'Properties'; state.totalItems = 7; state.currentStart = 1; state.currentEnd = 7; updateUI(); tableBodyContent.innerHTML = ` Big Villa 2 221.000.00 Trailer home Residential54321 110100,000.000.00Aug 10 Big Villa Residential12345 61001,600,000.001,500,000.00Dec 8 2030 VILLA Villa00000 2020,000.000.00 New Villa Villa00000 2020.000.00Aug 5 hi Villa 20500.000.00 Villa Villa0000 52-500.000.00Jul 23 `; } else { currentUserName.textContent = 'Test Agent'; currentUserAvatar.textContent = 'T'; currentUserAvatar.style.backgroundColor = '#008784'; breadcrumbTitle.textContent = 'Properties (Agent View)'; state.totalItems = 3; state.currentStart = 1; state.currentEnd = 3; updateUI(); tableBodyContent.innerHTML = ` Trailer home Residential54321 110400,000.000.00Aug 10 2030 VILLA Villa00000 2020,000.000.00 New Villa Villa00000 2020.000.00Aug 5 `; } document.getElementById('selectionToolbar').style.display = 'none'; document.getElementById('searchInput').style.display = 'block'; if (document.getElementById('selectAllCheckbox')) { document.getElementById('selectAllCheckbox').checked = false; } bindSelectionEvents(); dropdown.style.display = 'none'; }); }); window.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('tryItBtn'); if (btn) { btn.addEventListener('click', triggerTryIt); } const container = document.getElementById('appContainer'); if (container) { setDevice('phone', false); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const currentWidth = entry.contentRect.width; if (currentWidth >= 360 && currentWidth 540 && currentWidth 720) { setDevice('laptop', false); } } }); resizeObserver.observe(container); } bindSelectionEvents(); });

About This Guide

This guide breaks down how Odoo protects your data and controls user permissions across your business. It explains how roles, permissions, and security rules work together to ensure employees only see and manage the specific records they need—keeping your business information safe and organized.

What You Will Learn

  • User Roles & Access Control: How to set up job roles and assign specific read, edit, or delete permissions across your business modules.
  • Row-Level Security: How to automatically restrict record visibility so users only view data relevant to their role or branch.
  • Field Privacy: How to hide sensitive information (like private medical notes or financial data) from unauthorized team members.
  • Best Practices: How to avoid common configuration mistakes and keep your overall system safe from unintended access.

Odoo provides two main data-driven mechanisms to manage or restrict access to data without writing custom hardcoded logic. Both mechanisms link to users through User Groups (res.groups): a user belongs to any number of groups, and security mechanisms are attached to groups to govern permissions.

This guide provides a comprehensive breakdown of Odoo security architecture using a Hospital Management (custom_hospital_management) module as an example.

Group Architecture (res.groups)

User groups define roles and serve as the foundation for both Access Rights and Record Rules.

Model Attributes (res.groups)

  • name: Serves as user-readable identification for the group (spells out the role or purpose of the group, e.g., “Doctor”, “Patient Administrator”).
  • category_id: The module category. Associates groups with an Odoo App (a set of related business models) and converts them into an exclusive selection box on the User setup form.
  • implied_ids: Other groups to assign to the user alongside this one. This acts as a convenience pseudo-inheritance relationship (e.g., a “Hospital Manager” group implies the “Hospital User” group). It remains possible to explicitly remove implied groups from a user without removing the main group.
  • comment: Additional technical notes or descriptions detailing the purpose of the group.

Access Rights (ir.model.access)

Access Control Lists (ACLs) grant access to an entire model for a given set of operations (Create, Read, Update, Delete). If no access right matches an operation on a model for a user (through their assigned groups), the user is denied access.

Core Properties

  • Additive Nature: Access rights are cumulative. A user’s total access is the union of access rights from all groups they belong to.
    Example: If Group A grants Read and Create, and Group B grants Update, a user in both groups has Read, Create, and Update access.
  • Default Behavior: Unmatched operations default to denied access.

Model Attributes (ir.model.access)

  • name: Descriptive purpose or role of the access control record.
  • model_id: The target model whose access the ACL controls (formatted as model_<model_name_with_underscores>).
  • group_id: The res.groups record to which access is granted. Leaving group_id empty grants access to every user (including non-employees such as portal or public users).
  • CRUD Attributes (perm_*): Grant the corresponding operation when set to 1 (True). All permissions are unset (0 / False) by default:
    • perm_read: Read/View records.
    • perm_create: Create new records.
    • perm_write: Modify existing records.
    • perm_unlink: Delete records.

Implementation Example: Hospital Management

File location: custom_hospital_management/security/ir.model.access.csv

Code snippet

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink

access_hospital_patient_user,access.hospital.patient.user,model_hospital_patient,base.group_user,1,1,1,0

In this configuration, standard internal users (base.group_user) can Read, Write, and Create patient records (hospital.patient), but cannot Delete (perm_unlink = 0) them.

Record Rules (ir.rule)

Record rules are row-level conditions evaluated record-by-record after Access Rights pass. While Access Rights grant access to the table, Record Rules filter which individual rows inside that table a user can see or modify.

Core Properties

  • Default-Allow: If Access Rights grant permission and no record rule applies to the operation/model for the user, access is allowed.
  • Operation Selection: Unlike ir.model.access, setting perm_* flags on an ir.rule determines which operations the rule actively filters. If an operation is unset (False), the rule ignores that operation and permits it freely. All operations are selected (True) by default.

Model Attributes (ir.rule)

  • name: Description of the rule.
  • model_id: The model to which the rule applies.
  • groups: The res.groups to which the rule applies. If no group is specified, the rule is marked as Global.
  • global: Computed field indicating whether the rule applies globally across all users.
  • domain_force: A domain predicate (Python expression). Records matching the domain are allowed; non-matching records are forbidden.
  • CRUD Operation Flags: perm_read, perm_write, perm_create, perm_unlink.

Available Domain Evaluation Context

When writing expressions inside domain_force, Odoo provides the following evaluation variables:

  • user: The current user record (as a singleton recordset).
  • company_id: The user’s currently active company ID (integer).
  • company_ids: All company IDs accessible to the user (list of integers).
  • time: Python’s native time module.

Global Rules vs. Group Rules

 

Rule Type

Composition Behavior

Technical Rule

Global Rules (groups empty)

Intersection (AND)

Adding global rules always restricts access further. All global rules must be satisfied simultaneously.

Group Rules (groups specified)

Unification (OR)

Adding group rules expands access. If any group rule matches, access is granted.

Combined Evaluation

Intersection (AND)

Global rulesets and Group rulesets intersect. The user must pass ALL Global rules AND at least ONE matching Group rule.

DANGER: Creating multiple Global rules is risky. If two global rules are mutually exclusive (non-overlapping), all user access to the model will be completely blocked.

Field-Level Security

Fields can be restricted directly in Python models using the groups attribute:

Python

class HospitalPatient(models.Model):

    _name = ‘hospital.patient’

    _description = ‘Hospital Patient Record’

 

    name = fields.Char(string=’Full Name’, required=True)

    # Medical notes restricted strictly to Healthcare Managers

 

    note = fields.Text(string=’Medical History’, groups=’custom_hospital_management.group_hospital_manager’)

 

Security Enforcement Controls:

  1. Restricted fields are automatically stripped from view rendering XML layouts.
  2. Restricted fields are removed from fields_get() ORM calls.
  3. Direct attempts to read or write to restricted fields via RPC or Python trigger an explicit AccessError.

Security Pitfalls & Developer Guidelines

A. Unsafe Public Methods

Any public Python method (methods not starting with _) can be executed remotely via RPC calls. Access Rights are only enforced automatically during standard ORM CRUD operations.

Python

#INCORRECT: Arguments and state execution cannot be trusted

def action_discharge(self):

    if self.state == “draft” and self.env.user.has_group(‘base.group_user’):

        self._set_state(“discharged”)

 

#CORRECT: Keep business execution private, apply explicit checks

def _set_state(self, new_state):

 

    self.sudo().write({“state”: new_state})

 

B. Direct Database Cursor Access & SQL Injections

Never bypass the ORM to execute raw SQL queries via self.env.cr when ORM methods (search, write) can perform the task. Bypassing the ORM skips automated security checks, domain filtering, and record-level rules.

Vulnerability Examples & Fixes:

Python

# CRITICAL VULNERABILITY: SQL Injection via string concatenation

self.env.cr.execute(‘SELECT id FROM hospital_patient WHERE state = ‘ + user_input)

 

# POOR PRACTICE: Bypassing ORM security logic manually

self.env.cr.execute(‘SELECT id FROM hospital_patient WHERE state = %s’, (state_val,))

 

# CORRECT: Clean ORM usage enforcing access rights & domain safety

patients = self.search([(‘state’, ‘=’, state_val)])

 

If manual SQL queries are unavoidable, strictly use Odoo’s SQL wrapper to prevent injection:

Python

from odoo.tools import SQL

 

# SECURE: Safe parameter handling via SQL wrapper

self.env.cr.execute(SQL(“””

    SELECT id FROM hospital_patient 

    WHERE age > %s AND state = %s

 

“””, min_age, state_val))

 

C. Safe Domain Construction

Do not concatenate raw lists to build search domains, as unnormalized user inputs can manipulate domain logic. Use the Domain utility object:

Python

from odoo.osv.expression import Domain

 

# UNSAFE: User domain can override security rules via logical ORs

domain = user_provided_domain 

domain += [(‘create_uid’, ‘=’, self.env.uid)]

 

# SECURE: Enforce security parameters using domain intersection

domain = Domain(user_provided_domain) & Domain([(‘create_uid’, ‘=’, self.env.uid)])

 

patients = self.search(domain)

 

D. Cross-Site Scripting (XSS) Prevention & HTML Escaping

  • Never use t-raw in QWeb: Always use t-esc to render string variables safely in user interfaces.
  • Escaping vs. Sanitizing:
    • Escaping (html_escape): Converts plain text to safe HTML markup. Mandatory every time untrusted text is mixed into HTML strings.
    • Sanitizing (html_sanitize): Cleans existing HTML code by stripping malicious tags (<script>) or inline styles.

Safe HTML Generation using Markup:

Python

from odoo.tools import Markup, escape

 

# UNSAFE: Raw dynamic strings inserted directly into HTML structure

html_out = f”<div>User: {patient.name}</div>”

 

# SECURE: Using Markup to ensure variables are safely escaped

 

html_out = Markup(“<div>User: %s</div>”) % patient.name

 

E. Dynamic Attribute Access

Do not use generic getattr() or setattr() calls on ORM records, as they expose internal private methods and system attributes. Access fields dynamically using record dictionary indexing:

Python

# UNSAFE: Gives full access to internal private methods/attributes

value = getattr(patient_record, user_input_field)

 

# SECURE: Restricts dynamic access strictly to model fields

 

value = patient_record[user_input_field]

 

Safe Content Evaluation Reference Table

Avoid using Python’s eval() or safe_eval() to parse string inputs. Use specific non-evaluating data parsers:

Data Type

Target Language

Safe Parser

Integer / Float

Python

int(), float()

Integer / Float

JavaScript

parseInt(), parseFloat()

Dictionaries / Lists

Python

json.loads(), ast.literal_eval()

Objects / Arrays

JavaScript

JSON.parse()

Complete Developer’s Manual: Building Custom Modules in Odoo

Complete Developer's Manual: Building Custom Modules in Odoo

Explore the Real Estate App

Properties
1-7 / 7
Name Property Type Postcode Tags Bedrooms Living Area (m²) Expected Price Selling Price Date Availability
Big Villa 2 221.000.00
Trailer home Residential54321 110100,000.000.00Aug 10
Big Villa Residential12345 61001,600,000.001,500,000.00Dec 8
2030 VILLA Villa00000 2020,000.000.00
New Villa Villa00000 2020.000.00Aug 5
hi Villa 20500.000.00
Villa Villa0000 52-500.000.00Jul 23
None (1)
Residential (2)
Trailer home
Expected Price: 100,000.00
Big Villa
Expected Price: 1,600,000.00
Villa (4)
2030 VILLA
Expected Price: 20,000.00
New Villa
Expected Price: 20.00
hi
Expected Scale: 500.00
Villa
Expected Price: -500.00
.preview-wrapper { width: 100%; max-width: 970px; margin: 0 auto; background: #ffffff; padding: 10px; box-sizing: border-box; } .device-selector { display: flex; justify-content: center; gap: 6px; margin-bottom: 10px; background: #f1f3f5; padding: 4px; border-radius: 4px; width: fit-content; margin-left: auto; margin-right: auto; } .device-btn { background: transparent; border: none; padding: 5px 8px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #495057; } .device-btn.active { background-color: #714B67; color: white; } .device-btn svg { width: 16px; height: 16px; fill: currentColor; } .app-container { width: 100%; max-width: 100%; margin: 0 auto; background: #ffffff; box-shadow: 0 2px 8px rgba(0,0,0,0.05); border: 1px solid #dee2e6; border-radius: 4px; overflow: hidden; transition: width 0.3s ease, border 0.3s ease, box-shadow 0.3s ease; } .navbar { background-color: #714B67; color: white; display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; font-size: 14px; } .navbar-left { display: flex; gap: 20px; align-items: center; } .navbar a { color: white; text-decoration: none; } .control-panel { background: #ffffff; border-bottom: 1px solid #dee2e6; padding: 12px 16px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } .control-panel-left { display: flex; gap: 10px; align-items: center; } .control-panel-right { display: flex; align-items: center; gap: 12px; font-size: 13px; color: #495057; background: #ffffff; padding: 4px 8px; border-radius: 4px; } .pagination-counter { font-weight: 500; color: #212529; min-width: 50px; text-align: right; } .pager-buttons { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .pager-btn { background: transparent; border: none; padding: 2px 6px; cursor: pointer; color: #495057; font-size: 12px; } .pager-btn:hover { background-color: #f8f9fa; } .pager-btn:not(:last-child) { border-right: 1px solid #ced4da; } .view-switchers { display: flex; border: 1px solid #ced4da; border-radius: 4px; overflow: hidden; background: #fff; } .view-btn { background: transparent; border: none; padding: 4px 6px; cursor: pointer; color: #495057; display: flex; align-items: center; justify-content: center; } .view-btn:hover { background-color: #f8f9fa; } .view-btn.active { background-color: #e9ecef; color: #000; } .view-btn:not(:last-child) { border-right: 1px solid #ced4da; } .btn-primary { background-color: #714B67; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-weight: 500; } .search-bar { border: 1px solid #ced4da; padding: 6px 12px; border-radius: 4px; width: 220px; max-width: 100%; } .content-container { padding: 16px; width: 100%; box-sizing: border-box; } .table-container { width: 100%; overflow-x: auto; } table { width: 100%; background: white; border-collapse: collapse; white-space: nowrap; } th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; font-size: 14px; } th { background-color: #fff; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .kanban-board { display: none; gap: 16px; align-items: flex-start; overflow-x: auto; padding-bottom: 10px; } .kanban-column-vertical { writing-mode: vertical-lr; transform: rotate(180deg); background: #e9ecef; border: 1px solid #ced4da; padding: 12px 8px; font-size: 13px; font-weight: 600; color: #495057; border-radius: 4px; cursor: pointer; text-align: center; height: fit-content; } .kanban-column { background: #ffffff; border: 1px solid #dee2e6; border-radius: 4px; width: 320px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); display: flex; flex-direction: column; flex-shrink: 0; } .kanban-header { padding: 10px 14px; font-size: 14px; font-weight: 600; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; background: #fdfdfe; border-top-left-radius: 4px; border-top-right-radius: 4px; } .add-btn { background: none; border: none; cursor: pointer; font-size: 16px; color: #6c757d; } .kanban-cards-list { padding: 10px; display: flex; flex-direction: column; gap: 8px; } .kanban-card { background: #ffffff; dir: ltr; border: 1px solid #ced4da; border-radius: 4px; padding: 10px 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,0.02); transition: all 0.15s ease; } .kanban-card:hover { border-color: #714B67; box-shadow: 0 2px 5px rgba(0,0,0,0.08); } .card-title { font-weight: 600; font-size: 14px; color: #212529; margin-bottom: 4px; } .card-text { font-size: 12px; color: #495057; } function triggerTryIt() { const btn = document.getElementById('tryItBtn'); const container = document.getElementById('appContainer'); if (!btn || !container) return; // Change button style to matte white with shadow and purple text btn.style.backgroundColor = '#fdfdfd'; btn.style.color = '#714B67'; btn.style.boxShadow = '0 4px 12px rgba(113, 75, 103, 0.15)'; btn.style.border = '1px solid #714B67'; // Highlight app screen border temporarily container.style.border = '2px solid #714B67'; container.style.boxShadow = '0 0 15px rgba(113, 75, 103, 0.25)'; // Revert back after 1 second setTimeout(() => { btn.style.backgroundColor = '#714B67'; btn.style.color = '#ffffff'; btn.style.boxShadow = 'none'; btn.style.border = '1px solid #714B67'; container.style.border = '1px solid #dee2e6'; container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.05)'; }, 1000); } function setDevice(type, userClicked = false) { const container = document.getElementById('appContainer'); const buttons = document.querySelectorAll('.device-btn'); buttons.forEach(btn => btn.classList.remove('active')); if (type === 'phone') { if (userClicked) container.style.width = '380px'; document.getElementById('btnPhone')?.classList.add('active'); } else if (type === 'tab') { if (userClicked) container.style.width = '640px'; document.getElementById('btnTab')?.classList.add('active'); } else { if (userClicked) container.style.width = '100%'; document.getElementById('btnLaptop')?.classList.add('active'); } } window.addEventListener('DOMContentLoaded', () => { const btn = document.getElementById('tryItBtn'); if (btn) { btn.addEventListener('click', triggerTryIt); } const container = document.getElementById('appContainer'); if (container) { setDevice('phone', false); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const currentWidth = entry.contentRect.width; if (currentWidth >= 360 && currentWidth 540 && currentWidth 720) { setDevice('laptop', false); } } }); resizeObserver.observe(container); } }); let state = { currentStart: 1, currentEnd: 7, totalItems: 7, activeView: 'list' }; function updateUI() { const pageIndicator = document.getElementById('pageIndicator'); const prevBtn = document.getElementById('prevBtn'); const nextBtn = document.getElementById('nextBtn'); if (pageIndicator) pageIndicator.textContent = `${state.currentStart}-${state.currentEnd} / ${state.totalItems}`; if (prevBtn) prevBtn.style.opacity = state.currentStart === 1 ? '0.5' : '1'; if (nextBtn) nextBtn.style.opacity = state.currentEnd >= state.totalItems ? '0.5' : '1'; } document.getElementById('nextBtn')?.addEventListener('click', () => { if (state.currentEnd { if (state.currentStart > 1) { state.currentStart -= 1; state.currentEnd -= 1; updateUI(); } }); function switchView(type) { document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active')); document.querySelector(`[data-view="${type}"]`)?.classList.add('active'); state.activeView = type; const listView = document.getElementById('listView'); const kanbanView = document.getElementById('kanbanView'); if (type === 'kanban') { if (listView) listView.style.display = 'none'; if (kanbanView) kanbanView.style.display = 'flex'; } else { if (kanbanView) kanbanView.style.display = 'none'; if (listView) listView.style.display = 'block'; } } function openRecord(name) { alert(`Opening Odoo Form View for: "${name}"`); } function createNewRecord() { const name = prompt("Enter new property name:"); if (name) { alert(`Creating new record "${name}" via Odoo model controller.`); } } function addCard(groupName) { const name = prompt(`Add property to ${groupName}:`); if (name) { const list = document.getElementById(`cards-${groupName}`); if (list) { const card = document.createElement('div'); card.className = 'kanban-card'; card.innerHTML = `
${name}
Expected Price: 0.00
`; card.onclick = () => openRecord(name); list.appendChild(card); } } } function expandColumn(groupName) { alert(`Expanding collapsed group column: ${groupName}`); } function filterContent() { const searchInput = document.getElementById('searchInput'); if (!searchInput) return; const query = searchInput.value.toLowerCase(); document.querySelectorAll('#listView tbody tr').forEach(row => { row.style.display = row.textContent.toLowerCase().includes(query) ? '' : 'none'; }); document.querySelectorAll('.kanban-card').forEach(card => { card.style.display = card.textContent.toLowerCase().includes(query) ? 'block' : 'none'; }); }

About This Guide

This guide provides a beginner-friendly, step-by-step introduction to building custom applications in Odoo from scratch. Using a hospital management system as a practical example, it walks you through structuring your app, storing custom data, creating user-friendly screens, and making updates safely—all structured so anyone can follow along and build custom features easily.

What You Will Learn

  • App Organization: How to set up clean folder structures and manage how your app files load together.

  • Data & Screens: How to create custom fields to store information, build clean lists and detailed views, and manage record statuses.

  • Security & Access: How to control who can view, edit, or create records within your app.

  • App Updates: How to properly install, update, and apply changes to your custom Odoo apps.

Prerequisites: Finding Your Odoo Server & Custom Addons Directory

Before writing code, you need to know where Odoo lives on your computer and where to place your custom code so Odoo can discover it.

Step 1: Locate the custom-addons Directory in File Explorer

If you are running Odoo locally on Windows:

  1. Open File Explorer by pressing Ctrl + E.
  2. Navigate to your installation path (typically C:\Program Files\Odoo 19.0\server\odoo\addons or custom-addons).
  3. You can paste your folder path directly into the File Explorer address bar.

Crucial Rule on Folder Naming: Pick one clean, lowercase name for your custom module folder using underscores instead of spaces or hyphens (for example: custom_hospital_management or real_estate). Throughout this guide, we will strictly use custom_hospital_management across all code files, paths, and configurations.

Step 2: Create the Module Directory Structure

Inside your custom-addons directory, create a main folder named custom_hospital_management. Inside that main folder, create three sub-folders:

  • models/ — Holds all Python files that construct database tables and business logic.
  • views/ — Holds all XML files that render screens, forms, tables, and top navigation menus.
  • security/ — Holds CSV files that grant users access permissions to your database tables.

Chapter 1: The Odoo Module Architecture

Every Odoo module is a self-contained directory containing Python code, XML layouts, and security rules.

Complete Directory Layout

When viewed inside Visual Studio Code or File Explorer, your module structure must look like this:

Plaintext

custom_hospital_management/

├── __init__.py                  <– Root Python file initializing the module package

├── __manifest__.py              <– Module configuration, metadata, and loading sequence

├── models/                      <– Python database layer

│   ├── __init__.py              <– Registers all model Python files

│   └── patient.py               <– Patient database schema & logic

├── views/                       <– XML user interface layer

│   ├── patient_views.xml        <– Form and list (tree) view definitions

│   └── menu_views.xml           <– Top menu navigation links

└── security/                    <– Access control security layer

 

    └── ir.model.access.csv      <– Security rights (Read, Write, Create, Delete)

Chapter 2: Module Registration (__manifest__.py)

The __manifest__.py file tells Odoo what your app does, what base Odoo apps it depends on, and which data files to load into PostgreSQL.

When viewed inside Visual Studio Code or File Explorer, your module structure must look like this:

Manifest Configuration Code

File location: custom_hospital_management/__manifest__.py

Python

{

    ‘name’: ‘Hospital Management’,

    ‘version’: ‘1.0.0’,

    ‘summary’: ‘Manage patient records, appointments, and medical histories’,

    ‘category’: ‘Healthcare’,

    ‘author’: ‘Your Company Name’,

    ‘license’: ‘LGPL-3’,

    

    # Dependencies: List official Odoo apps required before this module can be installed

    ‘depends’: [‘base’], 

    

    # Data Files: Must be listed in STRICT sequential dependency order

    ‘data’: [

        ‘security/ir.model.access.csv’,  # 1. Security MUST load first

        ‘views/patient_views.xml’,       # 2. Views and window actions MUST load second

        ‘views/menu_views.xml’,          # 3. Menus referencing actions MUST load last

    ],

    

    ‘installable’: True,

    ‘application’: True,  # Setting to True places this app on the main Odoo dashboard

 

}

 Core Concept: Why Manifest Order Matters

Odoo executes files listed inside ‘data’: […] strictly from top to bottom:

  1. If views/menu_views.xml is placed above views/patient_views.xml, Odoo will crash with an External ID not found error because the menu will try to link to an action that hasn’t been created yet.
  2. Security files (ir.model.access.csv) must always sit at the top so permissions are active before screens are drawn.

Chapter 3: Creating Database Tables (Models & ORM Fields)

In Odoo, you do not write raw SQL statements (like CREATE TABLE). Instead, you define Python classes inheriting from models.Model. Odoo’s Object-Relational Mapping (ORM) creates and updates the underlying PostgreSQL database automatically.

Step 1: Register the Models Directory

File location: custom_hospital_management/__init__.py

Python

from . import models

File location: custom_hospital_management/models/__init__.py

Python

from . import patient

Step 2: Define the Model & Data Fields

File location: custom_hospital_management/models/patient.py

Python

from odoo import models, fields, api

 

class HospitalPatient(models.Model):

    # Technical database table identifier created in PostgreSQL: “hospital_patient”

    _name = ‘hospital.patient’

    _description = ‘Hospital Patient Record’

 

    # Standard Database Fields

    name = fields.Char(string=’Full Name’, required=True)

    age = fields.Integer(string=’Age’)

    gender = fields.Selection([

        (‘male’, ‘Male’),

        (‘female’, ‘Female’),

        (‘other’, ‘Other’),

    ], string=’Gender’, default=’male’)

    

    note = fields.Text(string=’Medical History’)

    active = fields.Boolean(string=’Active’, default=True)

 

    # State tracking field for record lifecycles

    state = fields.Selection([

        (‘draft’, ‘Draft’),

        (‘confirmed’, ‘Confirmed’),

        (‘done’, ‘Done’),

        (‘cancel’, ‘Cancelled’),

    ], string=’Status’, default=’draft’, required=True)

 

    # Computed Field: Calculates values dynamically on the fly

    is_minor = fields.Boolean(string=’Is Minor’, compute=’_compute_is_minor’)

 

    @api.depends(‘age’)

    def _compute_is_minor(self):

        # ALWAYS loop over ‘self’ because Odoo handles records in batches (recordsets)

        for record in self:

            if record.age and record.age < 18:

                record.is_minor = True

            else:

                record.is_minor = False

 

    # Workflow Action Methods (Connected to UI Buttons)

    def action_confirm(self):

        for record in self:

            record.state = ‘confirmed’

 

    def action_done(self):

        for record in self:

            record.state = ‘done’

 

    def action_cancel(self):

        for record in self:

 

            record.state = ‘cancel’

 

 Core Concept: Key Naming Rules

  • _name = ‘hospital.patient’: The technical ID for your model. Dots are automatically converted to underscores in SQL tables (hospital_patient).
  • for record in self:: Never omit this loop inside compute methods or action buttons. Odoo passes lists of records (self); failing to loop will crash the system when performing bulk edits.

Chapter 4: Setting Up Access Rights (ir.model.access.csv)

By default, Odoo hides and blocks access to new database tables for security. You must explicitly define permission rules in your security file.

Permission Configuration Code

File location: custom_hospital_management/security/ir.model.access.csv

Code snippet

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink

access_hospital_patient_user,access.hospital.patient.user,model_hospital_patient,base.group_user,1,1,1,1

Core Concept: The Model ID Rule

Look closely at column 3 (model_id:id):

  1. Take your model _name from Python: hospital.patient.
  2. Replace all dots with underscores: hospital_patient.
  3. Add model_ in front: model_hospital_patient.

The numbers 1,1,1,1 at the end grant Read, Write, Create, and Delete permissions respectively to standard internal users (base.group_user).

Chapter 5: Designing the User Interface (Views & Menus)

Odoo renders web views using XML. The two primary layouts are:

  • Tree View (List View): Displays records in a multi-row table.
  • Form View: Displays the detailed view when opening an individual record.

Step 1: Create Views & Window Actions

File location: custom_hospital_management/views/patient_views.xml

XML

<odoo>

    <!– 1. Tree (List) View Definition –>

    <record id=”view_hospital_patient_tree” model=”ir.ui.view”>

        <field name=”name”>hospital.patient.tree</field>

        <field name=”model”>hospital.patient</field>

        <field name=”arch” type=”xml”>

            <tree>

                <field name=”name”/>

                <field name=”age”/>

                <field name=”gender”/>

                <field name=”state”/>

                <field name=”is_minor”/>

            </tree>

        </field>

    </record>

 

    <!– 2. Form View Definition –>

    <record id=”view_hospital_patient_form” model=”ir.ui.view”>

        <field name=”name”>hospital.patient.form</field>

        <field name=”model”>hospital.patient</field>

        <field name=”arch” type=”xml”>

            <form>

                <!– Workflow Header Buttons & Statusbar –>

                <header>

                    <!– Modern Odoo 17+ invisible conditions –>

                    <button name=”action_confirm” string=”Confirm” type=”object” class=”oe_highlight” invisible=”state != ‘draft'”/>

                    <button name=”action_done” string=”Mark as Done” type=”object” class=”oe_highlight” invisible=”state != ‘confirmed'”/>

                    <button name=”action_cancel” string=”Cancel” type=”object” invisible=”state in (‘done’, ‘cancel’)”/>

                    

                    <field name=”state” widget=”statusbar” statusbar_visible=”draft,confirmed,done”/>

                </header>

                <sheet>

                    <group>

                        <group>

                            <field name=”name”/>

                            <field name=”age”/>

                            <field name=”gender”/>

                        </group>

                        <group>

                            <field name=”is_minor”/>

                            <field name=”active”/>

                        </group>

                    </group>

                    <notebook>

                        <page string=”Medical History”>

                            <field name=”note”/>

                        </page>

                    </notebook>

                </sheet>

            </form>

        </field>

    </record>

 

    <!– 3. Window Action: Instructions on what views to launch –>

    <record id=”action_hospital_patient” model=”ir.actions.act_window”>

        <field name=”name”>Patients</field>

        <field name=”res_model”>hospital.patient</field>

        <field name=”view_mode”>tree,form</field>

    </record>

 

</odoo>

 

Step 2: Create Navigation Menus

File location: custom_hospital_management/views/menu_views.xml

XML

<odoo>

    <!– Top-level Navigation App Icon –>

    <menuitem id=”menu_hospital_root” name=”Hospital” sequence=”10″/>

 

    <!– Sub-menu Category Dropdown –>

    <menuitem id=”menu_hospital_patients_cat” name=”Patients” parent=”menu_hospital_root” sequence=”10″/>

 

    <!– Action Link: Launches the window action defined in patient_views.xml –>

    <menuitem id=”menu_hospital_patient_records” name=”All Patients” parent=”menu_hospital_patients_cat” action=”action_hospital_patient” sequence=”10″/>

 

</odoo>

 

Core Concept: Modern Button Conditions (invisible=)

In modern versions of Odoo (Odoo 17, 18, and 19), legacy states=”…” tags on form buttons are deprecated. Use the invisible domain condition:

  • invisible=”state != ‘draft'” hides the button whenever the state is not draft.
  • invisible=”state in (‘done’, ‘cancel’)” hides the button if the record reaches completion or cancellation.

Chapter 6: Extending Native Odoo Apps (_inherit)

Most real-world projects involve modifying standard Odoo apps (like adding custom fields to Sales Orders or Customer records). This is done using inheritance.

Step 1: Python Extension

File location: custom_hospital_management/models/sale_order_inherit.py

Python

from odoo import models, fields

 

class SaleOrder(models.Model):

    # _inherit tells Odoo to modify the existing ‘sale.order’ table instead of creating a new one

    _inherit = ‘sale.order’

 

    driver_notes = fields.Char(string=’Delivery Instructions’)

Step 2: XML UI Extension (<xpath>)

File location: custom_hospital_management/views/sale_order_views.xml

XML

<odoo>

    <record id=”view_order_form_inherit” model=”ir.ui.view”>

        <field name=”name”>sale.order.form.inherit</field>

        <field name=”model”>sale.order</field>

        <field name=”inherit_id” ref=”sale.view_order_form”/>

        <field name=”arch” type=”xml”>

            

            <!– Target the existing payment_term_id field and insert driver_notes directly below it –>

            <xpath expr=”//field[@name=’payment_term_id’]” position=”after”>

                <field name=”driver_notes”/>

            </xpath>

            

        </field>

    </record>

 

</odoo>

Chapter 7: Updating & Deploying Custom Changes

Whenever you modify .py files or .xml layout files, follow these steps to load the updates into Odoo:

  1. Restart the Server Service:
  • Windows: Press Ctrl + Shift + Esc (Task Manager) ➔ Services tab ➔ Right-click odoo-serverRestart.
  • Linux: Run sudo systemctl restart odoo in your terminal.

   2.Upgrade the Module in Odoo:

  • Activate Developer Mode in Odoo (Settings ➔ scroll down and click Activate the developer mode).
  • Go to the Apps menu ➔ click Update Apps List in the top navigation bar.
  • Search for Hospital Management (or custom_hospital_management) and click Activate (or Upgrade).

Chapter 8: Fast CLI Terminal Commands

Instead of using the Odoo web UI to upgrade your module every time you edit code, run this terminal command to force Odoo to auto-upgrade your app on launch:

Terminal Command Syntax

  • Windows Command Prompt:
  • DOS

python odoo-bin -c odoo.conf -d your_database_name -u custom_hospital_management –dev=xml

  •  
  •  
  • Linux / Mac Terminal:
  • Bash

./odoo-bin -c /etc/odoo/odoo.conf -d your_database_name -u custom_hospital_management –dev=xml

  •  
  •  

Flag Reference

  • -d <db_name>: Specifies your active PostgreSQL database name.
  • -u <module_name>: (Update Flag) Forces an immediate upgrade of your custom module on boot.
  • –dev=xml: Allows live XML reloads—saving an XML file instantly updates the page on browser refresh without restarting the server.

Chapter 9: Common Errors & Troubleshooting

 

Error Message / Issue

Primary Cause

How to Fix

ParseError: External ID not found

XML/CSV files are listed in the wrong order inside __manifest__.py.

Reorder ‘data’: […] in __manifest__.py. Security files must be listed before views, and actions before menus.

AccessError: You are not allowed to modify…

Missing entry inside ir.model.access.csv or incorrect model ID syntax.

Verify ir.model.access.csv. Ensure column 3 follows the pattern model_your_model_name.

AttributeError: ‘hospital.patient’ object has no attribute ‘x’

Python code was edited, but the Odoo server service hasn’t reloaded yet.

Restart the Odoo service and upgrade your module via the Apps menu or terminal.

XML Validation Error (states=)

Using legacy Odoo 16 or earlier syntax for buttons on newer Odoo versions.

Replace states=”draft” with invisible=”state != ‘draft'” in your button XML.

Chapter 10: One-Page Developer Cheat Sheet

Directory Reference

Plaintext

custom_hospital_management/

├── __init__.py                  <– Root import: from . import models

├── __manifest__.py              <– Load order & dependencies

├── models/

│   ├── __init__.py              <– Model import: from . import patient

│   └── patient.py               <– Schema & Python logic

├── views/

│   ├── patient_views.xml        <– Form & Tree XML layouts

│   └── menu_views.xml           <– Navigation menu XML

└── security/

 

    └── ir.model.access.csv      <– CRUD Permissions

Essential ORM Fields

 

Field Type

Python Example

Usage

Char

name = fields.Char(string=’Name’, required=True)

Single line text

Text

notes = fields.Text(string=’Notes’)

Multi-line text box

Integer

age = fields.Integer(string=’Age’)

Whole numbers

Selection

gender = fields.Selection([(‘m’, ‘Male’), (‘f’, ‘Female’)])

Dropdown list

Boolean

active = fields.Boolean(string=’Active’, default=True)

Checkbox

Many2one

partner_id = fields.Many2one(‘res.partner’, string=’Customer’)

Foreign key link

Core Developer Rules

  1. Manifest File Sequence: Security CSV first, Views second, Menus last.
  2. Security ID Mapping: Model hospital.patient ➔ CSV ID model_hospital_patient.
  3. Recordset Loops: Always wrap computed logic in for record in self:.

Software Audit Simplified: How We Do It at Techbot

Software Audit Simplified: How We Do It at Techbot

Avatar photo

Moossa M. Alavi

Founder & CEO of Techbot ERP

Moossa M. Alavi is the Founder & CEO of Techbot ERP and Altamyz Advertising. He is a certified Odoo consultant with more than 27 years of experience in business, advertising, and ERP software. Moossa started his career in the UAE in 1997 with a well-known group in Abu Dhabi. Over the years, he built his own companies to help other businesses work better using technology. Moossa helps with customized ERP implementation for various industries, including manufacturing, insurance, supercar rental, and logistics, through Techbot ERP. He resolves these issues with Odoo ERP and supports businesses in growing with the right assets and guidance. Moossa has received many awards for his work, including the Arabian Best of Best Award and the Industry Leader Award from BNI UAE. He is also a BNI Ambassador and mentors other business owners. He believes in giving back to the community and helping others grow, following the “Givers Gain” principle.

Software Audit That Actually Improves Your Day to Day Work

Most teams already have the tools they need. Yet they struggle because the software they already have is not working the way it should.

You might be dealing with duplicate data entry, slow workflows, tools that do not talk to each other, or features you know exist but no one really uses. Over time, this creates friction that quietly drains productivity.

A good software audit fixes that. Not by adding more tools, but by making your current systems finally work the way your business needs them to.

Where Businesses Usually Lose Efficiency

Before we even look at tools, we look at how work is getting done. That is where the real problems show up.

Common issues we see:

  • Teams switching between multiple platforms to complete a single task
  • Manual work that could be automated but is not
  • Reports that take hours to prepare instead of being instant
  • Different departments using disconnected systems
  • Paying for software features that are never used

These are not technical problems. They are operational inefficiencies caused by misaligned systems.

How We Approach Software Audits at Techbot

Our process is built around improving how your team actually works, not just evaluating software on paper.

1. We Map How Work Really Happens

We start by understanding how your team actually works.

This includes mapping your processes, identifying dependencies, and uncovering how tasks move across departments. Only after this do we evaluate the role your current systems play.

This approach ensures that any recommendations are built around your business, not forced onto it.

2. We Find Quick Wins First

Not every improvement needs a major overhaul.

Sometimes small changes like enabling a feature, adjusting permissions, or restructuring a workflow can immediately save hours of work each week.

We prioritize these quick wins so you see results early.

3. We Eliminate Redundancy

Many businesses use multiple tools for things that could be handled in one system.

We identify overlaps and opportunities to consolidate, which reduces cost and simplifies operations.

4. We Improve Data Flow

Data should move seamlessly across your business.

We fix broken integrations, remove duplicate data entry, and ensure that information flows smoothly between departments.

This is where businesses start seeing real clarity in their operations.

5. We Align Software With Business Goals

Your software should support how your business wants to grow.

We evaluate whether your current setup can scale, and if not, what needs to change to support future expansion.

6. We Do Not Stop at Recommendations

A report alone does not create impact.

We help implement the improvements, train your team, and ensure the changes actually stick.

What You Get After a Proper Audit

When done right, a software audit leads to tangible improvements:

  • Faster workflows
  • Less manual work
  • Better reporting and visibility
  • Lower software costs
  • More confident decision making

Most importantly, your team spends less time fighting systems and more time doing meaningful work.

Transform the way you manage projects, sales, and finance with Odoo ERP designed for UAE real estate management.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.

UAE E‑Invoicing Phases and Deadlines: What Businesses Need to Know | A Complete Guide

UAE E‑Invoicing Phases and Deadlines: What Businesses Need to Know | A Complete Guide

What Is UAE E‑Invoicing and Why Is It Being Introduced?

E-invoicing refers to the electronic creation, exchange, validation, and storage of invoices in a structured format (not PDF or paper). The UAE’s implementation is aligned with global digital tax strategies promoting:

  • Real-time VAT validation
  • Increased transparency and anti-fraud measures
  • Automation of compliance and reporting
  • Alignment with systems like KSA’s ZATCA and EU’s Peppol

The UAE’s chosen structure is the PINT AE (Peppol International UAE) standard, an XML-based format tailored for local regulations.

UAE E‑Invoicing Rollout Timeline

The Ministry of Finance has announced a phased rollout to ensure smooth nationwide adoption:

Phase 1: Pilot (Voluntary Participation)

  • Starts 1 July 2026
  • Open to early adopters to test systems and integrations

Phase 2: Mandatory for Large Taxpayers

  • Applies to businesses with annual revenue ≥ AED 50 million
  • Must appoint an ASP by 31 July 2026
  • Mandatory go-live: 1 January 2027

Phase 3: Mandatory for All Other VAT-Registered Businesses

  • Revenue < AED 50 million
  • Must appoint ASP by 31 March 2027
  • Go-live: 1 July 2027

Phase 4: B2G Transactions (Government Entities)

  • ASP appointment by 31 March 2027
  • Mandatory go-live: 1 October 2027

What are Accredited Service Providers (ASPs), aka The ‘Call Connectors’ of E‑Invoicing

To simplify this: think of an Accredited Service Provider (ASP) like your telecom provider for example, Etisalat or du.

When you make a phone call, your phone doesn’t connect directly to the recipient. Instead, the telecom provider validates, routes, and secures the connection.

E-invoicing follows the same principle:

  • Your ERP system (like Odoo) creates the invoice.
  • The ASP validates and transmits it to the UAE’s Federal Tax Authority (FTA).
  • The FTA logs and accepts the transaction.
  • The ASP’s job includes:
    • Ensuring the invoice meets PINT AE format.
    • Digitally signing and timestamping the invoice.
    • Transmitting it in real time to the FTA.
    • Archiving it according to UAE regulations.

Important Clarification: ERP ≠ ASP

A common misconception is that your ERP or invoicing system handles everything.

But even if you use a powerful system like Odoo, it’s not an ASP and cannot legally submit invoices to the FTA on its own.

Odoo = Invoice Creator
ASP = Invoice Validator + Transmitter
FTA = Official Receiver & Auditor

At Techbot, we help you integrate Odoo with FTA-approved ASPs, ensuring end-to-end e-invoicing compliance.

Diagram showing UAE e-invoicing flow where ERP systems like Odoo create invoices, ASPs validate and transmit them, and the UAE Federal Tax Authority receives and audits e-invoices

The PINT AE Format

PINT AE (Peppol International for the UAE) is the mandated e-invoice format machine-readable, highly structured, and legally binding.

It includes data like:

  • Unique invoice number and timestamp
  • Seller and buyer details (TRN, VAT, etc.)
  • VAT breakdown
  • Total values in AED
  • Digital signature and ASP metadata

Paper and PDF invoices will no longer be acceptable once the mandate takes effect for your business segment.

Preparing Your Business for Compliance

Here’s how to stay ahead of the mandate:

  1. Review Your ERP System Ensure it can generate invoices in the PINT AE format and supports API integration with ASPs.
  2. Select a Certified ASP Choose an FTA-approved ASP early to allow testing, integration, and onboarding support.
  3. Integrate ERP + ASP Our team at Techbot ensures your Odoo ERP is integrated with your chosen ASP for real-time, compliant e-invoice flow.
  4. Train Finance & IT Teams Ensure everyone understands the new invoice flow, error handling, and audit trail requirements.

What Happens If You Don’t Comply?

Starting 2027, non-compliant businesses risk:

  • FTA penalties for delayed onboarding
  • Invoice rejections impacting payment cycles
  • Loss of VAT input claims
  • Exclusion from public tenders (B2G)
  • Operational delays and audits

Benefits Beyond Compliance

While compliance is mandatory, early adoption delivers real business value:
  • Faster invoice processing and payment
  • Reduced manual reconciliation
  • Real-time financial insights
  • Stronger internal controls
  • Readiness for future digital mandates

How Techbot + Odoo Empower You to Comply

As a certified Odoo Gold Partner, Techbot provides:

  • Customized Odoo configurations for PINT AE invoice generation.
  • Full ERP + ASP integration workflows.
  • Real-time validation & error-handling tools.
  • Training and onboarding support for your internal teams.
  • Continuous updates as UAE regulations evolve.

Conclusion: Take Action Before the Mandate Hits

E-invoicing in the UAE isn’t a future problem it’s a present-day priority.

By preparing early, integrating with ASPs, and optimizing your ERP, you’ll not only avoid penalties you’ll drive efficiency, speed, and digital maturity across your business.

At Techbot, we’re here to help you achieve 100% compliance and unlock the value beyond it.

FAQs

Is Odoo an ASP?

No. Odoo is your ERP system. You need to connect it to an FTA-accredited ASP to submit invoices.

Validate, digitally sign, and submit your invoices to the FTA in real time.

No. Only PINT AE-formatted XML invoices submitted via ASP will be accepted once the mandate starts.

  • 1 January 2027 for large businesses (annual revenue ≥ AED 50 million)
  • 1 July 2027 for others
  • 1 October 2027 for B2G transactions (businesses dealing with government entities)

We integrate your Odoo ERP with certified ASPs, configure PINT AE invoicing, train your teams, and ensure full compliance.

The FTA mandates UAE electronic invoicing to improve transparency, reduce tax errors, and ensure accurate VAT reporting across all registered businesses.

The Federal Tax Authority (FTA) defines all UAE e-invoicing requirements, including invoice format, submission process, and compliance timelines.

With UAE electronic invoicing, all invoices are digitally tracked by the FTA, making VAT reporting and compliance more accurate and easier to audit.

No. All UAE e-invoicing requirements mandate that invoices be submitted via an FTA-accredited ASP in the correct PINT AE XML format.

As defined by the Federal Tax Authority for UAE electronic invoicing and VAT compliance.

Transform the way you manage projects, sales, and finance with Odoo ERP designed for UAE real estate management.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.

Odoo Pricing Guide for the UAE 2026 | Key Factors to Consider Before Your Implementation

Odoo Pricing Guide for the UAE 2026 | Key Factors to Consider Before Your Implementation

Avatar photo

Moossa M. Alavi

Moossa M. Alavi is the Founder & CEO of Techbot ERP and Altamyz Advertising. He is a certified Odoo consultant with more than 27 years of experience in business, advertising, and ERP software. Moossa started his career in the UAE in 1997 with a well-known group in Abu Dhabi. Over the years, he built his own companies to help other businesses work better using technology. Moossa helps with customized ERP implementation for various industries, including manufacturing, insurance, supercar rental, and logistics, through Techbot ERP. He resolves these issues with Odoo ERP and supports businesses in growing with the right assets and guidance. Moossa has received many awards for his work, including the Arabian Best of Best Award and the Industry Leader Award from BNI UAE. He is also a BNI Ambassador and mentors other business owners. He believes in giving back to the community and helping others grow, following the “Givers Gain” principle.

Disclaimer:
This blog does not provide exact figures. Instead, it outlines the key factors you should consider when evaluating your Odoo implementation budget.
Read the TL;DR version (quick summary)

Understanding What Influences Your Odoo Pricing

Before diving into the details of Odoo pricing, it’s important to understand that no two businesses will have the exact same cost. Odoo’s pricing structure depends on several variables, and because every business has different workflows, team sizes, and module needs, giving a single fixed estimate is impossible.

If you’re trying to estimate your Odoo cost in the UAE, this guide gives a clear breakdown of every factor that affects the final price. These essentials we detail determine the baseline of your Odoo cost and are the first things you should consider when estimating your budget.

Below, we break down these key factors to help you understand what impacts your Odoo pricing the most.

Number of Users

Pricing is per user/month.

More users = higher subscription cost.

A user is any person who needs backend access to Odoo to do things like:

  • Create or edit records
  • Approve tasks
  • Manage inventory
  • Send invoices
  • Update CRM deals
  • Run reports
  • Access settings

Who does NOT count as a user?

Employees who only use a portal.

‍Portal access is free. Portal users can log in to a simple interface that lets them view their documents, track orders, check tickets, and approve or comment when allowed.

Typical real-world example

A company with 100 employees may only need 7–15 actual Odoo users, such as:

  • 3 salespeople
  • 2 accountants
  • 2 operations staff
  • 1 HR admin
  • 1 manager

The rest of the company never logs into the backend, so they don’t need user licenses.

Implementation

Even though Odoo provides the software, most businesses rely on
certified Odoo partners to set up the system correctly,
customize workflows, and ensure a smooth transition.

Here’s how we handle Odoo Implementation!

These four subcategories break down the implementation in terms of cost :-

1. Modules

When you buy the standard Odoo plan, you get all 12 core modules (listed below). You only pay for additional ones.

However, modules can affect your total cost indirectly:

  1. Implementation effort – Using many modules may require more setup, configuration, or partner support.
  2. More users – If more staff need to access these modules, your per-user subscription cost goes up.
  3. Custom or third-party modules – Extra apps or custom features may have separate fees.

In short: Standard modules are included, but more users, setup, and custom apps can increase your total cost.

Modules included in the standard plan:

List of Odoo standard modules included in the plan, such as CRM, Sales, Accounting, Inventory, HR, Manufacturing, Website, and Helpdesk
List of Odoo standard modules included in the plan, such as CRM, Sales, Accounting, Inventory, HR, Manufacturing, Website, and Helpdesk

2. Customization Hours

The total time required to tailor Odoo to your specific workflows, add custom features, or modify existing modules to match your business needs.

3. On-site Man-days

The number of days our consultants spend at your location to configure the system, train staff, and ensure everything runs smoothly.

4. Number of companies

The total instances or business entities you plan to manage in Odoo, as each company may require separate configurations and data setup.

For a full overview of our Odoo services, visit our homepage!

Support / AMC

An AMC (Annual Maintenance Contract) is basically a yearly service agreement that ensures your Odoo system keeps working smoothly after the initial setup. In our case,

  • We provide an initial support of 3 months for free.
  • After that, clients can opt for an AMC, which is typically
    25% of the implementation fees per year and provides ongoing, annual support.

For more details on our approach to support and AMC

Odoo Edition / Hosting Type

This refers to how and where your Odoo system is hosted, which affects both cost and setup.

You can choose:

1. Odoo Online (Cloud / SaaS) (FREE)

  • Fully hosted by Odoo, with automatic updates, backups, and maintenance.
  • Odoo Online is easy to use, but it doesn’t allow customizations.

2. Odoo.sh (Managed Cloud Platform)

  • This is also hosted by Odoo, but unlike Odoo Online, it gives you full freedom to customize, install your own modules, and develop anything you need. While Odoo still handles hosting, backups, and server management for you.

3. Self-Hosted

  • You manage everything yourself: server setup, updates, security, backups, and performance.
  • It’s the best choice for businesses that need deep customization, full data control, or specific infrastructure requirements.

4. Partner Managed Hosting

  • Your partner provides the server, handles setup, takes care of maintenance, monitors performance, applies updates, and manages backups. Giving you a fully managed, worry-free environment tailored to your needs.

Partner Support

What Partners Do That Odoo Doesn’t

We handle tasks that Odoo alone does not:

  • Custom configuration of modules and workflows to fit your unique business processes
  • Data migration from legacy systems or spreadsheets
  • Integration with other software your company uses
  • Training employees so they can use the system efficiently
  • Ongoing support and troubleshooting for issues specific to your setup
  • Ensuring best practices that reduce errors and maximize ROI

In short: partners bridge the gap between the software and your actual business operations, making sure Odoo works for your specific needs.

Read more about our Odoo support approach!

Support levels

  • Basic setup – A simple installation and configuration for small businesses, generally the lowest partner cost.
  • Full implementation – Complete setup, data migration, training, and ongoing support for medium to large companies, which commands a higher price.

For precise, personalized pricing, reach out to us and we’ll guide you!

Transform the way you manage projects, sales, and finance with Odoo ERP designed for UAE real estate management.

TL;DR: Odoo Pricing Guide for the UAE

  • Costs vary by business:
    No two companies have the same price; depends on users, modules, customization, and implementation needs.
  • Users matter:
    Only backend users count for subscription; portal users are free.
  • Implementation:
    Partners help with module setup, customization, on-site support, and managing multiple companies. Learn more
  • Support / AMC:
    Initial 3 months free, then optional AMC (≈25% of implementation fees per year) for ongoing support. Details here
  • Hosting options:
    Odoo Online (cloud), Odoo.sh (managed cloud), Self-Hosted, or Partner Managed Hosting.
    Each has different flexibility and cost.
  • Partner support adds value: Custom workflows, data migration, integrations, employee training, ongoing troubleshooting, and best practices.
  • Support levels:
    Basic setup (small businesses) vs Full implementation (medium–large businesses).

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.

Why Clean Data Drives The Highest ROI In Real Estate ERP Projects

Why Clean Data Drives The Highest ROI In Real Estate ERP Projects

Avatar photo

Moossa M. Alavi

Moossa M. Alavi is the Founder & CEO of Techbot ERP and Altamyz Advertising. He is a certified Odoo consultant with more than 27 years of experience in business, advertising, and ERP software. Moossa started his career in the UAE in 1997 with a well-known group in Abu Dhabi. Over the years, he built his own companies to help other businesses work better using technology. Moossa helps with customized ERP implementation for various industries, including manufacturing, insurance, supercar rental, and logistics, through Techbot ERP. He resolves these issues with Odoo ERP and supports businesses in growing with the right assets and guidance. Moossa has received many awards for his work, including the Arabian Best of Best Award and the Industry Leader Award from BNI UAE. He is also a BNI Ambassador and mentors other business owners. He believes in giving back to the community and helping others grow, following the “Givers Gain” principle.

The UAE real estate market is moving faster than ever. Developers are launching new communities, off plan units are selling rapidly, and customers expect clear information at every stage. Every sale, reservation, payment plan, handover, and compliance update depends on one foundation. Clean, accurate, and consistently maintained data. Yet many real estate teams in the UAE discover the same issue when they begin planning for ERP. Their internal data is scattered across spreadsheets, WhatsApp threads, PDFs, old CRMs, and disconnected legacy systems.

Before choosing any ERP system, whether Odoo, Oracle, SAP, or a real estate-specific platform, the first question leaders should ask is simple. Is our data ready for ERP? This is exactly where Techbot, your ERP growth partner in the UAE, brings clarity. Techbot has supported many developers and brokers across the region and has seen a clear pattern. When data is clean and consistent, ERP delivers measurable ROI from the first week. When data is messy or incomplete, teams spend months fixing issues before they can focus on growth.

This blog explains why data quality matters more than technology, why messy data slows down ERP projects, and the steps UAE real estate companies should take to prepare their data for a successful digital transformation journey.

Why Data Quality Matters in Real Estate Before an ERP Implementation

Real estate operations involve thousands of moving parts. Unit availability, pricing, customer records, commission structures, payment milestones, service charges, RERA documents, and financial entries all depend on accurate information. When data is clean, ERP becomes a powerful engine for performance and decision-making. When it is not, even the most advanced ERP fails to deliver reliable outcomes.

Clean data ensures that what teams see on the system reflects what is happening on the ground. It eliminates confusion between departments and removes the guesswork that slows down sales and operations. It also builds trust within the team because they know the ERP is showing the correct information. This sets the stage for faster adoption and smoother teamwork.

When the data foundation is strong, every ERP feature works smoothly. When the foundation is weak, the technology only exposes deeper problems.

Preparing your data before ERP ensures:

1. Faster implementation timelines
Clean data reduces rework and allows teams to complete validation cycles without delays.

2. Better user adoption
Teams trust the system when the information inside is accurate from day one.

3. Clearer financial reporting
Clean financial, customer, and unit data improves reconciliation, forecasting, and compliance reporting.

4. Accurate customer experiences
No more selling an unavailable unit, issuing incorrect invoices, or duplicating client records.

This reflects the primary keyword: why data quality matters in real estate before ERP. The real power of ERP comes only when the foundation is strong.

Transform the way you manage projects, sales, and finance with Odoo ERP designed for UAE real estate management.

The Hidden Cost of Messy Data in UAE Real Estate Companies

Most real estate companies in the UAE do not struggle because their ERP software is difficult. They struggle because their internal data is not ready to support the system. In many implementations, Techbot has observed that more than seventy percent of delays are caused by unstructured or inaccurate data. When teams rely on multiple versions of the same information, ERP cannot unify the business.

Messy data does more damage than most leaders realize. It slows down project timelines, creates accountability gaps, and leads to customer escalations. Incorrect unit availability can ruin sales deals. Duplicate customer entries can cause compliance issues. Missing payment schedules can trigger invoicing errors. All these problems increase the operational load and reduce the effectiveness of the ERP.

The UAE market moves quickly, and customers expect clarity. Poor data quality increases risk and makes it harder for leaders to rely on their reports during important decisions.

Here are the most common real estate data problems UAE businesses face:

1. Duplicate customer profiles

Leads captured through portals, WhatsApp, walk-ins, and brokers create multiple entries for the same person.

2. Inconsistent unit inventory data

Different departments have different versions of available units, sold units, blocked units, and reserved units.

3. Missing payment plans and handover schedules

Without updated payment structures, ERPs cannot generate accurate invoices or reminders.

4. Unorganized contract documents

Unsigned, outdated, or misplaced contracts distort legal and compliance workflows.

5. Mismatched financial entries

Finance teams often deal with inconsistent chart-of-accounts mapping across legacy systems.

This is where the keyword how bad data slows real estate teams becomes a real operational pain point. A small mistake in unit details or customer information can disrupt the entire property sale cycle.

Clean and Accurate Property Data Delivers Real ROI from Day One

Many companies assume ERP ROI appears after the system goes live. In reality, the strongest ROI begins long before go live when property, customer, and financial data becomes clean and organized. When data is accurate, everything from unit booking to invoicing to payment follow up becomes smoother.

Clean data gives teams confidence to shift from manual work to structured ERP workflows. It reduces repeated tasks and prevents errors that waste time. It also strengthens customer trust because documents, pricing, and invoices match what was promised. Finance teams benefit as well since accurate master data supports better forecasting and cash flow planning.

The true power of ERP becomes visible only when the information entering the system is reliable. Clean data multiplies efficiency and reduces friction across the organization.

Most companies believe ERP delivers ROI after implementation. However, real ROI begins before implementation, when property, financial, and customer data become consistent.

Better sales performance

Teams work with accurate unit and pricing information, reducing manual corrections and customer escalations.

Improved customer trust

Accurate invoices, payment plans, and receipts help maintain long-term relationships.

Reliable financial forecasting

Finance teams track cash flow accurately when legacy balances and outstanding amounts are aligned.

Stronger compliance reporting

RERA submissions, tax reports, and audit files require accurate property and customer records.

Clean data not only improves ERP readiness. It improves business performance even before the ERP goes live.

Data Readiness Before ERP Implementation: What Real Estate Should Prepare

Data readiness is the preparation stage that decides how successful the ERP project will be. Many businesses jump directly into technology implementation and ignore this step, which later leads to rework and delays. Preparing data early ensures that everyone uses the same information and that the ERP becomes a single source of truth across departments.

A structured readiness process helps teams understand what to clean, what to merge, and what to validate. It also brings clarity on data owners, responsibilities, and documentation. When every department prepares its data in a unified structure, the ERP flows smoothly without conflict between teams.

This preparation stage also improves decision-making because leaders can rely on accurate information even before the ERP launches.

No ERP project should begin without a clear data readiness before ERP implementation framework. Here is a checklist to evaluate before onboarding any ERP partner.

1. Clean your customer database

Merge duplicates, validate contact details, and structure leads based on source.

2. Standardize unit master data

Ensure unit numbers, categories, pricing, and availability are consistent across all documents.

3. Validate financial records

Reconcile balances, standardize GL codes, and clean vendor and customer ledgers.

4. Audit contract documents

Confirm that all Mutqan, SPA, tenancy, and leasing documents are up to date and correctly linked.

5. Structure your payment schedules

Ensure payment milestones match approved project timelines and sales agreements.

6. Organize attachments and files

Sort PDFs, images, and documents into proper folders with naming conventions.

Following these steps reduces migration risk and helps both business and ERP consultants work with confidence.

Real Estate Master Data Cleanup: The Step Most Companies Skip

Master data cleanup is often the most neglected stage of digital transformation in real estate. Developers focus on features and workflows but ignore inconsistent property lists, outdated customer records, scattered vendor information, and non-standard financial entries. When this data is loaded into the ERP, it creates confusion rather than clarity.

Master data cleanup ensures that unit lists, project details, customer information, payment plan structures, and contract templates follow a clean, unified format. This reduces problems during migration and ensures that the ERP behaves exactly the way the business expects. Cleanup also strengthens compliance and audit preparation since regulators expect accuracy in every document.

Many real estate developers and brokers in the UAE jump directly into ERP implementation without preparing real estate master data cleanup. This leads to major roadblocks later, such as:

  • Delayed go-live

  • Incorrect bookings and invoices

  • Mismatched customer balances

  • Confusion between sales and finance teams

  • Post-implementation firefighting

Master data cleanup includes standardizing:

  • Property and unit lists

  • Customer master

  • Vendor lists

  • Payment plans

  • Lease templates

  • Commission rules

When master data is clean, the ERP behaves exactly the way it should. When it is not, ERP becomes an expensive database instead of a business engine.

How Techbot Helps Real Estate Companies Build ERP-Ready Data

Techbot brings expertise in UAE property regulations, RERA guidelines, VAT rules, and real estate operational models. This allows Techbot to guide companies with a structured data readiness framework that works for developers, brokers, and property management businesses.

Techbot focuses on understanding the business model, identifying data inconsistencies, and helping teams move toward a unified structure. This approach helps companies avoid confusion during migration and ensures the ERP reflects how the business actually operates. It also helps departments work with confidence from day one.

With Techbot’s support, businesses build a strong data foundation that supports long-term growth and reduces the need for frequent corrections after go-live.

Data audit and assessment

Understanding gaps across sales, finance, CRM, and operations.

Master data standardization

Creating a unified format for units, properties, and customer records.

Pre-migration cleansing

Removing duplicates, correcting errors, and validating information.

Compliance alignment

Ensuring RERA, VAT, and audit documentation match your operational structure.

Migration-ready templates

Providing clean, ERP-compatible templates for all departments.

This ensures your ERP launch is smooth, predictable, and capable of producing real ROI without delays.

Clean Data Is the Real Foundation of Every Successful ERP Journey

Before any ERP system can transform a real estate business, the data behind it must be prepared to support that transformation. Companies that invest early in data quality experience predictable implementation timelines, stronger user adoption, and smoother operations. Those who skip this step face delays, misalignment, and unnecessary rework.

Techbot helps UAE developers, brokers, and property managers build this foundation with confidence. Through structured audits, cleanup frameworks, and market specific experience, Techbot ensures that your ERP begins with clarity and accuracy. Clean data strengthens your operations and improves every customer interaction. When your data is ready, your ERP becomes a reliable engine that supports long term real estate growth.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.

ERP Trends To Watch Before Stepping Into 2026: A Comprehensive Outlook

ERP Trends to Watch Before Stepping into 2026: A Comprehensive Outlook

Avatar photo

Moossa M. Alavi

Moossa M. Alavi is the Founder & CEO of Techbot ERP and Altamyz Advertising. He is a certified Odoo consultant with more than 27 years of experience in business, advertising, and ERP software. Moossa started his career in the UAE in 1997 with a well-known group in Abu Dhabi. Over the years, he built his own companies to help other businesses work better using technology. Moossa helps with customized ERP implementation for various industries, including manufacturing, insurance, supercar rental, and logistics, through Techbot ERP. He resolves these issues with Odoo ERP and supports businesses in growing with the right assets and guidance. Moossa has received many awards for his work, including the Arabian Best of Best Award and the Industry Leader Award from BNI UAE. He is also a BNI Ambassador and mentors other business owners. He believes in giving back to the community and helping others grow, following the “Givers Gain” principle.

The ERP market is entering a decisive phase. Decision makers expect systems that do more than record transactions. They want platforms that understand context, surface patterns, and help teams act with confidence. According to a Statista report, the global ERP market is projected to reach nearly 81 billion USD by 2026, driven by cloud ERP adoption, AI integration, and next-generation analytics.

At Techbot ERP, your ERP growth partner, we help businesses understand not only where the ERP market is heading, but how to make these trends work for them. As we move toward 2026, ERP is transforming into a decision-intelligent platform that enhances visibility, connects operations, and empowers leadership to act in real time.

ERP is no longer a back-office tool. It is becoming the intelligent command center of every growing organization. Companies that prepare now for this shift will not only modernize workflows but also gain a strategic edge in digital transformation.

Why 2026 Is a Turning Point for ERP Systems

Enterprise systems are evolving from data entry to decision-intelligent ecosystems. A recent Gartner analysis suggests that by 2026, over 70% of new ERP deployments will use AI-enhanced modules for planning, forecasting, and compliance.

This shift is redefining ERP future trends from transactional processing to predictive, connected decision-making. Companies adopting this mindset will lead digital transformation, while those staying on legacy systems risk falling behind faster, data-driven competitors.

Businesses are realizing that ERP is not just an IT investment but a growth enabler. The systems of 2026 will shape business agility, empower leaders with insights, and simplify decision-making across every layer of the organization.

Top ERP trends shaping the future of business operations including AI powered automation, real time analytics, industry specific ERP solutions, cloud migration and integrated digital workflows before entering 2026

AI Is No Longer Optional in ERP Platforms

Artificial intelligence is now the foundation of modern ERP. According to IBM, AI in ERP 2026 will automate up to 60% of manual decision processes across finance, procurement, and customer management.

AI-enabled ERP software improves accuracy and speeds up operations. Predictive analytics optimize cash flow and supply chain decisions, while AI assistants handle repetitive tasks such as invoice matching or lead qualification. Real-time ERP analytics convert raw data into actionable insights, improving both speed and quality of decision-making.

The coming years will see AI become the connective tissue of enterprise systems. It will guide employees through intelligent prompts, generate recommendations, and ensure that every workflow is optimized for speed, accuracy, and cost efficiency.

The Shift Toward Cloud-First and Hybrid ERP Architectures

Cloud ERP adoption is growing rapidly. A DocuClipper ERP report shows that by 2026, 70% of new ERP implementations will be cloud-based, providing scalability, flexibility, and lower maintenance costs.

At the same time, industries with data-sensitive operations are exploring hybrid ERP models, combining the security of on-prem systems with the agility of cloud analytics. This blended model supports multi-site coordination and ensures that critical data remains under organizational control while still benefiting from next-gen ERP systems.

Cloud ERP will become the backbone of continuous innovation. Businesses that migrate early will enjoy faster updates, better data integration, and reduced infrastructure costs, setting the foundation for long-term scalability.

Real-Time Analytics and Business Intelligence Take Center Stage

In the past, ERP systems focused on historical data. The next wave emphasizes real-time ERP analytics, offering leaders live visibility into performance metrics and financial health.

According to Apps Run the World, companies using real-time dashboards report 25% faster decision cycles and 30% improvement in operational efficiency. Integrated KPI tracking, anomaly alerts, and predictive reporting will become default features in 2026.

Real-time ERP systems bring accuracy and agility together. Leaders can make faster decisions with confidence, while teams access instant insights across every department from procurement to sales to finance, ensuring a single source of truth.

ERP Meets IoT — The Smart Manufacturing Revolution

The convergence of ERP and IoT is revolutionizing industrial operations. According to BJIT Group Research, more than 60% of manufacturers already use IoT-linked ERP for equipment monitoring and predictive maintenance.

IoT sensors feed live data into ERP, allowing real-time responses to downtime, stock levels, and energy usage. This real-time ERP insight helps optimize production schedules and improve supply chain coordination, leading to higher output and lower costs.

With IoT connectivity, organizations move from reactive to proactive decision-making. Predictive maintenance, connected logistics, and machine learning analytics are redefining how manufacturers measure efficiency and profitability.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Sustainability and Green ERP Systems

ESG integration has become a strategic requirement, not just a reporting need. Modern ERP systems now include carbon tracking, waste optimization, and automated sustainability dashboards.

An SAP report highlights that organizations using ESG-enabled ERP platforms have reduced compliance preparation time by 40%. These systems consolidate sustainability metrics alongside financial performance, aligning growth with responsibility.

Sustainability in ERP is now tied to corporate performance. Real-time visibility into carbon data allows companies to meet environmental targets while strengthening brand trust and investor confidence.

Hyperautomation and Intelligent RPA

The era of repetitive manual processing is ending. ERP vendors are embedding AI-driven hyperautomation to accelerate operations across procurement, finance, and HR.

A Forrester study reveals that 70% of enterprises will use RPA integrated with AI by 2026. Hyperautomation transforms ERP from workflow support into an intelligent orchestrator, identifying inefficiencies, automating approvals, and managing exceptions autonomously.

With hyperautomation, enterprises save time and ensure data consistency. The combined power of RPA, AI, and process mining enables end-to-end optimization that reshapes how business units collaborate and deliver value.

Industry-Specific ERP Platforms

Generic ERP is being replaced by industry-customized solutions with pre-configured modules and compliance support.
Sectors like healthcare, manufacturing, retail, and real estate increasingly rely on vertical ERP solutions to manage complex regulations and local market nuances.

Reports from NetSuite and Alphabold show that vertical ERP adoption cuts implementation time by up to 35% and reduces the need for costly customization.

Industry-specific ERP platforms allow businesses to start faster with built-in logic. These systems speak the language of the sector, minimizing configuration time while ensuring compliance and accuracy from day one.

Enhanced User Experience and Mobile ERP

User experience defines ERP success. Low-code and mobile-first interfaces make ERP accessible to non-technical users, improving adoption rates across departments.
By 2026, nearly 75% of enterprise apps will be built using low-code or no-code tools, according to Kissflow Research.

AI-driven personalization, natural language search, and voice-enabled commands are becoming standard across next-gen ERP systems, enabling faster responses and reducing training time.

Modern ERP is becoming as intuitive as consumer applications. Better UX ensures employees spend less time navigating software and more time making decisions that drive revenue and efficiency.

ERP Security and Zero-Trust Architecture

Security remains a top concern. A PwC cybersecurity report warns that ERP systems are now prime targets for AI-driven attacks.
By 2026, companies must adopt zero-trust ERP frameworks with identity validation, encryption, and continuous monitoring.

Integrating AI in ERP 2026 for threat detection and anomaly analysis will help organizations protect sensitive data while meeting GDPR and regional compliance mandates.

Future-ready ERP systems will combine predictive threat intelligence with real-time analytics to safeguard transactions and ensure uninterrupted operations across the enterprise.

Preparing for the Agentic ERP Era

As we step into 2026, ERP will evolve into agentic systems, intelligent assistants capable of taking action on behalf of users. AI agents will resolve invoices, predict demand, and trigger automated actions in connected workflows.

This evolution marks the beginning of autonomous ERP, where technology supports leadership in making accurate and timely business decisions. Techbot ERP helps organizations transition toward this model by combining expertise in AI integration, ERP security, and process intelligence.

Agentic ERP brings proactive intelligence to the enterprise. It allows organizations to move from data monitoring to data-driven execution, creating a truly connected and autonomous workflow platform.

Building the Foundation for the Future of ERP

The future of ERP software is not about adding more modules or dashboards. It is about building systems that think, learn, and adapt alongside your business. As AI, cloud ERP adoption, and real-time ERP analytics continue to shape enterprise strategy, 2026 will mark the rise of organizations that operate with precision, speed, and intelligence.

Techbot ERP partners with these forward-thinking businesses to design and deploy intelligent ERP systems that are secure, connected, and purpose-built for long-term success. By integrating AI-driven automation and predictive analytics, Techbot helps decision-makers move from data collection to data confidence, where every process drives measurable growth.

The companies that adopt these ERP trends for 2026 will become the benchmark for digital excellence. They will transform workflows into intelligent ecosystems, where every transaction, customer interaction, and insight fuels business agility. The journey toward smarter, autonomous ERP systems is already underway, and those who act today will define the most efficient, data-empowered enterprises of tomorrow.

In this transformation, Techbot ERP stands as more than a technology provider; it is your trusted growth partner in shaping the future of enterprise intelligence.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.

Why You Should Consider Odoo 19 For Your Next ERP Upgrade

Why You Should Consider Odoo 19 For Your Next ERP Upgrade

Avatar photo

Moossa M. Alavi

Moossa M. Alavi is the Founder & CEO of Techbot ERP and Altamyz Advertising. He is a certified Odoo consultant with more than 27 years of experience in business, advertising, and ERP software. Moossa started his career in the UAE in 1997 with a well-known group in Abu Dhabi. Over the years, he built his own companies to help other businesses work better using technology. Moossa helps with customized ERP implementation for various industries, including manufacturing, insurance, supercar rental, and logistics, through Techbot ERP. He resolves these issues with Odoo ERP and supports businesses in growing with the right assets and guidance. Moossa has received many awards for his work, including the Arabian Best of Best Award and the Industry Leader Award from BNI UAE. He is also a BNI Ambassador and mentors other business owners. He believes in giving back to the community and helping others grow, following the “Givers Gain” principle.

Across industries, leaders are realizing that traditional ERP systems cannot keep up with modern business speed. Companies now expect technology that can think, analyze, and guide actions automatically. As an Official Odoo Gold Partner in the UAE, Techbot ERP supports this shift by helping organizations adopt Odoo 19, a platform built for intelligence and adaptability.

Odoo 19 introduces a new era of AI in ERP, combining predictive analytics, automation, and data-driven insights in a single ecosystem. Through Odoo 19 AI integration, teams experience better coordination, faster approvals, and fewer manual tasks. This transformation empowers businesses to move beyond simple record keeping and toward continuous, intelligent decision making.

For enterprises in real estate, manufacturing, and services, the change is clear. Odoo 19 helps organizations operate faster, think smarter, and make confident choices supported by data.

6 AI Capabilities Powering the Future of ERP with Odoo 19

1. Smarter Decision Making with AI Integration

Odoo 19 AI integration replaces manual reporting with intelligent insights that evolve as your data grows.
The system detects business patterns, predicts outcomes, and provides contextual recommendations directly within your workflow.
Users no longer wait for end-of-month reports; they act on live information as events unfold.

This new level of AI in ERP ensures accuracy and agility in every department. Sales, finance, and operations teams collaborate on shared intelligence, creating a responsive organization that can adapt instantly to change.

2. Connected Decision ERP for Real-Time Clarity

The architecture behind Odoo 19 is designed as a connected decision ERP, meaning that every module communicates seamlessly.
When sales data updates, accounting, inventory, and HR automatically adjust their records to reflect the change.
This real-time synchronization eliminates duplicate entries and costly delays caused by outdated information.

By integrating real-time ERP insights, Odoo gives management a complete view of the business at any moment.
This transparency builds trust across departments and drives faster, evidence-based decisions that directly improve performance.

3. Odoo AI Automation for Daily Efficiency

The core of Odoo 19’s value lies in Odoo AI automation, the ability to complete routine tasks without human intervention.
Invoices, follow-up emails, and approval workflows can be handled automatically based on triggers and conditions.
Teams experience smoother handovers and fewer interruptions in their daily work.

This level of automation helps reduce human error while maintaining control and accountability.
Employees can shift their focus toward customer engagement, product development, and innovation, allowing productivity to rise naturally across the organization.

4. AI Agent Workflows That Learn from Your Business

Odoo 19 introduces AI agent workflows, intelligent assistants that learn and act based on company behavior.
These agents monitor activity, recognize patterns, and execute tasks such as invoice creation, data validation, or progress tracking.
Their adaptive nature means the more you use them, the better they understand your processes.

Through these self-learning systems, Odoo evolves into an autonomous workflow platform that supports employees like an extra team member.
It builds consistency, reduces repetitive work, and ensures critical operations run smoothly even during busy periods.

5. Real-Time ERP Insights That Drive Confidence

With Odoo 19, leaders no longer rely on static spreadsheets.
The system delivers real-time ERP insights that reflect the company’s current performance and upcoming trends.
AI-powered dashboards update continuously, enabling leaders to react instantly to opportunities or potential risks.

This visibility promotes better planning and accountability at all levels of management.
Every team benefits from having accurate, up-to-date data, ensuring that decisions are made with complete confidence.

6. The Autonomous Workflow Platform for the Future

Odoo 19 marks the beginning of a new phase in business management, the rise of the autonomous workflow platform.
Its AI agents operate responsibly, executing daily routines and optimizing processes over time.
From generating reports to handling compliance updates, tasks are completed automatically while maintaining accuracy.

This autonomy allows leaders to focus on long-term growth instead of operational firefighting.
Odoo 19 creates a self-improving environment where systems work intelligently alongside humans to achieve measurable results.

Why Businesses Choose Techbot ERP for Odoo 19 Integration

Implementing Odoo 19 AI integration requires both technical precision and a deep understanding of industry processes.
As a leading Odoo Gold Partner in the UAE, Techbot ERP ensures a structured migration strategy for every client.
Our consultants tailor each deployment around your workflows, compliance requirements, and business goals.

Beyond implementation, we help organizations leverage AI in ERP to transform data into decisions. Our approach focuses on achieving real-time ERP insights through optimized dashboards, automation, and intelligent reporting.
From migration and training to post-implementation optimization, Techbot ERP delivers measurable improvements in performance and user adoption.
We turn Odoo’s advanced AI tools into practical business value, helping your organization grow smarter with every transaction and preparing it for a future shaped by intelligent automation.

Your next ERP should support how your business thinks and grows.
Odoo 19 uses AI and smarter automation to deliver that shift from day one.

Building Smarter Businesses through Odoo 19 AI Integration

Migrating to Odoo 19 represents a clear step toward intelligent, connected business operations. This version unites automation, analytics, and adaptability through its advanced AI in the ERP framework. With Odoo AI automation, AI agent workflows, and real-time ERP insights, organizations gain a level of visibility and precision that older systems cannot match.

By transforming data into decisions, Odoo 19 functions as a true connected decision ERP, helping teams act faster and plan smarter. Its intelligent capabilities create an autonomous workflow platform where tasks are completed automatically and leadership gains instant clarity.

For UAE enterprises ready to modernize, Techbot ERP delivers the expertise needed to implement and customize Odoo 19 AI integration effectively. Together, we help you build a business environment driven by intelligence, efficiency, and long-term growth, the foundation of tomorrow’s intelligent enterprise.

Get the latest tips and updates on ERP software solutions. Subscribe to our newsletter and stay ahead in business!

Please enable JavaScript in your browser to complete this form.