from odoo import api, fields, models class HrEmployee(models.Model): _inherit = 'hr.employee' fl_hourly_rate = fields.Float( string='Family Law Billing Rate ($/hr)', help='Hourly billing rate used for family-law case timesheets. ' 'Falls back to the firm default (ir.config_parameter ' 'fl_timesheet.default_hourly_rate) when unset.' ) class FlTimesheet(models.Model): """ Family-law timesheet / AI audit entry. Wraps account.analytic.line via delegation inheritance: each fl.timesheet owns one analytic line, so billable hours flow through standard Odoo Accounting. duration_hours maps to the analytic line's unit_amount. AI agents create non-billable entries (is_billable=False, ai_agent set) for an audit trail; people create billable entries from the case Timesheet tab. """ _name = 'fl.timesheet' _description = 'Family Law Timesheet / AI Audit Entry' _inherits = {'account.analytic.line': 'analytic_line_id'} _order = 'date desc, id desc' analytic_line_id = fields.Many2one( 'account.analytic.line', string='Analytic Line', required=True, ondelete='cascade', auto_join=True, index=True ) case_id = fields.Many2one( 'fl.case', string='Case', required=True, ondelete='cascade', index=True ) employee_id = fields.Many2one('hr.employee', string='Employee') is_billable = fields.Boolean( string='Billable', default=True, help='Uncheck for non-billable entries (e.g. AI audit time).' ) ai_agent = fields.Selection([ ('paralegal', 'Paralegal Agent'), ('attorney', 'Attorney Agent'), ], string='AI Agent', help='Set when an AI agent auto-logged this entry for the audit trail.') duration_hours = fields.Float(string='Duration (Hours)', default=0.0) hourly_rate = fields.Float( string='Rate ($/hr)', compute='_compute_billable_amount' ) billable_amount = fields.Monetary( string='Amount', compute='_compute_billable_amount', currency_field='currency_id' ) @api.depends('duration_hours', 'is_billable', 'employee_id') def _compute_billable_amount(self): default_rate = float(self.env['ir.config_parameter'].sudo().get_param( 'fl_timesheet.default_hourly_rate', 0.0) or 0.0) for rec in self: rate = rec.employee_id.fl_hourly_rate or default_rate rec.hourly_rate = rate rec.billable_amount = ( rec.duration_hours * rate if rec.is_billable else 0.0 ) # ────────────────────────────────────────────────────────────────────── # CRUD — keep the wrapped analytic line in sync # ────────────────────────────────────────────────────────────────────── @api.model_create_multi def create(self, vals_list): for vals in vals_list: self._prepare_line_vals(vals) return super().create(vals_list) def write(self, vals): if 'duration_hours' in vals: vals['unit_amount'] = vals['duration_hours'] return super().write(vals) def _prepare_line_vals(self, vals): """Fill the analytic line's required fields before delegation create.""" case = self.env['fl.case'].browse(vals['case_id']) if vals.get('case_id') \ else self.env['fl.case'] if not vals.get('account_id'): vals['account_id'] = self._resolve_analytic_account(case).id if not vals.get('name'): if vals.get('ai_agent'): label = dict(self._fields['ai_agent'].selection).get( vals['ai_agent'], vals['ai_agent']) vals['name'] = f'AI audit: {label}' else: vals['name'] = case.name or 'Time entry' if 'duration_hours' in vals: vals['unit_amount'] = vals['duration_hours'] def _resolve_analytic_account(self, case): """ Resolve the analytic account for the wrapped line. Prefers the case project's analytic account (field name varies by version), falling back to a cached firm-wide account created under any available analytic plan. """ Account = self.env['account.analytic.account'].sudo() project = case.project_id if case else self.env['project.project'] if project: for fname in ('account_id', 'analytic_account_id'): acc = getattr(project, fname, False) if acc: return acc param = self.env['ir.config_parameter'].sudo() acc_id = param.get_param('fl_timesheet.analytic_account_id') if acc_id: acc = Account.browse(int(acc_id)) if acc.exists(): return acc Plan = self.env['account.analytic.plan'].sudo() plan = Plan.search([], limit=1) or Plan.create({'name': 'Family Law'}) acc = Account.create({ 'name': 'Family Law Cases', 'plan_id': plan.id, 'company_id': self.env.company.id, }) param.set_param('fl_timesheet.analytic_account_id', acc.id) return acc