Full fl_deadline.py: - Calendar event creation/update on every deadline (all-day events) - _cron_check_default_judgment: FL 12.922 — if respondent misses Day 20 answer deadline, auto-creates Motion for Default deadline (5 days), project task, and urgent chatter alert - _cron_deadline_alerts: 7/3/1-day and overdue chatter alerts - Complete service-anchored deadline set: response (Day 20), financial disclosure + mandatory disclosure cert (Day 45), discovery opens (Day 20), respondent parenting class (Day 60), 120-day service max - context-flag pattern (_no_calendar_sync) to prevent recursive write loops Full fl_hearing.py: - Calendar event sync (show_as=busy, confidential) - Pre-hearing checklist computed fields: parenting class (FL 61.21), discovery cutoff (hearing -30 days), financial disclosure status - Workflow buttons: Mark Completed, Mark Continued, Cancel - _create_hearing_deadline: creates fl.deadline record for each hearing New data/fl_deadline_rules.xml: - ir.cron: Daily Deadline Alerts (fl_deadline._cron_deadline_alerts) - ir.cron: Default Judgment Check (fl_deadline._cron_check_default_judgment) - ir.cron: Emancipation Alerts (fl_child._cron_emancipation_alerts) New data/mail_templates.xml: - Deadline Alert Upcoming (EN) - Deadline Alert OVERDUE (EN) - Portal Welcome (EN + ES bilingual) - Default Judgment Window Alert (EN) Enhanced views: deadline + hearing now have calendar view, search view with filters (overdue, due this week, by type), and group-by options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
327 lines
14 KiB
Python
327 lines
14 KiB
Python
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=(
|
|
'✅ <b>Hearing Completed</b><br/>'
|
|
'{} — {}<br/>'
|
|
'<i>Update the Outcome field with the judge\'s ruling.</i>'
|
|
).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=(
|
|
'❌ <b>Hearing Cancelled</b><br/>'
|
|
'{} — {} 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=(
|
|
'⏸️ <b>Hearing Continued</b><br/>'
|
|
'{} — {} has been continued (rescheduled by the court).<br/>'
|
|
'<b>Action required:</b> 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',
|
|
)
|