Files
famlaw/activeblue_familylaw/models/fl_support.py
Carlos Garcia 1d52d85a78 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>
2026-05-04 18:52:04 -04:00

427 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from odoo import api, fields, models
class FlSupportScheduleEntry(models.Model):
"""
FL Department of Revenue — Basic Support Obligation Schedule.
Loaded from data/fl_support_schedule.xml.
MUST be updated annually when FL updates the schedule.
Source: https://www.floridarevenue.com/childsupport/guidelines
"""
_name = 'fl.support.schedule.entry'
_description = 'FL Support Obligation Schedule Entry'
_order = 'income_min, children_count'
income_min = fields.Float(
string='Combined Income — Min ($/mo)',
required=True
)
income_max = fields.Float(
string='Combined Income — Max ($/mo)',
required=True
)
children_count = fields.Integer(
string='Number of Children',
required=True
)
obligation_amount = fields.Float(
string='Basic Obligation ($)',
required=True
)
effective_date = fields.Date(
string='Schedule Effective Date',
required=True
)
active = fields.Boolean(default=True)
def name_get(self):
result = []
for rec in self:
result.append((
rec.id,
f'${rec.income_min:,.0f}${rec.income_max:,.0f} / '
f'{rec.children_count} child(ren) = ${rec.obligation_amount:,.0f}'
))
return result
class FlSupportCalculation(models.Model):
"""
FL 61.30 Child Support Guidelines Worksheet.
One calculation record per case (current vs. proposed vs. historical).
"""
_name = 'fl.support.calculation'
_description = 'FL 61.30 Child Support Calculation'
_inherit = ['mail.thread']
_order = 'calculation_date desc'
case_id = fields.Many2one(
'fl.case', string='Case',
ondelete='cascade', index=True
)
calculation_date = fields.Date(
string='Calculation Date',
default=fields.Date.today
)
calculation_type = fields.Selection([
('current', 'Current Order (Baseline)'),
('proposed', 'Proposed Modification'),
('historical', 'Historical Reference'),
], string='Type', default='proposed', required=True)
notes = fields.Text(string='Notes')
# ── Party Net Incomes ──────────────────────────────────────────────────
petitioner_net_income = fields.Float(
string='Petitioner Net Monthly Income ($)',
tracking=True
)
respondent_net_income = fields.Float(
string='Respondent Net Monthly Income ($)',
tracking=True
)
combined_net_income = fields.Float(
string='Combined Net Monthly Income ($)',
compute='_compute_combined', store=True
)
petitioner_income_pct = fields.Float(
string='Petitioner Income %',
compute='_compute_income_pcts', store=True
)
respondent_income_pct = fields.Float(
string='Respondent Income %',
compute='_compute_income_pcts', store=True
)
# ── Basic Support Obligation ───────────────────────────────────────────
number_of_children = fields.Integer(
string='Number of Children',
related='case_id.children_count', store=True
)
basic_support_obligation = fields.Float(
string='Basic Support Obligation ($)',
compute='_compute_basic_obligation', store=True,
help='Looked up from FL DCF Schedule based on combined income and number of children'
)
support_schedule_id = fields.Many2one(
'fl.support.schedule.entry',
string='Schedule Entry Used',
compute='_compute_basic_obligation', store=True
)
above_schedule = fields.Boolean(
string='Above Schedule Maximum',
compute='_compute_basic_obligation', store=True,
help='If True, FL 61.30(6) percentage formula was applied'
)
# ── Adjustments (FL 61.30(7),(8),(9)) ─────────────────────────────────
# Health insurance for children (FL 61.30(8))
child_health_insurance_total = fields.Float(
string='Child Health Insurance Premium Total ($)',
help='Total monthly premium for children only — not self-only portion'
)
health_insurance_by_petitioner = fields.Float(
string=' Paid by Petitioner ($)'
)
health_insurance_by_respondent = fields.Float(
string=' Paid by Respondent ($)'
)
# Work-related childcare (FL 61.30(7))
childcare_total = fields.Float(
string='Work-Related Childcare Total ($)',
help='FL 61.30(7): Only work or job-search related childcare qualifies'
)
childcare_by_petitioner = fields.Float(
string=' Paid by Petitioner ($)'
)
childcare_by_respondent = fields.Float(
string=' Paid by Respondent ($)'
)
# Extraordinary expenses (FL 61.30(9))
extraordinary_expenses = fields.Float(
string='Extraordinary Medical / Educational Expenses ($)',
help='FL 61.30(9): Expenses beyond ordinary child-rearing costs'
)
# ── Adjusted Support Obligation ────────────────────────────────────────
adjusted_support_obligation = fields.Float(
string='Adjusted Support Obligation ($)',
compute='_compute_adjusted_obligation', store=True,
help='Basic Obligation + Health Insurance + Childcare + Extraordinary'
)
# ── Timesharing Adjustment (FL 61.30(11)(b)) ──────────────────────────
petitioner_overnights = fields.Integer(
string='Petitioner Overnights / Year',
related='case_id.petitioner_overnights', store=True
)
respondent_overnights = fields.Integer(
string='Respondent Overnights / Year',
related='case_id.respondent_overnights', store=True
)
substantial_timesharing = fields.Boolean(
string='Substantial Timesharing Applies',
related='case_id.substantial_timesharing_applies', store=True,
help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)'
)
timesharing_adjustment = fields.Float(
string='Timesharing Adjustment ($)',
compute='_compute_timesharing_adjustment', store=True
)
# ── Final Obligations ──────────────────────────────────────────────────
total_support_obligation = fields.Float(
string='Total Support Obligation ($)',
compute='_compute_total_obligation', store=True
)
petitioner_obligation = fields.Float(
string='Petitioner Share ($)',
compute='_compute_party_obligations', store=True
)
respondent_obligation = fields.Float(
string='Respondent Share ($)',
compute='_compute_party_obligations', store=True
)
net_payment_amount = fields.Float(
string='Net Payment Amount ($)',
compute='_compute_net_payment', store=True,
help='Net amount actually exchanged between parties after credits'
)
payment_direction = fields.Selection([
('petitioner_pays', 'Petitioner Pays Respondent'),
('respondent_pays', 'Respondent Pays Petitioner'),
('no_payment', 'No Net Payment'),
], string='Payment Direction',
compute='_compute_net_payment', store=True
)
# ── Deviation (FL 61.30(1)(a)) ────────────────────────────────────────
deviation_requested = fields.Boolean(
string='Deviation Requested',
help='FL 61.30(1)(a): Court may deviate from guidelines with written findings'
)
deviation_reason = fields.Text(
string='Deviation Reason',
help='Valid reasons: extraordinary medical needs, independent child income, '
'seasonal income variations, shared physical custody arrangements'
)
deviation_amount = fields.Float(
string='Deviation Amount ($)',
help='Positive = above guidelines; Negative = below guidelines'
)
final_amount_with_deviation = fields.Float(
string='Final Amount With Deviation ($)',
compute='_compute_final_with_deviation', store=True
)
# ── Summary ───────────────────────────────────────────────────────────
calculation_summary = fields.Text(
string='Calculation Summary',
compute='_compute_summary'
)
# ══════════════════════════════════════════════════════════════════════
# COMPUTED METHODS
# ══════════════════════════════════════════════════════════════════════
@api.depends('petitioner_net_income', 'respondent_net_income')
def _compute_combined(self):
for rec in self:
rec.combined_net_income = (
(rec.petitioner_net_income or 0.0)
+ (rec.respondent_net_income or 0.0)
)
@api.depends('combined_net_income', 'petitioner_net_income', 'respondent_net_income')
def _compute_income_pcts(self):
for rec in self:
combined = rec.combined_net_income or 0.0
if combined > 0:
rec.petitioner_income_pct = (
rec.petitioner_net_income or 0.0
) / combined
rec.respondent_income_pct = (
rec.respondent_net_income or 0.0
) / combined
else:
rec.petitioner_income_pct = 0.5
rec.respondent_income_pct = 0.5
@api.depends('combined_net_income', 'number_of_children')
def _compute_basic_obligation(self):
"""
Look up Basic Support Obligation from FL DCF Schedule.
If combined income exceeds schedule maximum, apply FL 61.30(6) percentages:
1 child = 5%
2 children = 7.5%
3 children = 9.5%
4 children = 11%
5 children = 12%
6+ children = 12.5%
"""
PCT_MAP = {1: 0.05, 2: 0.075, 3: 0.095, 4: 0.11, 5: 0.12, 6: 0.125}
for rec in self:
if not rec.combined_net_income or not rec.number_of_children:
rec.basic_support_obligation = 0.0
rec.support_schedule_id = False
rec.above_schedule = False
continue
children = min(rec.number_of_children, 6)
entry = self.env['fl.support.schedule.entry'].search([
('income_min', '<=', rec.combined_net_income),
('income_max', '>=', rec.combined_net_income),
('children_count', '=', children),
('active', '=', True),
], order='effective_date desc', limit=1)
if entry:
rec.basic_support_obligation = entry.obligation_amount
rec.support_schedule_id = entry
rec.above_schedule = False
else:
# Above schedule maximum — FL 61.30(6) formula
pct = PCT_MAP.get(children, 0.125)
rec.basic_support_obligation = round(
rec.combined_net_income * pct, 2
)
rec.support_schedule_id = False
rec.above_schedule = True
@api.depends(
'basic_support_obligation',
'child_health_insurance_total',
'childcare_total',
'extraordinary_expenses',
)
def _compute_adjusted_obligation(self):
for rec in self:
rec.adjusted_support_obligation = (
(rec.basic_support_obligation or 0.0)
+ (rec.child_health_insurance_total or 0.0)
+ (rec.childcare_total or 0.0)
+ (rec.extraordinary_expenses or 0.0)
)
@api.depends(
'adjusted_support_obligation',
'petitioner_overnights',
'substantial_timesharing',
'petitioner_income_pct',
'respondent_income_pct',
)
def _compute_timesharing_adjustment(self):
"""
FL 61.30(11)(b) Substantial Timesharing Adjustment.
Only applies if either parent has > 73 overnights/year (20%).
Formula:
1. Each parent's share = adjusted_obligation × parent's income %
2. Multiply each share × 1.5 (cross-credit factor)
3. Multiply each result × other parent's timesharing %
4. Net payment = |pet_adjusted - resp_adjusted|
"""
for rec in self:
if not rec.substantial_timesharing:
rec.timesharing_adjustment = 0.0
continue
pet_nights = rec.petitioner_overnights or 0
resp_nights = 365 - pet_nights
pet_time_pct = pet_nights / 365
resp_time_pct = resp_nights / 365
adj = rec.adjusted_support_obligation or 0.0
pet_income_pct = rec.petitioner_income_pct or 0.5
resp_income_pct = rec.respondent_income_pct or 0.5
pet_share = adj * pet_income_pct
resp_share = adj * resp_income_pct
pet_adjusted = pet_share * 1.5 * resp_time_pct
resp_adjusted = resp_share * 1.5 * pet_time_pct
rec.timesharing_adjustment = round(abs(pet_adjusted - resp_adjusted), 2)
@api.depends('adjusted_support_obligation', 'timesharing_adjustment', 'substantial_timesharing')
def _compute_total_obligation(self):
for rec in self:
if rec.substantial_timesharing:
rec.total_support_obligation = rec.timesharing_adjustment
else:
rec.total_support_obligation = rec.adjusted_support_obligation
@api.depends('total_support_obligation', 'petitioner_income_pct', 'respondent_income_pct')
def _compute_party_obligations(self):
for rec in self:
total = rec.total_support_obligation or 0.0
rec.petitioner_obligation = round(total * (rec.petitioner_income_pct or 0.5), 2)
rec.respondent_obligation = round(total * (rec.respondent_income_pct or 0.5), 2)
@api.depends(
'petitioner_obligation', 'respondent_obligation',
'health_insurance_by_petitioner', 'health_insurance_by_respondent',
'childcare_by_petitioner', 'childcare_by_respondent',
)
def _compute_net_payment(self):
for rec in self:
# Credit parties for amounts they directly pay
pet_credits = (
(rec.health_insurance_by_petitioner or 0.0)
+ (rec.childcare_by_petitioner or 0.0)
)
resp_credits = (
(rec.health_insurance_by_respondent or 0.0)
+ (rec.childcare_by_respondent or 0.0)
)
pet_net = (rec.petitioner_obligation or 0.0) - pet_credits
resp_net = (rec.respondent_obligation or 0.0) - resp_credits
net = resp_net - pet_net
rec.net_payment_amount = abs(net)
if net > 0.01:
rec.payment_direction = 'petitioner_pays'
elif net < -0.01:
rec.payment_direction = 'respondent_pays'
else:
rec.payment_direction = 'no_payment'
@api.depends('total_support_obligation', 'deviation_requested', 'deviation_amount')
def _compute_final_with_deviation(self):
for rec in self:
if rec.deviation_requested and rec.deviation_amount:
rec.final_amount_with_deviation = (
rec.total_support_obligation + rec.deviation_amount
)
else:
rec.final_amount_with_deviation = rec.total_support_obligation
@api.depends(
'combined_net_income', 'basic_support_obligation',
'adjusted_support_obligation', 'net_payment_amount', 'payment_direction'
)
def _compute_summary(self):
direction_map = {
'petitioner_pays': 'Petitioner → Respondent',
'respondent_pays': 'Respondent → Petitioner',
'no_payment': 'No net payment',
}
for rec in self:
direction = direction_map.get(rec.payment_direction or '', '')
rec.calculation_summary = (
f'Combined income: ${rec.combined_net_income:,.2f}/mo | '
f'Basic obligation: ${rec.basic_support_obligation:,.2f} | '
f'Adjusted: ${rec.adjusted_support_obligation:,.2f} | '
f'Net payment: ${rec.net_payment_amount:,.2f}{direction}'
)