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' )