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>
249 lines
8.9 KiB
Python
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',
|
|
)
|