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 = (
'
Active injunction on file.'
if rec.dv_injunction_active else ''
)
rec.dv_safety_note = (
'
Domestic violence has been flagged in this case. ' 'This affects your rights and safety throughout this proceeding.
' 'Resources:
'
'Legal Services of Greater Miami: (305) 576-0080
'
'Safespace Miami: (305) 536-5565
'
'National DV Hotline: 1-800-799-7233