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,318 @@
from odoo import api, fields, models
class FlIncomeSource(models.Model):
_name = 'fl.income.source'
_description = 'Party Income Source'
_order = 'monthly_amount desc'
party_id = fields.Many2one(
'fl.party', string='Party',
required=True, ondelete='cascade', index=True
)
source_type = fields.Selection([
('wages', 'Wages / Salary (W-2)'),
('self_employment', 'Self-Employment / Business Income'),
('rental', 'Rental Income'),
('investment', 'Investment / Dividend Income'),
('pension', 'Pension / Retirement'),
('social_security', 'Social Security'),
('disability', 'Disability Benefits (SSI/SSDI)'),
('unemployment', 'Unemployment Compensation'),
('workers_comp', "Workers' Compensation"),
('alimony_received', 'Alimony Received'),
('child_support_received', 'Child Support Received (other case)'),
('overtime', 'Overtime / Bonuses'),
('tips', 'Tips / Gratuities'),
('commission', 'Commissions'),
('trust', 'Trust / Estate Income'),
('other', 'Other Income'),
], string='Source Type', required=True)
description = fields.Char(string='Description / Payer')
monthly_amount = fields.Float(string='Monthly Amount ($)', required=True)
annual_amount = fields.Float(
string='Annual Amount ($)',
compute='_compute_annual', store=True
)
verified = fields.Boolean(
string='Verified',
help='Income verified via documentation (pay stub, tax return, bank statement)'
)
verification_document = fields.Char(
string='Verification Document',
help='e.g. "2023 W-2", "Last 3 pay stubs", "2023 1040 Schedule C"'
)
@api.depends('monthly_amount')
def _compute_annual(self):
for rec in self:
rec.annual_amount = rec.monthly_amount * 12
class FlParty(models.Model):
_name = 'fl.party'
_description = 'Case Party Details'
_inherit = ['mail.thread']
_order = 'role, id'
case_id = fields.Many2one(
'fl.case', string='Case',
required=True, ondelete='cascade', index=True
)
partner_id = fields.Many2one(
'res.partner', string='Contact',
required=True
)
role = fields.Selection([
('petitioner', 'Petitioner'),
('respondent', 'Respondent'),
], string='Role', required=True)
display_name_computed = fields.Char(
string='Display Name',
compute='_compute_display_name_field', store=True
)
# ── Employment ─────────────────────────────────────────────────────────
employment_type = fields.Selection([
('employed', 'W-2 Employed'),
('self_employed', 'Self-Employed / Business Owner'),
('unemployed', 'Unemployed'),
('underemployed', 'Voluntarily Underemployed'),
('disabled', 'Disabled — Receiving Benefits'),
('retired', 'Retired'),
('student', 'Full-Time Student'),
('unknown', 'Unknown / To Be Discovered'),
], string='Employment Type', required=True, default='employed', tracking=True)
employer_name = fields.Char(string='Employer Name')
employer_address = fields.Char(string='Employer Address')
employer_phone = fields.Char(
string='Employer Phone',
help='Used for deposition notice and employer subpoena'
)
# ── Income (FL 61.30(2)) ───────────────────────────────────────────────
gross_monthly_income = fields.Float(
string='Gross Monthly Income ($)',
tracking=True
)
income_source_ids = fields.One2many(
'fl.income.source', 'party_id',
string='Income Sources'
)
income_sources_total = fields.Float(
string='Income Sources Total ($)',
compute='_compute_income_sources_total', store=True,
help='Sum of all income source monthly amounts'
)
# Statutory deductions to reach Net Monthly Income (FL 61.30(3))
fed_tax_monthly = fields.Float(
string='Federal Income Tax Withholding ($)',
help='Actual federal tax withheld per month (from pay stub)'
)
fica_ss_monthly = fields.Float(
string='Social Security Tax (6.2%) ($)',
compute='_compute_fica', store=True
)
fica_medicare_monthly = fields.Float(
string='Medicare Tax (1.45%) ($)',
compute='_compute_fica', store=True
)
mandatory_retirement = fields.Float(
string='Mandatory Retirement Contribution ($)',
help='Only mandatory contributions qualify — not voluntary 401k'
)
mandatory_union_dues = fields.Float(string='Mandatory Union Dues ($)')
health_insurance_self = fields.Float(
string='Health Insurance — Self Only ($)',
help='FL 61.30(3)(e): Self-only portion of health insurance premium. '
'Do NOT include the child portion here.'
)
other_court_ordered_support = fields.Float(
string='Other Court-Ordered Child Support ($)',
help='Court-ordered support for children from other relationships (FL 61.30(3)(g))'
)
net_monthly_income = fields.Float(
string='Net Monthly Income ($)',
compute='_compute_net_income', store=True,
tracking=True,
help='Gross income minus all allowable FL 61.30(3) deductions'
)
# ── Income Imputation (FL 61.30(2)(b)) ───────────────────────────────
income_imputed = fields.Boolean(
string='Income Imputed',
help='Check if this party is voluntarily unemployed/underemployed '
'and income should be imputed'
)
imputed_amount = fields.Float(
string='Imputed Monthly Income ($)',
help='FL 61.30(2)(b): Requires showing BOTH ability AND availability of work. '
'Default to FL minimum wage if voluntarily unemployed.'
)
imputation_basis = fields.Text(
string='Basis for Imputation',
help='FL 61.30(2)(b): Document ability (education, skills, health) '
'AND availability (local job market, prior work history). '
'Cannot impute income to disabled party without medical evidence.'
)
fl_minimum_wage_hourly = fields.Float(
string='FL Minimum Wage ($/hr)',
default=13.00,
help='Update annually per Florida minimum wage schedule. '
'2025: $13.00/hr. Full-time = $13.00 × 40hr × 52wk / 12 = $2,253.33/mo'
)
fl_minimum_wage_monthly = fields.Float(
string='FL Minimum Wage ($/mo)',
compute='_compute_fl_min_wage_monthly', store=True
)
effective_monthly_income = fields.Float(
string='Effective Monthly Income ($)',
compute='_compute_effective_income', store=True,
help='Net monthly income used in support calculation — '
'uses imputed amount if income_imputed = True'
)
# ── Lifestyle Analysis (Barner v. Barner) ────────────────────────────
lifestyle_inconsistency_flag = fields.Boolean(
string='Income/Lifestyle Inconsistency Flagged',
help='Flag when reported income appears inconsistent with '
'observed lifestyle (Barner v. Barner). '
'Document indicators: vehicles, vacations, residence, spending.'
)
lifestyle_notes = fields.Text(
string='Lifestyle Analysis Notes',
help='Document specific lifestyle inconsistencies: '
'e.g., drives a 2023 BMW but reports $800/mo income; '
'took 3 vacations in past year; lives in $3,000/mo apartment.'
)
# ── Portal Access ──────────────────────────────────────────────────────
portal_user_id = fields.Many2one(
'res.users', string='Portal User Account'
)
portal_access_granted = fields.Boolean(
string='Portal Access Granted'
)
portal_invite_sent = fields.Boolean(
string='Portal Invitation Sent'
)
preferred_language = fields.Selection([
('en_US', 'English'),
('es_MX', 'Spanish / Español'),
], string='Preferred Language', default='en_US')
# ── SSN (FL-12.930(a)) ────────────────────────────────────────────────
# SECURITY: Store last 4 digits only.
# Full SSN is NOT stored in this system.
# User handwrites full SSN on the physical FL-12.930(a) form before filing.
ssn_last4 = fields.Char(
string='SSN Last 4 Digits',
size=4,
help='Last 4 digits only. Full SSN is not stored — '
'user handwrites SSN on physical FL-12.930(a) form.'
)
ssn_notice_filed = fields.Boolean(
string='Notice of Social Security Number Filed (FL-12.930(a))',
help='Required to be filed with the court'
)
ssn_handwritten_confirmed = fields.Boolean(
string='I have handwritten my SSN on the FL-12.930(a) form',
help='Checkbox confirms user has added SSN to physical form before filing'
)
# ── Service of Process ────────────────────────────────────────────────
service_address = fields.Text(
string='Address for Service of Process'
)
service_method = fields.Selection([
('personal', 'Personal Service (Process Server)'),
('certified_mail', 'Certified Mail (if agreed by both parties)'),
('publication', 'Service by Publication (last resort — respondent location unknown)'),
], string='Service Method')
process_server_id = fields.Many2one(
'res.partner', string='Process Server'
)
service_by_publication_warning = fields.Boolean(
compute='_compute_service_publication_warning'
)
# ── Computed ──────────────────────────────────────────────────────────
@api.depends('partner_id', 'role')
def _compute_display_name_field(self):
for rec in self:
role_label = dict(rec._fields['role'].selection).get(rec.role, '')
name = rec.partner_id.name or ''
rec.display_name_computed = f'{name} ({role_label})' if role_label else name
@api.depends('income_source_ids.monthly_amount')
def _compute_income_sources_total(self):
for rec in self:
rec.income_sources_total = sum(
rec.income_source_ids.mapped('monthly_amount')
)
@api.depends('gross_monthly_income')
def _compute_fica(self):
for rec in self:
gross = rec.gross_monthly_income or 0.0
rec.fica_ss_monthly = round(gross * 0.062, 2)
rec.fica_medicare_monthly = round(gross * 0.0145, 2)
@api.depends(
'gross_monthly_income',
'fed_tax_monthly',
'fica_ss_monthly',
'fica_medicare_monthly',
'mandatory_retirement',
'mandatory_union_dues',
'health_insurance_self',
'other_court_ordered_support',
)
def _compute_net_income(self):
for rec in self:
gross = rec.gross_monthly_income or 0.0
deductions = (
(rec.fed_tax_monthly or 0.0)
+ (rec.fica_ss_monthly or 0.0)
+ (rec.fica_medicare_monthly or 0.0)
+ (rec.mandatory_retirement or 0.0)
+ (rec.mandatory_union_dues or 0.0)
+ (rec.health_insurance_self or 0.0)
+ (rec.other_court_ordered_support or 0.0)
)
rec.net_monthly_income = max(gross - deductions, 0.0)
@api.depends('fl_minimum_wage_hourly')
def _compute_fl_min_wage_monthly(self):
for rec in self:
# Full-time: 40hr/wk × 52wk / 12 months
rec.fl_minimum_wage_monthly = round(
rec.fl_minimum_wage_hourly * 40 * 52 / 12, 2
)
@api.depends('net_monthly_income', 'income_imputed', 'imputed_amount')
def _compute_effective_income(self):
for rec in self:
if rec.income_imputed and rec.imputed_amount:
rec.effective_monthly_income = rec.imputed_amount
else:
rec.effective_monthly_income = rec.net_monthly_income
@api.depends('service_method')
def _compute_service_publication_warning(self):
for rec in self:
rec.service_by_publication_warning = (
rec.service_method == 'publication'
)