Skip to content

Commit

Permalink
[ADD] base_import_extended: New module
Browse files Browse the repository at this point in the history
TT52224
  • Loading branch information
carlosdauden committed Jan 31, 2025
1 parent d02602e commit 6eb397d
Show file tree
Hide file tree
Showing 17 changed files with 962 additions and 0 deletions.
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

0 comments on commit 6eb397d

Please sign in to comment.