Files
famlaw/activeblue_familylaw/models/fl_case.py
Carlos Garcia 26f58952b4 Phase 7: full wizards, auto-generated case tasks, config parameters
fl_intake_wizard.py:
  - Full multi-step intake: parties, income, children, DV flag, fee
    waiver, AI analysis option
  - Creates res.partner → fl.party → fl.case chain (mirrors portal)
  - Triggers fee waiver record creation and Ollama AI analysis
  - Residency warning computed field (FL 61.021 — 6-month check)

fl_generate_packet_wizard.py:
  - Generates selected documents as PDFs via _render_qweb_pdf
  - Handles 4 binding models: fl.case, fl.party, fl.fee.waiver,
    fl.support.calculation, fl.income.withholding
  - Attaches generated PDFs to case chatter with summary note
  - Bound to fl.case form as an action button

fl_analysis_wizard.py:
  - Checks for recent analysis (<24h) before running new one
  - force_reanalysis flag bypasses the lock
  - Shows last analysis age label in form; opens result as dialog
  - Bound to fl.case form as an action button

fl_case.py:
  - _CASE_TASK_TEMPLATES: standard task lists for 6 case types
  - _generate_case_tasks(): creates project.task records from templates
  - Called automatically from _create_case_project on case creation

fl_wizard_views.xml:
  - Form views for all 3 wizards with inline help text
  - Packet wizard bound to fl.case form via binding_model_id

data/case_task_templates.xml:
  - ir.config_parameter records for Ollama URL, model, deadline days,
    mandatory disclosure days, AI lockout hours — all admin-configurable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:49:10 -05:00

1077 lines
51 KiB
Python

from odoo import _, api, fields, models
from dateutil.relativedelta import relativedelta
class FlCase(models.Model):
_name = 'fl.case'
_description = 'Florida Family Law Case'
_inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin']
_order = 'create_date desc'
_rec_name = 'name'
# ══════════════════════════════════════════════════════════════════════
# IDENTITY
# ══════════════════════════════════════════════════════════════════════
name = fields.Char(
string='Case Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: _('New'),
help='Auto-generated internal reference (FL-YYYY-NNNNN)'
)
court_case_number = fields.Char(
string='Court Case Number',
tracking=True,
help='Assigned by Miami-Dade Clerk after filing. '
'Format: YYYY-DR-XXXXXX-XX (e.g. 2025-DR-012345-01)'
)
case_type = fields.Selection([
('modification', 'Child Support Modification'),
('dissolution_children', 'Dissolution of Marriage — With Children'),
('dissolution_no_children', 'Dissolution of Marriage — No Children'),
('paternity', 'Paternity'),
('alimony_modification', 'Alimony Modification'),
('custody_modification', 'Timesharing / Custody Modification'),
], string='Case Type', required=True, tracking=True)
circuit = fields.Char(
string='Circuit', default='11th', readonly=True,
help='11th Judicial Circuit — Miami-Dade County'
)
county = fields.Char(
string='County', default='Miami-Dade', readonly=True
)
division = fields.Selection([
('1', 'Division 1'), ('2', 'Division 2'),
('3', 'Division 3'), ('4', 'Division 4'),
('5', 'Division 5'), ('6', 'Division 6'),
('7', 'Division 7'), ('8', 'Division 8'),
], string='Court Division', tracking=True)
judge_id = fields.Many2one(
'res.partner', string='Assigned Judge',
domain=[('is_company', '=', False)]
)
mediator_id = fields.Many2one(
'res.partner', string='Mediator'
)
# ══════════════════════════════════════════════════════════════════════
# STAGE / STATUS
# ══════════════════════════════════════════════════════════════════════
stage = fields.Selection([
('intake', 'Intake & Qualification'),
('preparation', 'Document Preparation'),
('filed', 'Filed — Awaiting Service'),
('service_complete', 'Service Complete'),
('discovery', 'Discovery'),
('deposition', 'Deposition Stage'),
('mediation', 'Mediation'),
('hearing_scheduled', 'Hearing Scheduled'),
('order_entered', 'Order Entered'),
('closed', 'Closed'),
('referred_out', 'Referred to Attorney'),
], string='Stage', default='intake', tracking=True)
active = fields.Boolean(default=True)
# ══════════════════════════════════════════════════════════════════════
# PARTIES
# ══════════════════════════════════════════════════════════════════════
petitioner_id = fields.Many2one(
'res.partner', string='Petitioner',
required=True, tracking=True
)
respondent_id = fields.Many2one(
'res.partner', string='Respondent',
tracking=True
)
petitioner_attorney_id = fields.Many2one(
'res.partner', string='Petitioner Attorney',
help='Leave blank if petitioner is pro se'
)
respondent_attorney_id = fields.Many2one(
'res.partner', string='Respondent Attorney',
tracking=True,
help='If respondent has counsel, pro se petitioner faces significant disadvantage'
)
respondent_has_counsel = fields.Boolean(
string='Respondent Has Counsel',
compute='_compute_respondent_has_counsel', store=True
)
party_ids = fields.One2many(
'fl.party', 'case_id', string='Party Details'
)
# ══════════════════════════════════════════════════════════════════════
# CHILDREN
# ══════════════════════════════════════════════════════════════════════
child_ids = fields.One2many(
'fl.child', 'case_id', string='Children'
)
children_count = fields.Integer(
string='Number of Children',
compute='_compute_children_count', store=True
)
has_minor_children = fields.Boolean(
string='Has Minor Children',
compute='_compute_has_minor_children', store=True
)
# ══════════════════════════════════════════════════════════════════════
# KEY DATES
# ══════════════════════════════════════════════════════════════════════
filing_date = fields.Date(
string='Filing Date', tracking=True
)
service_date = fields.Date(
string='Service Date', tracking=True
)
last_order_date = fields.Date(
string='Date of Last Order',
tracking=True,
help='Required for modification cases — date of the existing order being modified'
)
marriage_date = fields.Date(string='Date of Marriage')
separation_date = fields.Date(string='Date of Separation')
years_of_marriage = fields.Float(
string='Years of Marriage',
compute='_compute_years_of_marriage', store=True
)
# ══════════════════════════════════════════════════════════════════════
# RESIDENCY (FL 61.021)
# ══════════════════════════════════════════════════════════════════════
petitioner_fl_resident_since = fields.Date(
string='Petitioner FL Resident Since',
help='FL 61.021: Must be FL resident for 6 months before filing'
)
residency_requirement_met = fields.Boolean(
string='Residency Requirement Met',
compute='_compute_residency_met', store=True
)
residency_warning = fields.Char(
string='Residency Warning',
compute='_compute_residency_met'
)
# ══════════════════════════════════════════════════════════════════════
# SAFETY FLAGS
# ══════════════════════════════════════════════════════════════════════
domestic_violence_flag = fields.Boolean(
string='Domestic Violence Present',
tracking=True,
help='FL 741.30 / FL 61.13(2)(c). '
'Affects mediation (separate rooms required), timesharing, '
'and ALL court proceedings. Forces attorney referral.'
)
dv_injunction_active = fields.Boolean(
string='Active Injunction (FL 741.30)'
)
dv_safety_note = fields.Html(
string='Safety Information',
compute='_compute_dv_safety_note'
)
# ══════════════════════════════════════════════════════════════════════
# EXISTING ORDER (MODIFICATION CASES)
# ══════════════════════════════════════════════════════════════════════
current_order_amount = fields.Float(
string='Current Monthly Support Order ($)',
tracking=True,
help='Amount in the existing court order that is being modified'
)
current_order_per_child = fields.Boolean(
string='Amount is Per-Child (not total)',
help='Check if the existing order states an amount per child rather than total'
)
current_order_total = fields.Float(
string='Current Order Total ($)',
compute='_compute_current_order_total', store=True
)
# ══════════════════════════════════════════════════════════════════════
# CHILD SUPPORT CALCULATION (FL 61.30)
# ══════════════════════════════════════════════════════════════════════
support_calc_id = fields.Many2one(
'fl.support.calculation',
string='Active Support Calculation',
ondelete='cascade'
)
calculated_support = fields.Float(
string='Calculated Support ($)',
related='support_calc_id.total_support_obligation',
store=True
)
support_difference = fields.Float(
string='Difference from Current Order ($)',
compute='_compute_support_difference', store=True
)
support_difference_pct = fields.Float(
string='Difference %',
compute='_compute_support_difference', store=True
)
# ══════════════════════════════════════════════════════════════════════
# MODIFICATION THRESHOLD TEST (FL 61.30(1)(b))
# ══════════════════════════════════════════════════════════════════════
threshold_met = fields.Boolean(
string='Modification Threshold Met',
compute='_compute_threshold', store=True,
help='FL 61.30(1)(b): Modification qualifies when change is '
'both ≥ $50 AND ≥ 15% of the current order'
)
threshold_result = fields.Html(
string='Threshold Analysis',
compute='_compute_threshold'
)
substantial_change_basis = fields.Selection([
('income_decrease_petitioner', 'Petitioner Income Decreased'),
('income_decrease_respondent', 'Respondent Income Decreased'),
('income_increase_petitioner', 'Petitioner Income Increased'),
('income_increase_respondent', 'Respondent Income Increased'),
('timesharing_change', 'Timesharing Schedule Changed'),
('child_needs_change', "Child's Needs Changed"),
('child_emancipation', 'Child Reaching 18 / Emancipation'),
('other', 'Other Substantial Change'),
], string='Basis for Modification (FL 61.14)')
substantial_change_detail = fields.Text(
string='Describe the Substantial Change'
)
# ══════════════════════════════════════════════════════════════════════
# TIMESHARING
# ══════════════════════════════════════════════════════════════════════
timesharing_changed = fields.Boolean(
string='Timesharing Schedule Has Changed'
)
petitioner_overnights = fields.Integer(
string='Petitioner Overnights / Year',
help='Number of overnights per year the petitioner has with the children'
)
respondent_overnights = fields.Integer(
string='Respondent Overnights / Year',
compute='_compute_respondent_overnights', store=True
)
petitioner_timesharing_pct = fields.Float(
string='Petitioner Timesharing %',
compute='_compute_timesharing_pct', store=True
)
substantial_timesharing_applies = fields.Boolean(
string='Substantial Timesharing Adjustment Applies',
compute='_compute_timesharing_pct', store=True,
help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)'
)
# ══════════════════════════════════════════════════════════════════════
# FEE WAIVER (FL 57.082)
# ══════════════════════════════════════════════════════════════════════
fee_waiver_eligible = fields.Boolean(
string='Fee Waiver Potentially Eligible',
compute='_compute_fee_waiver_eligibility', store=True,
help='Based on petitioner income vs 200% FPL (FL 57.082)'
)
fee_waiver_id = fields.Many2one(
'fl.fee.waiver', string='Fee Waiver Application'
)
# ══════════════════════════════════════════════════════════════════════
# PARENTING CLASS (FL 61.21)
# ══════════════════════════════════════════════════════════════════════
parenting_class_required = fields.Boolean(
string='Parenting Class Required',
compute='_compute_parenting_class_required', store=True,
help='FL 61.21: Mandatory when minor children are involved'
)
petitioner_parenting_class_done = fields.Boolean(
string='Petitioner Parenting Class Complete'
)
petitioner_parenting_class_date = fields.Date(
string='Petitioner Completion Date'
)
respondent_parenting_class_done = fields.Boolean(
string='Respondent Parenting Class Complete'
)
respondent_parenting_class_date = fields.Date(
string='Respondent Completion Date'
)
parenting_class_warning = fields.Char(
string='Parenting Class Status',
compute='_compute_parenting_class_warning'
)
# ══════════════════════════════════════════════════════════════════════
# DEADLINES & CALENDAR
# ══════════════════════════════════════════════════════════════════════
deadline_ids = fields.One2many(
'fl.deadline', 'case_id', string='Deadlines'
)
next_deadline = fields.Date(
string='Next Deadline',
compute='_compute_next_deadline', store=True
)
next_deadline_label = fields.Char(
string='Next Deadline Description',
compute='_compute_next_deadline'
)
overdue_deadline_count = fields.Integer(
string='Overdue Deadlines',
compute='_compute_next_deadline'
)
# ══════════════════════════════════════════════════════════════════════
# HEARINGS
# ══════════════════════════════════════════════════════════════════════
hearing_ids = fields.One2many(
'fl.hearing', 'case_id', string='Hearings'
)
next_hearing_date = fields.Datetime(
string='Next Hearing',
compute='_compute_next_hearing', store=True
)
discovery_cutoff_date = fields.Date(
string='Discovery Cutoff',
compute='_compute_discovery_cutoff', store=True,
help='30 days before next hearing date'
)
# ══════════════════════════════════════════════════════════════════════
# DEPOSITIONS & DISCOVERY
# ══════════════════════════════════════════════════════════════════════
income_disputed = fields.Boolean(
string='Income Figures Disputed',
tracking=True,
help='If True, deposition workflow is activated'
)
deposition_ids = fields.One2many(
'fl.deposition', 'case_id', string='Depositions'
)
discovery_ids = fields.One2many(
'fl.discovery', 'case_id', string='Discovery Items'
)
# ══════════════════════════════════════════════════════════════════════
# DOCUMENTS
# ══════════════════════════════════════════════════════════════════════
document_ids = fields.One2many(
'fl.document', 'case_id', string='Case Documents'
)
# ══════════════════════════════════════════════════════════════════════
# PROJECT / TASK INTEGRATION
# ══════════════════════════════════════════════════════════════════════
project_id = fields.Many2one(
'project.project', string='Case Project',
help='Auto-created project for case task management'
)
task_count = fields.Integer(
string='Tasks', compute='_compute_task_count'
)
# ══════════════════════════════════════════════════════════════════════
# CASE LAW & AI ANALYSIS
# ══════════════════════════════════════════════════════════════════════
caselaw_ids = fields.Many2many(
'fl.caselaw', string='Applicable Case Law'
)
analysis_ids = fields.One2many(
'fl.analysis', 'case_id', string='AI Analyses'
)
latest_analysis_id = fields.Many2one(
'fl.analysis',
string='Latest Analysis',
compute='_compute_latest_analysis', store=True
)
attorney_referral_flag = fields.Boolean(
string='Attorney Referral Recommended',
compute='_compute_attorney_referral_flag', store=True
)
ai_plain_english = fields.Text(
string='AI Summary (English)',
related='latest_analysis_id.plain_english_summary'
)
ai_plain_english_es = fields.Text(
string='AI Summary (Spanish)',
related='latest_analysis_id.plain_english_summary_es'
)
# ══════════════════════════════════════════════════════════════════════
# EXPENSES
# ══════════════════════════════════════════════════════════════════════
expense_ids = fields.One2many(
'hr.expense', 'fl_case_id', string='Case Expenses'
)
total_expenses = fields.Float(
string='Total Case Expenses ($)',
compute='_compute_total_expenses', store=True
)
# ══════════════════════════════════════════════════════════════════════
# POST-ORDER
# ══════════════════════════════════════════════════════════════════════
new_order_amount = fields.Float(
string='New Order Amount ($)', tracking=True
)
new_order_date = fields.Date(
string='New Order Date', tracking=True
)
income_withholding_id = fields.Many2one(
'fl.income.withholding',
string='Income Withholding Order'
)
retroactivity_date = fields.Date(
string='Retroactivity Date',
compute='_compute_retroactivity_date', store=True,
help='FL 61.30(17): Modification is retroactive to filing date ONLY. '
'Cannot seek support modification for periods before filing.'
)
respondent_answered = fields.Boolean(
string='Respondent Filed Answer',
tracking=True
)
# ══════════════════════════════════════════════════════════════════════
# COMPUTED METHODS
# ══════════════════════════════════════════════════════════════════════
@api.depends('respondent_attorney_id')
def _compute_respondent_has_counsel(self):
for rec in self:
rec.respondent_has_counsel = bool(rec.respondent_attorney_id)
@api.depends('child_ids')
def _compute_children_count(self):
for rec in self:
rec.children_count = len(rec.child_ids)
@api.depends('child_ids.emancipated', 'child_ids.date_of_birth')
def _compute_has_minor_children(self):
for rec in self:
rec.has_minor_children = any(
not c.emancipated for c in rec.child_ids
)
@api.depends('marriage_date', 'separation_date', 'filing_date')
def _compute_years_of_marriage(self):
for rec in self:
end_date = rec.separation_date or rec.filing_date
if rec.marriage_date and end_date:
delta = relativedelta(end_date, rec.marriage_date)
rec.years_of_marriage = round(
delta.years + delta.months / 12 + delta.days / 365, 2
)
else:
rec.years_of_marriage = 0.0
@api.depends('petitioner_fl_resident_since', 'filing_date')
def _compute_residency_met(self):
for rec in self:
if rec.petitioner_fl_resident_since and rec.filing_date:
required_date = rec.petitioner_fl_resident_since + relativedelta(months=6)
rec.residency_requirement_met = rec.filing_date >= required_date
if not rec.residency_requirement_met:
months_short = relativedelta(required_date, rec.filing_date)
rec.residency_warning = (
f'⚠️ FL 61.021: Petitioner must be a FL resident for 6 months '
f'before filing. Currently {months_short.months} month(s) short.'
)
else:
rec.residency_warning = '✅ Residency requirement met (FL 61.021)'
elif rec.petitioner_fl_resident_since and not rec.filing_date:
rec.residency_requirement_met = False
rec.residency_warning = 'Enter filing date to verify residency requirement'
else:
rec.residency_requirement_met = False
rec.residency_warning = '⚠️ Enter FL residency start date to verify FL 61.021'
@api.depends('domestic_violence_flag', 'dv_injunction_active')
def _compute_dv_safety_note(self):
for rec in self:
if rec.domestic_violence_flag:
injunction_note = (
'<br/><b>Active injunction on file.</b>'
if rec.dv_injunction_active else ''
)
rec.dv_safety_note = (
'<div style="background:#ffeeba;border:2px solid #e6ac00;'
'padding:12px;border-radius:4px;">'
'<h4 style="color:#856404;margin:0 0 8px 0;">'
'⚠️ DOMESTIC VIOLENCE SAFETY NOTICE</h4>'
'<p>Domestic violence has been flagged in this case. '
'This affects your rights and safety throughout this proceeding.</p>'
'<ul>'
'<li><b>Mediation:</b> Separate rooms are REQUIRED (FL 44.102)</li>'
'<li><b>Representation:</b> Pro se representation is strongly discouraged</li>'
'<li><b>Contact:</b> Do not communicate with the opposing party directly</li>'
'</ul>'
f'{injunction_note}'
'<p><b>Resources:</b><br/>'
'Legal Services of Greater Miami: <a href="tel:3055760080">(305) 576-0080</a><br/>'
'Safespace Miami: <a href="tel:3055365565">(305) 536-5565</a><br/>'
'National DV Hotline: <a href="tel:18007997233">1-800-799-7233</a></p>'
'</div>'
)
else:
rec.dv_safety_note = ''
@api.depends('current_order_amount', 'current_order_per_child', 'children_count')
def _compute_current_order_total(self):
for rec in self:
if rec.current_order_per_child and rec.children_count:
rec.current_order_total = (
rec.current_order_amount * rec.children_count
)
else:
rec.current_order_total = rec.current_order_amount
@api.depends('calculated_support', 'current_order_total')
def _compute_support_difference(self):
for rec in self:
current = rec.current_order_total or 0.0
proposed = rec.calculated_support or 0.0
rec.support_difference = proposed - current
if current > 0:
rec.support_difference_pct = (
abs(proposed - current) / current
) * 100
else:
rec.support_difference_pct = 0.0
@api.depends('support_difference', 'support_difference_pct', 'case_type')
def _compute_threshold(self):
"""
FL 61.30(1)(b): Modification qualifies when BOTH:
- |difference| >= $50 AND
- |difference| / current_amount >= 15%
"""
for rec in self:
if rec.case_type != 'modification':
rec.threshold_met = False
rec.threshold_result = ''
continue
diff = abs(rec.support_difference)
pct = rec.support_difference_pct
current = rec.current_order_total or 0.0
dollar_test = diff >= 50.0
pct_test = pct >= 15.0
if current == 0:
rec.threshold_met = False
rec.threshold_result = (
'<span style="color:orange;">⚠️ Enter current order amount '
'to run threshold test</span>'
)
continue
color_dollar = '#28a745' if dollar_test else '#dc3545'
color_pct = '#28a745' if pct_test else '#dc3545'
direction = 'increase' if rec.support_difference > 0 else 'decrease'
rec.threshold_met = dollar_test and pct_test
overall_color = '#28a745' if rec.threshold_met else '#dc3545'
overall_icon = '' if rec.threshold_met else ''
overall_label = 'QUALIFIES FOR MODIFICATION' if rec.threshold_met else 'DOES NOT QUALIFY'
rec.threshold_result = (
f'<div style="border:1px solid {overall_color};padding:10px;'
f'border-radius:4px;">'
f'<b style="color:{overall_color};">{overall_icon} {overall_label}</b>'
f'<br/><br/>'
f'Current order: <b>${current:,.2f}/mo</b><br/>'
f'Proposed: <b>${rec.calculated_support:,.2f}/mo</b> '
f'({direction} of <b>${diff:,.2f}</b>)<br/><br/>'
f'<span style="color:{color_dollar};">'
f'{"" if dollar_test else ""} $50 test: '
f'${diff:,.2f} {"" if dollar_test else "<"} $50</span><br/>'
f'<span style="color:{color_pct};">'
f'{"" if pct_test else ""} 15% test: '
f'{pct:.1f}% {"" if pct_test else "<"} 15%</span>'
f'</div>'
)
@api.depends('petitioner_overnights')
def _compute_respondent_overnights(self):
for rec in self:
if rec.petitioner_overnights:
rec.respondent_overnights = 365 - rec.petitioner_overnights
else:
rec.respondent_overnights = 0
@api.depends('petitioner_overnights')
def _compute_timesharing_pct(self):
for rec in self:
if rec.petitioner_overnights:
pct = rec.petitioner_overnights / 365
rec.petitioner_timesharing_pct = round(pct * 100, 2)
# FL 61.30(11)(b): substantial = either parent > 73 overnights (20%)
pet_substantial = rec.petitioner_overnights > 73
resp_substantial = (365 - rec.petitioner_overnights) > 73
rec.substantial_timesharing_applies = pet_substantial or resp_substantial
else:
rec.petitioner_timesharing_pct = 0.0
rec.substantial_timesharing_applies = False
@api.depends('party_ids.effective_monthly_income', 'party_ids.role')
def _compute_fee_waiver_eligibility(self):
"""
Quick eligibility check: petitioner income < 200% FPL (FL 57.082).
Full determination via fl.fee.waiver model.
2025: 1-person 200% FPL = $30,120/yr = $2,510/mo
"""
FPL_200PCT_1PERSON_MONTHLY = 2510.0
for rec in self:
petitioner_party = rec.party_ids.filtered(
lambda p: p.role == 'petitioner'
)
if petitioner_party:
income = petitioner_party[0].effective_monthly_income or 0.0
rec.fee_waiver_eligible = income <= FPL_200PCT_1PERSON_MONTHLY
else:
rec.fee_waiver_eligible = False
@api.depends('has_minor_children')
def _compute_parenting_class_required(self):
for rec in self:
rec.parenting_class_required = rec.has_minor_children
@api.depends(
'parenting_class_required',
'petitioner_parenting_class_done',
'respondent_parenting_class_done',
)
def _compute_parenting_class_warning(self):
for rec in self:
if not rec.parenting_class_required:
rec.parenting_class_warning = 'Not required'
continue
pet_done = rec.petitioner_parenting_class_done
resp_done = rec.respondent_parenting_class_done
if pet_done and resp_done:
rec.parenting_class_warning = '✅ Both parents completed'
elif pet_done:
rec.parenting_class_warning = '⚠️ Respondent parenting class pending'
elif resp_done:
rec.parenting_class_warning = '⚠️ Petitioner parenting class pending'
else:
rec.parenting_class_warning = '❌ Neither parent has completed parenting class'
@api.depends('deadline_ids.due_date', 'deadline_ids.completed', 'deadline_ids.waived')
def _compute_next_deadline(self):
today = fields.Date.today()
for rec in self:
pending = rec.deadline_ids.filtered(
lambda d: not d.completed and not d.waived
)
overdue = pending.filtered(lambda d: d.due_date < today)
upcoming = pending.filtered(
lambda d: d.due_date >= today
).sorted('due_date')
rec.overdue_deadline_count = len(overdue)
if upcoming:
next_dl = upcoming[0]
rec.next_deadline = next_dl.due_date
rec.next_deadline_label = next_dl.name
elif overdue:
oldest = overdue.sorted('due_date')[0]
rec.next_deadline = oldest.due_date
rec.next_deadline_label = f'OVERDUE: {oldest.name}'
else:
rec.next_deadline = False
rec.next_deadline_label = 'No pending deadlines'
@api.depends('hearing_ids.hearing_date')
def _compute_next_hearing(self):
now = fields.Datetime.now()
for rec in self:
future = rec.hearing_ids.filtered(
lambda h: h.hearing_date and h.hearing_date > now
and h.state not in ('cancelled',)
).sorted('hearing_date')
rec.next_hearing_date = future[0].hearing_date if future else False
@api.depends('next_hearing_date')
def _compute_discovery_cutoff(self):
for rec in self:
if rec.next_hearing_date:
rec.discovery_cutoff_date = (
rec.next_hearing_date.date() - relativedelta(days=30)
)
else:
rec.discovery_cutoff_date = False
def _compute_task_count(self):
for rec in self:
if rec.project_id:
rec.task_count = self.env['project.task'].search_count([
('project_id', '=', rec.project_id.id)
])
else:
rec.task_count = 0
@api.depends('analysis_ids')
def _compute_latest_analysis(self):
for rec in self:
latest = rec.analysis_ids.sorted('create_date', reverse=True)
rec.latest_analysis_id = latest[0] if latest else False
@api.depends('latest_analysis_id.attorney_referral_flag', 'domestic_violence_flag', 'respondent_has_counsel')
def _compute_attorney_referral_flag(self):
for rec in self:
rec.attorney_referral_flag = (
rec.domestic_violence_flag
or rec.respondent_has_counsel
or (rec.latest_analysis_id and rec.latest_analysis_id.attorney_referral_flag)
)
@api.depends('expense_ids.total_amount')
def _compute_total_expenses(self):
for rec in self:
rec.total_expenses = sum(rec.expense_ids.mapped('total_amount'))
@api.depends('filing_date')
def _compute_retroactivity_date(self):
"""
FL 61.30(17): Modification is retroactive to the filing date ONLY.
"""
for rec in self:
rec.retroactivity_date = rec.filing_date
# ══════════════════════════════════════════════════════════════════════
# CRUD OVERRIDES
# ══════════════════════════════════════════════════════════════════════
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code(
'fl.case'
) or _('New')
records = super().create(vals_list)
for record in records:
# 1. Create Odoo project for task management
record._create_case_project()
# 2. Generate deadlines if filing date already set
if record.filing_date:
self.env['fl.deadline'].generate_deadlines_for_case(record)
# 3. Check fee waiver eligibility (quick check)
record._check_fee_waiver_eligibility()
# 4. Handle DV flag
if record.domestic_violence_flag:
record._handle_dv_flag()
return records
def write(self, vals):
result = super().write(vals)
# Recalculate service-anchored deadlines when service_date is set/changed
if 'service_date' in vals:
for rec in self:
if rec.service_date:
self.env['fl.deadline'].recalculate_service_deadlines(rec)
# Handle DV flag being set
if vals.get('domestic_violence_flag'):
for rec in self:
if rec.domestic_violence_flag:
rec._handle_dv_flag()
# Generate income withholding order when new_order_amount is set
if vals.get('new_order_amount') and vals.get('new_order_date'):
for rec in self:
rec._check_generate_income_withholding()
return result
# ══════════════════════════════════════════════════════════════════════
# WORKFLOW METHODS
# ══════════════════════════════════════════════════════════════════════
# Standard task templates keyed by case_type.
# Each entry: (name, description, sequence)
_CASE_TASK_TEMPLATES = {
'modification': [
('Gather Financial Documents',
'Collect last 3 years tax returns, pay stubs, bank statements '
'(FL 12.285 mandatory disclosure — 45 days from service)',
10),
('Complete Financial Affidavit',
'Complete FL-12.902(b) Short Form or FL-12.902(c) Long Form '
'financial affidavit (required for support proceedings)',
20),
('Calculate New Support Amount',
'Run FL 61.30 child support calculation using updated income figures. '
'Verify modification threshold: 15% AND $50 difference (FL 61.30(1)(b))',
30),
('File Supplemental Petition',
'File FL-12.905 Supplemental Petition to Modify Child Support '
'with the clerk and pay filing fee (or submit fee waiver)',
40),
('Serve Respondent',
'Serve Respondent with Summons + Petition. '
'Start 20-day answer deadline clock (FL 12.285, Rule 1.070)',
50),
('Track Answer Deadline',
'Monitor 20-day answer deadline. If no response, prepare '
'FL 12.922 default motion packet after deadline passes.',
60),
('Schedule / Attend Mediation',
'Attend court-ordered mediation (required in most family cases). '
'Confirm separate rooms if DV flag is set (FL 44.102)',
70),
('Attend Final Hearing',
'Appear at final hearing with all documents. '
'Bring original + 2 copies of all filed pleadings.',
80),
],
'dissolution_children': [
('Gather Financial Documents', 'FL 12.285 mandatory disclosure package', 10),
('Complete Financial Affidavit (Long Form)',
'FL-12.902(c) required when income > $50,000/year', 20),
('Calculate Child Support', 'FL 61.30 worksheet (FL-12.902(e))', 30),
('Draft Parenting Plan', 'FL-12.995(a) Parenting Plan — timesharing schedule', 40),
('File Petition for Dissolution', 'FL-12.901(b)(1) with all required attachments', 50),
('Serve Respondent', 'Serve Summons + Petition; track 20-day answer deadline', 60),
('Attend Parenting Class',
'FL 61.21 — both parties must complete before final hearing', 70),
('Schedule Mediation', 'Court-ordered mediation (FL 44.102)', 80),
('Attend Final Hearing', 'Bring parenting plan, support worksheet, all exhibits', 90),
],
'dissolution_no_children': [
('Gather Financial Documents', 'FL 12.285 mandatory disclosure', 10),
('Complete Financial Affidavit', 'FL-12.902(b) Short Form', 20),
('Identify and Value Marital Assets', 'Real property, accounts, retirement funds', 30),
('File Petition for Dissolution', 'FL-12.901(b)(2) — no minor children', 40),
('Serve Respondent', 'Serve Summons + Petition; track 20-day answer deadline', 50),
('Schedule Mediation', 'Property division mediation if contested', 60),
('Attend Final Hearing', 'Bring financial affidavit, marital settlement agreement', 70),
],
'paternity': [
('Gather Birth Records', 'Obtain certified birth certificate', 10),
('Complete Financial Affidavit', 'FL-12.902(b) required for support', 20),
('Calculate Child Support', 'FL 61.30 worksheet', 30),
('File Petition to Determine Paternity', 'FL-12.983(a)', 40),
('Serve Respondent', 'Summons + Petition; 20-day deadline', 50),
('Draft Parenting Plan', 'FL-12.995(a) if timesharing is requested', 60),
('Attend Parenting Class', 'FL 61.21 — required before final hearing', 70),
('Attend Final Hearing', 'Bring all documents and parenting plan', 80),
],
'alimony_modification': [
('Gather Financial Documents', 'Tax returns, pay stubs — show substantial change', 10),
('Complete Financial Affidavit (Long Form)', 'FL-12.902(c) required', 20),
('Document Change in Circumstances',
'Document the substantial change in circumstances required for modification', 30),
('File Supplemental Petition', 'FL-12.905 Supplemental Petition to Modify Alimony', 40),
('Serve Respondent', 'Summons + Petition; track 20-day answer deadline', 50),
('Schedule Mediation', 'FL 44.102 mediation', 60),
('Attend Final Hearing', 'Bring all financial documents and exhibits', 70),
],
'custody_modification': [
('Document Changed Circumstances',
'Substantial change must affect child welfare — document thoroughly', 10),
('Gather Supporting Evidence', 'School records, medical records, witness statements', 20),
('Complete Financial Affidavit', 'FL-12.902(b) if support change is involved', 30),
('Draft Proposed Parenting Plan', 'FL-12.995(a) — proposed new timesharing', 40),
('File Supplemental Petition', 'FL-12.905 Supplemental Petition to Modify Custody', 50),
('Serve Respondent', 'Summons + Petition; track 20-day answer deadline', 60),
('Attend Parenting Class', 'FL 61.21 if not already completed', 70),
('Schedule Mediation', 'Court-ordered mediation (FL 44.102)', 80),
('Attend Final Hearing', 'Bring parenting plan, evidence, exhibits', 90),
],
}
def _create_case_project(self):
"""Create a linked Odoo project for case task management."""
project = self.env['project.project'].create({
'name': f'Case: {self.name}{self.petitioner_id.name}',
'partner_id': self.petitioner_id.id,
'description': (
f'Family law case project for {self.petitioner_id.name}. '
f'Case type: {dict(self._fields["case_type"].selection).get(self.case_type, "")}'
),
})
self.project_id = project
self._generate_case_tasks()
def _generate_case_tasks(self):
"""
Create standard project tasks for this case based on its case_type.
Templates are defined in _CASE_TASK_TEMPLATES above.
"""
if not self.project_id:
return
templates = self._CASE_TASK_TEMPLATES.get(self.case_type, [])
task_vals_list = []
for name, description, sequence in templates:
task_vals_list.append({
'name': name,
'description': description,
'project_id': self.project_id.id,
'sequence': sequence,
})
if task_vals_list:
self.env['project.task'].create(task_vals_list)
def _check_fee_waiver_eligibility(self):
"""Post a chatter note if petitioner appears to qualify for fee waiver."""
if self.fee_waiver_eligible and not self.fee_waiver_id:
self.message_post(
body=(
'💡 <b>Fee Waiver Opportunity:</b> Based on the income information '
'provided, this petitioner may qualify for a civil indigent fee '
'waiver (FL 57.082). This would waive filing fees, service fees, '
'and potentially mediator fees. '
'Consider creating a Fee Waiver application from the case form.'
),
subtype_xmlid='mail.mt_note',
)
def _handle_dv_flag(self):
"""
Handle domestic violence flag being set on a case.
Per spec: Force attorney referral, post safety resources,
require separate mediation rooms, disable direct contact.
"""
self.message_post(
body=(
'<div style="background:#f8d7da;border:2px solid #dc3545;'
'padding:12px;border-radius:4px;">'
'<h4 style="color:#721c24;margin:0 0 8px 0;">'
'🚨 SAFETY ALERT — DOMESTIC VIOLENCE FLAGGED</h4>'
'<ul>'
'<li>Separate mediation rooms are <b>REQUIRED</b> (FL 44.102)</li>'
'<li>Pro se representation in DV cases is <b>STRONGLY DISCOURAGED</b></li>'
'<li>Attorney referral has been flagged on this case</li>'
'<li>Do NOT allow direct portal contact between parties</li>'
'</ul>'
'<b>Resources:</b><br/>'
'Legal Services of Greater Miami: (305) 576-0080<br/>'
'Safespace Miami: (305) 536-5565<br/>'
'National DV Hotline: 1-800-799-7233<br/>'
'TTY: 1-800-787-3224'
'</div>'
),
subtype_xmlid='mail.mt_note',
)
def _check_generate_income_withholding(self):
"""
FL 61.1301: Auto-generate income withholding order when new order amount is set.
Mandatory unless good cause or written agreement.
"""
if self.income_withholding_id:
return # Already exists
iwo = self.env['fl.income.withholding'].create({
'case_id': self.id,
'obligor_id': self.respondent_id.id,
'obligee_id': self.petitioner_id.id,
'monthly_support_amount': self.new_order_amount,
})
self.income_withholding_id = iwo
self.message_post(
body=(
f'⚖️ <b>Income Withholding Order created</b> (FL 61.1301).<br/>'
f'Amount: <b>${self.new_order_amount:,.2f}/mo</b>.<br/>'
f'Income withholding is MANDATORY under FL 61.1301 unless good cause '
f'is shown or both parties agree in writing to an alternative payment method.'
),
subtype_xmlid='mail.mt_note',
)
def trigger_ai_analysis(self):
"""
Trigger AI case analysis via Ollama (fl.ai.engine).
Phase 5 — full implementation.
"""
self.ensure_one()
engine = self.env['fl.ai.engine']
self.message_post(
body='🤖 AI analysis started. This may take up to 3 minutes...',
subtype_xmlid='mail.mt_note',
)
analysis = engine.analyze_case(self.id)
return {
'type': 'ir.actions.act_window',
'name': 'AI Analysis Result',
'res_model': 'fl.analysis',
'res_id': analysis.id,
'view_mode': 'form',
'target': 'new',
}
# ══════════════════════════════════════════════════════════════════════
# ACTIONS
# ══════════════════════════════════════════════════════════════════════
def action_view_tasks(self):
return {
'type': 'ir.actions.act_window',
'name': 'Case Tasks',
'res_model': 'project.task',
'view_mode': 'tree,form',
'domain': [('project_id', '=', self.project_id.id)],
'context': {'default_project_id': self.project_id.id},
}
def action_open_support_calculator(self):
if not self.support_calc_id:
calc = self.env['fl.support.calculation'].create({
'case_id': self.id,
'calculation_type': 'proposed',
})
self.support_calc_id = calc
return {
'type': 'ir.actions.act_window',
'name': 'FL 61.30 Support Calculator',
'res_model': 'fl.support.calculation',
'res_id': self.support_calc_id.id,
'view_mode': 'form',
'target': 'current',
}
def action_create_fee_waiver(self):
waiver = self.env['fl.fee.waiver'].create({
'case_id': self.id,
'party_id': self.petitioner_id.id,
})
self.fee_waiver_id = waiver
return {
'type': 'ir.actions.act_window',
'name': 'Fee Waiver Application',
'res_model': 'fl.fee.waiver',
'res_id': waiver.id,
'view_mode': 'form',
'target': 'current',
}
def action_run_ai_analysis(self):
return {
'type': 'ir.actions.act_window',
'name': 'Run AI Analysis',
'res_model': 'fl.analysis.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_case_id': self.id},
}