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.

283 lines
10 KiB

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