product_config.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. # -*- coding: utf-8 -*-
  2. from odoo import models, fields, api, _
  3. from odoo.exceptions import Warning, ValidationError
  4. from ast import literal_eval
  5. class ProductConfigDomain(models.Model):
  6. _name = 'product.config.domain'
  7. @api.multi
  8. @api.depends('implied_ids')
  9. def _get_trans_implied(self):
  10. "Computes the transitive closure of relation implied_ids"
  11. def linearize(domains):
  12. trans_domains = domains
  13. for domain in domains:
  14. implied_domains = domain.implied_ids - domain
  15. if implied_domains:
  16. trans_domains |= linearize(implied_domains)
  17. return trans_domains
  18. for domain in self:
  19. domain.trans_implied_ids = linearize(domain)
  20. @api.multi
  21. def compute_domain(self):
  22. """ Returns a list of domains defined on a product.config.domain_line_ids
  23. and all implied_ids"""
  24. # TODO: Enable the usage of OR operators between domain lines and
  25. # implied_ids
  26. # TODO: Prevent circular dependencies
  27. computed_domain = []
  28. for domain in self:
  29. for line in domain.trans_implied_ids.mapped('domain_line_ids'):
  30. computed_domain.append(
  31. (line.attribute_id.id, line.condition, line.value_ids.ids)
  32. )
  33. return computed_domain
  34. name = fields.Char(
  35. string='Name',
  36. required=True,
  37. size=256
  38. )
  39. domain_line_ids = fields.One2many(
  40. comodel_name='product.config.domain.line',
  41. inverse_name='domain_id',
  42. string='Restrictions',
  43. required=True
  44. )
  45. implied_ids = fields.Many2many(
  46. comodel_name='product.config.domain',
  47. relation='product_config_domain_implied_rel',
  48. string='Inherited',
  49. column1='domain_id',
  50. column2='parent_id'
  51. )
  52. trans_implied_ids = fields.Many2many(
  53. comodel_name='product.config.domain',
  54. compute=_get_trans_implied,
  55. column1='domain_id',
  56. column2='parent_id',
  57. string='Transitively inherits'
  58. )
  59. class ProductConfigDomainLine(models.Model):
  60. _name = 'product.config.domain.line'
  61. def _get_domain_conditions(self):
  62. operators = [
  63. ('in', 'In'),
  64. ('not in', 'Not In')
  65. ]
  66. return operators
  67. def _get_domain_operators(self):
  68. andor = [
  69. ('and', 'And'),
  70. # ('or', 'Or')
  71. # TODO: Not implemented in domain computation yet
  72. ]
  73. return andor
  74. attribute_id = fields.Many2one(
  75. comodel_name='product.attribute',
  76. string='Attribute',
  77. required=True)
  78. domain_id = fields.Many2one(
  79. comodel_name='product.config.domain',
  80. required=True,
  81. string='Rule')
  82. condition = fields.Selection(
  83. selection=_get_domain_conditions,
  84. string="Condition",
  85. required=True)
  86. value_ids = fields.Many2many(
  87. comodel_name='product.attribute.value',
  88. relation='product_config_domain_line_attr_rel',
  89. column1='line_id',
  90. column2='attribute_id',
  91. string='Values',
  92. required=True
  93. )
  94. operator = fields.Selection(
  95. selection=_get_domain_operators,
  96. string='Operators',
  97. default='and',
  98. required=True
  99. )
  100. class ProductConfigLine(models.Model):
  101. _name = 'product.config.line'
  102. # TODO: Prevent config lines having dependencies that are not set in other
  103. # config lines
  104. # TODO: Prevent circular depdencies: Length -> Color, Color -> Length
  105. @api.onchange('attribute_line_id')
  106. def onchange_attribute(self):
  107. self.value_ids = False
  108. self.domain_id = False
  109. product_tmpl_id = fields.Many2one(
  110. comodel_name='product.template',
  111. string='Product Template',
  112. ondelete='cascade',
  113. required=True
  114. )
  115. attribute_line_id = fields.Many2one(
  116. comodel_name='product.attribute.line',
  117. string='Attribute Line',
  118. ondelete='cascade',
  119. required=True
  120. )
  121. # TODO: Find a more elegant way to restrict the value_ids
  122. attr_line_val_ids = fields.Many2many(
  123. comodel_name='product.attribute.value',
  124. related='attribute_line_id.value_ids'
  125. )
  126. value_ids = fields.Many2many(
  127. comodel_name='product.attribute.value',
  128. id1="cfg_line_id",
  129. id2="attr_val_id",
  130. string="Values"
  131. )
  132. domain_id = fields.Many2one(
  133. comodel_name='product.config.domain',
  134. required=True,
  135. string='Restrictions'
  136. )
  137. sequence = fields.Integer(string='Sequence', default=10)
  138. _order = 'product_tmpl_id, sequence, id'
  139. @api.multi
  140. @api.constrains('value_ids')
  141. def check_value_attributes(self):
  142. for line in self:
  143. value_attributes = line.value_ids.mapped('attribute_id')
  144. if value_attributes != line.attribute_line_id.attribute_id:
  145. raise ValidationError(
  146. _("Values must belong to the attribute of the "
  147. "corresponding attribute_line set on the configuration "
  148. "line")
  149. )
  150. class ProductConfigImage(models.Model):
  151. _name = 'product.config.image'
  152. name = fields.Char('Name', size=128, required=True, translate=True)
  153. product_tmpl_id = fields.Many2one(
  154. comodel_name='product.template',
  155. string='Product',
  156. ondelete='cascade',
  157. required=True
  158. )
  159. image = fields.Binary('Image', required=True)
  160. sequence = fields.Integer(string='Sequence', default=10)
  161. value_ids = fields.Many2many(
  162. comodel_name='product.attribute.value',
  163. string='Configuration'
  164. )
  165. _order = 'sequence'
  166. @api.multi
  167. @api.constrains('value_ids')
  168. def _check_value_ids(self):
  169. for cfg_img in self:
  170. valid = cfg_img.product_tmpl_id.validate_configuration(
  171. cfg_img.value_ids.ids, final=False)
  172. if not valid:
  173. raise ValidationError(
  174. _("Values entered for line '%s' generate "
  175. "a incompatible configuration" % cfg_img.name)
  176. )
  177. class ProductConfigStep(models.Model):
  178. _name = 'product.config.step'
  179. # TODO: Prevent values which have dependencies to be set in a
  180. # step with higher sequence than the dependency
  181. name = fields.Char(
  182. string='Name',
  183. size=128,
  184. required=True,
  185. translate=True
  186. )
  187. class ProductConfigStepLine(models.Model):
  188. _name = 'product.config.step.line'
  189. name = fields.Char(related='config_step_id.name')
  190. config_step_id = fields.Many2one(
  191. comodel_name='product.config.step',
  192. string='Configuration Step',
  193. required=True
  194. )
  195. attribute_line_ids = fields.Many2many(
  196. comodel_name='product.attribute.line',
  197. relation='config_step_line_attr_id_rel',
  198. column1='cfg_line_id',
  199. column2='attr_id',
  200. string='Attribute Lines'
  201. )
  202. product_tmpl_id = fields.Many2one(
  203. comodel_name='product.template',
  204. string='Product Template',
  205. ondelete='cascade',
  206. required=True
  207. )
  208. sequence = fields.Integer(
  209. string='Sequence',
  210. default=10
  211. )
  212. _order = 'sequence, config_step_id, id'
  213. @api.constrains('config_step_id')
  214. def _check_config_step(self):
  215. cfg_step_lines = self.product_tmpl_id.config_step_line_ids
  216. cfg_steps = cfg_step_lines.filtered(
  217. lambda l: l != self).mapped('config_step_id')
  218. if self.config_step_id in cfg_steps:
  219. raise Warning(_('Cannot have a configuration step defined twice.'))
  220. class ProductConfigSession(models.Model):
  221. _name = 'product.config.session'
  222. @api.multi
  223. @api.depends('value_ids')
  224. def _compute_cfg_price(self):
  225. for session in self:
  226. custom_vals = session._get_custom_vals_dict()
  227. price = session.product_tmpl_id.get_cfg_price(
  228. session.value_ids.ids, custom_vals)
  229. session.price = price['total']
  230. @api.multi
  231. def _get_custom_vals_dict(self):
  232. """Retrieve session custom values as a dictionary of the form
  233. {attribute_id: parsed_custom_value}"""
  234. self.ensure_one()
  235. custom_vals = {}
  236. for val in self.custom_value_ids:
  237. if val.attribute_id.custom_type in ['float', 'int']:
  238. custom_vals[val.attribute_id.id] = literal_eval(val.value)
  239. else:
  240. custom_vals[val.attribute_id.id] = val.value
  241. return custom_vals
  242. product_tmpl_id = fields.Many2one(
  243. comodel_name='product.template',
  244. domain=[('config_ok', '=', True)],
  245. string='Configurable Template',
  246. required=True
  247. )
  248. value_ids = fields.Many2many(
  249. comodel_name='product.attribute.value',
  250. relation='product_config_session_attr_values_rel',
  251. column1='cfg_session_id',
  252. column2='attr_val_id',
  253. )
  254. user_id = fields.Many2one(
  255. comodel_name='res.users',
  256. required=True,
  257. string='User'
  258. )
  259. custom_value_ids = fields.One2many(
  260. comodel_name='product.config.session.custom.value',
  261. inverse_name='cfg_session_id',
  262. string='Custom Values'
  263. )
  264. price = fields.Float(
  265. compute='_compute_cfg_price',
  266. string='Price',
  267. store=True,
  268. )
  269. state = fields.Selection(
  270. string='State',
  271. required=True,
  272. selection=[
  273. ('draft', 'Draft'),
  274. ('done', 'Done')
  275. ],
  276. default='draft'
  277. )
  278. @api.multi
  279. def action_confirm(self):
  280. # TODO: Implement method to generate dict from custom vals
  281. custom_val_dict = {
  282. x.attribute_id.id: x.value or x.attachment_ids
  283. for x in self.custom_value_ids
  284. }
  285. valid = self.product_tmpl_id.validate_configuration(
  286. self.value_ids.ids, custom_val_dict)
  287. if valid:
  288. self.state = 'done'
  289. return valid
  290. @api.multi
  291. def update_config(self, attr_val_dict=None, custom_val_dict=None):
  292. """Update the session object with the given value_ids and custom values.
  293. Use this method instead of write in order to prevent incompatible
  294. configurations as this removed duplicate values for the same attribute.
  295. :param attr_val_dict: Dictionary of the form {
  296. int (attribute_id): attribute_value_id OR [attribute_value_ids]
  297. }
  298. :custom_val_dict: Dictionary of the form {
  299. int (attribute_id): {
  300. 'value': 'custom val',
  301. OR
  302. 'attachment_ids': {
  303. [{
  304. 'name': 'attachment name',
  305. 'datas': base64_encoded_string
  306. }]
  307. }
  308. }
  309. }
  310. """
  311. if attr_val_dict is None:
  312. attr_val_dict = {}
  313. if custom_val_dict is None:
  314. custom_val_dict = {}
  315. update_vals = {}
  316. value_ids = self.value_ids.ids
  317. for attr_id, vals in attr_val_dict.iteritems():
  318. attr_val_ids = self.value_ids.filtered(
  319. lambda x: x.attribute_id.id == int(attr_id)).ids
  320. # Remove all values for this attribute and add vals from dict
  321. value_ids = list(set(value_ids) - set(attr_val_ids))
  322. if not vals:
  323. continue
  324. if isinstance(vals, list):
  325. value_ids += vals
  326. elif isinstance(vals, int):
  327. value_ids.append(vals)
  328. if value_ids != self.value_ids.ids:
  329. update_vals.update({
  330. 'value_ids': [(6, 0, value_ids)]
  331. })
  332. # Remove all custom values included in the custom_vals dict
  333. self.custom_value_ids.filtered(
  334. lambda x: x.attribute_id.id in custom_val_dict.keys()).unlink()
  335. if custom_val_dict:
  336. binary_field_ids = self.env['product.attribute'].search([
  337. ('id', 'in', custom_val_dict.keys()),
  338. ('custom_type', '=', 'binary')
  339. ]).ids
  340. for attr_id, vals in custom_val_dict.iteritems():
  341. if not vals:
  342. continue
  343. if 'custom_value_ids' not in update_vals:
  344. update_vals['custom_value_ids'] = []
  345. custom_vals = {'attribute_id': attr_id}
  346. if attr_id in binary_field_ids:
  347. attachments = [(0, 0, {
  348. 'name': val.get('name'),
  349. 'datas': val.get('datas')
  350. }) for val in vals]
  351. custom_vals.update({'attachment_ids': attachments})
  352. else:
  353. custom_vals.update({'value': vals})
  354. update_vals['custom_value_ids'].append((0, 0, custom_vals))
  355. self.write(update_vals)
  356. @api.multi
  357. def write(self, vals):
  358. """Validate configuration when writing new values to session"""
  359. # TODO: Issue warning when writing to value_ids or custom_val_ids
  360. res = super(ProductConfigSession, self).write(vals)
  361. custom_val_dict = {
  362. x.attribute_id.id: x.value or x.attachment_ids
  363. for x in self.custom_value_ids
  364. }
  365. valid = self.product_tmpl_id.validate_configuration(
  366. self.value_ids.ids, custom_val_dict, final=False)
  367. if not valid:
  368. raise ValidationError(_('Invalid Configuration'))
  369. return res
  370. # TODO: Disallow duplicates
  371. class ProductConfigSessionCustomValue(models.Model):
  372. _name = 'product.config.session.custom.value'
  373. _rec_name = 'attribute_id'
  374. attribute_id = fields.Many2one(
  375. comodel_name='product.attribute',
  376. string='Attribute',
  377. required=True
  378. )
  379. cfg_session_id = fields.Many2one(
  380. comodel_name='product.config.session',
  381. required=True,
  382. ondelete='cascade',
  383. string='Session'
  384. )
  385. value = fields.Char(
  386. string='Value',
  387. help='Custom value held as string',
  388. )
  389. attachment_ids = fields.Many2many(
  390. comodel_name='ir.attachment',
  391. relation='product_config_session_custom_value_attachment_rel',
  392. column1='cfg_sesion_custom_val_id',
  393. column2='attachment_id',
  394. string='Attachments'
  395. )
  396. def eval(self):
  397. """Return custom value evaluated using the related custom field type"""
  398. field_type = self.attribute_id.custom_type
  399. if field_type == 'binary':
  400. vals = self.attachment_ids.mapped('datas')
  401. if len(vals) == 1:
  402. return vals[0]
  403. return vals
  404. elif field_type == 'int':
  405. return int(self.value)
  406. elif field_type == 'float':
  407. return float(self.value)
  408. return self.value
  409. @api.constrains('cfg_session_id', 'attribute_id')
  410. def unique_attribute(self):
  411. if len(self.cfg_session_id.custom_value_ids.filtered(
  412. lambda x: x.attribute_id == self.attribute_id)) > 1:
  413. raise ValidationError(
  414. _("Configuration cannot have the same value inserted twice")
  415. )
  416. # @api.constrains('cfg_session_id.value_ids')
  417. # def custom_only(self):
  418. # """Verify that the attribute_id is not present in vals as well"""
  419. # import ipdb;ipdb.set_trace()
  420. # if self.cfg_session_id.value_ids.filtered(
  421. # lambda x: x.attribute_id == self.attribute_id):
  422. # raise ValidationError(
  423. # _("Configuration cannot have a selected option and a custom "
  424. # "value with the same attribute")
  425. # )
  426. @api.constrains('attachment_ids', 'value')
  427. def check_custom_type(self):
  428. custom_type = self.attribute_id.custom_type
  429. if self.value and custom_type == 'binary':
  430. raise ValidationError(
  431. _("Attribute custom type is binary, attachments are the only "
  432. "accepted values with this custom field type")
  433. )
  434. if self.attachment_ids and custom_type != 'binary':
  435. raise ValidationError(
  436. _("Attribute custom type must be 'binary' for saving "
  437. "attachments to custom value")
  438. )