Files
famlaw/activeblue_familylaw/models/fl_deadline.py
Carlos Garcia 1d52d85a78 Phase 1: core models, security, seed data, and backend views
Implements full Phase 1 of the activeblue_familylaw Odoo 18 module:
- 17 Python models (fl.case, fl.party, fl.child, fl.support.calculation,
  fl.fee.waiver, fl.income.withholding, fl.deadline, fl.hearing,
  fl.deposition, fl.discovery, fl.document, fl.caselaw, fl.analysis,
  fl.ai.engine, fl.argument, fl.statute, fl.issue.tag) + hr.expense extension
- 3 wizard stubs (intake, analysis, generate-packet)
- Security: 4 groups (admin/paralegal/portal-petitioner/portal-respondent)
  + record rules scoping portal users to their own cases
- Seed data: issue tags, FL statutes, FL DCF support schedule, ir.sequence
- 13 backend view XML files with FL 61.30 worksheet, fee waiver
  eligibility banner, DV safety resources, emancipation alerts
- Static CSS/JS stubs for Phase 6 portal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 18:52:04 -04:00

249 lines
8.9 KiB
Python

from datetime import date, datetime, time
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
class FlDeadline(models.Model):
"""
Phase 2 — Full implementation with calendar integration and cron alerts.
Phase 1: Core fields and generate_deadlines_for_case stub.
"""
_name = 'fl.deadline'
_description = 'Case Deadline'
_inherit = ['mail.thread']
_order = 'due_date asc'
case_id = fields.Many2one(
'fl.case', required=True, ondelete='cascade', index=True
)
name = fields.Char(string='Deadline', required=True)
deadline_type = fields.Selection([
('filing', 'Filing'),
('service', 'Service of Process'),
('response', 'Response / Answer'),
('discovery_open', 'Discovery Opens'),
('financial_disclosure', 'Financial Disclosure Exchange'),
('deposition_notice', 'Deposition Notice Deadline'),
('deposition', 'Deposition'),
('discovery_cutoff', 'Discovery Cutoff'),
('parenting_class', 'Parenting Class Completion'),
('mediation', 'Mediation'),
('hearing', 'Hearing'),
('compliance', 'Compliance Deadline'),
('emancipation', 'Emancipation'),
('default_motion', 'Motion for Default'),
('custom', 'Custom'),
], required=True)
statute_reference = fields.Char(
string='Statute / Rule',
help='e.g. FL 1.140 — 20 days to answer'
)
due_date = fields.Date(string='Due Date', required=True, tracking=True)
anchor_date = fields.Date(
string='Anchor Date',
help='Date this deadline is calculated from'
)
offset_days = fields.Integer(
string='Offset Days',
help='Days from anchor date to due date'
)
completed = fields.Boolean(string='Completed', tracking=True)
completed_date = fields.Date(string='Completion Date')
waived = fields.Boolean(string='Waived')
notes = fields.Text(string='Notes')
# ── Calendar Integration (Phase 2) ────────────────────────────────────
calendar_event_id = fields.Many2one(
'calendar.event', string='Calendar Event'
)
# ── Alerts ────────────────────────────────────────────────────────────
alert_7day_sent = fields.Boolean(default=False)
alert_3day_sent = fields.Boolean(default=False)
alert_1day_sent = fields.Boolean(default=False)
overdue_alert_sent = fields.Boolean(default=False)
is_overdue = fields.Boolean(
string='Overdue',
compute='_compute_is_overdue', store=True
)
days_until_due = fields.Integer(
string='Days Until Due',
compute='_compute_days_until_due'
)
@api.depends('due_date', 'completed', 'waived')
def _compute_is_overdue(self):
today = fields.Date.today()
for rec in self:
rec.is_overdue = (
not rec.completed
and not rec.waived
and bool(rec.due_date)
and rec.due_date < today
)
@api.depends('due_date')
def _compute_days_until_due(self):
today = fields.Date.today()
for rec in self:
if rec.due_date:
rec.days_until_due = (rec.due_date - today).days
else:
rec.days_until_due = 0
def action_mark_complete(self):
self.completed = True
self.completed_date = fields.Date.today()
# ── Generation Engine ─────────────────────────────────────────────────
@api.model
def generate_deadlines_for_case(self, case):
"""
Auto-generate procedural deadlines from filing_date.
Phase 1: Core deadlines only.
Phase 2: Full calendar events + alert setup.
"""
if not case.filing_date:
return
# Avoid duplicates on re-run
existing_types = case.deadline_ids.mapped('deadline_type')
rules = [
{
'name': 'Serve Respondent (Target)',
'deadline_type': 'service',
'offset_days': 30,
'anchor': case.filing_date,
'statute_reference': 'FL 1.070 — 120-day maximum',
},
]
if case.has_minor_children:
rules.append({
'name': 'Parenting Class — Petitioner (FL 61.21)',
'deadline_type': 'parenting_class',
'offset_days': 45,
'anchor': case.filing_date,
'statute_reference': 'FL 61.21',
})
for rule in rules:
if rule['deadline_type'] in existing_types:
continue
due = rule['anchor'] + relativedelta(days=rule['offset_days'])
self.create({
'case_id': case.id,
'name': rule['name'],
'deadline_type': rule['deadline_type'],
'due_date': due,
'anchor_date': rule['anchor'],
'offset_days': rule['offset_days'],
'statute_reference': rule.get('statute_reference', ''),
})
@api.model
def recalculate_service_deadlines(self, case):
"""
Recalculate deadlines anchored to service_date when it is set.
Called from fl_case.write when service_date changes.
"""
if not case.service_date:
return
service_anchored = [
{
'name': 'Respondent Answer Deadline',
'deadline_type': 'response',
'offset_days': 20,
'statute_reference': 'FL 1.140 — 20 days to answer',
},
{
'name': 'Mandatory Financial Disclosure Exchange',
'deadline_type': 'financial_disclosure',
'offset_days': 45,
'statute_reference': 'FL 12.285 — 45 days',
},
{
'name': 'Discovery Opens (Case at Issue)',
'deadline_type': 'discovery_open',
'offset_days': 20,
'statute_reference': 'FL 12.280',
},
]
if case.has_minor_children:
service_anchored.append({
'name': 'Parenting Class — Respondent (FL 61.21)',
'deadline_type': 'parenting_class',
'offset_days': 60,
'statute_reference': 'FL 61.21',
})
existing = {d.deadline_type: d for d in case.deadline_ids}
for rule in service_anchored:
due = case.service_date + relativedelta(days=rule['offset_days'])
dl_type = rule['deadline_type']
if dl_type in existing:
existing[dl_type].write({
'due_date': due,
'anchor_date': case.service_date,
})
else:
self.create({
'case_id': case.id,
'name': rule['name'],
'deadline_type': dl_type,
'due_date': due,
'anchor_date': case.service_date,
'offset_days': rule['offset_days'],
'statute_reference': rule.get('statute_reference', ''),
})
def _cron_deadline_alerts(self):
"""Run daily — send deadline alerts at 7, 3, 1 days and overdue."""
today = fields.Date.today()
upcoming = self.search([
('completed', '=', False),
('waived', '=', False),
('due_date', '>=', today),
])
for dl in upcoming:
days = (dl.due_date - today).days
if days == 7 and not dl.alert_7day_sent:
dl._send_deadline_alert('7 days')
dl.alert_7day_sent = True
elif days == 3 and not dl.alert_3day_sent:
dl._send_deadline_alert('3 days')
dl.alert_3day_sent = True
elif days == 1 and not dl.alert_1day_sent:
dl._send_deadline_alert('1 day')
dl.alert_1day_sent = True
overdue = self.search([
('completed', '=', False),
('waived', '=', False),
('due_date', '<', today),
('overdue_alert_sent', '=', False),
])
for dl in overdue:
dl._send_deadline_alert('OVERDUE')
dl.overdue_alert_sent = True
def _send_deadline_alert(self, timing):
icon = '🔴' if timing == 'OVERDUE' else ''
self.case_id.message_post(
body=(
f'{icon} <b>Deadline Alert — {timing}</b><br/>'
f'<b>{self.name}</b> is due on <b>{self.due_date}</b>.<br/>'
f'Statute: {self.statute_reference or "N/A"}'
),
subtype_xmlid='mail.mt_note',
)