hr_efficiency.py 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import logging
  4. import pytz
  5. from datetime import datetime, date
  6. from dateutil.relativedelta import relativedelta
  7. from odoo import api, fields, models, _
  8. from odoo.exceptions import UserError
  9. from odoo.tools import float_round
  10. _logger = logging.getLogger(__name__)
  11. class HrEfficiency(models.Model):
  12. _name = 'hr.efficiency'
  13. _description = 'Employee Efficiency'
  14. _order = 'month_year desc, employee_id'
  15. _rec_name = 'display_name'
  16. _active_name = 'active'
  17. # Basic fields
  18. name = fields.Char('Name', compute='_compute_display_name', store=True)
  19. employee_id = fields.Many2one('hr.employee', 'Employee', required=True, domain=[('employee_type', '=', 'employee')])
  20. month_year = fields.Char('Month Year', required=True, help="Format: YYYY-MM (e.g., 2024-08)")
  21. date = fields.Date('Date', compute='_compute_date', store=True, help="Date field for standard Odoo date filters")
  22. company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
  23. active = fields.Boolean('Active', default=True)
  24. calculation_date = fields.Datetime('Calculation Date', default=fields.Datetime.now, help='When this calculation was performed')
  25. # Available hours (what employee should work)
  26. available_hours = fields.Float('Available Hours', digits=(10, 2), help="Total hours employee should work in the month")
  27. # Employee contract information
  28. wage = fields.Float('Gross Salary', digits=(10, 2), aggregator='sum', help="Employee's gross salary from their contract at the time of calculation")
  29. currency_id = fields.Many2one('res.currency', 'Currency', help="Currency for the wage field")
  30. # Employee utilization and company overhead (stored at calculation time)
  31. utilization_rate = fields.Float('Utilization Rate (%)', digits=(5, 2), default=100.0, aggregator='avg', help="Employee's utilization rate at the time of calculation")
  32. overhead = fields.Float('Company Overhead (%)', digits=(5, 2), default=40.0, aggregator='avg', help="Company's overhead percentage at the time of calculation")
  33. expected_profitability = fields.Float('Expected Profitability (%)', digits=(5, 2), default=30.0, aggregator='avg', help="Company's expected profitability percentage at the time of calculation")
  34. efficiency_factor = fields.Float('Efficiency Factor (%)', digits=(5, 2), default=85.0, aggregator='avg', help="Company's efficiency factor percentage at the time of calculation")
  35. # Planned hours (what was planned)
  36. planned_hours = fields.Float('Planned Hours', digits=(10, 2), help="Total hours planned for the month")
  37. planned_billable_hours = fields.Float('Planned Billable Hours', digits=(10, 2), help="Hours planned on billable projects")
  38. planned_non_billable_hours = fields.Float('Planned Non-Billable Hours', digits=(10, 2), help="Hours planned on non-billable projects")
  39. # Actual hours
  40. actual_billable_hours = fields.Float('Actual Billable Hours', digits=(10, 2), help="Hours actually worked on billable projects")
  41. actual_non_billable_hours = fields.Float('Actual Non-Billable Hours', digits=(10, 2), help="Hours actually worked on non-billable projects")
  42. # Calculated fields (stored for performance)
  43. total_actual_hours = fields.Float('Total Actual Hours', digits=(10, 2), help="Total actual hours (billable + non-billable)")
  44. expected_hours_to_date = fields.Float('Expected Hours to Date', digits=(10, 2), help='Hours that should be registered based on planning until current date')
  45. # Precio por Hora (stored field)
  46. precio_por_hora = fields.Float('Precio por Hora', digits=(10, 2), help='Precio que cobramos al cliente por hora (hourly_cost + overhead + rentabilidad_esperada)', aggregator='avg')
  47. # Dynamic indicator fields (will be created automatically)
  48. # These fields are managed dynamically based on hr.efficiency.indicator records
  49. # Overall efficiency (always present) - Now stored fields calculated on save
  50. overall_efficiency = fields.Float('Overall Efficiency (%)', digits=(5, 2), help='Overall efficiency based on configured indicators')
  51. overall_efficiency_display = fields.Char('Overall Efficiency Display', help='Overall efficiency formatted for display with badge widget')
  52. display_name = fields.Char('Display Name', compute='_compute_display_name', store=True)
  53. # Note: Removed unique constraint to allow historical tracking
  54. # Multiple records can exist for the same employee and month
  55. @api.depends('month_year')
  56. def _compute_date(self):
  57. """
  58. Compute date field from month_year for standard Odoo date filters
  59. """
  60. for record in self:
  61. if record.month_year:
  62. try:
  63. year, month = record.month_year.split('-')
  64. record.date = date(int(year), int(month), 1)
  65. except (ValueError, AttributeError):
  66. record.date = False
  67. else:
  68. record.date = False
  69. def _calculate_expected_hours_to_date(self, record):
  70. """
  71. Calculate expected hours to date based on planned hours and working days
  72. """
  73. if not record.month_year or not record.planned_hours or not record.employee_id.contract_id:
  74. return 0.0
  75. try:
  76. # Parse month_year (format: YYYY-MM)
  77. year, month = record.month_year.split('-')
  78. start_date = date(int(year), int(month), 1)
  79. end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
  80. # Get current date
  81. current_date = date.today()
  82. # If current date is outside the month, use end of month
  83. if current_date > end_date:
  84. calculation_date = end_date
  85. elif current_date < start_date:
  86. calculation_date = start_date
  87. else:
  88. calculation_date = current_date
  89. # Calculate working days in the month
  90. total_working_days = self._count_working_days(start_date, end_date, record.employee_id)
  91. # Calculate working days until current date
  92. working_days_until_date = self._count_working_days(start_date, calculation_date, record.employee_id)
  93. if total_working_days > 0:
  94. # Calculate expected hours based on planned hours (what was planned to work)
  95. expected_hours = (record.planned_hours / total_working_days) * working_days_until_date
  96. # Ensure we don't exceed planned hours for the month
  97. expected_hours = min(expected_hours, record.planned_hours)
  98. return float_round(expected_hours, 2)
  99. else:
  100. return 0.0
  101. except (ValueError, AttributeError) as e:
  102. return 0.0
  103. def _calculate_precio_por_hora(self, record):
  104. """
  105. Calculate precio por hora: hourly_cost * (1 + overhead/100) * (1 + expected_profitability/100)
  106. """
  107. try:
  108. # Get hourly_cost from employee
  109. employee = getattr(record, 'employee_id', None)
  110. if not employee:
  111. return 0.0
  112. hourly_cost = getattr(employee, 'hourly_cost', 0.0) or 0.0
  113. overhead = getattr(record, 'overhead', 40.0) or 40.0
  114. expected_profitability = getattr(record, 'expected_profitability', 30.0) or 30.0
  115. if hourly_cost > 0:
  116. # Apply overhead and expected profitability margin
  117. precio_por_hora = hourly_cost * (1 + (overhead / 100)) * (1 + (expected_profitability / 100))
  118. return float_round(precio_por_hora, 2)
  119. else:
  120. return 0.0
  121. except (ValueError, AttributeError, ZeroDivisionError) as e:
  122. return 0.0
  123. def _calculate_all_indicators(self):
  124. """
  125. Calculate all indicators and overall efficiency for stored fields
  126. This method is called on create/write instead of using computed fields
  127. """
  128. # Prepare values to update without triggering write recursively
  129. values_to_update = {}
  130. # Pre-fetch necessary fields to avoid CacheMiss during iteration
  131. fields_to_load = [
  132. 'available_hours', 'planned_hours', 'planned_billable_hours',
  133. 'planned_non_billable_hours', 'actual_billable_hours',
  134. 'actual_non_billable_hours', 'total_actual_hours',
  135. 'expected_hours_to_date', 'wage', 'utilization_rate', 'overhead',
  136. 'precio_por_hora', 'employee_id', 'company_id', 'month_year', 'active',
  137. ]
  138. # Load fields for all records in 'self' with error handling
  139. try:
  140. self.read(fields_to_load)
  141. except Exception as e:
  142. import logging
  143. _logger = logging.getLogger(__name__)
  144. _logger.warning(f"Error loading fields: {e}")
  145. # Continue with empty cache if needed
  146. for record in self:
  147. # Get all manual fields for this model
  148. manual_fields = self.env['ir.model.fields'].search([
  149. ('model', '=', 'hr.efficiency'),
  150. ('state', '=', 'manual'),
  151. ('ttype', '=', 'float')
  152. ])
  153. # Prepare efficiency data with safe field access
  154. efficiency_data = {
  155. 'available_hours': getattr(record, 'available_hours', 0.0) or 0.0,
  156. 'planned_hours': getattr(record, 'planned_hours', 0.0) or 0.0,
  157. 'planned_billable_hours': getattr(record, 'planned_billable_hours', 0.0) or 0.0,
  158. 'planned_non_billable_hours': getattr(record, 'planned_non_billable_hours', 0.0) or 0.0,
  159. 'actual_billable_hours': getattr(record, 'actual_billable_hours', 0.0) or 0.0,
  160. 'actual_non_billable_hours': getattr(record, 'actual_non_billable_hours', 0.0) or 0.0,
  161. 'total_actual_hours': getattr(record, 'total_actual_hours', 0.0) or 0.0,
  162. 'expected_hours_to_date': getattr(record, 'expected_hours_to_date', 0.0) or 0.0,
  163. 'wage': getattr(record, 'wage', 0.0) or 0.0,
  164. 'utilization_rate': getattr(record, 'utilization_rate', 100.0) or 100.0,
  165. 'overhead': getattr(record, 'overhead', 40.0) or 40.0,
  166. 'precio_por_hora': getattr(record, 'precio_por_hora', 0.0) or 0.0,
  167. }
  168. # STEP 1: Calculate total_actual_hours FIRST (needed for indicators)
  169. try:
  170. total_actual_hours = getattr(record, 'actual_billable_hours', 0.0) + getattr(record, 'actual_non_billable_hours', 0.0)
  171. except Exception as e:
  172. import logging
  173. _logger = logging.getLogger(__name__)
  174. _logger.error(f"Error calculating total_actual_hours for record {record.id}: {e}")
  175. total_actual_hours = 0.0
  176. record_values = {'total_actual_hours': float_round(total_actual_hours, 2)}
  177. # STEP 2: Calculate expected_hours_to_date
  178. expected_hours = self._calculate_expected_hours_to_date(record)
  179. record_values['expected_hours_to_date'] = expected_hours
  180. # STEP 3: Calculate precio_por_hora (needed for profitability indicators)
  181. precio_por_hora = self._calculate_precio_por_hora(record)
  182. record_values['precio_por_hora'] = precio_por_hora
  183. # STEP 4: Update efficiency_data with ALL calculated base fields
  184. efficiency_data.update({
  185. 'total_actual_hours': total_actual_hours,
  186. 'expected_hours_to_date': expected_hours,
  187. 'precio_por_hora': precio_por_hora,
  188. })
  189. # STEP 5: Calculate all indicators dynamically (in sequence order)
  190. active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  191. for indicator in active_indicators:
  192. field_name = self._get_indicator_field_name(indicator.name)
  193. # Check if the field exists in the record
  194. if field_name in record._fields:
  195. # Calculate indicator value using the indicator formula
  196. indicator_value = indicator.evaluate_formula(efficiency_data)
  197. # Store the value to update later
  198. record_values[field_name] = float_round(indicator_value, 2)
  199. # Calculate overall efficiency
  200. overall_efficiency = self._calculate_overall_efficiency(record)
  201. record_values['overall_efficiency'] = overall_efficiency
  202. # Calculate overall efficiency display
  203. if overall_efficiency == 0:
  204. record_values['overall_efficiency_display'] = '0.00'
  205. else:
  206. record_values['overall_efficiency_display'] = f"{overall_efficiency:.2f}"
  207. # Store values for this record
  208. values_to_update[record.id] = record_values
  209. # Update all records at once to avoid recursion
  210. for record_id, values in values_to_update.items():
  211. record = self.browse(record_id)
  212. # Use direct SQL update to avoid triggering write method
  213. if values:
  214. record.env.cr.execute(
  215. "UPDATE hr_efficiency SET " +
  216. ", ".join([f"{key} = %s" for key in values.keys()]) +
  217. " WHERE id = %s",
  218. list(values.values()) + [record_id]
  219. )
  220. def _update_stored_manual_fields(self):
  221. """Update stored manual fields with computed values"""
  222. for record in self:
  223. # Get all manual fields for this model
  224. manual_fields = self.env['ir.model.fields'].search([
  225. ('model', '=', 'hr.efficiency'),
  226. ('state', '=', 'manual'),
  227. ('ttype', '=', 'float'),
  228. ('store', '=', True),
  229. ], order='id')
  230. # Prepare efficiency data with safe field access
  231. efficiency_data = {
  232. 'available_hours': record.available_hours or 0.0,
  233. 'planned_hours': record.planned_hours or 0.0,
  234. 'planned_billable_hours': record.planned_billable_hours or 0.0,
  235. 'planned_non_billable_hours': record.planned_non_billable_hours or 0.0,
  236. 'actual_billable_hours': record.actual_billable_hours or 0.0,
  237. 'actual_non_billable_hours': record.actual_non_billable_hours or 0.0,
  238. 'total_actual_hours': record.total_actual_hours or 0.0,
  239. 'expected_hours_to_date': record.expected_hours_to_date or 0.0,
  240. 'wage': record.wage or 0.0,
  241. 'utilization_rate': record.utilization_rate or 100.0,
  242. 'overhead': record.overhead or 40.0,
  243. }
  244. # Calculate and update stored manual fields
  245. for field in manual_fields:
  246. # Find the corresponding indicator
  247. indicator = self.env['hr.efficiency.indicator'].search([
  248. ('name', 'ilike', field.field_description)
  249. ], limit=1)
  250. if indicator and field.name in record._fields:
  251. # Calculate indicator value using the indicator formula
  252. indicator_value = indicator.evaluate_formula(efficiency_data)
  253. # Update the stored field value
  254. record[field.name] = float_round(indicator_value, 2)
  255. @api.model
  256. def _recompute_all_indicators(self):
  257. """Recompute all indicator fields for all records"""
  258. records = self.search([])
  259. if records:
  260. records._calculate_all_indicators()
  261. def _get_indicator_field_name(self, indicator_name):
  262. """
  263. Convert indicator name to valid field name
  264. """
  265. import re
  266. # Remove special characters and convert to lowercase
  267. field_name = indicator_name.lower()
  268. # Replace spaces, hyphens, and other special characters with underscores
  269. field_name = re.sub(r'[^a-z0-9_]', '_', field_name)
  270. # Remove multiple consecutive underscores
  271. field_name = re.sub(r'_+', '_', field_name)
  272. # Remove leading and trailing underscores
  273. field_name = field_name.strip('_')
  274. # Ensure it starts with x_ for manual fields
  275. if not field_name.startswith('x_'):
  276. field_name = 'x_' + field_name
  277. # Ensure it doesn't exceed 63 characters (PostgreSQL limit)
  278. if len(field_name) > 63:
  279. field_name = field_name[:63]
  280. return field_name
  281. @api.model
  282. def _create_default_dynamic_fields(self):
  283. """
  284. Create default dynamic field records for existing indicators
  285. """
  286. # Get all active indicators
  287. indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  288. for indicator in indicators:
  289. # Check if field already exists in ir.model.fields
  290. field_name = self._get_indicator_field_name(indicator.name)
  291. existing_field = self.env['ir.model.fields'].search([
  292. ('model', '=', 'hr.efficiency'),
  293. ('name', '=', field_name)
  294. ], limit=1)
  295. if not existing_field:
  296. # Create field in ir.model.fields (like Studio does)
  297. self.env['ir.model.fields'].create({
  298. 'name': field_name,
  299. 'model': 'hr.efficiency',
  300. 'model_id': self.env['ir.model'].search([('model', '=', 'hr.efficiency')], limit=1).id,
  301. 'ttype': 'float',
  302. 'field_description': indicator.name,
  303. 'state': 'manual', # This is the key - it makes it a custom field
  304. 'store': True,
  305. 'compute': '_compute_indicators',
  306. })
  307. def _calculate_overall_efficiency(self, record):
  308. """
  309. Calculate overall efficiency based on configured indicators
  310. """
  311. indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  312. if not indicators:
  313. # Default calculation if no indicators configured
  314. return 0.0
  315. # Check if there's any data to calculate
  316. if (record.available_hours == 0 and
  317. record.planned_hours == 0 and
  318. record.actual_billable_hours == 0 and
  319. record.actual_non_billable_hours == 0):
  320. return 0.0
  321. total_weight = 0
  322. weighted_sum = 0
  323. valid_indicators = 0
  324. efficiency_data = {
  325. 'available_hours': getattr(record, 'available_hours', 0.0) or 0.0,
  326. 'planned_hours': getattr(record, 'planned_hours', 0.0) or 0.0,
  327. 'planned_billable_hours': getattr(record, 'planned_billable_hours', 0.0) or 0.0,
  328. 'planned_non_billable_hours': getattr(record, 'planned_non_billable_hours', 0.0) or 0.0,
  329. 'actual_billable_hours': getattr(record, 'actual_billable_hours', 0.0) or 0.0,
  330. 'actual_non_billable_hours': getattr(record, 'actual_non_billable_hours', 0.0) or 0.0,
  331. 'total_actual_hours': getattr(record, 'total_actual_hours', 0.0) or 0.0,
  332. 'expected_hours_to_date': getattr(record, 'expected_hours_to_date', 0.0) or 0.0,
  333. 'wage': getattr(record, 'wage', 0.0) or 0.0,
  334. 'utilization_rate': getattr(record, 'utilization_rate', 100.0) or 100.0,
  335. 'overhead': getattr(record, 'overhead', 40.0) or 40.0,
  336. }
  337. for indicator in indicators:
  338. if indicator.weight > 0:
  339. indicator_value = indicator.evaluate_formula(efficiency_data)
  340. # Count indicators with valid values (including 0 when it's a valid result)
  341. if indicator_value is not None:
  342. weighted_sum += indicator_value * indicator.weight
  343. total_weight += indicator.weight
  344. valid_indicators += 1
  345. # If no valid indicators or no total weight, return 0
  346. if total_weight <= 0 or valid_indicators == 0:
  347. return 0.0
  348. # Multiply by 100 to show as percentage
  349. return float_round((weighted_sum / total_weight) * 100, 2)
  350. @api.depends('employee_id', 'month_year')
  351. def _compute_display_name(self):
  352. for record in self:
  353. if record.employee_id and record.month_year:
  354. record.display_name = f"{record.employee_id.name} - {record.month_year}"
  355. else:
  356. record.display_name = "New Efficiency Record"
  357. @api.model
  358. def _calculate_employee_efficiency(self, employee, month_year):
  359. """
  360. Calculate efficiency for a specific employee and month
  361. """
  362. # Parse month_year (format: YYYY-MM)
  363. try:
  364. year, month = month_year.split('-')
  365. start_date = date(int(year), int(month), 1)
  366. end_date = (start_date + relativedelta(months=1)) - relativedelta(days=1)
  367. except ValueError:
  368. raise UserError(_("Invalid month_year format. Expected: YYYY-MM"))
  369. # Calculate available hours (considering holidays and time off)
  370. available_hours = self._calculate_available_hours(employee, start_date, end_date)
  371. # Calculate planned hours
  372. planned_hours, planned_billable_hours, planned_non_billable_hours = self._calculate_planned_hours(employee, start_date, end_date)
  373. # Calculate actual hours
  374. actual_billable_hours, actual_non_billable_hours = self._calculate_actual_hours(employee, start_date, end_date)
  375. # Calculate wage and currency from employee's contract
  376. # Use current date for wage calculation (when the record is being created/calculated)
  377. wage_date = date.today()
  378. wage, currency_id = self._get_employee_wage_and_currency(employee, wage_date)
  379. # Get employee's utilization rate and company's overhead at calculation time
  380. utilization_rate = employee.utilization_rate or 100.0
  381. overhead = employee.company_id.overhead or 40.0
  382. expected_profitability = employee.company_id.expected_profitability or 30.0
  383. efficiency_factor = employee.company_id.efficiency_factor or 85.0
  384. # Apply utilization_rate to actual_billable_hours to reflect real billable capacity
  385. adjusted_actual_billable_hours = actual_billable_hours * (utilization_rate / 100)
  386. return {
  387. 'month_year': month_year,
  388. 'employee_id': employee.id,
  389. 'company_id': employee.company_id.id,
  390. 'available_hours': available_hours,
  391. 'planned_hours': planned_hours,
  392. 'planned_billable_hours': planned_billable_hours,
  393. 'planned_non_billable_hours': planned_non_billable_hours,
  394. 'actual_billable_hours': adjusted_actual_billable_hours,
  395. 'actual_non_billable_hours': actual_non_billable_hours,
  396. 'wage': wage,
  397. 'currency_id': currency_id,
  398. 'utilization_rate': utilization_rate,
  399. 'overhead': overhead,
  400. 'expected_profitability': expected_profitability,
  401. 'efficiency_factor': efficiency_factor,
  402. }
  403. @api.model
  404. def _get_employee_wage_and_currency(self, employee, target_date):
  405. """
  406. Get employee's wage and currency from their contract for a specific date
  407. Always take the contract that is active on the target date
  408. If no active contract on that date, return 0 with company currency
  409. """
  410. if not employee:
  411. return 0.0, self.env.company.currency_id.id
  412. # Get all contracts for the employee
  413. contracts = self.env['hr.contract'].search([
  414. ('employee_id', '=', employee.id),
  415. ('state', '=', 'open')
  416. ], order='date_start desc')
  417. if not contracts:
  418. return 0.0, self.env.company.currency_id.id
  419. # Find the contract that is active on the target date
  420. for contract in contracts:
  421. contract_start = contract.date_start
  422. # Check if contract is active on target date
  423. if contract.date_end:
  424. # Contract has end date
  425. if contract_start <= target_date <= contract.date_end:
  426. wage = contract.wage or 0.0
  427. currency_id = contract.currency_id.id if contract.currency_id else self.env.company.currency_id.id
  428. return wage, currency_id
  429. else:
  430. # Contract has no end date, it's active for any date after start
  431. if contract_start <= target_date:
  432. wage = contract.wage or 0.0
  433. currency_id = contract.currency_id.id if contract.currency_id else self.env.company.currency_id.id
  434. return wage, currency_id
  435. # If no contract is active on the target date, return defaults
  436. return 0.0, self.env.company.currency_id.id
  437. def _calculate_available_hours(self, employee, start_date, end_date):
  438. """
  439. Calculate available hours considering holidays and time off
  440. """
  441. if not employee.resource_calendar_id:
  442. return 0.0
  443. # Convert dates to datetime for the method
  444. start_datetime = datetime.combine(start_date, datetime.min.time())
  445. end_datetime = datetime.combine(end_date, datetime.max.time())
  446. # Get working hours from calendar
  447. work_hours_data = employee._list_work_time_per_day(start_datetime, end_datetime)
  448. total_work_hours = sum(hours for _, hours in work_hours_data[employee.id])
  449. # Subtract hours from approved time off
  450. time_off_hours = self._get_time_off_hours(employee, start_date, end_date)
  451. return max(0.0, total_work_hours - time_off_hours)
  452. def _get_time_off_hours(self, employee, start_date, end_date):
  453. """
  454. Get hours from approved time off requests
  455. """
  456. # Get approved time off requests
  457. leaves = self.env['hr.leave'].search([
  458. ('employee_id', '=', employee.id),
  459. ('state', '=', 'validate'),
  460. ('date_from', '<=', end_date),
  461. ('date_to', '>=', start_date),
  462. ])
  463. total_hours = 0.0
  464. for leave in leaves:
  465. # Calculate overlap with the month
  466. overlap_start = max(leave.date_from.date(), start_date)
  467. overlap_end = min(leave.date_to.date(), end_date)
  468. if overlap_start <= overlap_end:
  469. # Get hours for the overlap period
  470. overlap_start_dt = datetime.combine(overlap_start, datetime.min.time())
  471. overlap_end_dt = datetime.combine(overlap_end, datetime.max.time())
  472. work_hours_data = employee._list_work_time_per_day(overlap_start_dt, overlap_end_dt)
  473. total_hours += sum(hours for _, hours in work_hours_data[employee.id])
  474. return total_hours
  475. def _calculate_planned_hours(self, employee, start_date, end_date):
  476. """
  477. Calculate planned hours from planning module
  478. """
  479. # Get planning slots for the employee that overlap with the date range
  480. # This is the same logic as Odoo Planning's Gantt chart
  481. start_datetime = datetime.combine(start_date, datetime.min.time())
  482. end_datetime = datetime.combine(end_date, datetime.max.time())
  483. planning_slots = self.env['planning.slot'].search([
  484. ('employee_id', '=', employee.id),
  485. ('start_datetime', '<=', end_datetime),
  486. ('end_datetime', '>=', start_datetime),
  487. # Removed state restriction to include all planning slots regardless of state
  488. ])
  489. total_planned = 0.0
  490. total_billable = 0.0
  491. total_non_billable = 0.0
  492. # Get working intervals for the resource and company calendar
  493. # This is the same approach as Odoo Planning's Gantt chart
  494. start_utc = pytz.utc.localize(start_datetime)
  495. end_utc = pytz.utc.localize(end_datetime)
  496. if employee.resource_id:
  497. resource_work_intervals, calendar_work_intervals = employee.resource_id._get_valid_work_intervals(
  498. start_utc, end_utc, calendars=employee.company_id.resource_calendar_id
  499. )
  500. else:
  501. # Fallback to company calendar if no resource
  502. calendar_work_intervals = {employee.company_id.resource_calendar_id.id: []}
  503. resource_work_intervals = {}
  504. for slot in planning_slots:
  505. # Use the same logic as Odoo Planning's Gantt chart
  506. # Calculate duration only within the specified period
  507. hours = slot._get_duration_over_period(
  508. start_utc, end_utc,
  509. resource_work_intervals, calendar_work_intervals, has_allocated_hours=False
  510. )
  511. # Check if the slot is linked to a billable project
  512. if slot.project_id and slot.project_id.allow_billable:
  513. total_billable += hours
  514. else:
  515. total_non_billable += hours
  516. total_planned += hours
  517. return total_planned, total_billable, total_non_billable
  518. def _calculate_actual_hours(self, employee, start_date, end_date):
  519. """
  520. Calculate actual hours from timesheets
  521. """
  522. # Get timesheets for the employee in the date range (excluding time off)
  523. timesheets = self.env['account.analytic.line'].search([
  524. ('employee_id', '=', employee.id),
  525. ('date', '>=', start_date),
  526. ('date', '<=', end_date),
  527. ('project_id', '!=', False), # Only project timesheets
  528. ('holiday_id', '=', False), # Exclude time off timesheets
  529. ])
  530. total_billable = 0.0
  531. total_non_billable = 0.0
  532. for timesheet in timesheets:
  533. hours = timesheet.unit_amount or 0.0
  534. # Additional filter: exclude time off tasks
  535. if timesheet.task_id and timesheet.task_id.name and 'time off' in timesheet.task_id.name.lower():
  536. continue # Skip time off tasks
  537. # Additional filter: exclude time off from name
  538. if timesheet.name and 'tiempo personal' in timesheet.name.lower():
  539. continue # Skip personal time entries
  540. # Check if the project is billable
  541. if timesheet.project_id and timesheet.project_id.allow_billable:
  542. total_billable += hours
  543. else:
  544. total_non_billable += hours
  545. return total_billable, total_non_billable
  546. @api.model
  547. def calculate_efficiency_for_period(self, start_month=None, end_month=None):
  548. """
  549. Calculate efficiency for all employees for a given period
  550. """
  551. if not start_month:
  552. # Default: last 3 months and next 6 months
  553. current_date = date.today()
  554. start_month = (current_date - relativedelta(months=3)).strftime('%Y-%m')
  555. end_month = (current_date + relativedelta(months=6)).strftime('%Y-%m')
  556. # Generate list of months
  557. months = self._generate_month_list(start_month, end_month)
  558. # Get all active employees of type 'employee'
  559. employees = self.env['hr.employee'].search([
  560. ('active', '=', True),
  561. ('employee_type', '=', 'employee')
  562. ])
  563. created_records = []
  564. for employee in employees:
  565. for month in months:
  566. # Calculate efficiency data
  567. efficiency_data = self._calculate_employee_efficiency(employee, month)
  568. # Check if there are changes compared to the latest record
  569. latest_record = self.search([
  570. ('employee_id', '=', employee.id),
  571. ('month_year', '=', month),
  572. ('company_id', '=', employee.company_id.id),
  573. ], order='calculation_date desc', limit=1)
  574. has_changes = False
  575. if latest_record:
  576. # Compare current data with latest record
  577. fields_to_compare = [
  578. 'available_hours', 'planned_hours', 'planned_billable_hours',
  579. 'planned_non_billable_hours', 'actual_billable_hours',
  580. 'actual_non_billable_hours'
  581. ]
  582. # Check basic fields
  583. for field in fields_to_compare:
  584. if abs(efficiency_data[field] - latest_record[field]) > 0.01: # Tolerance for floating point
  585. has_changes = True
  586. break
  587. # If no changes in basic fields, check dynamic indicators
  588. if not has_changes:
  589. # Get all active indicators
  590. active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  591. for indicator in active_indicators:
  592. field_name = self._get_indicator_field_name(indicator.name)
  593. # Calculate current indicator value
  594. current_value = indicator.evaluate_formula(efficiency_data)
  595. # Get previous indicator value from record
  596. previous_value = getattr(latest_record, field_name, None)
  597. # Compare values with tolerance
  598. if (current_value is not None and previous_value is not None and
  599. abs(current_value - previous_value) > 0.01):
  600. has_changes = True
  601. break
  602. elif current_value != previous_value: # Handle None vs value cases
  603. has_changes = True
  604. break
  605. else:
  606. # No previous record exists, so this is a change
  607. has_changes = True
  608. # Only create new record if there are changes
  609. if has_changes:
  610. # Archive existing records for this employee and month
  611. existing_records = self.search([
  612. ('employee_id', '=', employee.id),
  613. ('month_year', '=', month),
  614. ('company_id', '=', employee.company_id.id),
  615. ])
  616. if existing_records:
  617. existing_records.write({'active': False})
  618. # Create new record
  619. new_record = self.create(efficiency_data)
  620. created_records.append(new_record)
  621. # Calculate indicators for all newly created records
  622. if created_records:
  623. # Convert list to recordset
  624. created_recordset = self.browse([record.id for record in created_records])
  625. created_recordset._calculate_all_indicators()
  626. return {
  627. 'created': len(created_records),
  628. 'updated': 0, # No longer updating existing records
  629. 'total_processed': len(created_records),
  630. }
  631. @api.model
  632. def _init_dynamic_system(self):
  633. """
  634. Initialize dynamic fields and views when module is installed
  635. """
  636. import logging
  637. _logger = logging.getLogger(__name__)
  638. try:
  639. _logger.info("Starting dynamic system initialization...")
  640. # Step 1: Create dynamic field records for existing indicators
  641. self._create_default_dynamic_fields()
  642. _logger.info("Default dynamic fields created successfully")
  643. # Step 2: Dynamic fields are created via hr.efficiency.dynamic.field model
  644. _logger.info("Dynamic fields creation handled by hr.efficiency.dynamic.field model")
  645. # Step 3: Update views
  646. self._update_views_with_dynamic_fields()
  647. _logger.info("Views updated successfully")
  648. # Step 4: Force recompute of existing records
  649. records = self.search([])
  650. if records:
  651. records._invalidate_cache()
  652. _logger.info(f"Invalidated cache for {len(records)} records")
  653. _logger.info("Dynamic system initialization completed successfully")
  654. except Exception as e:
  655. _logger.error(f"Error during dynamic system initialization: {str(e)}")
  656. raise
  657. @api.model
  658. def _post_init_hook(self):
  659. """
  660. Post-install hook to ensure dynamic fields are created for existing indicators
  661. """
  662. import logging
  663. _logger = logging.getLogger(__name__)
  664. try:
  665. _logger.info("Running post-install hook for hr_efficiency module")
  666. # Ensure all active indicators have manual fields
  667. active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  668. for indicator in active_indicators:
  669. self.env['hr.efficiency.indicator']._create_dynamic_field(indicator)
  670. # Update views with dynamic fields
  671. self._update_views_with_dynamic_fields()
  672. # Apply default values to existing records
  673. self._apply_default_values_to_existing_records()
  674. _logger.info(f"Post-install hook completed. Processed {len(active_indicators)} indicators")
  675. except Exception as e:
  676. _logger.error(f"Error in post-install hook: {str(e)}")
  677. raise
  678. @api.model
  679. def create(self, vals_list):
  680. """
  681. Override create to calculate indicators when records are created
  682. """
  683. records = super().create(vals_list)
  684. # Calculate indicators for newly created records
  685. if records:
  686. records._calculate_all_indicators()
  687. return records
  688. def write(self, vals):
  689. """
  690. Override write to recalculate indicators when records are updated
  691. """
  692. result = super().write(vals)
  693. # Recalculate indicators for updated records
  694. self._calculate_all_indicators()
  695. return result
  696. @api.model
  697. def _register_hook(self):
  698. """
  699. Called when the registry is loaded.
  700. Update views with dynamic fields on every module load/restart.
  701. """
  702. super()._register_hook()
  703. try:
  704. # Update views with current dynamic fields on every module load
  705. self._update_views_with_dynamic_fields()
  706. except Exception as e:
  707. # Log error but don't prevent module loading
  708. import logging
  709. _logger = logging.getLogger(__name__)
  710. _logger.warning(f"Could not update dynamic views on module load: {str(e)}")
  711. @api.model
  712. def _update_views_with_dynamic_fields(self):
  713. """
  714. Update inherited views to include dynamic fields after module is loaded
  715. """
  716. import logging
  717. _logger = logging.getLogger(__name__)
  718. try:
  719. # Get active indicators ordered by sequence (consistent with other parts)
  720. active_indicators = self.env['hr.efficiency.indicator'].search([('active', '=', True)], order='sequence')
  721. fields_to_display = []
  722. for indicator in active_indicators:
  723. field_name = self._get_indicator_field_name(indicator.name)
  724. # Check if this indicator has a manual field
  725. manual_field = self.env['ir.model.fields'].search([
  726. ('model', '=', 'hr.efficiency'),
  727. ('state', '=', 'manual'),
  728. ('ttype', '=', 'float'),
  729. ('name', '=', field_name),
  730. ], limit=1)
  731. if manual_field:
  732. # Indicator with manual field
  733. fields_to_display.append({
  734. 'name': field_name,
  735. 'field_description': indicator.name,
  736. 'indicator': indicator
  737. })
  738. else:
  739. # Create manual field for this indicator
  740. _logger.info(f"Creating manual field for indicator '{indicator.name}'")
  741. self.env['hr.efficiency.indicator']._create_dynamic_field(indicator)
  742. # Add to display list after creation
  743. fields_to_display.append({
  744. 'name': field_name,
  745. 'field_description': indicator.name,
  746. 'indicator': indicator
  747. })
  748. _logger.info(f"Found {len(fields_to_display)} fields to add to views")
  749. # Build dynamic fields XML for list view
  750. dynamic_fields_xml = ''
  751. for field_info in fields_to_display:
  752. # Determine widget based on indicator type
  753. indicator = field_info['indicator']
  754. widget_map = {
  755. 'percentage': 'percentage', # Changed back to 'percentage' to show % symbol
  756. 'hours': 'float_time',
  757. 'currency': 'monetary',
  758. 'number': 'float'
  759. }
  760. widget = widget_map.get(indicator.indicator_type, 'float') if indicator else 'float'
  761. field_xml = f'<field name="{field_info["name"]}" widget="{widget}" optional="show"'
  762. # Add field name as string (tooltip will be shown automatically from field description)
  763. if indicator:
  764. field_xml += f' string="{indicator.name}"'
  765. # Add decorations based on indicator thresholds
  766. if indicator:
  767. # Use dynamic thresholds from indicator configuration
  768. green_threshold = indicator.color_threshold_green / 100.0
  769. yellow_threshold = indicator.color_threshold_yellow / 100.0
  770. field_xml += f' decoration-success="{field_info["name"]} &gt;= {green_threshold}"'
  771. field_xml += f' decoration-warning="{field_info["name"]} &gt;= {yellow_threshold} and {field_info["name"]} &lt; {green_threshold}"'
  772. field_xml += f' decoration-danger="{field_info["name"]} &lt; {yellow_threshold}"'
  773. # Add priority background color using CSS classes
  774. if hasattr(indicator, 'priority') and indicator.priority != 'none':
  775. if indicator.priority == 'low':
  776. field_xml += f' class="priority-low-bg"'
  777. elif indicator.priority == 'medium':
  778. field_xml += f' class="priority-medium-bg"'
  779. elif indicator.priority == 'high':
  780. field_xml += f' class="priority-high-bg"'
  781. field_xml += '/>'
  782. dynamic_fields_xml += field_xml
  783. # Update inherited list view
  784. inherited_list_view = self.env.ref('hr_efficiency.view_hr_efficiency_list_inherited', raise_if_not_found=False)
  785. if inherited_list_view:
  786. if dynamic_fields_xml:
  787. new_arch = f"""
  788. <xpath expr=\"//field[@name='expected_hours_to_date']\" position=\"after\">{dynamic_fields_xml}</xpath>
  789. """
  790. else:
  791. # If no dynamic fields, remove any existing dynamic fields from the view
  792. new_arch = """
  793. <xpath expr=\"//field[@name='expected_hours_to_date']\" position=\"after\">
  794. <!-- No dynamic fields to display -->
  795. </xpath>
  796. """
  797. inherited_list_view.write({'arch': new_arch})
  798. _logger.info(f"Updated inherited list view with {len(fields_to_display)} dynamic fields")
  799. # Build dynamic fields XML for form view
  800. form_dynamic_fields_xml = ''
  801. for field_info in fields_to_display:
  802. # Determine widget based on indicator type
  803. indicator = field_info['indicator']
  804. widget_map = {
  805. 'percentage': 'badge',
  806. 'hours': 'float_time',
  807. 'currency': 'monetary',
  808. 'number': 'float'
  809. }
  810. widget = widget_map.get(indicator.indicator_type, 'badge') if indicator else 'badge'
  811. field_xml = f'<field name="{field_info["name"]}" widget="{widget}"'
  812. # Add help text with indicator description (valid in form views)
  813. if indicator and indicator.description:
  814. # Escape quotes in description for XML
  815. help_text = indicator.description.replace('"', '&quot;')
  816. field_xml += f' help="{help_text}"'
  817. # Add decorations based on indicator thresholds
  818. if indicator:
  819. # Use dynamic thresholds from indicator configuration
  820. green_threshold = indicator.color_threshold_green / 100.0
  821. yellow_threshold = indicator.color_threshold_yellow / 100.0
  822. field_xml += f' decoration-success="{field_info["name"]} &gt;= {green_threshold}"'
  823. field_xml += f' decoration-warning="{field_info["name"]} &gt;= {yellow_threshold} and {field_info["name"]} &lt; {green_threshold}"'
  824. field_xml += f' decoration-danger="{field_info["name"]} &lt; {yellow_threshold}"'
  825. # Add priority background color using CSS classes
  826. if hasattr(indicator, 'priority') and indicator.priority != 'none':
  827. if indicator.priority == 'low':
  828. field_xml += f' class="priority-low-bg"'
  829. elif indicator.priority == 'medium':
  830. field_xml += f' class="priority-medium-bg"'
  831. elif indicator.priority == 'high':
  832. field_xml += f' class="priority-high-bg"'
  833. field_xml += '/>'
  834. form_dynamic_fields_xml += field_xml
  835. # Update inherited form view
  836. inherited_form_view = self.env.ref('hr_efficiency.view_hr_efficiency_form_inherited', raise_if_not_found=False)
  837. if inherited_form_view:
  838. if form_dynamic_fields_xml:
  839. new_form_arch = f"""
  840. <xpath expr=\"//field[@name='overall_efficiency']\" position=\"before\">{form_dynamic_fields_xml}</xpath>
  841. """
  842. else:
  843. # If no dynamic fields, remove any existing dynamic fields from the view
  844. new_form_arch = """
  845. <xpath expr=\"//field[@name='overall_efficiency']\" position=\"before\">
  846. <!-- No dynamic fields to display -->
  847. </xpath>
  848. """
  849. inherited_form_view.write({'arch': new_form_arch})
  850. _logger.info("Updated inherited form view with dynamic fields")
  851. except Exception as e:
  852. _logger.error(f"Error updating views with dynamic fields: {str(e)}")
  853. raise
  854. def _generate_month_list(self, start_month, end_month):
  855. """
  856. Generate list of months between start_month and end_month (inclusive)
  857. """
  858. months = []
  859. # Convert date objects to datetime if needed
  860. if isinstance(start_month, date):
  861. start_month = start_month.strftime('%Y-%m')
  862. if isinstance(end_month, date):
  863. end_month = end_month.strftime('%Y-%m')
  864. current = datetime.strptime(start_month, '%Y-%m')
  865. end = datetime.strptime(end_month, '%Y-%m')
  866. while current <= end:
  867. months.append(current.strftime('%Y-%m'))
  868. current = current + relativedelta(months=1)
  869. return months
  870. @api.model
  871. def _cron_calculate_efficiency(self):
  872. """
  873. Cron job to automatically calculate efficiency
  874. """
  875. self.calculate_efficiency_for_period()
  876. self._update_dynamic_filter_labels()
  877. @api.model
  878. def _update_dynamic_filter_labels(self):
  879. """
  880. Update dynamic filter labels based on current date
  881. """
  882. labels = self._get_dynamic_month_labels()
  883. # Update filter labels
  884. filter_mapping = {
  885. 'filter_two_months_ago': labels['two_months_ago'],
  886. 'filter_last_month': labels['last_month'],
  887. 'filter_current_month': labels['current_month'],
  888. 'filter_next_month': labels['next_month'],
  889. 'filter_two_months_ahead': labels['two_months_ahead'],
  890. }
  891. for filter_name, label in filter_mapping.items():
  892. try:
  893. filter_record = self.env.ref(f'hr_efficiency.{filter_name}', raise_if_not_found=False)
  894. if filter_record:
  895. filter_record.write({'name': label})
  896. except Exception as e:
  897. _logger.warning(f"Could not update filter {filter_name}: {str(e)}")
  898. @api.model
  899. def _get_month_filter_options(self):
  900. """
  901. Get dynamic month filter options for search view
  902. Returns a list of tuples (month_year, display_name) for the last 2, current, and next 2 months
  903. """
  904. current_date = date.today()
  905. months = []
  906. # Last 2 months
  907. for i in range(2, 0, -1):
  908. month_date = current_date - relativedelta(months=i)
  909. month_year = month_date.strftime('%Y-%m')
  910. month_name = month_date.strftime('%B %Y') # e.g., "August 2024"
  911. months.append((month_year, month_name))
  912. # Current month
  913. current_month_year = current_date.strftime('%Y-%m')
  914. current_month_name = current_date.strftime('%B %Y')
  915. months.append((current_month_year, current_month_name))
  916. # Next 2 months
  917. for i in range(1, 3):
  918. month_date = current_date + relativedelta(months=i)
  919. month_year = month_date.strftime('%Y-%m')
  920. month_name = month_date.strftime('%B %Y')
  921. months.append((month_year, month_name))
  922. return months
  923. @api.model
  924. def _get_dynamic_month_labels(self):
  925. """
  926. Get dynamic month labels for filters
  927. Returns a dictionary with month labels like "Mes 1", "Mes 2", "Mes 3 << actual", etc.
  928. """
  929. current_date = date.today()
  930. labels = {}
  931. # 2 months ago
  932. month_2_ago = current_date - relativedelta(months=2)
  933. labels['two_months_ago'] = f"Mes {month_2_ago.strftime('%m')}"
  934. # 1 month ago
  935. month_1_ago = current_date - relativedelta(months=1)
  936. labels['last_month'] = f"Mes {month_1_ago.strftime('%m')}"
  937. # Current month
  938. labels['current_month'] = f"Mes {current_date.strftime('%m')} << actual"
  939. # Next month
  940. month_1_ahead = current_date + relativedelta(months=1)
  941. labels['next_month'] = f"Mes {month_1_ahead.strftime('%m')}"
  942. # 2 months ahead
  943. month_2_ahead = current_date + relativedelta(months=2)
  944. labels['two_months_ahead'] = f"Mes {month_2_ahead.strftime('%m')}"
  945. return labels
  946. def _count_working_days(self, start_date, end_date, employee=None):
  947. """
  948. Count working days between two dates considering employee calendar
  949. """
  950. working_days = 0
  951. current_date = start_date
  952. while current_date <= end_date:
  953. # Check if it's a working day according to employee calendar
  954. if self._is_working_day(current_date, employee):
  955. working_days += 1
  956. current_date += relativedelta(days=1)
  957. return working_days
  958. def _is_working_day(self, check_date, employee=None):
  959. """
  960. Check if a date is a working day considering employee calendar
  961. """
  962. if not employee or not employee.resource_calendar_id:
  963. # Fallback to basic weekday check
  964. return check_date.weekday() < 5
  965. try:
  966. # Convert date to datetime for calendar check
  967. check_datetime = datetime.combine(check_date, datetime.min.time())
  968. # Check if the day is a working day according to employee calendar
  969. working_hours = employee.resource_calendar_id._list_work_time_per_day(
  970. check_datetime, check_datetime, compute_leaves=True
  971. )
  972. # If there are working hours, it's a working day
  973. return bool(working_hours)
  974. except Exception:
  975. # Fallback to basic weekday check if there's any error
  976. return check_date.weekday() < 5
  977. @api.model
  978. def _apply_default_values_to_existing_records(self):
  979. """Apply default values to existing employee and company records"""
  980. import logging
  981. _logger = logging.getLogger(__name__)
  982. try:
  983. # Update employees with utilization_rate = 100.0 if not set
  984. employees_to_update = self.env['hr.employee'].search([
  985. ('utilization_rate', '=', 0.0)
  986. ])
  987. if employees_to_update:
  988. employees_to_update.write({'utilization_rate': 100.0})
  989. _logger.info(f"Updated {len(employees_to_update)} employees with default utilization_rate = 100.0%")
  990. # Update companies with overhead = 40.0 if not set
  991. companies_to_update = self.env['res.company'].search([
  992. ('overhead', '=', 0.0)
  993. ])
  994. if companies_to_update:
  995. companies_to_update.write({'overhead': 40.0})
  996. _logger.info(f"Updated {len(companies_to_update)} companies with default overhead = 40.0%")
  997. _logger.info("Default values applied successfully to existing records")
  998. except Exception as e:
  999. _logger.error(f"Error applying default values to existing records: {str(e)}")
  1000. # Don't raise the exception to avoid breaking the installation