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:
318
activeblue_familylaw/models/fl_party.py
Normal file
318
activeblue_familylaw/models/fl_party.py
Normal 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'
|
||||
)
|
||||
Reference in New Issue
Block a user