Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[17.0][ADD] base_import_extended: New module #1003

Draft
wants to merge 1 commit into
base: 17.0
Choose a base branch
from
Draft
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
83 changes: 83 additions & 0 deletions base_import_extended/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
====================
Base import extended
====================

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

.. |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--ux-lightgray.png?logo=github
:target: https://github.com/OCA/server-ux/tree/17.0/base_import_extended
:alt: OCA/server-ux
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-ux-17-0/server-ux-17-0-base_import_extended
: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-ux&target_branch=17.0
:alt: Try me on Runboat

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

This module allows to create templates for import data files

**Table of contents**

.. contents::
:local:

Usage
=====



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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-ux/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-ux/issues/new?body=module:%20base_import_extended%0Aversion:%2017.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
-------

* Tecnativa

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

- `Tecnativa <https://www.tecnativa.com>`__:

- Carlos Dauden

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.

This module is part of the `OCA/server-ux <https://github.com/OCA/server-ux/tree/17.0/base_import_extended>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions base_import_extended/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions base_import_extended/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2025 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

{
"name": "Base import extended",
"summary": "Base import extended to manage templates and extra conversion methods",
"version": "17.0.1.0.0",
"category": "Tools",
"website": "https://github.com/OCA/server-ux",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": [
"base_import",
],
"data": [
"security/ir.model.access.csv",
"views/base_import_mapping_template_views.xml",
"views/base_import_mapping_value_map_views.xml",
# "views/invoice_import_xls_view.xml",
],
}
4 changes: 4 additions & 0 deletions base_import_extended/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import base_import_mapping
from . import base_import_mapping_template
from . import base_import_mapping_value_map
from . import ir_fields
70 changes: 70 additions & 0 deletions base_import_extended/models/base_import_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2025 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
from odoo.osv import expression


class ImportMapping(models.Model):
_inherit = "base_import.mapping"
_rec_name = "column_name"

mapping_template_id = fields.Many2one(
comodel_name="base_import.mapping.template",
ondelete="cascade",
)
pre_process_method = fields.Selection(
[
("str_abs_value", "Absolute value (string)"),
("prefix_c", "Prefix C"),
("prefix_p", "Prefix P"),
]
)
mapped_value_ids = fields.Many2many(
comodel_name="base_import.mapping.value.map",
relation="base_import_mapping_value_map_rel",
column1="mapping_id",
column2="value_map_id",
)
python_code = fields.Char()
# Convert to compute to manage from template if is set
res_model = fields.Char(compute="_compute_res_model", store=True, readonly=False)

@api.depends("mapping_template_id")
def _compute_res_model(self):
for line in self.filtered("mapping_template_id"):
line.res_model = line.mapping_template_id.res_model

Check warning on line 36 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L36

Added line #L36 was not covered by tests

@api.model
def search(self, domain, offset=0, limit=None, order=None):
domain = expression.AND(

Check warning on line 40 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L40

Added line #L40 was not covered by tests
[
domain,
[
(
"mapping_template_id",
"=",
self.env.context.get("use_mapping_template_id", False),
)
],
]
)
return super().search(domain, offset=offset, limit=limit, order=order)

Check warning on line 52 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L52

Added line #L52 was not covered by tests

@api.model_create_multi
def create(self, vals_list):
mapping_template_id = self.env.context.get("use_mapping_template_id", False)

Check warning on line 56 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L56

Added line #L56 was not covered by tests
if mapping_template_id:
for vals in vals_list:
if "mapping_template_id" not in vals:
vals["mapping_template_id"] = mapping_template_id
return super().create(vals_list)

Check warning on line 61 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L60-L61

Added lines #L60 - L61 were not covered by tests

def pre_process_method_str_abs_value(self, value):
return value.replace("-", "")

Check warning on line 64 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L64

Added line #L64 was not covered by tests

def pre_process_method_prefix_c(self, value):
return f"C-{value}"

Check warning on line 67 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L67

Added line #L67 was not covered by tests

def pre_process_method_prefix_p(self, value):
return f"P-{value}"

Check warning on line 70 in base_import_extended/models/base_import_mapping.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping.py#L70

Added line #L70 was not covered by tests
177 changes: 177 additions & 0 deletions base_import_extended/models/base_import_mapping_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright 2025 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import base64
import math

from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval


class ImportMappingTemplate(models.Model):
_inherit = ["base_import.import"]
_name = "base_import.mapping.template"
_description = "Template to group import mapping data"
_transient = False
_transient_max_hours = 0
_transient_max_count = 0

name = fields.Char()
action_id = fields.Many2one(comodel_name="ir.actions.act_window")
mapping_ids = fields.One2many(
comodel_name="base_import.mapping",
inverse_name="mapping_template_id",
)
forced_context = fields.Char(string="Context Value", default={}, required=True)
forced_options = fields.Char(default={}, required=True)

def execute_import_template(self):
options = {

Check warning on line 30 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L30

Added line #L30 was not covered by tests
"import_skip_records": [],
"import_set_empty_fields": [],
"fallback_values": {},
"name_create_enabled_fields": {},
"encoding": "",
"separator": "",
"quoting": '"',
"date_format": "%Y-%m-%d",
"datetime_format": "",
"float_thousand_separator": ",",
"float_decimal_separator": ".",
"advanced": True,
"has_headers": True,
"keep_matches": False,
"limit": 2000,
"sheets": [],
"sheet": "",
"skip": 0,
"tracking_disable": True,
}
eval_ctx = dict(self.env.context)
ctx = {}

Check warning on line 52 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L51-L52

Added lines #L51 - L52 were not covered by tests
if self.action_id:
ctx.update(**safe_eval(self.action_id.context, eval_ctx))

Check warning on line 54 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L54

Added line #L54 was not covered by tests
if self.forced_context != "{}":
ctx.update(**safe_eval(self.forced_context, eval_ctx))
self = self.with_context(**ctx)
self.file = base64.b64decode(self.file)
preview = self.parse_preview(options=options)

Check warning on line 59 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L56-L59

Added lines #L56 - L59 were not covered by tests
if self.forced_options != "{}":
options.update(**safe_eval(self.forced_options, eval_ctx))
columns = [col.lower() for col in preview.get("headers", [])]
fields = []

Check warning on line 63 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L61-L63

Added lines #L61 - L63 were not covered by tests
for column in columns:
line = self.mapping_ids.filtered(lambda x, col=column: x.column_name == col)
fields.append(line.field_name)
limit = options["limit"]
steps_number = math.ceil((preview.get("file_length", 1) - 1) / limit)
all_ids = []

Check warning on line 69 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L65-L69

Added lines #L65 - L69 were not covered by tests
for step in range(steps_number):
options["skip"] = step * limit
options["limit"] = limit
res = self.with_context(

Check warning on line 73 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L71-L73

Added lines #L71 - L73 were not covered by tests
use_mapping_template_id=self.id, use_cached_db_id_for=True
).execute_import(fields, columns, options, dryrun=False)
messages = res.get("messages", [])

Check warning on line 76 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L76

Added line #L76 was not covered by tests
if messages:
text_message = "\n".join(m.get("message", "") for m in messages)

Check warning on line 78 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L78

Added line #L78 was not covered by tests
if step > 0:
text_message = (

Check warning on line 80 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L80

Added line #L80 was not covered by tests
f"Already imported {step * limit} records, but \n{text_message}"
)
raise UserError(text_message)
res_ids = res.get("ids", [])

Check warning on line 84 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L83-L84

Added lines #L83 - L84 were not covered by tests
if not res_ids:
raise UserError(_("No records were imported"))
all_ids.extend(res_ids)

Check warning on line 87 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L86-L87

Added lines #L86 - L87 were not covered by tests
# self.env.registry.clear_cache()
self.file = False
return self.action_view_imported_records(all_ids, ctx)

Check warning on line 90 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L89-L90

Added lines #L89 - L90 were not covered by tests

@api.model
def _convert_import_data(self, fields, options):
data, import_fields = super()._convert_import_data(fields, options)

Check warning on line 94 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L94

Added line #L94 was not covered by tests
if not self.env.context.get("use_mapping_template_id"):
return data, import_fields
multilevel = "id" in import_fields and any(

Check warning on line 97 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L96-L97

Added lines #L96 - L97 were not covered by tests
"_ids/" in f for f in import_fields if f
)
if multilevel:
id_index = import_fields.index("id")
index_line_dict, index_column_dict = self.get_index_dictionaries(import_fields)

Check warning on line 102 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L101-L102

Added lines #L101 - L102 were not covered by tests
# for index, field_name in enumerate(import_fields):
# line = self.mapping_ids.filtered(
# lambda x, f_name=field_name: x.field_name == f_name
# )
for index, line in index_line_dict.items():
field_name = line.field_name

Check warning on line 108 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L108

Added line #L108 was not covered by tests
if line.pre_process_method:
process_fnc = getattr(

Check warning on line 110 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L110

Added line #L110 was not covered by tests
line, f"pre_process_method_{line.pre_process_method}"
)
for row in data:
row[index] = process_fnc(row[index])

Check warning on line 114 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L114

Added line #L114 was not covered by tests
elif line.python_code:
self.update_data_with_python_code(

Check warning on line 116 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L116

Added line #L116 was not covered by tests
data, index, line.python_code, index_column_dict
)
elif field_name == "id":
prefix = self.res_model.replace(".", "_")

Check warning on line 120 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L120

Added line #L120 was not covered by tests
for row in data:
row[index] = f"{prefix}_{row[index]}_{self.id}"

Check warning on line 122 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L122

Added line #L122 was not covered by tests
if line.mapped_value_ids:
mapped_dict = {

Check warning on line 124 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L124

Added line #L124 was not covered by tests
map_line.value: map_line.new_value_ref
and str(map_line.new_value_ref.id)
or map_line.new_value
for map_line in line.mapped_value_ids
}
for row in data:
row[index] = mapped_dict.get(row[index], row[index])

Check warning on line 131 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L131

Added line #L131 was not covered by tests
# Empty repeat values for principal record fields
if multilevel and "_ids/" not in field_name:
last_value = ""

Check warning on line 134 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L134

Added line #L134 was not covered by tests
for row in data:
if row[id_index] in ("", last_value):
row[index] = ""

Check warning on line 137 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L137

Added line #L137 was not covered by tests
else:
last_value = row[id_index]
return data, import_fields

Check warning on line 140 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L139-L140

Added lines #L139 - L140 were not covered by tests

def get_index_dictionaries(self, import_fields):
index_line_dict = {}
index_column_dict = {}

Check warning on line 144 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L143-L144

Added lines #L143 - L144 were not covered by tests
for index, field_name in enumerate(import_fields):
line = self.mapping_ids.filtered(

Check warning on line 146 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L146

Added line #L146 was not covered by tests
lambda x, f_name=field_name: x.field_name == f_name
)
index_line_dict[index] = line
index_column_dict[index] = line.column_name
return index_line_dict, index_column_dict

Check warning on line 151 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L149-L151

Added lines #L149 - L151 were not covered by tests

def update_data_with_python_code(self, data, index, python_code, index_column_dict):
for row in data:
col_vals = {}

Check warning on line 155 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L155

Added line #L155 was not covered by tests
if "col_vals" in python_code:
for idx, col in index_column_dict.items():
col_vals[col] = row[idx]
row[index] = safe_eval(

Check warning on line 159 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L158-L159

Added lines #L158 - L159 were not covered by tests
python_code,
{"value": row[index], "col_vals": col_vals},
)

def action_view_imported_records(self, res_ids, context=None):
if self.action_id:
action = self.env["ir.actions.actions"]._for_xml_id(self.action_id.xml_id)

Check warning on line 166 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L166

Added line #L166 was not covered by tests
else:
action = {

Check warning on line 168 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L168

Added line #L168 was not covered by tests
"type": "ir.actions.act_window",
"res_model": self.res_model,
"name": _("Imported Records"),
"views": [[False, "tree"], [False, "kanban"], [False, "form"]],
}
action["domain"] = [("id", "in", res_ids)]

Check warning on line 174 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L174

Added line #L174 was not covered by tests
if context:
action["context"] = context
return action

Check warning on line 177 in base_import_extended/models/base_import_mapping_template.py

View check run for this annotation

Codecov / codecov/patch

base_import_extended/models/base_import_mapping_template.py#L176-L177

Added lines #L176 - L177 were not covered by tests
25 changes: 25 additions & 0 deletions base_import_extended/models/base_import_mapping_value_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2025 Tecnativa - Carlos Dauden
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models


class ImportMappingValueMap(models.Model):
_name = "base_import.mapping.value.map"
_description = "Map import value with odoo value"
_rec_name = "value"

mapping_ids = fields.Many2many(
comodel_name="base_import.mapping",
relation="base_import_mapping_value_map_rel",
column1="value_map_id",
column2="mapping_id",
)
value = fields.Char()
new_value = fields.Char()
new_value_ref = fields.Reference(
lambda self: [
(m.model, m.name) for m in self.env["ir.model"].sudo().search([])
],
string="Object",
)
Loading
Loading