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:
952
activeblue_familylaw/models/fl_case.py
Normal file
952
activeblue_familylaw/models/fl_case.py
Normal file
@@ -0,0 +1,952 @@
|
||||
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
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
Stub for Phase 1 — full implementation in Phase 5.
|
||||
"""
|
||||
self.message_post(
|
||||
body='🤖 AI analysis queued. Full AI analysis will be available in Phase 5.',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 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},
|
||||
}
|
||||
Reference in New Issue
Block a user