product_attribute.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # -*- coding: utf-8 -*-
  2. from odoo import models, fields, api, _
  3. from odoo.exceptions import ValidationError
  4. from ast import literal_eval
  5. # TODO: Implement a default attribute value field/method to load up on wizard
  6. class ProductAttribute(models.Model):
  7. _inherit = 'product.attribute'
  8. @api.multi
  9. def copy(self, default=None):
  10. for attr in self:
  11. default.update({'name': attr.name + " (copy)"})
  12. attr = super(ProductAttribute, attr).copy(default)
  13. return attr
  14. @api.model
  15. def _get_nosearch_fields(self):
  16. """Return a list of custom field types that do not support searching"""
  17. return ['binary']
  18. @api.onchange('custom_type')
  19. def onchange_custom_type(self):
  20. if self.custom_type in self._get_nosearch_fields():
  21. self.search_ok = False
  22. CUSTOM_TYPES = [
  23. ('char', 'Char'),
  24. ('int', 'Integer'),
  25. ('float', 'Float'),
  26. ('text', 'Textarea'),
  27. ('color', 'Color'),
  28. ('binary', 'Attachment'),
  29. ('date', 'Date'),
  30. ('datetime', 'DateTime'),
  31. ]
  32. active = fields.Boolean(
  33. string='Active',
  34. default=True,
  35. help='By unchecking the active field you can '
  36. 'disable a attribute without deleting it'
  37. )
  38. min_val = fields.Integer(string="Min Value", help="Minimum value allowed")
  39. max_val = fields.Integer(string="Max Value", help="Minimum value allowed")
  40. # TODO: Exclude self from result-set of dependency
  41. val_custom = fields.Boolean(
  42. string='Custom Value',
  43. help='Allow custom value for this attribute?'
  44. )
  45. custom_type = fields.Selection(
  46. selection=CUSTOM_TYPES,
  47. string='Field Type',
  48. size=64,
  49. help='The type of the custom field generated in the frontend'
  50. )
  51. description = fields.Text(string='Description', translate=True)
  52. search_ok = fields.Boolean(
  53. string='Searchable',
  54. help='When checking for variants with '
  55. 'the same configuration, do we '
  56. 'include this field in the search?'
  57. )
  58. required = fields.Boolean(
  59. string='Required',
  60. default=True,
  61. help='Determines the required value of this '
  62. 'attribute though it can be change on '
  63. 'the template level'
  64. )
  65. multi = fields.Boolean(
  66. string="Multi",
  67. help='Allow selection of multiple values for '
  68. 'this attribute?'
  69. )
  70. uom_id = fields.Many2one(
  71. comodel_name='product.uom',
  72. string='Unit of Measure'
  73. )
  74. image = fields.Binary(string='Image')
  75. # TODO prevent the same attribute from being defined twice on the
  76. # attribute lines
  77. _order = 'sequence'
  78. @api.constrains('custom_type', 'search_ok')
  79. def check_searchable_field(self):
  80. nosearch_fields = self._get_nosearch_fields()
  81. if self.custom_type in nosearch_fields and self.search_ok:
  82. raise ValidationError(
  83. _("Selected custom field type '%s' is not searchable" %
  84. self.custom_type)
  85. )
  86. def validate_custom_val(self, val):
  87. """ Pass in a desired custom value and ensure it is valid.
  88. Probaly should check type, etc, but let's assume fine for the moment.
  89. """
  90. self.ensure_one()
  91. if self.custom_type in ('int', 'float'):
  92. minv = self.min_val
  93. maxv = self.max_val
  94. val = literal_eval(val)
  95. if minv and maxv and (val < minv or val > maxv):
  96. raise ValidationError(
  97. _("Selected custom value '%s' must be between %s and %s"
  98. % (self.name, self.min_val, self.max_val))
  99. )
  100. elif minv and val < minv:
  101. raise ValidationError(
  102. _("Selected custom value '%s' must be at least %s" %
  103. (self.name, self.min_val))
  104. )
  105. elif maxv and val > maxv:
  106. raise ValidationError(
  107. _("Selected custom value '%s' must be lower than %s" %
  108. (self.name, self.max_val + 1))
  109. )
  110. class ProductAttributeLine(models.Model):
  111. _inherit = 'product.attribute.line'
  112. @api.onchange('attribute_id')
  113. def onchange_attribute(self):
  114. self.value_ids = False
  115. self.required = self.attribute_id.required
  116. # TODO: Remove all dependencies pointed towards the attribute being
  117. # changed
  118. custom = fields.Boolean(
  119. string='Custom',
  120. help="Allow custom values for this attribute?"
  121. )
  122. required = fields.Boolean(
  123. string='Required',
  124. help="Is this attribute required?"
  125. )
  126. multi = fields.Boolean(
  127. string='Multi',
  128. help='Allow selection of multiple values for this attribute?'
  129. )
  130. sequence = fields.Integer(string='Sequence', default=10)
  131. # TODO: Order by dependencies first and then sequence so dependent fields
  132. # do not come before master field
  133. _order = 'product_tmpl_id, sequence, id'
  134. # TODO: Constraint not allowing introducing dependencies that do not exist
  135. # on the product.template
  136. class ProductAttributeValue(models.Model):
  137. _inherit = 'product.attribute.value'
  138. @api.multi
  139. def copy(self, default=None):
  140. default.update({'name': self.name + " (copy)"})
  141. product = super(ProductAttributeValue, self).copy(default)
  142. return product
  143. active = fields.Boolean(
  144. string='Active',
  145. default=True,
  146. help='By unchecking the active field you can '
  147. 'disable a attribute value without deleting it'
  148. )
  149. product_id = fields.Many2one(
  150. comodel_name='product.product',
  151. string='Related Product'
  152. )
  153. @api.model
  154. def name_search(self, name='', args=None, operator='ilike', limit=100):
  155. """Use name_search as a domain restriction for the frontend to show
  156. only values set on the product template taking all the configuration
  157. restrictions into account.
  158. TODO: This only works when activating the selection not when typing
  159. """
  160. product_tmpl_id = self.env.context.get('_cfg_product_tmpl_id')
  161. if product_tmpl_id:
  162. # TODO: Avoiding browse here could be a good performance enhancer
  163. product_tmpl = self.env['product.template'].browse(product_tmpl_id)
  164. tmpl_vals = product_tmpl.attribute_line_ids.mapped('value_ids')
  165. attr_restrict_ids = []
  166. preset_val_ids = []
  167. new_args = []
  168. for arg in args:
  169. # Restrict values only to value_ids set on product_template
  170. if arg[0] == 'id' and arg[1] == 'not in':
  171. preset_val_ids = arg[2]
  172. # TODO: Check if all values are available for configuration
  173. else:
  174. new_args.append(arg)
  175. val_ids = set(tmpl_vals.ids)
  176. if preset_val_ids:
  177. val_ids -= set(arg[2])
  178. val_ids = [v for v in val_ids if product_tmpl.value_available(
  179. v, preset_val_ids)]
  180. new_args.append(('id', 'in', val_ids))
  181. mono_tmpl_lines = product_tmpl.attribute_line_ids.filtered(
  182. lambda l: not l.multi)
  183. for line in mono_tmpl_lines:
  184. line_val_ids = set(line.mapped('value_ids').ids)
  185. if line_val_ids & set(preset_val_ids):
  186. attr_restrict_ids.append(line.attribute_id.id)
  187. if attr_restrict_ids:
  188. new_args.append(('attribute_id', 'not in', attr_restrict_ids))
  189. args = new_args
  190. res = super(ProductAttributeValue, self).name_search(
  191. name=name, args=args, operator=operator, limit=limit)
  192. return res
  193. # TODO: Prevent unlinking custom options by overriding unlink
  194. # _sql_constraints = [
  195. # ('unique_custom', 'unique(id,allow_custom_value)',
  196. # 'Only one custom value per dimension type is allowed')
  197. # ]
  198. class ProductAttributeValueCustom(models.Model):
  199. @api.multi
  200. @api.depends('attribute_id', 'attribute_id.uom_id')
  201. def _compute_val_name(self):
  202. for attr_val_custom in self:
  203. uom = attr_val_custom.attribute_id.uom_id.name
  204. attr_val_custom.name = '%s%s' % (attr_val_custom.value, uom or '')
  205. _name = 'product.attribute.value.custom'
  206. name = fields.Char(
  207. string='Name',
  208. readonly=True,
  209. compute="_compute_val_name",
  210. store=True,
  211. )
  212. product_id = fields.Many2one(
  213. comodel_name='product.product',
  214. string='Product ID',
  215. required=True,
  216. ondelete='cascade'
  217. )
  218. attribute_id = fields.Many2one(
  219. comodel_name='product.attribute',
  220. string='Attribute',
  221. required=True
  222. )
  223. attachment_ids = fields.Many2many(
  224. comodel_name='ir.attachment',
  225. relation='product_attr_val_custom_value_attachment_rel',
  226. column1='attr_val_custom_id',
  227. column2='attachment_id',
  228. string='Attachments'
  229. )
  230. value = fields.Char(
  231. string='Custom Value',
  232. )
  233. _sql_constraints = [
  234. ('attr_uniq', 'unique(product_id, attribute_id)',
  235. 'Cannot have two custom values for the same attribute')
  236. ]