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.

244 lines
9.8 KiB

  1. # Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
  2. # Copyright 2017 Pedro M. Baeza <pedro.baeza@tecnativa.com>
  3. # License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
  4. from odoo import _, api, fields, models, SUPERUSER_ID
  5. from odoo.exceptions import ValidationError
  6. from odoo.tools.safe_eval import safe_eval
  7. class CustomInfoValue(models.Model):
  8. _description = "Custom information value"
  9. _name = "custom.info.value"
  10. _rec_name = 'value'
  11. _order = ("model, res_id, category_sequence, category_id, "
  12. "property_sequence, property_id")
  13. _sql_constraints = [
  14. ("property_owner",
  15. "UNIQUE (property_id, model, res_id)",
  16. "Another property with that name exists for that resource."),
  17. ]
  18. model = fields.Char(
  19. related="property_id.model", index=True, readonly=True,
  20. auto_join=True, store=True, compute_sudo=True,
  21. )
  22. owner_id = fields.Reference(
  23. selection="_selection_owner_id", string="Owner",
  24. compute="_compute_owner_id", inverse="_inverse_owner_id",
  25. help="Record that owns this custom value.",
  26. compute_sudo=True,
  27. )
  28. res_id = fields.Integer(
  29. string="Resource ID", required=True, index=True, store=True,
  30. ondelete="cascade",
  31. )
  32. property_id = fields.Many2one(
  33. comodel_name='custom.info.property', required=True, string='Property',
  34. readonly=True,
  35. )
  36. property_sequence = fields.Integer(
  37. related="property_id.sequence", store=True, index=True, readonly=True,
  38. )
  39. category_sequence = fields.Integer(
  40. string="Category Sequence",
  41. related="property_id.category_id.sequence", store=True, readonly=True,
  42. )
  43. category_id = fields.Many2one(
  44. related="property_id.category_id", store=True, readonly=True,
  45. )
  46. name = fields.Char(related='property_id.name', readonly=True)
  47. field_type = fields.Selection(
  48. related="property_id.field_type", readonly=True,
  49. )
  50. field_name = fields.Char(
  51. compute="_compute_field_name",
  52. help="Technical name of the field where the value is stored.",
  53. )
  54. required = fields.Boolean(related="property_id.required", readonly=True)
  55. value = fields.Char(
  56. compute="_compute_value", inverse="_inverse_value",
  57. search="_search_value",
  58. help="Value, always converted to/from the typed field.",
  59. )
  60. value_str = fields.Char(string="Text value", translate=True, index=True)
  61. value_int = fields.Integer(string="Whole number value", index=True)
  62. value_float = fields.Float(string="Decimal number value", index=True)
  63. value_bool = fields.Boolean(string="Yes/No value", index=True)
  64. value_id = fields.Many2one(
  65. comodel_name="custom.info.option", string="Selection value",
  66. ondelete="cascade", domain="[('property_ids', '=', property_id)]",
  67. )
  68. @api.multi
  69. def check_access_rule(self, operation):
  70. """You access a value if you access its owner record."""
  71. if self.env.uid != SUPERUSER_ID:
  72. for record in self.filtered('owner_id'):
  73. record.owner_id.check_access_rights(operation)
  74. record.owner_id.check_access_rule(operation)
  75. return super().check_access_rule(operation)
  76. @api.model
  77. def _selection_owner_id(self):
  78. """You can choose among models linked to a template."""
  79. models = self.env["ir.model.fields"].search([
  80. ("ttype", "=", "many2one"),
  81. ("relation", "=", "custom.info.template"),
  82. ("model_id.transient", "=", False),
  83. "!", ("model", "=like", "custom.info.%"),
  84. ]).mapped("model_id")
  85. models = models.search([("id", "in", models.ids)], order="name")
  86. return [(m.model, m.name) for m in models
  87. if m.model in self.env and self.env[m.model]._auto]
  88. @api.multi
  89. @api.depends("property_id.field_type")
  90. def _compute_field_name(self):
  91. """Get the technical name where the real typed value is stored."""
  92. for s in self:
  93. s.field_name = "value_{!s}".format(s.property_id.field_type)
  94. @api.multi
  95. @api.depends("res_id", "model")
  96. def _compute_owner_id(self):
  97. """Get the id from the linked record."""
  98. for record in self:
  99. record.owner_id = "{},{}".format(record.model, record.res_id)
  100. @api.multi
  101. def _inverse_owner_id(self):
  102. """Store the owner according to the model and ID."""
  103. for record in self.filtered('owner_id'):
  104. record.model = record.owner_id._name
  105. record.res_id = record.owner_id.id
  106. @api.multi
  107. @api.depends("property_id.field_type", "field_name", "value_str",
  108. "value_int", "value_float", "value_bool", "value_id")
  109. def _compute_value(self):
  110. """Get the value as a string, from the original field."""
  111. for s in self:
  112. if s.field_type == "id":
  113. s.value = s.value_id.display_name
  114. elif s.field_type == "bool":
  115. s.value = _("Yes") if s.value_bool else _("No")
  116. else:
  117. s.value = getattr(s, s.field_name, False)
  118. @api.multi
  119. def _inverse_value(self):
  120. """Write the value correctly converted in the typed field."""
  121. for record in self:
  122. if (record.field_type == "id"
  123. and record.value == record.value_id.display_name):
  124. # Avoid another search that can return a different value
  125. continue
  126. record[record.field_name] = self._transform_value(
  127. record.value, record.field_type, record.property_id,
  128. )
  129. @api.one
  130. @api.constrains("property_id", "value_str", "value_int", "value_float")
  131. def _check_min_max_limits(self):
  132. """Ensure value falls inside the property's stablished limits."""
  133. minimum, maximum = self.property_id.minimum, self.property_id.maximum
  134. if minimum <= maximum:
  135. value = self[self.field_name]
  136. if not value:
  137. # This is a job for :meth:`.~_check_required`
  138. return
  139. if self.field_type == "str":
  140. number = len(self.value_str)
  141. message = _(
  142. "Length for %(prop)s is %(val)s, but it should be "
  143. "between %(min)d and %(max)d.")
  144. elif self.field_type in {"int", "float"}:
  145. number = value
  146. if self.field_type == "int":
  147. message = _(
  148. "Value for %(prop)s is %(val)s, but it should be "
  149. "between %(min)d and %(max)d.")
  150. else:
  151. message = _(
  152. "Value for %(prop)s is %(val)s, but it should be "
  153. "between %(min)f and %(max)f.")
  154. else:
  155. return
  156. if not minimum <= number <= maximum:
  157. raise ValidationError(message % {
  158. "prop": self.property_id.display_name,
  159. "val": number,
  160. "min": minimum,
  161. "max": maximum,
  162. })
  163. @api.multi
  164. @api.onchange("property_id")
  165. def _onchange_property_set_default_value(self):
  166. """Load default value for this property."""
  167. for record in self:
  168. if not record.value and record.property_id.default_value:
  169. record.value = record.property_id.default_value
  170. if not record.field_type and record.property_id.field_type:
  171. record.field_type = record.property_id.field_type
  172. @api.onchange('value')
  173. def _onchange_value(self):
  174. """Inverse function is not launched after writing, so we need to
  175. trigger it right now."""
  176. self._inverse_value()
  177. @api.model
  178. def _transform_value(self, value, format_, properties=None):
  179. """Transforms a text value to the expected format.
  180. :param str/bool value:
  181. Custom value in raw string.
  182. :param str format_:
  183. Target conversion format for the value. Must be available among
  184. ``custom.info.property`` options.
  185. :param recordset properties:
  186. Useful when :param:`format_` is ``id``, as it helps to ensure the
  187. option is available in these properties. If :param:`format_` is
  188. ``id`` and :param:`properties` is ``None``, no transformation will
  189. be made for :param:`value`.
  190. """
  191. if not value:
  192. value = False
  193. elif format_ == "id" and properties:
  194. value = self.env["custom.info.option"].search([
  195. ("property_ids", "in", properties.ids),
  196. ("name", "ilike", u"%{}%".format(value)),
  197. ], limit=1)
  198. elif format_ == "bool":
  199. value = value.strip().lower() not in {
  200. "0", "false", "", "no", "off", _("No").lower()}
  201. elif format_ not in {"str", "id"}:
  202. value = safe_eval("{!s}({!r})".format(format_, value))
  203. return value
  204. @api.model
  205. def _search_value(self, operator, value):
  206. """Search from the stored field directly."""
  207. options = (
  208. o[0] for o in
  209. self.property_id._fields["field_type"]
  210. .get_description(self.env)["selection"])
  211. domain = []
  212. for fmt in options:
  213. try:
  214. _value = (self._transform_value(value, fmt)
  215. if not isinstance(value, list) else
  216. [self._transform_value(v, fmt) for v in value])
  217. except ValueError:
  218. # If you are searching something that cannot be casted, then
  219. # your property is probably from another type
  220. continue
  221. domain += [
  222. "&",
  223. ("field_type", "=", fmt),
  224. ("value_" + fmt, operator, _value),
  225. ]
  226. return ["|"] * int(len(domain) / 3 - 1) + domain