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:
Carlos Garcia
2026-05-04 18:52:04 -04:00
commit 1d52d85a78
46 changed files with 5626 additions and 0 deletions

View File

@@ -0,0 +1,426 @@
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}'
)