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.

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