You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

177 lines
6.2 KiB

  1. # Copyright 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
  2. # Copyright 2016 Tecnativa - Vicent Cubells
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import logging
  5. from odoo import api, fields, models, tools
  6. _logger = logging.getLogger(__name__)
  7. class BaseImportMatch(models.Model):
  8. _name = "base_import.match"
  9. _description = "Deduplicate settings prior to CSV imports."
  10. _order = "sequence, name"
  11. name = fields.Char(compute="_compute_name", store=True, index=True)
  12. sequence = fields.Integer(index=True)
  13. model_id = fields.Many2one(
  14. "ir.model",
  15. "Model",
  16. required=True,
  17. ondelete="cascade",
  18. domain=[("transient", "=", False)],
  19. help="In this model you will apply the match.",
  20. )
  21. model_name = fields.Char(
  22. string="Model name", related="model_id.model", store=True, index=True
  23. )
  24. field_ids = fields.One2many(
  25. comodel_name="base_import.match.field",
  26. inverse_name="match_id",
  27. string="Fields",
  28. required=True,
  29. help="Fields that will define an unique key.",
  30. )
  31. @api.onchange("model_id")
  32. def _onchange_model_id(self):
  33. self.field_ids = False
  34. @api.depends("model_id", "field_ids")
  35. def _compute_name(self):
  36. """Automatic self-descriptive name for the setting records."""
  37. for one in self:
  38. one.name = u"{}: {}".format(
  39. one.model_id.display_name,
  40. " + ".join(one.field_ids.mapped("display_name")),
  41. )
  42. @api.model
  43. def _match_find(self, model, converted_row, imported_row):
  44. """Find a update target for the given row.
  45. This will traverse by order all match rules that can be used with the
  46. imported data, and return a match for the first rule that returns a
  47. single result.
  48. :param odoo.models.Model model:
  49. Model object that is being imported.
  50. :param dict converted_row:
  51. Row converted to Odoo api format, like the 3rd value that
  52. :meth:`odoo.models.Model._convert_records` returns.
  53. :param dict imported_row:
  54. Row as it is being imported, in format::
  55. {
  56. "field_name": "string value",
  57. "other_field": "True",
  58. ...
  59. }
  60. :return odoo.models.Model:
  61. Return a dataset with one single match if it was found, or an
  62. empty dataset if none or multiple matches were found.
  63. """
  64. # Get usable rules to perform matches
  65. usable = self._usable_rules(model._name, converted_row)
  66. # Traverse usable combinations
  67. for combination in usable:
  68. combination_valid = True
  69. domain = list()
  70. for field in combination.field_ids:
  71. # Check imported value if it is a conditional field
  72. if field.conditional:
  73. # Invalid combinations are skipped
  74. if imported_row[field.name] != field.imported_value:
  75. combination_valid = False
  76. break
  77. domain.append((field.name, "=", converted_row[field.name]))
  78. if not combination_valid:
  79. continue
  80. match = model.search(domain)
  81. # When a single match is found, stop searching
  82. if len(match) == 1:
  83. return match
  84. elif match:
  85. _logger.warning(
  86. "Found multiple matches for model %s and domain %s; "
  87. "falling back to default behavior (create new record)",
  88. model._name,
  89. domain,
  90. )
  91. # Return an empty match if none or multiple was found
  92. return model
  93. @api.model
  94. @tools.ormcache("model_name", "frozenset(fields)")
  95. def _usable_rules(self, model_name, fields):
  96. """Return a set of elements usable for calling ``load()``.
  97. :param str model_name:
  98. Technical name of the model where you are loading data.
  99. E.g. ``res.partner``.
  100. :param list(str|bool) fields:
  101. List of field names being imported.
  102. :return bool:
  103. Indicates if we should patch its load method.
  104. """
  105. result = self
  106. available = self.search([("model_name", "=", model_name)])
  107. # Use only criteria with all required fields to match
  108. for record in available:
  109. if all(f.name in fields for f in record.field_ids):
  110. result |= record
  111. return result
  112. class BaseImportMatchField(models.Model):
  113. _name = "base_import.match.field"
  114. _description = "Field import match definition"
  115. name = fields.Char(related="field_id.name")
  116. field_id = fields.Many2one(
  117. comodel_name="ir.model.fields",
  118. string="Field",
  119. required=True,
  120. ondelete="cascade",
  121. domain="[('model_id', '=', model_id)]",
  122. help="Field that will be part of an unique key.",
  123. )
  124. match_id = fields.Many2one(
  125. comodel_name="base_import.match",
  126. string="Match",
  127. ondelete="cascade",
  128. required=True,
  129. )
  130. model_id = fields.Many2one(related="match_id.model_id")
  131. conditional = fields.Boolean(
  132. help="Enable if you want to use this field only in some conditions."
  133. )
  134. imported_value = fields.Char(
  135. help="If the imported value is not this, the whole matching rule will "
  136. "be discarded. Be careful, this data is always treated as a "
  137. "string, and comparison is case-sensitive so if you set 'True', "
  138. "it will NOT match '1' nor 'true', only EXACTLY 'True'."
  139. )
  140. @api.depends("conditional", "field_id", "imported_value")
  141. def name_get(self):
  142. result = []
  143. for one in self:
  144. pattern = u"{name} ({cond})" if one.conditional else u"{name}"
  145. name = pattern.format(
  146. name=one.field_id.name,
  147. cond=one.imported_value,
  148. )
  149. result.append((one.id, name))
  150. return result
  151. @api.onchange("field_id", "match_id", "conditional", "imported_value")
  152. def _onchange_match_id_name(self):
  153. """Update match name."""
  154. self.mapped("match_id")._compute_name()