Files
famlaw/activeblue_familylaw/models/fl_party.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

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