from datetime import timedelta from odoo import api, fields, models from dateutil.relativedelta import relativedelta class FlHearing(models.Model): """ Phase 2 — Full implementation with calendar integration, workflow buttons, and pre-hearing checklist for Florida family law hearings. """ _name = 'fl.hearing' _description = 'Case Hearing' _inherit = ['mail.thread'] _order = 'hearing_date asc' _rec_name = 'name' case_id = fields.Many2one( 'fl.case', required=True, ondelete='cascade', index=True, string='Case' ) name = fields.Char(string='Hearing Description', required=True) hearing_date = fields.Datetime( string='Hearing Date / Time', tracking=True ) duration_hours = fields.Float( string='Duration (hours)', default=1.0, help='Expected hearing duration. Default 1 hour.' ) location = fields.Char( string='Location', default='Lawson E. Thomas Courthouse Center, 175 NW 1st Ave, Miami, FL 33128' ) courtroom = fields.Char(string='Courtroom / Floor') judge_id = fields.Many2one( 'res.partner', string='Judge', related='case_id.judge_id', store=True ) hearing_type = fields.Selection([ ('status_conference', 'Status Conference'), ('motion', 'Motion Hearing'), ('temporary_relief', 'Temporary Relief Hearing'), ('mediation', 'Mediation'), ('final', 'Final Hearing'), ('contempt', 'Contempt Hearing'), ('other', 'Other'), ], string='Hearing Type', default='final', required=True, tracking=True) state = fields.Selection([ ('scheduled', 'Scheduled'), ('completed', 'Completed'), ('cancelled', 'Cancelled'), ('continued', 'Continued'), ], string='Status', default='scheduled', tracking=True) notes = fields.Text(string='Pre-Hearing Notes / Preparation') outcome = fields.Text(string='Outcome / Result') order_entered = fields.Boolean(string='Order Entered After Hearing') continued_date = fields.Datetime(string='New Date (if Continued)') # ── Calendar Integration ────────────────────────────────────────────────── calendar_event_id = fields.Many2one( 'calendar.event', string='Calendar Event', ondelete='set null' ) # ── Pre-Hearing Checklist ───────────────────────────────────────────────── parenting_class_warning = fields.Char( string='Parenting Class Status', compute='_compute_parenting_class_warning', help='FL 61.21 — Both parents must complete before final hearing' ) discovery_cutoff_warning = fields.Char( string='Discovery Cutoff', compute='_compute_discovery_cutoff_warning', help='Discovery cutoff is 30 days before hearing date' ) financial_disclosure_warning = fields.Char( string='Financial Disclosure Status', compute='_compute_financial_disclosure_warning', ) # ══════════════════════════════════════════════════════════════════════════ # COMPUTED: Pre-Hearing Checks # ══════════════════════════════════════════════════════════════════════════ @api.depends( 'case_id.petitioner_parenting_class_done', 'case_id.respondent_parenting_class_done', 'case_id.parenting_class_required', ) def _compute_parenting_class_warning(self): for rec in self: case = rec.case_id if not case.parenting_class_required: rec.parenting_class_warning = '✅ Not required (no minor children)' elif (case.petitioner_parenting_class_done and case.respondent_parenting_class_done): rec.parenting_class_warning = '✅ Both parties completed (FL 61.21)' else: missing = [] if not case.petitioner_parenting_class_done: missing.append('Petitioner') if not case.respondent_parenting_class_done: missing.append('Respondent') rec.parenting_class_warning = ( '⚠️ FL 61.21: Parenting class NOT completed by: {}. ' 'Final hearing may not proceed without completion.' ).format(', '.join(missing)) @api.depends('hearing_date') def _compute_discovery_cutoff_warning(self): today = fields.Date.today() for rec in self: if not rec.hearing_date: rec.discovery_cutoff_warning = '⚪ Set hearing date to see discovery cutoff' continue cutoff = rec.hearing_date.date() - relativedelta(days=30) if today > cutoff: rec.discovery_cutoff_warning = ( '🔴 Discovery cutoff PASSED ({}) — 30 days before hearing. ' 'No new discovery requests may be served.' ).format(cutoff) else: days_left = (cutoff - today).days rec.discovery_cutoff_warning = ( '✅ Discovery cutoff: {} ({} days remaining)' ).format(cutoff, days_left) @api.depends('case_id.deadline_ids.completed', 'case_id.deadline_ids.deadline_type') def _compute_financial_disclosure_warning(self): for rec in self: disc_deadline = rec.case_id.deadline_ids.filtered( lambda d: d.deadline_type == 'financial_disclosure' ) if not disc_deadline: rec.financial_disclosure_warning = '⚪ No financial disclosure deadline set' elif any(d.completed for d in disc_deadline): rec.financial_disclosure_warning = '✅ Financial disclosure exchanged (FL 12.285)' elif any(d.is_overdue for d in disc_deadline): rec.financial_disclosure_warning = ( '🔴 OVERDUE: Financial disclosure not yet exchanged (FL 12.285)' ) else: due = min(d.due_date for d in disc_deadline) rec.financial_disclosure_warning = ( '⏳ Financial disclosure due: {} (FL 12.285)' ).format(due) # ══════════════════════════════════════════════════════════════════════════ # CRUD — calendar event lifecycle # ══════════════════════════════════════════════════════════════════════════ @api.model_create_multi def create(self, vals_list): records = super().create(vals_list) for rec in records: if rec.hearing_date: rec.with_context(_no_hearing_sync=True)._create_or_update_calendar_event() rec._create_hearing_deadline() return records def write(self, vals): result = super().write(vals) if not self.env.context.get('_no_hearing_sync'): sync_fields = {'hearing_date', 'name', 'state', 'duration_hours', 'location', 'courtroom'} if sync_fields.intersection(vals.keys()): for rec in self: rec.with_context(_no_hearing_sync=True)._create_or_update_calendar_event() return result # ══════════════════════════════════════════════════════════════════════════ # CALENDAR INTEGRATION # ══════════════════════════════════════════════════════════════════════════ def _create_or_update_calendar_event(self): """Create or sync a calendar.event for this hearing.""" if not self.hearing_date: return if self.state in ('cancelled',): if self.calendar_event_id: self.calendar_event_id.write({'active': False}) return # Build attendee list partners = self.env['res.partner'] if self.case_id.petitioner_id: partners |= self.case_id.petitioner_id if self.env.user.partner_id: partners |= self.env.user.partner_id stop_dt = self.hearing_date + timedelta(hours=self.duration_hours or 1.0) type_label = dict(self._fields['hearing_type'].selection).get( self.hearing_type, 'Hearing' ) event_name = '[{}] {} — {}'.format( self.case_id.name, type_label, self.name ) description = ( 'Florida Family Law Hearing\n' 'Case: {}\n' 'Type: {}\n' 'Location: {}\n' 'Courtroom: {}\n' 'Judge: {}\n' 'Status: {}' ).format( self.case_id.name, type_label, self.location or 'TBD', self.courtroom or 'TBD', self.judge_id.name if self.judge_id else 'TBD', dict(self._fields['state'].selection).get(self.state, ''), ) if self.calendar_event_id: self.calendar_event_id.write({ 'name': event_name, 'start': self.hearing_date, 'stop': stop_dt, 'description': description, 'active': self.state != 'cancelled', 'show_as': 'busy', }) else: event = self.env['calendar.event'].sudo().create({ 'name': event_name, 'start': self.hearing_date, 'stop': stop_dt, 'description': description, 'partner_ids': [(6, 0, partners.ids)], 'show_as': 'busy', 'privacy': 'confidential', }) self.write({'calendar_event_id': event.id}) def _create_hearing_deadline(self): """ Create an fl.deadline 'hearing' record for this hearing so it appears in the case deadline tracker. """ if not self.hearing_date: return existing = self.env['fl.deadline'].search([ ('case_id', '=', self.case_id.id), ('deadline_type', '=', 'hearing'), ], limit=1) if not existing: self.env['fl.deadline'].create({ 'case_id': self.case_id.id, 'name': '{} Hearing — {}'.format( dict(self._fields['hearing_type'].selection).get( self.hearing_type, 'Hearing' ), self.name, ), 'deadline_type': 'hearing', 'due_date': self.hearing_date.date(), 'statute_reference': 'Florida Rules of Civil Procedure', 'notes': ( 'Location: {}\n' 'Courtroom: {}' ).format(self.location or '', self.courtroom or 'TBD'), }) # ══════════════════════════════════════════════════════════════════════════ # WORKFLOW BUTTONS # ══════════════════════════════════════════════════════════════════════════ def action_mark_completed(self): """Mark hearing as completed and archive calendar event.""" self.write({'state': 'completed'}) if self.calendar_event_id: self.calendar_event_id.write({'active': False}) # Update linked hearing deadline self.env['fl.deadline'].search([ ('case_id', '=', self.case_id.id), ('deadline_type', '=', 'hearing'), ]).write({'completed': True, 'completed_date': fields.Date.today()}) self.case_id.message_post( body=( '✅ Hearing Completed
' '{} — {}
' 'Update the Outcome field with the judge\'s ruling.' ).format( dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''), self.name, ), subtype_xmlid='mail.mt_note', ) def action_cancel(self): """Cancel this hearing and archive calendar event.""" self.write({'state': 'cancelled'}) if self.calendar_event_id: self.calendar_event_id.write({'active': False}) self.case_id.message_post( body=( '❌ Hearing Cancelled
' '{} — {} has been cancelled.' ).format( dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''), self.name, ), subtype_xmlid='mail.mt_note', ) def action_mark_continued(self): """ Mark hearing as continued (rescheduled by court). User must enter the new hearing date separately. """ self.write({'state': 'continued'}) self.case_id.message_post( body=( '⏸️ Hearing Continued
' '{} — {} has been continued (rescheduled by the court).
' 'Action required: Contact the court for the new hearing date ' 'and create a new hearing record.' ).format( dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''), self.name, ), subtype_xmlid='mail.mt_note', )