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>
319 lines
13 KiB
Python
319 lines
13 KiB
Python
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'
|
||
)
|