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.

220 lines
7.2 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. from openerp import api, fields, models
  5. from openerp.exceptions import except_orm as ValueError # TODO remove in v9
  6. from openerp import SUPERUSER_ID # TODO remove in v10
  7. class BaseImportMatch(models.Model):
  8. _name = "base_import.match"
  9. _description = "Deduplicate settings prior to CSV imports."
  10. _order = "sequence, name"
  11. _sql_constraints = [
  12. ("name_unique", "UNIQUE(name)", "Duplicated match!"),
  13. ]
  14. name = fields.Char(
  15. compute="_compute_name",
  16. store=True,
  17. index=True)
  18. sequence = fields.Integer(index=True)
  19. model_id = fields.Many2one(
  20. "ir.model",
  21. "Model",
  22. required=True,
  23. ondelete="cascade",
  24. domain=[("osv_memory", "=", False)],
  25. help="In this model you will apply the match.")
  26. model_name = fields.Char(
  27. related="model_id.model",
  28. store=True,
  29. index=True)
  30. field_ids = fields.Many2many(
  31. "ir.model.fields",
  32. string="Fields",
  33. required=True,
  34. domain="[('model_id', '=', model_id)]",
  35. help="Fields that will define an unique key.")
  36. @api.multi
  37. @api.onchange("model_id")
  38. def _onchange_model_id(self):
  39. self.field_ids = False
  40. @api.model
  41. def create(self, vals):
  42. """Wrap the model after creation."""
  43. result = super(BaseImportMatch, self).create(vals)
  44. self._load_autopatch(result.model_name)
  45. return result
  46. @api.multi
  47. def unlink(self):
  48. """Unwrap the model after deletion."""
  49. models = set(self.mapped("model_name"))
  50. result = super(BaseImportMatch, self).unlink()
  51. for model in models:
  52. self._load_autopatch(model)
  53. return result
  54. @api.multi
  55. def write(self, vals):
  56. """Wrap the model after writing."""
  57. result = super(BaseImportMatch, self).write(vals)
  58. if "model_id" in vals or "model_name" in vals:
  59. for s in self:
  60. self._load_autopatch(s.model_name)
  61. return result
  62. # TODO convert to @api.model_cr in v10
  63. def _register_hook(self, cr):
  64. """Autopatch on init."""
  65. models = set(
  66. self.browse(
  67. cr,
  68. SUPERUSER_ID,
  69. self.search(cr, SUPERUSER_ID, list()))
  70. .mapped("model_name"))
  71. for model in models:
  72. self._load_autopatch(cr, SUPERUSER_ID, model)
  73. @api.multi
  74. @api.depends("model_id", "field_ids")
  75. def _compute_name(self):
  76. """Automatic self-descriptive name for the setting records."""
  77. for s in self:
  78. s.name = "{}: {}".format(
  79. s.model_id.display_name,
  80. " + ".join(s.field_ids.mapped("display_name")))
  81. @api.model
  82. def _load_wrapper(self):
  83. """Create a new load patch method."""
  84. @api.model
  85. def wrapper(self, fields, data):
  86. """Try to identify rows by other pseudo-unique keys.
  87. It searches for rows that have no XMLID specified, and gives them
  88. one if any :attr:`~.field_ids` combination is found. With a valid
  89. XMLID in place, Odoo will understand that it must *update* the
  90. record instead of *creating* a new one.
  91. """
  92. newdata = list()
  93. # Mock Odoo to believe the user is importing the ID field
  94. if "id" not in fields:
  95. fields.append("id")
  96. # Needed to work with relational fields
  97. clean_fields = [
  98. models.fix_import_export_id_paths(f)[0] for f in fields]
  99. # Get usable rules to perform matches
  100. usable = self.env["base_import.match"]._usable_for_load(
  101. self._name, clean_fields)
  102. for row in (dict(zip(clean_fields, r)) for r in data):
  103. # All rows need an ID
  104. if "id" not in row:
  105. row["id"] = u""
  106. # Skip rows with ID, they do not need all this
  107. elif row["id"]:
  108. continue
  109. # Store records that match a combination
  110. match = self
  111. for combination in usable:
  112. match |= self.search(
  113. [(field.name, "=", row[field.name])
  114. for field in combination.field_ids])
  115. # When a single match is found, stop searching
  116. if len(match) != 1:
  117. break
  118. # Only one record should have been found
  119. try:
  120. match.ensure_one()
  121. # You hit this because...
  122. # a. No match. Odoo must create a new record.
  123. # b. Multiple matches. No way to know which is the right
  124. # one, so we let Odoo create a new record or raise
  125. # the corresponding exception.
  126. # In any case, we must do nothing.
  127. except ValueError:
  128. continue
  129. # Give a valid XMLID to this row
  130. row["id"] = match._BaseModel__export_xml_id()
  131. # Store the modified row, in the same order as fields
  132. newdata.append(tuple(row[f] for f in clean_fields))
  133. # Leave the rest to Odoo itself
  134. del data
  135. return wrapper.origin(self, fields, newdata)
  136. # Flag to avoid confusions with other possible wrappers
  137. wrapper.__base_import_match = True
  138. return wrapper
  139. @api.model
  140. def _load_autopatch(self, model_name):
  141. """[Un]apply patch automatically."""
  142. self._load_unpatch(model_name)
  143. if self.search([("model_name", "=", model_name)]):
  144. self._load_patch(model_name)
  145. @api.model
  146. def _load_patch(self, model_name):
  147. """Apply patch for :param:`model_name`'s load method.
  148. :param str model_name:
  149. Model technical name, such as ``res.partner``.
  150. """
  151. self.env[model_name]._patch_method(
  152. "load", self._load_wrapper())
  153. @api.model
  154. def _load_unpatch(self, model_name):
  155. """Apply patch for :param:`model_name`'s load method.
  156. :param str model_name:
  157. Model technical name, such as ``res.partner``.
  158. """
  159. model = self.env[model_name]
  160. # Unapply patch only if there is one
  161. try:
  162. if model.load.__base_import_match:
  163. model._revert_method("load")
  164. except AttributeError:
  165. pass
  166. @api.model
  167. def _usable_for_load(self, model_name, fields):
  168. """Return a set of elements usable for calling ``load()``.
  169. :param str model_name:
  170. Technical name of the model where you are loading data.
  171. E.g. ``res.partner``.
  172. :param list(str|bool) fields:
  173. List of field names being imported.
  174. """
  175. result = self
  176. available = self.search([("model_name", "=", model_name)])
  177. # Use only criteria with all required fields to match
  178. for record in available:
  179. if all(f.name in fields for f in record.field_ids):
  180. result += record
  181. return result