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.

254 lines
9.0 KiB

7 years ago
7 years ago
  1. # Copyright 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis
  2. # Copyright 2017 Akretion (http://www.akretion.com)
  3. # Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
  4. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  5. import time
  6. from functools import wraps
  7. from odoo import api, models, fields, _
  8. from odoo.exceptions import UserError, ValidationError
  9. from odoo.tools.safe_eval import safe_eval
  10. def implemented_by_base_exception(func):
  11. """Call a prefixed function based on 'namespace'."""
  12. @wraps(func)
  13. def wrapper(cls, *args, **kwargs):
  14. fun_name = func.__name__
  15. fun = '_%s%s' % (cls.rule_group, fun_name)
  16. if not hasattr(cls, fun):
  17. fun = '_default%s' % (fun_name)
  18. return getattr(cls, fun)(*args, **kwargs)
  19. return wrapper
  20. class ExceptionRule(models.Model):
  21. _name = 'exception.rule'
  22. _description = "Exception Rules"
  23. _order = 'active desc, sequence asc'
  24. name = fields.Char('Exception Name', required=True, translate=True)
  25. description = fields.Text('Description', translate=True)
  26. sequence = fields.Integer(
  27. string='Sequence',
  28. help="Gives the sequence order when applying the test")
  29. rule_group = fields.Selection(
  30. selection=[],
  31. help="Rule group is used to group the rules that must validated "
  32. "at same time for a target object. Ex: "
  33. "validate sale.order.line rules with sale order rules.",
  34. required=True)
  35. model = fields.Selection(
  36. selection=[],
  37. string='Apply on', required=True)
  38. active = fields.Boolean('Active')
  39. next_state = fields.Char(
  40. 'Next state',
  41. help="If we detect exception we set de state of object (ex purchase) "
  42. "to the next_state (ex 'to approve'). If there are more than one "
  43. "exception detected and all have a value for next_state, we use"
  44. "the exception having the smallest sequence value")
  45. code = fields.Text(
  46. 'Python Code',
  47. help="Python code executed to check if the exception apply or "
  48. "not. The code must apply block = True to apply the "
  49. "exception.",
  50. default="""
  51. # Python code. Use failed = True to block the base.exception.
  52. # You can use the following variables :
  53. # - self: ORM model of the record which is checked
  54. # - "rule_group" or "rule_group_"line:
  55. # browse_record of the base.exception or
  56. # base.exception line (ex rule_group = sale for sale order)
  57. # - object: same as order or line, browse_record of the base.exception or
  58. # base.exception line
  59. # - pool: ORM model pool (i.e. self.pool)
  60. # - time: Python time module
  61. # - cr: database cursor
  62. # - uid: current user id
  63. # - context: current context
  64. """)
  65. @api.constrains('next_state')
  66. def _check_next_state_value(self):
  67. """ Ensure that the next_state value is in the state values of
  68. destination model """
  69. for rule in self:
  70. if rule.next_state:
  71. select_vals = self.env[
  72. rule.model].fields_get()[
  73. 'state']['selection']
  74. if rule.next_state\
  75. not in [s[0] for s in select_vals]:
  76. raise ValidationError(
  77. _('The value "%s" you chose for the "next state" '
  78. 'field state of "%s" is wrong.'
  79. ' Value must be in this list %s')
  80. % (rule.next_state,
  81. rule.model,
  82. select_vals)
  83. )
  84. class BaseException(models.AbstractModel):
  85. _name = 'base.exception'
  86. _order = 'main_exception_id asc'
  87. main_exception_id = fields.Many2one(
  88. 'exception.rule',
  89. compute='_compute_main_error',
  90. string='Main Exception',
  91. store=True)
  92. rule_group = fields.Selection(
  93. [],
  94. readonly=True,
  95. )
  96. exception_ids = fields.Many2many(
  97. 'exception.rule',
  98. string='Exceptions')
  99. ignore_exception = fields.Boolean('Ignore Exceptions', copy=False)
  100. @api.depends('exception_ids', 'ignore_exception')
  101. def _compute_main_error(self):
  102. for obj in self:
  103. if not obj.ignore_exception and obj.exception_ids:
  104. obj.main_exception_id = obj.exception_ids[0]
  105. else:
  106. obj.main_exception_id = False
  107. @api.multi
  108. def _popup_exceptions(self):
  109. action = self._get_popup_action()
  110. action = action.read()[0]
  111. action.update({
  112. 'context': {
  113. 'active_id': self.ids[0],
  114. 'active_ids': self.ids
  115. }
  116. })
  117. return action
  118. @api.model
  119. def _get_popup_action(self):
  120. action = self.env.ref('base_exception.action_exception_rule_confirm')
  121. return action
  122. @api.multi
  123. def _check_exception(self):
  124. """
  125. This method must be used in a constraint that must be created in the
  126. object that inherits for base.exception.
  127. for sale :
  128. @api.constrains('ignore_exception',)
  129. def sale_check_exception(self):
  130. ...
  131. ...
  132. self._check_exception
  133. """
  134. exception_ids = self.detect_exceptions()
  135. if exception_ids:
  136. exceptions = self.env['exception.rule'].browse(exception_ids)
  137. raise ValidationError('\n'.join(exceptions.mapped('name')))
  138. @api.multi
  139. def test_exceptions(self):
  140. """
  141. Condition method for the workflow from draft to confirm
  142. """
  143. if self.detect_exceptions():
  144. return False
  145. return True
  146. @api.multi
  147. def detect_exceptions(self):
  148. """returns the list of exception_ids for all the considered base.exceptions
  149. """
  150. if not self:
  151. return []
  152. exception_obj = self.env['exception.rule']
  153. all_exceptions = exception_obj.sudo().search(
  154. [('rule_group', '=', self[0].rule_group)])
  155. model_exceptions = all_exceptions.filtered(
  156. lambda ex: ex.model == self._name)
  157. sub_exceptions = all_exceptions.filtered(
  158. lambda ex: ex.model != self._name)
  159. all_exception_ids = []
  160. for obj in self:
  161. if obj.ignore_exception:
  162. continue
  163. exception_ids = obj._detect_exceptions(
  164. model_exceptions, sub_exceptions)
  165. obj.exception_ids = [(6, 0, exception_ids)]
  166. all_exception_ids += exception_ids
  167. return all_exception_ids
  168. @api.model
  169. def _exception_rule_eval_context(self, obj_name, rec):
  170. user = self.env['res.users'].browse(self._uid)
  171. return {obj_name: rec,
  172. 'self': self.pool.get(rec._name),
  173. 'object': rec,
  174. 'obj': rec,
  175. 'pool': self.pool,
  176. 'cr': self._cr,
  177. 'uid': self._uid,
  178. 'user': user,
  179. 'time': time,
  180. # copy context to prevent side-effects of eval
  181. 'context': self._context.copy()}
  182. @api.model
  183. def _rule_eval(self, rule, obj_name, rec):
  184. expr = rule.code
  185. space = self._exception_rule_eval_context(obj_name, rec)
  186. try:
  187. safe_eval(expr,
  188. space,
  189. mode='exec',
  190. nocopy=True) # nocopy allows to return 'result'
  191. except Exception as e:
  192. raise UserError(
  193. _('Error when evaluating the exception.rule '
  194. 'rule:\n %s \n(%s)') % (rule.name, e))
  195. return space.get('failed', False)
  196. @api.multi
  197. def _detect_exceptions(self, model_exceptions,
  198. sub_exceptions):
  199. self.ensure_one()
  200. exception_ids = []
  201. next_state_rule = False
  202. for rule in model_exceptions:
  203. if self._rule_eval(rule, self.rule_group, self):
  204. exception_ids.append(rule.id)
  205. if rule.next_state:
  206. if not next_state_rule:
  207. next_state_rule = rule
  208. elif next_state_rule and\
  209. rule.sequence < next_state_rule.sequence:
  210. next_state_rule = rule
  211. if sub_exceptions:
  212. for obj_line in self._get_lines():
  213. for rule in sub_exceptions:
  214. if rule.id in exception_ids:
  215. # we do not matter if the exception as already been
  216. # found for an line of this object
  217. # (ex sale order line if obj is sale order)
  218. continue
  219. group_line = self.rule_group + '_line'
  220. if self._rule_eval(rule, group_line, obj_line):
  221. exception_ids.append(rule.id)
  222. # set object to next state
  223. if next_state_rule:
  224. self.state = next_state_rule.next_state
  225. return exception_ids
  226. @implemented_by_base_exception
  227. def _get_lines(self):
  228. pass
  229. def _default_get_lines(self):
  230. return []