Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions excel_import_required/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
============================
Excel Import Required Fields
============================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c6c8d49c0d271d1aeb7ef010e6233738cb02c72b9bb798e728fdc5bb612c99af
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
:target: https://github.com/OCA/server-tools/tree/18.0/excel_import_required
:alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-excel_import_required
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Excel Import Required Fields
============================

This module extends the OCA ``excel_import_export`` functionality to
allow marking fields as **required** in XLSX import templates.

When a field is marked as required, the system checks the Excel file
during import. If any required field is empty or missing, the import is
stopped.

An error message is shown like:

::

Following fields are required to import
- Field Name 1
- Field Name 2

Features
--------

- Mark fields as required in XLSX templates
- Validate both header and row data
- Prevent import if required fields are missing
- Show clear error messages with field names

**Table of contents**

.. contents::
:local:

Configuration
=============

This module (``excel_import_required``) extends the standard OCA
``excel_import_export`` functionality to allow administrators to enforce
specific data fields as strictly required when importing datasets from
Excel matrices.

If an Excel sheet is uploaded containing empty rows or blank cells for
fields mapped as required, the system intercepts the database write and
presents a consolidated ``ValidationError`` popup displaying all the
missing parameters at once using their human-readable field labels.

--------------

How to Configure
----------------

To flag an existing field map as **Required** directly from the system
frontend:

1. Navigate to: **System Menus → Excel Import/Export → XLSX Templates**
2. Select your desired Template from the list view.
3. Open the template in **Form View**.
4. Go to the **Import** tab.
5. Locate the **Import Lines (``import_ids``)** section.
6. Find the column labeled **Required** (next to *Field Condition*).
7. Enable the **Required** checkbox for any field that must be mandatory
(e.g., ``Cell B4 → Field ref``).
8. Click **Save**.

--------------

Validation Behavior
-------------------

Once a field is marked as **Required**:

- During XLSX import, the system validates all required fields.

- If any required field contains:

- Empty value
- Blank cell
- ``False`` / ``None``

The import process will be **blocked**, and a validation warning will be
displayed.

--------------

If required values are missing, the system raises the following error:

::

Following fields are required to import
- Field Name 1
- Field Name 2
- Field Name 3

- Validation applies only to fields marked as **Required**
- Validation is executed **before data is written to the database**

--------------

Usage
=====

Usage
=====

To use this module, follow these steps:

1. Go to **Excel Import/Export → XLSX Templates** in Odoo.
2. Open or create a template and configure your import fields.
3. Mark the required fields by enabling the **Required** checkbox.
4. Save the template.
5. Import your XLSX file using this configured template.
6. **Validation in Action**: Odoo will scan the file before creating any
records. If any required field is missing or empty, the import will
be stopped.
7. An error message will appear showing which fields are missing.
Correct your file and retry the import.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_required%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Heliconia Solutions Pvt. Ltd.

Contributors
------------

- `Heliconia Solutions Pvt. Ltd. <https://www.heliconia.io>`__

- Bhavesh Heliconia

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-Bhavesh Heliconia| image:: https://github.com/Bhavesh Heliconia.png?size=40px
:target: https://github.com/Bhavesh Heliconia
:alt: Bhavesh Heliconia

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-Bhavesh Heliconia|

This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/18.0/excel_import_required>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions excel_import_required/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 Heliconia Solutions Pvt. Ltd.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import models
21 changes: 21 additions & 0 deletions excel_import_required/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 Heliconia Solutions Pvt. Ltd.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Excel Import Required Fields",
"summary": "Enforce required fields during excel import",
"version": "18.0.1.0.0",
"category": "Tools",
"author": "Heliconia Solutions Pvt. Ltd., Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
"license": "AGPL-3",
"development_status": "Beta",
"maintainers": ["Bhavesh Heliconia"],
"depends": ["excel_import_export"],
"data": [
"views/xlsx_template_view.xml",
],
"installable": True,
"application": False,
"auto_install": False,
}
5 changes: 5 additions & 0 deletions excel_import_required/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2026 Heliconia Solutions Pvt. Ltd.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import xlsx_template_import
from . import xlsx_import
167 changes: 167 additions & 0 deletions excel_import_required/models/xlsx_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright 2026 Heliconia Solutions Pvt. Ltd.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import api, models
from odoo.exceptions import ValidationError

from odoo.addons.excel_import_export.models import common as co


class XLSXImport(models.AbstractModel):
_inherit = "xlsx.import"

@api.model
def import_xlsx(self, import_file, template, res_model=False, res_id=False):
required_cells = {}
current_sheet = None
current_row_field = None

for line in template.import_ids:
if line.section_type == "sheet":
current_sheet = (
int(line.sheet) if str(line.sheet).isdigit() else line.sheet
)

elif line.section_type in ("head", "row"):
current_row_field = line.row_field
if line.section_type == "row" and line.no_delete:
current_row_field = f"_NODEL_{current_row_field}"

elif line.section_type == "data" and line.required:
required_cells.setdefault(current_sheet, {}).setdefault(
current_row_field, set()
).add(line.excel_cell)

return super(
XLSXImport,
self.with_context(
excel_required_cells=required_cells,
excel_import_errors=[],
),
).import_xlsx(import_file, template, res_model=res_model, res_id=res_id)

@api.model
def _process_worksheet(
self, wb, out_st, model, data_dict, header_fields, is_xlsx=False
):
required_cells = self.env.context.get("excel_required_cells")
errors = self.env.context.get("excel_import_errors")

if not required_cells:
return super()._process_worksheet(
wb, out_st, model, data_dict, header_fields, is_xlsx=is_xlsx
)
self._validate_header_fields(
wb, model, data_dict, required_cells, errors, is_xlsx
)

res = super()._process_worksheet(
wb, out_st, model, data_dict, header_fields, is_xlsx=is_xlsx
)

if errors:
raise ValidationError(self._format_errors(errors))
return res

def _validate_header_fields(
self, wb, model, data_dict, required_cells, errors, is_xlsx
):
for sheet_name, worksheet in data_dict.items():
req_cells = required_cells.get(sheet_name, {}).get("_HEAD_")
if not req_cells:
continue

sheet = self._get_sheet(wb, sheet_name, is_xlsx)
if not sheet:
continue

for rc, field in worksheet.get("_HEAD_", {}).items():
rc_pos, _ = co.get_field_condition(rc)
if rc_pos not in req_cells:
continue
field_name, _ = co.get_field_condition(field)
value = self._read_cell(sheet, rc_pos, model, field_name, is_xlsx)

if not value:
errors.append(self._get_field_string(model, field_name))

@api.model
def _get_line_vals(self, st, worksheet, model, line_field, is_xlsx=False):
vals = super()._get_line_vals(st, worksheet, model, line_field, is_xlsx=is_xlsx)

required_cells = self.env.context.get("excel_required_cells")
errors = self.env.context.get("excel_import_errors")
if not required_cells or errors is None:
return vals

req_cells = set()
for sheet in required_cells.values():
req_cells |= sheet.get(line_field, set())

if not req_cells:
return vals

for rc, columns in worksheet.get(line_field, {}).items():
rc_pos, _ = co.get_field_condition(rc)

if rc_pos not in req_cells:
continue
columns = columns if isinstance(columns, list) else [columns]
for field in columns:
field_name, _ = co.get_field_condition(field)
new_line_field, _ = co.get_line_max(line_field)
out_field = f"{new_line_field}/{field_name}"

values = vals.get(out_field, [])
if not values or any(not v for v in values):
errors.append(self._get_field_string(model, out_field))
return vals

def _read_cell(self, sheet, rc_pos, model, field_name, is_xlsx):
try:
row, col = co.pos2idx(rc_pos)
cell = (
sheet.cell(row=row + 1, column=col + 1)
if is_xlsx
else sheet.cell(row, col)
)
field_type = self._get_field_type(model, field_name)
return co._get_cell_value(cell, field_type=field_type)
except Exception:
return False

def _get_sheet(self, wb, sheet_name, is_xlsx):
try:
if isinstance(sheet_name, str):
return (
wb[sheet_name]
if is_xlsx
else co.xlrd_get_sheet_by_name(wb, sheet_name)
)
return (
wb.worksheets[sheet_name - 1]
if is_xlsx
else wb.sheet_by_index(sheet_name - 1)
)
except Exception:
return None

def _get_field_string(self, model, field_path):
try:
record = self.env[model].new()
field = None
for f in field_path.split("/"):
f_name = f.split(".")[0]
field = record._fields.get(f_name)
if not field:
return field_path
if field.type in ("one2many", "many2many", "many2one"):
record = record[f_name]
return field.string if field else field_path
except Exception:
return field_path

def _format_errors(self, errors):
unique_errors = sorted(set(errors))
msg = self.env._("Following fields are required to import\n")
return msg + "\n".join(f"- {e}" for e in unique_errors)
Loading
Loading