11# Part of Odoo. See LICENSE file for full copyright and licensing details.
22
33from odoo import _ , api , fields , models
4- from odoo .exceptions import ValidationError
4+ from odoo .exceptions import UserError , ValidationError
5+ from odoo .tools .safe_eval import safe_eval
56
67
78class HrContractAdvantage (models .Model ):
@@ -21,22 +22,176 @@ class HrContractAdvantage(models.Model):
2122 advantage_upper_bound = fields .Float (
2223 string = "Upper Bound" , related = "advantage_template_id.upper_bound" , readonly = True
2324 )
24- amount = fields .Float ()
25+
26+ # Definition copied from the template on selection, then editable
27+ # per contract. Default "fixed" + fixed_value reproduces the
28+ # historical amount behaviour (see migration).
29+ computation_mode = fields .Selection (
30+ selection = [
31+ ("fixed" , "Fixed value" ),
32+ ("percentage" , "Percentage of a contract field" ),
33+ ("python" , "Python code" ),
34+ ],
35+ default = "fixed" ,
36+ required = True ,
37+ )
38+ fixed_value = fields .Float (help = "Value used in 'Fixed value' mode." )
39+ percentage = fields .Float (help = "Percentage applied to the base field." )
40+ percentage_base = fields .Char (
41+ string = "Percentage Base Field" ,
42+ help = "Numeric hr.contract field used as base (e.g. 'wage')." ,
43+ )
44+ python_code = fields .Text (
45+ help = "Expression with: advantage, contract, employee, payslip. "
46+ "Set 'result'." ,
47+ )
48+ quantity_mode = fields .Selection (
49+ selection = [
50+ ("fixed" , "Fixed quantity" ),
51+ ("python" , "Python code" ),
52+ ],
53+ default = "fixed" ,
54+ required = True ,
55+ )
56+ quantity_fixed_value = fields .Float (
57+ string = "Quantity" ,
58+ default = 1.0 ,
59+ help = "Quantity used in 'Fixed quantity' mode." ,
60+ )
61+ quantity_python_code = fields .Text (
62+ help = "Expression with: advantage, contract, employee, payslip. "
63+ "Set 'result'." ,
64+ )
65+ amount = fields .Float (
66+ help = "Latest evaluated amount (quantity x unit value), "
67+ "recomputed per payslip for non-fixed modes."
68+ )
2569
2670 @api .onchange ("advantage_template_id" )
2771 def _onchange_advantage_template_id (self ):
72+ """Copy the template definition onto the advantage.
73+
74+ ``amount`` is still populated from the template default value so
75+ existing flows and the 'fixed' mode behave as before.
76+ """
2877 for record in self :
29- record .amount = record .advantage_template_id .default_value
78+ template = record .advantage_template_id
79+ if not template :
80+ continue
81+ record .computation_mode = template .computation_mode
82+ record .fixed_value = template .default_value
83+ record .percentage = template .percentage
84+ record .percentage_base = template .percentage_base
85+ record .python_code = template .python_code
86+ record .quantity_mode = template .quantity_mode
87+ record .quantity_fixed_value = template .quantity_fixed_value
88+ record .quantity_python_code = template .quantity_python_code
89+ record .amount = template .default_value
90+
91+ def _compute_advantage_amount (self , payslip = None ):
92+ """Return quantity x unit value, bounded. Evaluated per payslip.
93+
94+ :param payslip: optional hr.payslip, exposed to python formulas.
95+ """
96+ self .ensure_one ()
97+ unit_value = self ._compute_unit_value (payslip = payslip )
98+ quantity = self ._compute_quantity (payslip = payslip )
99+ amount = quantity * unit_value
100+ self ._check_bounds (amount )
101+ return amount
102+
103+ def _compute_unit_value (self , payslip = None ):
104+ """Unit value per the computation mode."""
105+ self .ensure_one ()
106+ contract = self .contract_id
107+ mode = self .computation_mode or "fixed"
108+
109+ if mode == "fixed" :
110+ # Backward compatibility: historically the amount was typed
111+ # directly on the advantage (no fixed_value field). If
112+ # fixed_value was never set but amount was, keep using
113+ # amount so existing flows/records are unaffected.
114+ if not self .fixed_value and self .amount :
115+ value = self .amount
116+ else :
117+ value = self .fixed_value
118+ elif mode == "percentage" :
119+ base_field = (self .percentage_base or "" ).strip ()
120+ base_value = 0.0
121+ if base_field and contract :
122+ base_value = contract [base_field ] if base_field in contract else 0.0
123+ value = (base_value or 0.0 ) * (self .percentage or 0.0 ) / 100.0
124+ elif mode == "python" :
125+ value = self ._eval_code (self .python_code , payslip = payslip )
126+ else :
127+ value = 0.0
128+
129+ return self ._coerce_float (value , _ ("unit value" ))
130+
131+ def _compute_quantity (self , payslip = None ):
132+ """Quantity per the quantity mode. Default fixed 1.0."""
133+ self .ensure_one ()
134+ mode = self .quantity_mode or "fixed"
135+ if mode == "python" :
136+ value = self ._eval_code (self .quantity_python_code , payslip = payslip )
137+ else :
138+ # An explicit 0 quantity is valid (amount 0).
139+ value = (
140+ self .quantity_fixed_value
141+ if self .quantity_fixed_value is not False
142+ else 1.0
143+ )
144+ return self ._coerce_float (value , _ ("quantity" ))
145+
146+ def _coerce_float (self , value , label ):
147+ """Float guarantee, mirroring hr.salary.rule._compute_rule."""
148+ try :
149+ return float (value )
150+ except (TypeError , ValueError ) as err :
151+ raise UserError (
152+ _ (
153+ "The computed %(label)s of advantage "
154+ "'%(advantage)s' must be a float."
155+ )
156+ % {
157+ "label" : label ,
158+ "advantage" : self .advantage_template_id .name
159+ or self .advantage_template_code
160+ or self .id ,
161+ }
162+ ) from err
163+
164+ def _eval_code (self , code , payslip = None ):
165+ """Safely evaluate a generic python expression."""
166+ self .ensure_one ()
167+ if not code :
168+ return 0.0
169+ localdict = {
170+ "advantage" : self ,
171+ "contract" : self .contract_id ,
172+ "employee" : self .contract_id .employee_id
173+ if self .contract_id
174+ else self .env ["hr.employee" ],
175+ "payslip" : payslip ,
176+ "result" : 0.0 ,
177+ }
178+ safe_eval (code , localdict , mode = "exec" , nocopy = True )
179+ return localdict .get ("result" , 0.0 ) or 0.0
180+
181+ def _check_bounds (self , value ):
182+ """Enforce template lower/upper bounds on a candidate amount."""
183+ self .ensure_one ()
184+ if value and value != 0.00 :
185+ if self .advantage_upper_bound and value > self .advantage_upper_bound :
186+ raise ValidationError (
187+ _ ("Advantage amount can't be greater than upper bound limit." )
188+ )
189+ elif self .advantage_lower_bound and value < self .advantage_lower_bound :
190+ raise ValidationError (
191+ _ ("Advantage amount can't be less than lower bound limit." )
192+ )
30193
31194 @api .constrains ("amount" )
32195 def _check_bound_limits (self ):
33196 for record in self :
34- if record .amount and record .amount != 0.00 :
35- if record .amount > record .advantage_upper_bound :
36- raise ValidationError (
37- _ ("Advantage amount can't be greater than upper bound limit." )
38- )
39- elif record .amount < record .advantage_lower_bound :
40- raise ValidationError (
41- _ ("Advantage amount can't be less than lower bound limit." )
42- )
197+ record ._check_bounds (record .amount )
0 commit comments