Browse Source
Merge pull request #420 from Yajo/base_import_deduplicator
Merge pull request #420 from Yajo/base_import_deduplicator
[ADD] base_import_match: Import deduplicator.pull/238/merge
Daniel Reis
9 years ago
18 changed files with 781 additions and 0 deletions
-
128base_import_match/README.rst
-
5base_import_match/__init__.py
-
26base_import_match/__openerp__.py
-
29base_import_match/data/base_import_match.yml
-
40base_import_match/demo/base_import_match.yml
-
83base_import_match/i18n/es.po
-
5base_import_match/models/__init__.py
-
297base_import_match/models/base_import.py
-
5base_import_match/security/ir.model.access.csv
-
BINbase_import_match/static/description/icon.png
-
5base_import_match/tests/__init__.py
-
2base_import_match/tests/import_data/res_partner_email.csv
-
2base_import_match/tests/import_data/res_partner_name.csv
-
2base_import_match/tests/import_data/res_partner_parent_name_is_company.csv
-
2base_import_match/tests/import_data/res_partner_vat.csv
-
2base_import_match/tests/import_data/res_users_login.csv
-
68base_import_match/tests/test_import.py
-
80base_import_match/views/base_import_match_view.xml
@ -0,0 +1,128 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
================= |
||||
|
Base Import Match |
||||
|
================= |
||||
|
|
||||
|
By default, when importing data (like CSV import) with the ``base_import`` |
||||
|
module, Odoo follows this rule: |
||||
|
|
||||
|
- If you import the XMLID of a record, make an **update**. |
||||
|
- If you do not, **create** a new record. |
||||
|
|
||||
|
This module allows you to set additional rules to match if a given import is an |
||||
|
update or a new record. |
||||
|
|
||||
|
This is useful when you need to sync heterogeneous databases, and the field you |
||||
|
use to match records in those databases with Odoo's is not the XMLID but the |
||||
|
name, VAT, email, etc. |
||||
|
|
||||
|
After installing this module, the import logic will be changed to: |
||||
|
|
||||
|
- If you import the XMLID of a record, make an **update**. |
||||
|
- If you do not: |
||||
|
|
||||
|
- If there are import match rules for the model you are importing: |
||||
|
|
||||
|
- Discard the rules that require fields you are not importing. |
||||
|
- Traverse the remaining rules one by one in order to find a match in the database. |
||||
|
|
||||
|
- Skip the rule if it requires a special condition that is not |
||||
|
satisfied. |
||||
|
- If one match is found: |
||||
|
|
||||
|
- Stop traversing the rest of valid rules. |
||||
|
- **Update** that record. |
||||
|
- If zero or multiple matches are found: |
||||
|
|
||||
|
- Continue with the next rule. |
||||
|
- If all rules are exhausted and no single match is found: |
||||
|
|
||||
|
- **Create** a new record. |
||||
|
- If there are no match rules for your model: |
||||
|
|
||||
|
- **Create** a new record. |
||||
|
|
||||
|
By default 2 rules are installed for production instances: |
||||
|
|
||||
|
- One rule that will allow you to update companies based on their VAT, when |
||||
|
``is_company`` is ``True``. |
||||
|
- One rule that will allow you to update users based on their login. |
||||
|
|
||||
|
In demo instances there are more examples. |
||||
|
|
||||
|
Configuration |
||||
|
============= |
||||
|
|
||||
|
To configure this module, you need to: |
||||
|
|
||||
|
#. Go to *Settings > Technical > Database Structure > Import Match*. |
||||
|
#. *Create*. |
||||
|
#. Choose a *Model*. |
||||
|
#. Choose the *Fields* that conform an unique key in that model. |
||||
|
#. If the rule must be used only for certain imported values, check |
||||
|
*Conditional* and enter the **exact string** that is going to be imported |
||||
|
in *Imported value*. |
||||
|
|
||||
|
#. Keep in mind that the match here is evaluated as a case sensitive |
||||
|
**text string** always. If you enter e.g. ``True``, it will match that |
||||
|
string, but will not match ``1`` or ``true``. |
||||
|
#. *Save*. |
||||
|
|
||||
|
In that list view, you can sort rules by drag and drop. |
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
To use this module, you need to: |
||||
|
|
||||
|
#. Follow steps in **Configuration** section above. |
||||
|
#. Go to any list view. |
||||
|
#. Press *Import* and follow the import procedure as usual. |
||||
|
|
||||
|
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
||||
|
:alt: Try me on Runbot |
||||
|
:target: https://runbot.odoo-community.org/runbot/149/8.0 |
||||
|
|
||||
|
Known Issues / Roadmap |
||||
|
====================== |
||||
|
|
||||
|
* Add a setting to throw an error when multiple matches are found, instead of |
||||
|
falling back to creation of new record. |
||||
|
|
||||
|
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 smashing it by providing a detailed and welcomed `feedback |
||||
|
<https://github.com/OCA/ |
||||
|
server-tools/issues/new?body=module:%20 |
||||
|
base_import_match%0Aversion:%20 |
||||
|
8.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Jairo Llopis <yajo.sk8@gmail.com> |
||||
|
|
||||
|
Maintainer |
||||
|
---------- |
||||
|
|
||||
|
.. image:: https://odoo-community.org/logo.png |
||||
|
:alt: Odoo Community Association |
||||
|
:target: https://odoo-community.org |
||||
|
|
||||
|
This module is maintained by the OCA. |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import models |
@ -0,0 +1,26 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
{ |
||||
|
"name": "Base Import Match", |
||||
|
"summary": "Try to avoid duplicates before importing", |
||||
|
"version": "8.0.1.0.0", |
||||
|
"category": "Tools", |
||||
|
"website": "https://grupoesoc.es", |
||||
|
"author": "Grupo ESOC Ingeniería de Servicios, " |
||||
|
"Odoo Community Association (OCA)", |
||||
|
"license": "AGPL-3", |
||||
|
"application": False, |
||||
|
"installable": True, |
||||
|
"depends": [ |
||||
|
"base_import", |
||||
|
], |
||||
|
"data": [ |
||||
|
"security/ir.model.access.csv", |
||||
|
"data/base_import_match.yml", |
||||
|
"views/base_import_match_view.xml", |
||||
|
], |
||||
|
"demo": [ |
||||
|
"demo/base_import_match.yml", |
||||
|
], |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
- !context {noupdate: True} |
||||
|
|
||||
|
# Match partners by VAT when is_company is True |
||||
|
- !record {id: res_partner_vat, model: base_import.match}: |
||||
|
model_id: base.model_res_partner |
||||
|
sequence: 10 |
||||
|
|
||||
|
- !record {id: res_partner_vat_vat, model: base_import.match.field}: |
||||
|
match_id: res_partner_vat |
||||
|
field_id: base.field_res_partner_vat |
||||
|
|
||||
|
- !record {id: res_partner_vat_is_company, model: base_import.match.field}: |
||||
|
match_id: res_partner_vat |
||||
|
field_id: base.field_res_partner_is_company |
||||
|
conditional: True |
||||
|
imported_value: "True" |
||||
|
|
||||
|
# Match users by login |
||||
|
- !record {id: res_users_login, model: base_import.match}: |
||||
|
model_id: base.model_res_users |
||||
|
sequence: 50 |
||||
|
|
||||
|
- !record {id: res_users_login_login, model: base_import.match.field}: |
||||
|
match_id: res_users_login |
||||
|
field_id: base.field_res_users_login |
@ -0,0 +1,40 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
||||
|
|
||||
|
- !context {noupdate: True} |
||||
|
|
||||
|
# Match partners by name, parent_id and is_company |
||||
|
- !record {id: res_partner_parent_name_is_company, model: base_import.match}: |
||||
|
model_id: base.model_res_partner |
||||
|
sequence: 20 |
||||
|
|
||||
|
- !record {id: res_partner_parent_name_is_company_name, model: base_import.match.field}: |
||||
|
match_id: res_partner_parent_name_is_company |
||||
|
field_id: base.field_res_partner_name |
||||
|
|
||||
|
- !record {id: res_partner_parent_name_is_company_parent, model: base_import.match.field}: |
||||
|
match_id: res_partner_parent_name_is_company |
||||
|
field_id: base.field_res_partner_parent_id |
||||
|
|
||||
|
- !record {id: res_partner_parent_name_is_company_is_company, model: base_import.match.field}: |
||||
|
match_id: res_partner_parent_name_is_company |
||||
|
field_id: base.field_res_partner_is_company |
||||
|
|
||||
|
# Match partner by email |
||||
|
- !record {id: res_partner_email, model: base_import.match}: |
||||
|
model_id: base.model_res_partner |
||||
|
sequence: 30 |
||||
|
|
||||
|
- !record {id: res_partner_email_email, model: base_import.match.field}: |
||||
|
match_id: res_partner_email |
||||
|
field_id: base.field_res_partner_email |
||||
|
|
||||
|
# Match partner by name |
||||
|
- !record {id: res_partner_name, model: base_import.match}: |
||||
|
model_id: base.model_res_partner |
||||
|
sequence: 40 |
||||
|
|
||||
|
- !record {id: res_partner_name_name, model: base_import.match.field}: |
||||
|
match_id: res_partner_name |
||||
|
field_id: base.field_res_partner_name |
@ -0,0 +1,83 @@ |
|||||
|
# Translation of Odoo Server. |
||||
|
# This file contains the translation of the following modules: |
||||
|
# * base_import_match |
||||
|
# |
||||
|
msgid "" |
||||
|
msgstr "" |
||||
|
"Project-Id-Version: Odoo Server 8.0\n" |
||||
|
"Report-Msgid-Bugs-To: \n" |
||||
|
"POT-Creation-Date: 2016-05-11 12:24+0200\n" |
||||
|
"PO-Revision-Date: 2016-05-11 12:26+0200\n" |
||||
|
"Language-Team: \n" |
||||
|
"MIME-Version: 1.0\n" |
||||
|
"Content-Type: text/plain; charset=UTF-8\n" |
||||
|
"Content-Transfer-Encoding: 8bit\n" |
||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n" |
||||
|
"X-Generator: Poedit 1.8.7.1\n" |
||||
|
"Last-Translator: Jairo Llopis <j.llopis@grupoesoc.es>\n" |
||||
|
"Language: es_ES\n" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: model:ir.model,name:base_import_match.model_base_import_match |
||||
|
msgid "Deduplicate settings prior to CSV imports." |
||||
|
msgstr "Configuración para deduplicar antes de importar CSV." |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: field:base_import.match,display_name:0 |
||||
|
msgid "Display Name" |
||||
|
msgstr "Nombre a mostrar" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: sql_constraint:base_import.match:0 |
||||
|
msgid "Duplicated match!" |
||||
|
msgstr "¡Coincidencia duplicada!" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: field:base_import.match,field_ids:0 |
||||
|
msgid "Fields" |
||||
|
msgstr "Campos" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: help:base_import.match,field_ids:0 |
||||
|
msgid "Fields that will define an unique key." |
||||
|
msgstr "Campos que definirán una clave única." |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: view:base_import.match:base_import_match.match_search_view |
||||
|
msgid "Group By" |
||||
|
msgstr "Agrupar por" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: view:base_import.match:base_import_match.match_form_view |
||||
|
#: view:base_import.match:base_import_match.match_search_view |
||||
|
#: view:base_import.match:base_import_match.match_tree_view |
||||
|
#: model:ir.actions.act_window,name:base_import_match.match_action |
||||
|
#: model:ir.ui.menu,name:base_import_match.match_menu |
||||
|
msgid "Import Match" |
||||
|
msgstr "Coincidencia de importación" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: help:base_import.match,model_id:0 |
||||
|
msgid "In this model you will apply the match." |
||||
|
msgstr "En este modelo se aplicará la coincidencia." |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: field:base_import.match,__last_update:0 |
||||
|
msgid "Last Modified on" |
||||
|
msgstr "Última actualización por" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: view:base_import.match:base_import_match.match_search_view |
||||
|
#: field:base_import.match,model_id:0 field:base_import.match,model_name:0 |
||||
|
msgid "Model" |
||||
|
msgstr "Modelo" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: field:base_import.match,name:0 |
||||
|
msgid "Name" |
||||
|
msgstr "Nombre" |
||||
|
|
||||
|
#. module: base_import_match |
||||
|
#: field:base_import.match,sequence:0 |
||||
|
msgid "Sequence" |
||||
|
msgstr "Secuencia" |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import base_import |
@ -0,0 +1,297 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from openerp import api, fields, models |
||||
|
from openerp import SUPERUSER_ID # TODO remove in v10 |
||||
|
|
||||
|
|
||||
|
class BaseImportMatch(models.Model): |
||||
|
_name = "base_import.match" |
||||
|
_description = "Deduplicate settings prior to CSV imports." |
||||
|
_order = "sequence, name" |
||||
|
|
||||
|
name = fields.Char( |
||||
|
compute="_compute_name", |
||||
|
store=True, |
||||
|
index=True) |
||||
|
sequence = fields.Integer(index=True) |
||||
|
model_id = fields.Many2one( |
||||
|
"ir.model", |
||||
|
"Model", |
||||
|
required=True, |
||||
|
ondelete="cascade", |
||||
|
domain=[("osv_memory", "=", False)], |
||||
|
help="In this model you will apply the match.") |
||||
|
model_name = fields.Char( |
||||
|
related="model_id.model", |
||||
|
store=True, |
||||
|
index=True) |
||||
|
field_ids = fields.One2many( |
||||
|
comodel_name="base_import.match.field", |
||||
|
inverse_name="match_id", |
||||
|
string="Fields", |
||||
|
required=True, |
||||
|
help="Fields that will define an unique key.") |
||||
|
|
||||
|
@api.multi |
||||
|
@api.onchange("model_id") |
||||
|
def _onchange_model_id(self): |
||||
|
self.field_ids.unlink() |
||||
|
|
||||
|
@api.model |
||||
|
def create(self, vals): |
||||
|
"""Wrap the model after creation.""" |
||||
|
result = super(BaseImportMatch, self).create(vals) |
||||
|
self._load_autopatch(result.model_name) |
||||
|
return result |
||||
|
|
||||
|
@api.multi |
||||
|
def unlink(self): |
||||
|
"""Unwrap the model after deletion.""" |
||||
|
models = set(self.mapped("model_name")) |
||||
|
result = super(BaseImportMatch, self).unlink() |
||||
|
for model in models: |
||||
|
self._load_autopatch(model) |
||||
|
return result |
||||
|
|
||||
|
@api.multi |
||||
|
def write(self, vals): |
||||
|
"""Wrap the model after writing.""" |
||||
|
result = super(BaseImportMatch, self).write(vals) |
||||
|
|
||||
|
if "model_id" in vals or "model_name" in vals: |
||||
|
for s in self: |
||||
|
self._load_autopatch(s.model_name) |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
# TODO convert to @api.model_cr in v10 |
||||
|
def _register_hook(self, cr): |
||||
|
"""Autopatch on init.""" |
||||
|
models = set( |
||||
|
self.browse( |
||||
|
cr, |
||||
|
SUPERUSER_ID, |
||||
|
self.search(cr, SUPERUSER_ID, list())) |
||||
|
.mapped("model_name")) |
||||
|
for model in models: |
||||
|
self._load_autopatch(cr, SUPERUSER_ID, model) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends("model_id", "field_ids") |
||||
|
def _compute_name(self): |
||||
|
"""Automatic self-descriptive name for the setting records.""" |
||||
|
for s in self: |
||||
|
s.name = "{}: {}".format( |
||||
|
s.model_id.display_name, |
||||
|
" + ".join( |
||||
|
s.field_ids.mapped( |
||||
|
lambda r: ( |
||||
|
str(r.field_id.name) + |
||||
|
(" ({})".format(r.imported_value) |
||||
|
if r.conditional else ""))))) |
||||
|
|
||||
|
@api.model |
||||
|
def _match_find(self, model, converted_row, imported_row): |
||||
|
"""Find a update target for the given row. |
||||
|
|
||||
|
This will traverse by order all match rules that can be used with the |
||||
|
imported data, and return a match for the first rule that returns a |
||||
|
single result. |
||||
|
|
||||
|
:param openerp.models.Model model: |
||||
|
Model object that is being imported. |
||||
|
|
||||
|
:param dict converted_row: |
||||
|
Row converted to Odoo api format, like the 3rd value that |
||||
|
:meth:`openerp.models.Model._convert_records` returns. |
||||
|
|
||||
|
:param dict imported_row: |
||||
|
Row as it is being imported, in format:: |
||||
|
|
||||
|
{ |
||||
|
"field_name": "string value", |
||||
|
"other_field": "True", |
||||
|
... |
||||
|
} |
||||
|
|
||||
|
:return openerp.models.Model: |
||||
|
Return a dataset with one single match if it was found, or an |
||||
|
empty dataset if none or multiple matches were found. |
||||
|
""" |
||||
|
# Get usable rules to perform matches |
||||
|
usable = self._usable_for_load(model._name, converted_row.keys()) |
||||
|
|
||||
|
# Traverse usable combinations |
||||
|
for combination in usable: |
||||
|
combination_valid = True |
||||
|
domain = list() |
||||
|
|
||||
|
for field in combination.field_ids: |
||||
|
# Check imported value if it is a conditional field |
||||
|
if field.conditional: |
||||
|
# Invalid combinations are skipped |
||||
|
if imported_row[field.name] != field.imported_value: |
||||
|
combination_valid = False |
||||
|
break |
||||
|
|
||||
|
domain.append((field.name, "=", converted_row[field.name])) |
||||
|
|
||||
|
if not combination_valid: |
||||
|
continue |
||||
|
|
||||
|
match = model.search(domain) |
||||
|
|
||||
|
# When a single match is found, stop searching |
||||
|
if len(match) == 1: |
||||
|
return match |
||||
|
|
||||
|
# Return an empty match if none or multiple was found |
||||
|
return model |
||||
|
|
||||
|
@api.model |
||||
|
def _load_wrapper(self): |
||||
|
"""Create a new load patch method.""" |
||||
|
@api.model |
||||
|
def wrapper(self, fields, data): |
||||
|
"""Try to identify rows by other pseudo-unique keys. |
||||
|
|
||||
|
It searches for rows that have no XMLID specified, and gives them |
||||
|
one if any :attr:`~.field_ids` combination is found. With a valid |
||||
|
XMLID in place, Odoo will understand that it must *update* the |
||||
|
record instead of *creating* a new one. |
||||
|
""" |
||||
|
newdata = list() |
||||
|
|
||||
|
# Data conversion to ORM format |
||||
|
import_fields = map(models.fix_import_export_id_paths, fields) |
||||
|
converted_data = self._convert_records( |
||||
|
self._extract_records(import_fields, data)) |
||||
|
|
||||
|
# Mock Odoo to believe the user is importing the ID field |
||||
|
if "id" not in fields: |
||||
|
fields.append("id") |
||||
|
import_fields.append(["id"]) |
||||
|
|
||||
|
# Needed to match with converted data field names |
||||
|
clean_fields = [f[0] for f in import_fields] |
||||
|
|
||||
|
for dbid, xmlid, record, info in converted_data: |
||||
|
row = dict(zip(clean_fields, data[info["record"]])) |
||||
|
match = self |
||||
|
|
||||
|
if xmlid: |
||||
|
# Skip rows with ID, they do not need all this |
||||
|
row["id"] = xmlid |
||||
|
elif dbid: |
||||
|
# Find the xmlid for this dbid |
||||
|
match = self.browse(dbid) |
||||
|
else: |
||||
|
# Store records that match a combination |
||||
|
match = self.env["base_import.match"]._match_find( |
||||
|
self, record, row) |
||||
|
|
||||
|
# Give a valid XMLID to this row if a match was found |
||||
|
row["id"] = (match._BaseModel__export_xml_id() |
||||
|
if match else row.get("id", u"")) |
||||
|
|
||||
|
# Store the modified row, in the same order as fields |
||||
|
newdata.append(tuple(row[f] for f in clean_fields)) |
||||
|
|
||||
|
# Leave the rest to Odoo itself |
||||
|
del data |
||||
|
return wrapper.origin(self, fields, newdata) |
||||
|
|
||||
|
# Flag to avoid confusions with other possible wrappers |
||||
|
wrapper.__base_import_match = True |
||||
|
|
||||
|
return wrapper |
||||
|
|
||||
|
@api.model |
||||
|
def _load_autopatch(self, model_name): |
||||
|
"""[Un]apply patch automatically.""" |
||||
|
self._load_unpatch(model_name) |
||||
|
if self.search([("model_name", "=", model_name)]): |
||||
|
self._load_patch(model_name) |
||||
|
|
||||
|
@api.model |
||||
|
def _load_patch(self, model_name): |
||||
|
"""Apply patch for :param:`model_name`'s load method. |
||||
|
|
||||
|
:param str model_name: |
||||
|
Model technical name, such as ``res.partner``. |
||||
|
""" |
||||
|
self.env[model_name]._patch_method( |
||||
|
"load", self._load_wrapper()) |
||||
|
|
||||
|
@api.model |
||||
|
def _load_unpatch(self, model_name): |
||||
|
"""Apply patch for :param:`model_name`'s load method. |
||||
|
|
||||
|
:param str model_name: |
||||
|
Model technical name, such as ``res.partner``. |
||||
|
""" |
||||
|
model = self.env[model_name] |
||||
|
|
||||
|
# Unapply patch only if there is one |
||||
|
try: |
||||
|
if model.load.__base_import_match: |
||||
|
model._revert_method("load") |
||||
|
except AttributeError: |
||||
|
pass |
||||
|
|
||||
|
@api.model |
||||
|
def _usable_for_load(self, model_name, fields): |
||||
|
"""Return a set of elements usable for calling ``load()``. |
||||
|
|
||||
|
:param str model_name: |
||||
|
Technical name of the model where you are loading data. |
||||
|
E.g. ``res.partner``. |
||||
|
|
||||
|
:param list(str|bool) fields: |
||||
|
List of field names being imported. |
||||
|
""" |
||||
|
result = self |
||||
|
available = self.search([("model_name", "=", model_name)]) |
||||
|
|
||||
|
# Use only criteria with all required fields to match |
||||
|
for record in available: |
||||
|
if all(f.name in fields for f in record.field_ids): |
||||
|
result += record |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
|
||||
|
class BaseImportMatchField(models.Model): |
||||
|
_name = "base_import.match.field" |
||||
|
_description = "Field import match definition" |
||||
|
|
||||
|
name = fields.Char( |
||||
|
related="field_id.name") |
||||
|
field_id = fields.Many2one( |
||||
|
comodel_name="ir.model.fields", |
||||
|
string="Field", |
||||
|
required=True, |
||||
|
ondelete="cascade", |
||||
|
domain="[('model_id', '=', model_id)]", |
||||
|
help="Field that will be part of an unique key.") |
||||
|
match_id = fields.Many2one( |
||||
|
comodel_name="base_import.match", |
||||
|
string="Match", |
||||
|
required=True) |
||||
|
model_id = fields.Many2one( |
||||
|
related="match_id.model_id") |
||||
|
conditional = fields.Boolean( |
||||
|
help="Enable if you want to use this field only in some conditions.") |
||||
|
imported_value = fields.Char( |
||||
|
help="If the imported value is not this, the whole matching rule will " |
||||
|
"be discarded. Be careful, this data is always treated as a " |
||||
|
"string, and comparison is case-sensitive so if you set 'True', " |
||||
|
"it will NOT match '1' nor 'true', only EXACTLY 'True'.") |
||||
|
|
||||
|
@api.multi |
||||
|
@api.onchange("field_id", "match_id", "conditional", "imported_value") |
||||
|
def _onchange_match_id_name(self): |
||||
|
"""Update match name.""" |
||||
|
self.mapped("match_id")._compute_name() |
@ -0,0 +1,5 @@ |
|||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
||||
|
access_base_import_match,Access base_import.match,model_base_import_match,base.group_user,1,0,0,0 |
||||
|
access_base_import_match_field,Access base_import.match.field,model_base_import_match_field,base.group_user,1,0,0,0 |
||||
|
write_base_import_match,Write base_import.match,model_base_import_match,base.group_system,1,1,1,1 |
||||
|
write_base_import_match_field,Write base_import.match.field,model_base_import_match_field,base.group_system,1,1,1,1 |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from . import test_import |
@ -0,0 +1,2 @@ |
|||||
|
email,name |
||||
|
michel.fletcher@agrolait.example.com,Michel Fletcher Changed |
@ -0,0 +1,2 @@ |
|||||
|
function,name |
||||
|
Function Changed,Michel Fletcher |
@ -0,0 +1,2 @@ |
|||||
|
name,is_company,parent_id/id,email |
||||
|
Michel Fletcher,False,base.res_partner_2,changed@agrolait.example.com |
@ -0,0 +1,2 @@ |
|||||
|
name,vat,is_company |
||||
|
Agrolait Changed,BE0477472701,True |
@ -0,0 +1,2 @@ |
|||||
|
login,name |
||||
|
demo,Demo User Changed |
@ -0,0 +1,68 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from os import path |
||||
|
from openerp.tests.common import TransactionCase |
||||
|
|
||||
|
|
||||
|
PATH = path.join(path.dirname(__file__), "import_data", "%s.csv") |
||||
|
OPTIONS = { |
||||
|
"headers": True, |
||||
|
"quoting": '"', |
||||
|
"separator": ",", |
||||
|
} |
||||
|
|
||||
|
|
||||
|
class ImportCase(TransactionCase): |
||||
|
def _base_import_record(self, res_model, file_name): |
||||
|
"""Create and return a ``base_import.import`` record.""" |
||||
|
with open(PATH % file_name) as demo_file: |
||||
|
return self.env["base_import.import"].create({ |
||||
|
"res_model": res_model, |
||||
|
"file": demo_file.read(), |
||||
|
"file_name": "%s.csv" % file_name, |
||||
|
"file_type": "csv", |
||||
|
}) |
||||
|
|
||||
|
def test_res_partner_vat(self): |
||||
|
"""Change name based on VAT.""" |
||||
|
agrolait = self.env.ref("base.res_partner_2") |
||||
|
agrolait.vat = "BE0477472701" |
||||
|
record = self._base_import_record("res.partner", "res_partner_vat") |
||||
|
record.do(["name", "vat", "is_company"], OPTIONS) |
||||
|
agrolait.env.invalidate_all() |
||||
|
self.assertEqual(agrolait.name, "Agrolait Changed") |
||||
|
|
||||
|
def test_res_partner_parent_name_is_company(self): |
||||
|
"""Change email based on parent_id, name and is_company.""" |
||||
|
record = self._base_import_record( |
||||
|
"res.partner", "res_partner_parent_name_is_company") |
||||
|
record.do(["name", "is_company", "parent_id/id", "email"], OPTIONS) |
||||
|
self.assertEqual( |
||||
|
self.env.ref("base.res_partner_address_4").email, |
||||
|
"changed@agrolait.example.com") |
||||
|
|
||||
|
def test_res_partner_email(self): |
||||
|
"""Change name based on email.""" |
||||
|
record = self._base_import_record("res.partner", "res_partner_email") |
||||
|
record.do(["email", "name"], OPTIONS) |
||||
|
self.assertEqual( |
||||
|
self.env.ref("base.res_partner_address_4").name, |
||||
|
"Michel Fletcher Changed") |
||||
|
|
||||
|
def test_res_partner_name(self): |
||||
|
"""Change function based on name.""" |
||||
|
record = self._base_import_record("res.partner", "res_partner_name") |
||||
|
record.do(["function", "name"], OPTIONS) |
||||
|
self.assertEqual( |
||||
|
self.env.ref("base.res_partner_address_4").function, |
||||
|
"Function Changed") |
||||
|
|
||||
|
def test_res_users_login(self): |
||||
|
"""Change name based on login.""" |
||||
|
record = self._base_import_record("res.users", "res_users_login") |
||||
|
record.do(["login", "name"], OPTIONS) |
||||
|
self.assertEqual( |
||||
|
self.env.ref("base.user_demo").name, |
||||
|
"Demo User Changed") |
@ -0,0 +1,80 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis |
||||
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> |
||||
|
|
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record id="match_form_view" model="ir.ui.view"> |
||||
|
<field name="name">Import match form view</field> |
||||
|
<field name="model">base_import.match</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Import Match"> |
||||
|
<sheet> |
||||
|
<h1> |
||||
|
<field name="name"/> |
||||
|
</h1> |
||||
|
<group> |
||||
|
<field name="model_id"/> |
||||
|
<field name="field_ids"> |
||||
|
<tree editable="bottom"> |
||||
|
<field name="field_id" |
||||
|
options="{'no_create': True}"/> |
||||
|
<field name="match_id" invisible="True"/> |
||||
|
<field name="model_id" invisible="True"/> |
||||
|
<field name="conditional"/> |
||||
|
<field |
||||
|
name="imported_value" |
||||
|
attrs="{ |
||||
|
'readonly': [ |
||||
|
('conditional', '=', False), |
||||
|
], |
||||
|
}"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
<field name="sequence"/> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="match_tree_view" model="ir.ui.view"> |
||||
|
<field name="name">Import match tree view</field> |
||||
|
<field name="model">base_import.match</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Import Match"> |
||||
|
<field name="name"/> |
||||
|
<field name="sequence" invisible="True"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="match_search_view" model="ir.ui.view"> |
||||
|
<field name="name">Import match search view</field> |
||||
|
<field name="model">base_import.match</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<search string="Import Match"> |
||||
|
<field name="name"/> |
||||
|
<field name="model_id"/> |
||||
|
<field name="field_ids"/> |
||||
|
<separator/> |
||||
|
<group expand="0" string="Group By"> |
||||
|
<filter string="Model" context="{'group_by': 'model_id'}"/> |
||||
|
</group> |
||||
|
</search> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<act_window |
||||
|
name="Import Match" |
||||
|
res_model="base_import.match" |
||||
|
id="match_action"/> |
||||
|
|
||||
|
<menuitem |
||||
|
id="match_menu" |
||||
|
action="match_action" |
||||
|
parent="base.next_id_9"/> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue