Skip to content

Commit

Permalink
Merge PR #120 into 14.0
Browse files Browse the repository at this point in the history
Signed-off-by hparfr
  • Loading branch information
shopinvader-git-bot committed Dec 21, 2023
2 parents 4bca831 + 5bcf998 commit f35401b
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 8 deletions.
15 changes: 10 additions & 5 deletions pattern_import_export/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,22 @@ def _clean_identifier_key(self, res, ident_keys):
res[key.replace(IDENTIFIER_SUFFIX, "")] = res.pop(key)

def _convert_value_to_domain(self, field_name, value):
# fieldname may be None
# todo: rename field_name to prefix
if isinstance(value, dict):
domain = []
subdom = []
for key, val in value.items():
if key == ".id":
# .id is internal db id, so we rename it
key = "id"
domain.append(("{}.{}".format(field_name, key), "=", val))
# field_name may be None
# then key = value directly
dom_key = f"{field_name}.{key}" if field_name else key
subdom += self._convert_value_to_domain(dom_key, val)
domain = subdom
else:
domain = [(field_name, "=", value)]
domain = [[field_name, "=", value]]
return domain

def _get_domain_from_identifier_key(self, res):
Expand All @@ -120,9 +127,7 @@ def _get_domain_from_identifier_key(self, res):
for key in list(res.keys()):
if key.endswith(IDENTIFIER_SUFFIX):
field_name = key.replace(IDENTIFIER_SUFFIX, "")
domain = expression.AND(
[domain, self._convert_value_to_domain(field_name, res[key])]
)
domain += self._convert_value_to_domain(field_name, res[key])
ident_keys.append(key)
return domain, ident_keys

Expand Down
25 changes: 24 additions & 1 deletion pattern_import_export/models/ir_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,30 @@ def _str_to_many2many(self, model, field, value):
def _str_to_many2one(self, model, field, value):
if isinstance(value, dict):
# odoo expect a list with one item
value = [value]
if len(value) == 1:
one_value = [value]
return super()._str_to_many2one(model, field, one_value)
else:
domain = model._convert_value_to_domain(None, value)
tosearch = field._related_comodel_name
record = self.env[tosearch].search(domain)
if len(record) > 1:
# TODO improve here
raise self._format_import_error(
ValueError,
_("%s Too many records found for %s in field '%s'"),
(_(record._description), domain, tosearch),
)
if len(record) == 0:
raise self._format_import_error(
ValueError,
_("%s No matching record found for %s in field '%s'"),
(_(record._description), domain, tosearch),
)

# call core function to be sure not to miss something
an_id, donotcare, w2 = self.db_id_for(model, field, ".id", record.id)
return an_id, [] + w2
return super()._str_to_many2one(model, field, value)

@api.model
Expand Down
17 changes: 15 additions & 2 deletions pattern_import_export/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,29 @@ You have two options:

* Open the tree view of any model and tick some record selection boxes (for this step, these don't matter, we only just want to show the sidebar).
* In the sidebar, click on the "Import with Pattern" button
* Select the pattern that you used to generate the export, upload your file and click import.
* Select a pattern, upload your file and click import.
* A "Pattern file" is created, and its job along with it. Depending on the success or failure of the job, you
will receive a red/green notification on your window. You can check the details in the appropriate Import/Export menu.

Or:

* Access the Import wizard through the Import/Export menu
* Select the Pattern that you want to use
* Select a pattern
* Click on the "Import" button


Import syntax
-------------

One of the strength of pattern_import_export module is the ability to
reference records by natural keys (business keys) instead of technical keys (xmlid or database id).

One or more columns can be the natural key of the record to find and update or to create a new record.
Each column in the natural key has to be suffixed by "#key".

One or more columns can be used as foreign keys can be accessed with "|" syntax. (for instance on partner: country_id|code )

Example
-------

Expand Down
57 changes: 57 additions & 0 deletions pattern_import_export/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,60 @@ def test_string_domain_is_ignored(self):
self._patch_search("res.country.state")
self.converter.db_id_for(model, field, "name", "Rio de Janeiro")
self.assertEqual(self.search_domain, [[("name", "=", "Rio de Janeiro")]])

def test_convert_value_to_domain(self):
field_name = None
value = {
"partner_id": {
"name": "abcdef",
"phone": "06707507",
"country_id": {
"code": "FR",
"name": "France",
},
"user_id": {
"name": "Someone",
"false": False,
"true": True,
"none": None,
},
},
"direct_value": "some string",
"another_partner_id": {"name": "abcdef", "phone": "0000000"},
}
expected = [
["partner_id.name", "=", "abcdef"],
["partner_id.phone", "=", "06707507"],
["partner_id.country_id.code", "=", "FR"],
["partner_id.country_id.name", "=", "France"],
["partner_id.user_id.name", "=", "Someone"],
["partner_id.user_id.false", "=", False],
["partner_id.user_id.true", "=", True],
["partner_id.user_id.none", "=", None],
["direct_value", "=", "some string"],
["another_partner_id.name", "=", "abcdef"],
["another_partner_id.phone", "=", "0000000"],
]

result = self.env["res.partner"]._convert_value_to_domain(field_name, value)
for expectation in expected:
self.assertIn(expectation, result)
self.assertEqual(len(expected), len(result))

# now test with a field_name
expected2 = [
["partner_id.name", "=", "abcdef"],
["partner_id.phone", "=", "06707507"],
["partner_id.country_id.code", "=", "FR"],
["partner_id.country_id.name", "=", "France"],
["partner_id.user_id.name", "=", "Someone"],
["partner_id.user_id.false", "=", False],
["partner_id.user_id.true", "=", True],
["partner_id.user_id.none", "=", None],
]
result2 = self.env["res.partner"]._convert_value_to_domain(
"partner_id", value["partner_id"]
)
for expectation in expected2:
self.assertIn(expectation, result2)
self.assertEqual(len(expected2), len(result2))
78 changes: 78 additions & 0 deletions pattern_import_export/tests/test_pattern_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TestPatternImport(PatternCommon, SavepointCase):
def setUpClass(cls):
super().setUpClass()
cls.pattern_config_m2m.export_format = "json"
cls.pattern_config_o2m.export_format = "json"
cls.pattern_config.export_format = "json"

def run_pattern_file(self, pattern_file):
Expand Down Expand Up @@ -229,6 +230,44 @@ def test_update_with_key(self):
self.assertFalse(records)
self.assertEqual(unique_name, self.user3.name)

def test_update_with_muli_cols_pkey(self):
"""ensure we identify the row to update
based on multiple columns with
different depths
"""
unique_name = str(uuid4())
data = [
{
"partner_id#key|email": self.user3.partner_id.email,
"partner_id#key|name": self.user3.partner_id.name,
"partner_id#key|country_id|code": self.user3.partner_id.country_id.code,
"name": unique_name,
}
]
pattern_file = self.create_pattern(self.pattern_config_m2m, "import", data)
records = self.run_pattern_file(pattern_file)
self.assertFalse(records)
self.assertEqual(unique_name, self.user3.name)

# same test with ambigus key
# the record should not be updated because the key
# returns more than 1 record
ambigus = self.user3.search(
[["country_id.code", "=", self.user3.partner_id.country_id.code]]
)
self.assertTrue(len(ambigus) > 1, "Verify conditions")
unique_name_2 = str(uuid4())
data = [
{
"partner_id#key|country_id|code": self.user3.partner_id.country_id.code,
"name": unique_name_2,
}
]
pattern_file = self.create_pattern(self.pattern_config_m2m, "import", data)
records = self.run_pattern_file(pattern_file)
self.assertFalse(records)
self.assertEqual(unique_name, self.user3.name, "Ensure value not updated")

def test_update_o2m_with_key(self):
unique_name = str(uuid4())
contact_1_name = str(uuid4())
Expand Down Expand Up @@ -273,6 +312,45 @@ def test_update_o2m_with_key_only_one_record(self):
self.run_pattern_file(pattern_file)
self.assertEqual(unique_name, self.partner_1.name)

def test_update_o2m_with_sub_keys(self):
unique_name = str(uuid4())

# state_ie_27,ie,"Antrim","AM"
# there is multiple state with code = AM
# in demo data, but only one with Currency = euro

# ensure we picked a reference to only one record
only_one_rec = self.env["res.country"].search(
[["state_ids.code", "=", "AM"], ["currency_id.symbol", "=", "€"]]
)
self.assertEqual(len(only_one_rec), 1, "Ensure data for test valid")
previous_country = self.partner_1.country_id
new_country = only_one_rec
data = [
{
"email#key": self.partner_1.email,
"phone#key": self.partner_1.phone,
"name": unique_name,
"country_id|currency_id|symbol": "€",
"country_id|state_ids|code": "AM",
"title|name": "Professor",
}
]
pattern_file = self.create_pattern(self.pattern_config_o2m, "import", data)
self.run_pattern_file(pattern_file)
self.assertNotEqual(previous_country.id, new_country.id)
self.assertEqual(
unique_name, self.partner_1.name, "direct field has been updated"
)
self.assertEqual(
new_country.id,
self.partner_1.country_id.id,
"relation field has been updated",
)
self.assertEqual(
"Professor", self.partner_1.title.name, "relation field has been updated"
)

@mute_logger("odoo.sql_db")
def test_wrong_import(self):
data = [{"login#key": self.user3.login, "name": ""}]
Expand Down

0 comments on commit f35401b

Please sign in to comment.