from odoo import api, fields, models class FlSupportScheduleEntry(models.Model): """ FL Department of Revenue — Basic Support Obligation Schedule. Loaded from data/fl_support_schedule.xml. MUST be updated annually when FL updates the schedule. Source: https://www.floridarevenue.com/childsupport/guidelines """ _name = 'fl.support.schedule.entry' _description = 'FL Support Obligation Schedule Entry' _order = 'income_min, children_count' income_min = fields.Float( string='Combined Income — Min ($/mo)', required=True ) income_max = fields.Float( string='Combined Income — Max ($/mo)', required=True ) children_count = fields.Integer( string='Number of Children', required=True ) obligation_amount = fields.Float( string='Basic Obligation ($)', required=True ) effective_date = fields.Date( string='Schedule Effective Date', required=True ) active = fields.Boolean(default=True) def name_get(self): result = [] for rec in self: result.append(( rec.id, f'${rec.income_min:,.0f}–${rec.income_max:,.0f} / ' f'{rec.children_count} child(ren) = ${rec.obligation_amount:,.0f}' )) return result class FlSupportCalculation(models.Model): """ FL 61.30 Child Support Guidelines Worksheet. One calculation record per case (current vs. proposed vs. historical). """ _name = 'fl.support.calculation' _description = 'FL 61.30 Child Support Calculation' _inherit = ['mail.thread'] _order = 'calculation_date desc' case_id = fields.Many2one( 'fl.case', string='Case', ondelete='cascade', index=True ) calculation_date = fields.Date( string='Calculation Date', default=fields.Date.today ) calculation_type = fields.Selection([ ('current', 'Current Order (Baseline)'), ('proposed', 'Proposed Modification'), ('historical', 'Historical Reference'), ], string='Type', default='proposed', required=True) notes = fields.Text(string='Notes') # ── Party Net Incomes ────────────────────────────────────────────────── petitioner_net_income = fields.Float( string='Petitioner Net Monthly Income ($)', tracking=True ) respondent_net_income = fields.Float( string='Respondent Net Monthly Income ($)', tracking=True ) combined_net_income = fields.Float( string='Combined Net Monthly Income ($)', compute='_compute_combined', store=True ) petitioner_income_pct = fields.Float( string='Petitioner Income %', compute='_compute_income_pcts', store=True ) respondent_income_pct = fields.Float( string='Respondent Income %', compute='_compute_income_pcts', store=True ) # ── Basic Support Obligation ─────────────────────────────────────────── number_of_children = fields.Integer( string='Number of Children', related='case_id.children_count', store=True ) basic_support_obligation = fields.Float( string='Basic Support Obligation ($)', compute='_compute_basic_obligation', store=True, help='Looked up from FL DCF Schedule based on combined income and number of children' ) support_schedule_id = fields.Many2one( 'fl.support.schedule.entry', string='Schedule Entry Used', compute='_compute_basic_obligation', store=True ) above_schedule = fields.Boolean( string='Above Schedule Maximum', compute='_compute_basic_obligation', store=True, help='If True, FL 61.30(6) percentage formula was applied' ) # ── Adjustments (FL 61.30(7),(8),(9)) ───────────────────────────────── # Health insurance for children (FL 61.30(8)) child_health_insurance_total = fields.Float( string='Child Health Insurance Premium Total ($)', help='Total monthly premium for children only — not self-only portion' ) health_insurance_by_petitioner = fields.Float( string=' Paid by Petitioner ($)' ) health_insurance_by_respondent = fields.Float( string=' Paid by Respondent ($)' ) # Work-related childcare (FL 61.30(7)) childcare_total = fields.Float( string='Work-Related Childcare Total ($)', help='FL 61.30(7): Only work or job-search related childcare qualifies' ) childcare_by_petitioner = fields.Float( string=' Paid by Petitioner ($)' ) childcare_by_respondent = fields.Float( string=' Paid by Respondent ($)' ) # Extraordinary expenses (FL 61.30(9)) extraordinary_expenses = fields.Float( string='Extraordinary Medical / Educational Expenses ($)', help='FL 61.30(9): Expenses beyond ordinary child-rearing costs' ) # ── Adjusted Support Obligation ──────────────────────────────────────── adjusted_support_obligation = fields.Float( string='Adjusted Support Obligation ($)', compute='_compute_adjusted_obligation', store=True, help='Basic Obligation + Health Insurance + Childcare + Extraordinary' ) # ── Timesharing Adjustment (FL 61.30(11)(b)) ────────────────────────── petitioner_overnights = fields.Integer( string='Petitioner Overnights / Year', related='case_id.petitioner_overnights', store=True ) respondent_overnights = fields.Integer( string='Respondent Overnights / Year', related='case_id.respondent_overnights', store=True ) substantial_timesharing = fields.Boolean( string='Substantial Timesharing Applies', related='case_id.substantial_timesharing_applies', store=True, help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)' ) timesharing_adjustment = fields.Float( string='Timesharing Adjustment ($)', compute='_compute_timesharing_adjustment', store=True ) # ── Final Obligations ────────────────────────────────────────────────── total_support_obligation = fields.Float( string='Total Support Obligation ($)', compute='_compute_total_obligation', store=True ) petitioner_obligation = fields.Float( string='Petitioner Share ($)', compute='_compute_party_obligations', store=True ) respondent_obligation = fields.Float( string='Respondent Share ($)', compute='_compute_party_obligations', store=True ) net_payment_amount = fields.Float( string='Net Payment Amount ($)', compute='_compute_net_payment', store=True, help='Net amount actually exchanged between parties after credits' ) payment_direction = fields.Selection([ ('petitioner_pays', 'Petitioner Pays Respondent'), ('respondent_pays', 'Respondent Pays Petitioner'), ('no_payment', 'No Net Payment'), ], string='Payment Direction', compute='_compute_net_payment', store=True ) # ── Deviation (FL 61.30(1)(a)) ──────────────────────────────────────── deviation_requested = fields.Boolean( string='Deviation Requested', help='FL 61.30(1)(a): Court may deviate from guidelines with written findings' ) deviation_reason = fields.Text( string='Deviation Reason', help='Valid reasons: extraordinary medical needs, independent child income, ' 'seasonal income variations, shared physical custody arrangements' ) deviation_amount = fields.Float( string='Deviation Amount ($)', help='Positive = above guidelines; Negative = below guidelines' ) final_amount_with_deviation = fields.Float( string='Final Amount With Deviation ($)', compute='_compute_final_with_deviation', store=True ) # ── Summary ─────────────────────────────────────────────────────────── calculation_summary = fields.Text( string='Calculation Summary', compute='_compute_summary' ) # ══════════════════════════════════════════════════════════════════════ # COMPUTED METHODS # ══════════════════════════════════════════════════════════════════════ @api.depends('petitioner_net_income', 'respondent_net_income') def _compute_combined(self): for rec in self: rec.combined_net_income = ( (rec.petitioner_net_income or 0.0) + (rec.respondent_net_income or 0.0) ) @api.depends('combined_net_income', 'petitioner_net_income', 'respondent_net_income') def _compute_income_pcts(self): for rec in self: combined = rec.combined_net_income or 0.0 if combined > 0: rec.petitioner_income_pct = ( rec.petitioner_net_income or 0.0 ) / combined rec.respondent_income_pct = ( rec.respondent_net_income or 0.0 ) / combined else: rec.petitioner_income_pct = 0.5 rec.respondent_income_pct = 0.5 @api.depends('combined_net_income', 'number_of_children') def _compute_basic_obligation(self): """ Look up Basic Support Obligation from FL DCF Schedule. If combined income exceeds schedule maximum, apply FL 61.30(6) percentages: 1 child = 5% 2 children = 7.5% 3 children = 9.5% 4 children = 11% 5 children = 12% 6+ children = 12.5% """ PCT_MAP = {1: 0.05, 2: 0.075, 3: 0.095, 4: 0.11, 5: 0.12, 6: 0.125} for rec in self: if not rec.combined_net_income or not rec.number_of_children: rec.basic_support_obligation = 0.0 rec.support_schedule_id = False rec.above_schedule = False continue children = min(rec.number_of_children, 6) entry = self.env['fl.support.schedule.entry'].search([ ('income_min', '<=', rec.combined_net_income), ('income_max', '>=', rec.combined_net_income), ('children_count', '=', children), ('active', '=', True), ], order='effective_date desc', limit=1) if entry: rec.basic_support_obligation = entry.obligation_amount rec.support_schedule_id = entry rec.above_schedule = False else: # Above schedule maximum — FL 61.30(6) formula pct = PCT_MAP.get(children, 0.125) rec.basic_support_obligation = round( rec.combined_net_income * pct, 2 ) rec.support_schedule_id = False rec.above_schedule = True @api.depends( 'basic_support_obligation', 'child_health_insurance_total', 'childcare_total', 'extraordinary_expenses', ) def _compute_adjusted_obligation(self): for rec in self: rec.adjusted_support_obligation = ( (rec.basic_support_obligation or 0.0) + (rec.child_health_insurance_total or 0.0) + (rec.childcare_total or 0.0) + (rec.extraordinary_expenses or 0.0) ) @api.depends( 'adjusted_support_obligation', 'petitioner_overnights', 'substantial_timesharing', 'petitioner_income_pct', 'respondent_income_pct', ) def _compute_timesharing_adjustment(self): """ FL 61.30(11)(b) Substantial Timesharing Adjustment. Only applies if either parent has > 73 overnights/year (20%). Formula: 1. Each parent's share = adjusted_obligation × parent's income % 2. Multiply each share × 1.5 (cross-credit factor) 3. Multiply each result × other parent's timesharing % 4. Net payment = |pet_adjusted - resp_adjusted| """ for rec in self: if not rec.substantial_timesharing: rec.timesharing_adjustment = 0.0 continue pet_nights = rec.petitioner_overnights or 0 resp_nights = 365 - pet_nights pet_time_pct = pet_nights / 365 resp_time_pct = resp_nights / 365 adj = rec.adjusted_support_obligation or 0.0 pet_income_pct = rec.petitioner_income_pct or 0.5 resp_income_pct = rec.respondent_income_pct or 0.5 pet_share = adj * pet_income_pct resp_share = adj * resp_income_pct pet_adjusted = pet_share * 1.5 * resp_time_pct resp_adjusted = resp_share * 1.5 * pet_time_pct rec.timesharing_adjustment = round(abs(pet_adjusted - resp_adjusted), 2) @api.depends('adjusted_support_obligation', 'timesharing_adjustment', 'substantial_timesharing') def _compute_total_obligation(self): for rec in self: if rec.substantial_timesharing: rec.total_support_obligation = rec.timesharing_adjustment else: rec.total_support_obligation = rec.adjusted_support_obligation @api.depends('total_support_obligation', 'petitioner_income_pct', 'respondent_income_pct') def _compute_party_obligations(self): for rec in self: total = rec.total_support_obligation or 0.0 rec.petitioner_obligation = round(total * (rec.petitioner_income_pct or 0.5), 2) rec.respondent_obligation = round(total * (rec.respondent_income_pct or 0.5), 2) @api.depends( 'petitioner_obligation', 'respondent_obligation', 'health_insurance_by_petitioner', 'health_insurance_by_respondent', 'childcare_by_petitioner', 'childcare_by_respondent', ) def _compute_net_payment(self): for rec in self: # Credit parties for amounts they directly pay pet_credits = ( (rec.health_insurance_by_petitioner or 0.0) + (rec.childcare_by_petitioner or 0.0) ) resp_credits = ( (rec.health_insurance_by_respondent or 0.0) + (rec.childcare_by_respondent or 0.0) ) pet_net = (rec.petitioner_obligation or 0.0) - pet_credits resp_net = (rec.respondent_obligation or 0.0) - resp_credits net = resp_net - pet_net rec.net_payment_amount = abs(net) if net > 0.01: rec.payment_direction = 'petitioner_pays' elif net < -0.01: rec.payment_direction = 'respondent_pays' else: rec.payment_direction = 'no_payment' @api.depends('total_support_obligation', 'deviation_requested', 'deviation_amount') def _compute_final_with_deviation(self): for rec in self: if rec.deviation_requested and rec.deviation_amount: rec.final_amount_with_deviation = ( rec.total_support_obligation + rec.deviation_amount ) else: rec.final_amount_with_deviation = rec.total_support_obligation @api.depends( 'combined_net_income', 'basic_support_obligation', 'adjusted_support_obligation', 'net_payment_amount', 'payment_direction' ) def _compute_summary(self): direction_map = { 'petitioner_pays': 'Petitioner → Respondent', 'respondent_pays': 'Respondent → Petitioner', 'no_payment': 'No net payment', } for rec in self: direction = direction_map.get(rec.payment_direction or '', '') rec.calculation_summary = ( f'Combined income: ${rec.combined_net_income:,.2f}/mo | ' f'Basic obligation: ${rec.basic_support_obligation:,.2f} | ' f'Adjusted: ${rec.adjusted_support_obligation:,.2f} | ' f'Net payment: ${rec.net_payment_amount:,.2f} — {direction}' )