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.

225 lines
7.9 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 odoo import api, fields, models, _
  7. from odoo.exceptions import UserError, ValidationError
  8. from odoo.tools.safe_eval import safe_eval
  9. from odoo import osv
  10. class ExceptionRule(models.Model):
  11. _name = 'exception.rule'
  12. _description = 'Exception Rule'
  13. _order = 'active desc, sequence asc'
  14. name = fields.Char('Exception Name', required=True, translate=True)
  15. description = fields.Text('Description', translate=True)
  16. sequence = fields.Integer(
  17. string='Sequence',
  18. help="Gives the sequence order when applying the test",
  19. )
  20. model = fields.Selection(selection=[], string='Apply on', required=True)
  21. exception_type = fields.Selection(
  22. selection=[('by_domain', 'By domain'),
  23. ('by_py_code', 'By python code')],
  24. string='Exception Type', required=True, default='by_py_code',
  25. help="By python code: allow to define any arbitrary check\n"
  26. "By domain: limited to a selection by an odoo domain:\n"
  27. " performance can be better when exceptions "
  28. " are evaluated with several records")
  29. domain = fields.Char('Domain')
  30. active = fields.Boolean('Active', default=True)
  31. code = fields.Text(
  32. 'Python Code',
  33. help="Python code executed to check if the exception apply or "
  34. "not. Use failed = True to block the exception",
  35. )
  36. @api.constrains('exception_type', 'domain', 'code')
  37. def check_exception_type_consistency(self):
  38. for rule in self:
  39. if ((rule.exception_type == 'by_py_code' and not rule.code) or
  40. (rule.exception_type == 'by_domain' and not rule.domain)):
  41. raise ValidationError(
  42. _("There is a problem of configuration, python code or "
  43. "domain is missing to match the exception type.")
  44. )
  45. @api.multi
  46. def _get_domain(self):
  47. """ override me to customize domains according exceptions cases """
  48. self.ensure_one()
  49. return safe_eval(self.domain)
  50. class BaseExceptionMethod(models.AbstractModel):
  51. _name = 'base.exception.method'
  52. _description = 'Exception Rule Methods'
  53. @api.multi
  54. def _reverse_field(self):
  55. raise NotImplementedError()
  56. def _rule_domain(self):
  57. """Filter exception.rules.
  58. By default, only the rules with the correct model
  59. will be used.
  60. """
  61. return [('model', '=', self._name)]
  62. @api.multi
  63. def detect_exceptions(self):
  64. """List all exception_ids applied on self
  65. Exception ids are also written on records
  66. If self is empty, check exceptions on all active records.
  67. """
  68. rules = self.env['exception.rule'].sudo().search(
  69. self._rule_domain())
  70. all_exception_ids = []
  71. for rule in rules:
  72. records_with_exception = self._detect_exceptions(rule)
  73. reverse_field = self._reverse_field()
  74. if self:
  75. commons = self & rule[reverse_field]
  76. to_remove = commons - records_with_exception
  77. to_add = records_with_exception - commons
  78. to_remove_list = [(3, x.id, _) for x in to_remove]
  79. to_add_list = [(4, x.id, _) for x in to_add]
  80. rule.write({reverse_field: to_remove_list + to_add_list})
  81. else:
  82. rule.write({
  83. reverse_field: [(6, 0, records_with_exception.ids)]
  84. })
  85. if records_with_exception:
  86. all_exception_ids.append(rule.id)
  87. return all_exception_ids
  88. @api.model
  89. def _exception_rule_eval_context(self, rec):
  90. return {
  91. 'time': time,
  92. 'self': rec,
  93. # object, obj: deprecated.
  94. # should be removed in future migrations
  95. 'object': rec,
  96. 'obj': rec,
  97. # copy context to prevent side-effects of eval
  98. # should be deprecated too, accesible through self.
  99. 'context': self.env.context.copy()
  100. }
  101. @api.model
  102. def _rule_eval(self, rule, rec):
  103. expr = rule.code
  104. space = self._exception_rule_eval_context(rec)
  105. try:
  106. safe_eval(expr,
  107. space,
  108. mode='exec',
  109. nocopy=True) # nocopy allows to return 'result'
  110. except Exception as e:
  111. raise UserError(
  112. _('Error when evaluating the exception.rule '
  113. 'rule:\n %s \n(%s)') % (rule.name, e))
  114. return space.get('failed', False)
  115. @api.multi
  116. def _detect_exceptions(self, rule):
  117. if rule.exception_type == 'by_py_code':
  118. return self._detect_exceptions_by_py_code(rule)
  119. elif rule.exception_type == 'by_domain':
  120. return self._detect_exceptions_by_domain(rule)
  121. @api.multi
  122. def _get_base_domain(self):
  123. domain = [('ignore_exception', '=', False)]
  124. if self:
  125. domain = osv.expression.AND([domain, [('id', 'in', self.ids)]])
  126. return domain
  127. @api.multi
  128. def _detect_exceptions_by_py_code(self, rule):
  129. """
  130. Find exceptions found on self.
  131. If self is empty, check on all records.
  132. """
  133. domain = self._get_base_domain()
  134. records = self.search(domain)
  135. records_with_exception = self.env[self._name]
  136. for record in records:
  137. if self._rule_eval(rule, record):
  138. records_with_exception |= record
  139. return records_with_exception
  140. @api.multi
  141. def _detect_exceptions_by_domain(self, rule):
  142. """
  143. Find exceptions found on self.
  144. If self is empty, check on all records.
  145. """
  146. base_domain = self._get_base_domain()
  147. rule_domain = rule._get_domain()
  148. domain = osv.expression.AND([base_domain, rule_domain])
  149. return self.search(domain)
  150. class BaseException(models.AbstractModel):
  151. _inherit = 'base.exception.method'
  152. _name = 'base.exception'
  153. _order = 'main_exception_id asc'
  154. _description = 'Exception'
  155. main_exception_id = fields.Many2one(
  156. 'exception.rule',
  157. compute='_compute_main_error',
  158. string='Main Exception',
  159. store=True,
  160. )
  161. exception_ids = fields.Many2many('exception.rule', string='Exceptions')
  162. ignore_exception = fields.Boolean('Ignore Exceptions', copy=False)
  163. @api.depends('exception_ids', 'ignore_exception')
  164. def _compute_main_error(self):
  165. for rec in self:
  166. if not rec.ignore_exception and rec.exception_ids:
  167. rec.main_exception_id = rec.exception_ids[0]
  168. else:
  169. rec.main_exception_id = False
  170. @api.multi
  171. def _popup_exceptions(self):
  172. action = self._get_popup_action().read()[0]
  173. action.update({
  174. 'context': {
  175. 'active_id': self.ids[0],
  176. 'active_ids': self.ids,
  177. 'active_model': self._name,
  178. }
  179. })
  180. return action
  181. @api.model
  182. def _get_popup_action(self):
  183. return self.env.ref('base_exception.action_exception_rule_confirm')
  184. @api.multi
  185. def _check_exception(self):
  186. """
  187. This method must be used in a constraint that must be created in the
  188. object that inherits for base.exception.
  189. for sale :
  190. @api.constrains('ignore_exception',)
  191. def sale_check_exception(self):
  192. ...
  193. ...
  194. self._check_exception
  195. """
  196. exception_ids = self.detect_exceptions()
  197. if exception_ids:
  198. exceptions = self.env['exception.rule'].browse(exception_ids)
  199. raise ValidationError('\n'.join(exceptions.mapped('name')))