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.

297 lines
10 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 import SUPERUSER_ID # TODO remove in v10
  6. class BaseImportMatch(models.Model):
  7. _name = "base_import.match"
  8. _description = "Deduplicate settings prior to CSV imports."
  9. _order = "sequence, name"
  10. name = fields.Char(
  11. compute="_compute_name",
  12. store=True,
  13. index=True)
  14. sequence = fields.Integer(index=True)
  15. model_id = fields.Many2one(
  16. "ir.model",
  17. "Model",
  18. required=True,
  19. ondelete="cascade",
  20. domain=[("osv_memory", "=", False)],
  21. help="In this model you will apply the match.")
  22. model_name = fields.Char(
  23. related="model_id.model",
  24. store=True,
  25. index=True)
  26. field_ids = fields.One2many(
  27. comodel_name="base_import.match.field",
  28. inverse_name="match_id",
  29. string="Fields",
  30. required=True,
  31. help="Fields that will define an unique key.")
  32. @api.multi
  33. @api.onchange("model_id")
  34. def _onchange_model_id(self):
  35. self.field_ids.unlink()
  36. @api.model
  37. def create(self, vals):
  38. """Wrap the model after creation."""
  39. result = super(BaseImportMatch, self).create(vals)
  40. self._load_autopatch(result.model_name)
  41. return result
  42. @api.multi
  43. def unlink(self):
  44. """Unwrap the model after deletion."""
  45. models = set(self.mapped("model_name"))
  46. result = super(BaseImportMatch, self).unlink()
  47. for model in models:
  48. self._load_autopatch(model)
  49. return result
  50. @api.multi
  51. def write(self, vals):
  52. """Wrap the model after writing."""
  53. result = super(BaseImportMatch, self).write(vals)
  54. if "model_id" in vals or "model_name" in vals:
  55. for s in self:
  56. self._load_autopatch(s.model_name)
  57. return result
  58. # TODO convert to @api.model_cr in v10
  59. def _register_hook(self, cr):
  60. """Autopatch on init."""
  61. models = set(
  62. self.browse(
  63. cr,
  64. SUPERUSER_ID,
  65. self.search(cr, SUPERUSER_ID, list()))
  66. .mapped("model_name"))
  67. for model in models:
  68. self._load_autopatch(cr, SUPERUSER_ID, model)
  69. @api.multi
  70. @api.depends("model_id", "field_ids")
  71. def _compute_name(self):
  72. """Automatic self-descriptive name for the setting records."""
  73. for s in self:
  74. s.name = u"{}: {}".format(
  75. s.model_id.display_name,
  76. " + ".join(
  77. s.field_ids.mapped(
  78. lambda r: (
  79. (u"{} ({})" if r.conditional else u"{}").format(
  80. r.field_id.name,
  81. r.imported_value)))))
  82. @api.model
  83. def _match_find(self, model, converted_row, imported_row):
  84. """Find a update target for the given row.
  85. This will traverse by order all match rules that can be used with the
  86. imported data, and return a match for the first rule that returns a
  87. single result.
  88. :param openerp.models.Model model:
  89. Model object that is being imported.
  90. :param dict converted_row:
  91. Row converted to Odoo api format, like the 3rd value that
  92. :meth:`openerp.models.Model._convert_records` returns.
  93. :param dict imported_row:
  94. Row as it is being imported, in format::
  95. {
  96. "field_name": "string value",
  97. "other_field": "True",
  98. ...
  99. }
  100. :return openerp.models.Model:
  101. Return a dataset with one single match if it was found, or an
  102. empty dataset if none or multiple matches were found.
  103. """
  104. # Get usable rules to perform matches
  105. usable = self._usable_for_load(model._name, converted_row.keys())
  106. # Traverse usable combinations
  107. for combination in usable:
  108. combination_valid = True
  109. domain = list()
  110. for field in combination.field_ids:
  111. # Check imported value if it is a conditional field
  112. if field.conditional:
  113. # Invalid combinations are skipped
  114. if imported_row[field.name] != field.imported_value:
  115. combination_valid = False
  116. break
  117. domain.append((field.name, "=", converted_row[field.name]))
  118. if not combination_valid:
  119. continue
  120. match = model.search(domain)
  121. # When a single match is found, stop searching
  122. if len(match) == 1:
  123. return match
  124. # Return an empty match if none or multiple was found
  125. return model
  126. @api.model
  127. def _load_wrapper(self):
  128. """Create a new load patch method."""
  129. @api.model
  130. def wrapper(self, fields, data):
  131. """Try to identify rows by other pseudo-unique keys.
  132. It searches for rows that have no XMLID specified, and gives them
  133. one if any :attr:`~.field_ids` combination is found. With a valid
  134. XMLID in place, Odoo will understand that it must *update* the
  135. record instead of *creating* a new one.
  136. """
  137. newdata = list()
  138. # Data conversion to ORM format
  139. import_fields = map(models.fix_import_export_id_paths, fields)
  140. converted_data = self._convert_records(
  141. self._extract_records(import_fields, data))
  142. # Mock Odoo to believe the user is importing the ID field
  143. if "id" not in fields:
  144. fields.append("id")
  145. import_fields.append(["id"])
  146. # Needed to match with converted data field names
  147. clean_fields = [f[0] for f in import_fields]
  148. for dbid, xmlid, record, info in converted_data:
  149. row = dict(zip(clean_fields, data[info["record"]]))
  150. match = self
  151. if xmlid:
  152. # Skip rows with ID, they do not need all this
  153. row["id"] = xmlid
  154. elif dbid:
  155. # Find the xmlid for this dbid
  156. match = self.browse(dbid)
  157. else:
  158. # Store records that match a combination
  159. match = self.env["base_import.match"]._match_find(
  160. self, record, row)
  161. # Give a valid XMLID to this row if a match was found
  162. row["id"] = (match._BaseModel__export_xml_id()
  163. if match else row.get("id", u""))
  164. # Store the modified row, in the same order as fields
  165. newdata.append(tuple(row[f] for f in clean_fields))
  166. # Leave the rest to Odoo itself
  167. del data
  168. return wrapper.origin(self, fields, newdata)
  169. # Flag to avoid confusions with other possible wrappers
  170. wrapper.__base_import_match = True
  171. return wrapper
  172. @api.model
  173. def _load_autopatch(self, model_name):
  174. """[Un]apply patch automatically."""
  175. self._load_unpatch(model_name)
  176. if self.search([("model_name", "=", model_name)]):
  177. self._load_patch(model_name)
  178. @api.model
  179. def _load_patch(self, model_name):
  180. """Apply patch for :param:`model_name`'s load method.
  181. :param str model_name:
  182. Model technical name, such as ``res.partner``.
  183. """
  184. self.env[model_name]._patch_method(
  185. "load", self._load_wrapper())
  186. @api.model
  187. def _load_unpatch(self, model_name):
  188. """Apply patch for :param:`model_name`'s load method.
  189. :param str model_name:
  190. Model technical name, such as ``res.partner``.
  191. """
  192. model = self.env[model_name]
  193. # Unapply patch only if there is one
  194. try:
  195. if model.load.__base_import_match:
  196. model._revert_method("load")
  197. except AttributeError:
  198. pass
  199. @api.model
  200. def _usable_for_load(self, model_name, fields):
  201. """Return a set of elements usable for calling ``load()``.
  202. :param str model_name:
  203. Technical name of the model where you are loading data.
  204. E.g. ``res.partner``.
  205. :param list(str|bool) fields:
  206. List of field names being imported.
  207. """
  208. result = self
  209. available = self.search([("model_name", "=", model_name)])
  210. # Use only criteria with all required fields to match
  211. for record in available:
  212. if all(f.name in fields for f in record.field_ids):
  213. result += record
  214. return result
  215. class BaseImportMatchField(models.Model):
  216. _name = "base_import.match.field"
  217. _description = "Field import match definition"
  218. name = fields.Char(
  219. related="field_id.name")
  220. field_id = fields.Many2one(
  221. comodel_name="ir.model.fields",
  222. string="Field",
  223. required=True,
  224. ondelete="cascade",
  225. domain="[('model_id', '=', model_id)]",
  226. help="Field that will be part of an unique key.")
  227. match_id = fields.Many2one(
  228. comodel_name="base_import.match",
  229. string="Match",
  230. ondelete="cascade",
  231. required=True)
  232. model_id = fields.Many2one(
  233. related="match_id.model_id")
  234. conditional = fields.Boolean(
  235. help="Enable if you want to use this field only in some conditions.")
  236. imported_value = fields.Char(
  237. help="If the imported value is not this, the whole matching rule will "
  238. "be discarded. Be careful, this data is always treated as a "
  239. "string, and comparison is case-sensitive so if you set 'True', "
  240. "it will NOT match '1' nor 'true', only EXACTLY 'True'.")
  241. @api.multi
  242. @api.onchange("field_id", "match_id", "conditional", "imported_value")
  243. def _onchange_match_id_name(self):
  244. """Update match name."""
  245. self.mapped("match_id")._compute_name()