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>
This commit is contained in:
248
activeblue_familylaw/models/fl_deadline.py
Normal file
248
activeblue_familylaw/models/fl_deadline.py
Normal file
@@ -0,0 +1,248 @@
|
||||
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',
|
||||
)
|
||||
Reference in New Issue
Block a user