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.

350 lines
13 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2011 Raphaël Valyi, Renato Lima, Guewen Baconnier, Sodexis
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. import time
  5. from functools import wraps
  6. from odoo import api, models, fields, _
  7. from odoo.exceptions import UserError, ValidationError
  8. from odoo.tools.safe_eval import safe_eval
  9. def implemented_by_base_exception(func):
  10. """Call a prefixed function based on 'namespace'."""
  11. @wraps(func)
  12. def wrapper(cls, *args, **kwargs):
  13. fun_name = func.__name__
  14. fun = '_%s%s' % (cls.rule_group, fun_name)
  15. if not hasattr(cls, fun):
  16. fun = '_default%s' % (fun_name)
  17. return getattr(cls, fun)(*args, **kwargs)
  18. return wrapper
  19. class ExceptionRule(models.Model):
  20. _name = 'exception.rule'
  21. _description = "Exception Rules"
  22. _order = 'active desc, sequence asc'
  23. name = fields.Char('Exception Name', required=True, translate=True)
  24. description = fields.Text('Description', translate=True)
  25. sequence = fields.Integer(
  26. string='Sequence',
  27. help="Gives the sequence order when applying the test")
  28. rule_group = fields.Selection(
  29. selection=[],
  30. help="Rule group is used to group the rules that must validated "
  31. "at same time for a target object. Ex: "
  32. "validate sale.order.line rules with sale order rules.",
  33. required=True)
  34. model = fields.Selection(
  35. selection=[],
  36. string='Apply on', required=True)
  37. exception_type = fields.Selection(
  38. selection=[('by_domain', 'By domain'),
  39. ('by_py_code', 'By python code')],
  40. string='Exception Type', required=True, default='by_py_code',
  41. help="By python code: allow to define any arbitrary check\n"
  42. "By domain: limited to a selection by an odoo domain:\n"
  43. " performance can be better when exceptions "
  44. " are evaluated with several records")
  45. domain = fields.Char('Domain')
  46. active = fields.Boolean('Active')
  47. code = fields.Text(
  48. 'Python Code',
  49. help="Python code executed to check if the exception apply or "
  50. "not. The code must apply failed = True to apply the "
  51. "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. # - pool: ORM model pool (i.e. self.pool, deprecated in new api)
  62. # - obj: same as object
  63. # - env: ORM model pool (i.e. self.env)
  64. # - time: Python time module
  65. # - cr: database cursor
  66. # - uid: current user id
  67. # - context: current context
  68. """)
  69. @api.multi
  70. def _get_domain(self):
  71. """ override me to customize domains according exceptions cases """
  72. self.ensure_one()
  73. return safe_eval(self.domain)
  74. @api.onchange('exception_type',)
  75. def onchange_exception_type(self):
  76. if self.exception_type == 'by_domain':
  77. self.code = False
  78. elif self.exception_type == 'by_py_code':
  79. self.domain = False
  80. class BaseException(models.AbstractModel):
  81. _name = 'base.exception'
  82. _order = 'main_exception_id asc'
  83. main_exception_id = fields.Many2one(
  84. 'exception.rule',
  85. compute='_compute_main_error',
  86. string='Main Exception',
  87. store=True)
  88. rule_group = fields.Selection(
  89. [],
  90. readonly=True,
  91. )
  92. exception_ids = fields.Many2many(
  93. 'exception.rule',
  94. string='Exceptions')
  95. ignore_exception = fields.Boolean('Ignore Exceptions', copy=False)
  96. @api.depends('exception_ids', 'ignore_exception')
  97. def _compute_main_error(self):
  98. for obj in self:
  99. if not obj.ignore_exception and obj.exception_ids:
  100. obj.main_exception_id = obj.exception_ids[0]
  101. else:
  102. obj.main_exception_id = False
  103. @api.multi
  104. def _popup_exceptions(self):
  105. action = self._get_popup_action()
  106. action = action.read()[0]
  107. action.update({
  108. 'context': {
  109. 'active_model': self._name,
  110. 'active_id': self.ids[0],
  111. 'active_ids': self.ids
  112. }
  113. })
  114. return action
  115. @api.model
  116. def _get_popup_action(self):
  117. action = self.env.ref('base_exception.action_exception_rule_confirm')
  118. return action
  119. @api.multi
  120. def _check_exception(self):
  121. """
  122. This method must be used in a constraint that must be created in the
  123. object that inherits for base.exception.
  124. for sale :
  125. @api.constrains('ignore_exception',)
  126. def sale_check_exception(self):
  127. ...
  128. ...
  129. self._check_exception
  130. """
  131. exception_ids = self.detect_exceptions()
  132. if exception_ids:
  133. exceptions = self.env['exception.rule'].browse(exception_ids)
  134. raise ValidationError('\n'.join(exceptions.mapped('name')))
  135. @api.multi
  136. def test_exceptions(self):
  137. """
  138. Condition method for the workflow from draft to confirm
  139. """
  140. if self.detect_exceptions():
  141. return False
  142. return True
  143. @api.multi
  144. def _reverse_field(self):
  145. """Name of the many2many field from exception rule to self.
  146. In order to take advantage of domain optimisation, exception rule
  147. model should have a many2many field to inherited object.
  148. The opposit relation already exists in the name of exception_ids
  149. Example:
  150. class ExceptionRule(models.Model):
  151. _inherit = 'exception.rule'
  152. model = fields.Selection(
  153. selection_add=[
  154. ('sale.order', 'Sale order'),
  155. [...]
  156. ])
  157. sale_ids = fields.Many2many(
  158. 'sale.order',
  159. string='Sales')
  160. [...]
  161. """
  162. exception_obj = self.env['exception.rule']
  163. reverse_fields = self.env['ir.model.fields'].search([
  164. ['model', '=', 'exception.rule'],
  165. ['ttype', '=', 'many2many'],
  166. ['relation', '=', self[0]._name],
  167. ])
  168. # ir.model.fields may contain old variable name
  169. # so we check if the field exists on exception rule
  170. return ([
  171. field.name for field in reverse_fields
  172. if hasattr(exception_obj, field.name)
  173. ] or [None])[0]
  174. @api.multi
  175. def detect_exceptions(self):
  176. """List all exception_ids applied on self
  177. Exception ids are also written on records
  178. """
  179. if not self:
  180. return []
  181. exception_obj = self.env['exception.rule']
  182. all_exceptions = exception_obj.sudo().search(
  183. [('rule_group', '=', self[0].rule_group)])
  184. # TODO fix self[0] : it may not be the same on all ids in self
  185. model_exceptions = all_exceptions.filtered(
  186. lambda ex: ex.model == self._name)
  187. sub_exceptions = all_exceptions.filtered(
  188. lambda ex: ex.model != self._name)
  189. reverse_field = self._reverse_field()
  190. if reverse_field:
  191. optimize = True
  192. else:
  193. optimize = False
  194. exception_by_rec, exception_by_rule = self._detect_exceptions(
  195. model_exceptions, sub_exceptions, optimize)
  196. all_exception_ids = []
  197. for obj, exception_ids in exception_by_rec.iteritems():
  198. obj.exception_ids = [(6, 0, exception_ids)]
  199. all_exception_ids += exception_ids
  200. for rule, exception_ids in exception_by_rule.iteritems():
  201. rule[reverse_field] = [(6, 0, exception_ids.ids)]
  202. if exception_ids:
  203. all_exception_ids += [rule.id]
  204. return list(set(all_exception_ids))
  205. @api.model
  206. def _exception_rule_eval_context(self, obj_name, rec):
  207. return {obj_name: rec,
  208. 'self': self.pool.get(rec._name),
  209. 'object': rec,
  210. 'obj': rec,
  211. 'pool': self.pool,
  212. 'env': self.env,
  213. 'cr': self.env.cr,
  214. 'uid': self.env.uid,
  215. 'user': self.env.user,
  216. 'time': time,
  217. # copy context to prevent side-effects of eval
  218. 'context': self.env.context.copy()}
  219. @api.model
  220. def _rule_eval(self, rule, obj_name, rec):
  221. expr = rule.code
  222. space = self._exception_rule_eval_context(obj_name, rec)
  223. try:
  224. safe_eval(expr,
  225. space,
  226. mode='exec',
  227. nocopy=True) # nocopy allows to return 'result'
  228. except Exception, e:
  229. raise UserError(
  230. _('Error when evaluating the exception.rule '
  231. 'rule:\n %s \n(%s)') % (rule.name, e))
  232. return space.get('failed', False)
  233. @api.multi
  234. def _detect_exceptions(
  235. self, model_exceptions, sub_exceptions,
  236. optimize=False,
  237. ):
  238. """Find exceptions found on self.
  239. @returns
  240. exception_by_rec: (record_id, exception_ids)
  241. exception_by_rule: (rule_id, record_ids)
  242. """
  243. exception_by_rec = {}
  244. exception_by_rule = {}
  245. exception_set = set()
  246. python_rules = []
  247. dom_rules = []
  248. optim_rules = []
  249. for rule in model_exceptions:
  250. if rule.exception_type == 'by_py_code':
  251. python_rules.append(rule)
  252. elif rule.exception_type == 'by_domain' and rule.domain:
  253. if optimize:
  254. optim_rules.append(rule)
  255. else:
  256. dom_rules.append(rule)
  257. for rule in optim_rules:
  258. domain = rule._get_domain()
  259. domain.append(['ignore_exception', '=', False])
  260. domain.append(['id', 'in', self.ids])
  261. records_with_exception = self.search(domain)
  262. exception_by_rule[rule] = records_with_exception
  263. if records_with_exception:
  264. exception_set.add(rule.id)
  265. if len(python_rules) or len(dom_rules) or sub_exceptions:
  266. for rec in self:
  267. for rule in python_rules:
  268. if (
  269. not rec.ignore_exception and
  270. self._rule_eval(rule, rec.rule_group, rec)
  271. ):
  272. exception_by_rec.setdefault(rec, []).append(rule.id)
  273. exception_set.add(rule.id)
  274. for rule in dom_rules:
  275. # there is no reverse many2many, so this rule
  276. # can't be optimized, see _reverse_field
  277. domain = rule._get_domain()
  278. domain.append(['ignore_exception', '=', False])
  279. domain.append(['id', '=', rec.id])
  280. if self.search_count(domain):
  281. exception_by_rec.setdefault(
  282. rec, []).append(rule.id)
  283. exception_set.add(rule.id)
  284. if sub_exceptions:
  285. group_line = rec.rule_group + '_line'
  286. for obj_line in rec._get_lines():
  287. for rule in sub_exceptions:
  288. if rule.id in exception_set:
  289. # we do not matter if the exception as
  290. # already been
  291. # found for an line of this object
  292. # (ex sale order line if obj is sale order)
  293. continue
  294. if rule.exception_type == 'by_py_code':
  295. if self._rule_eval(
  296. rule, group_line, obj_line
  297. ):
  298. exception_by_rec.setdefault(
  299. rec, []).append(rule.id)
  300. elif (
  301. rule.exception_type == 'by_domain' and
  302. rule.domain
  303. ):
  304. # sub_exception are currently not optimizable
  305. domain = rule._get_domain()
  306. domain.append(('id', '=', obj_line.id))
  307. if obj_line.search_count(domain):
  308. exception_by_rec.setdefault(
  309. rec, []).append(rule.id)
  310. return exception_by_rec, exception_by_rule
  311. @implemented_by_base_exception
  312. def _get_lines(self):
  313. pass
  314. def _default_get_lines(self):
  315. return []