from odoo import _, api, fields, models from dateutil.relativedelta import relativedelta class FlCase(models.Model): _name = 'fl.case' _description = 'Florida Family Law Case' _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _order = 'create_date desc' _rec_name = 'name' # ══════════════════════════════════════════════════════════════════════ # IDENTITY # ══════════════════════════════════════════════════════════════════════ name = fields.Char( string='Case Reference', required=True, copy=False, readonly=True, default=lambda self: _('New'), help='Auto-generated internal reference (FL-YYYY-NNNNN)' ) court_case_number = fields.Char( string='Court Case Number', tracking=True, help='Assigned by Miami-Dade Clerk after filing. ' 'Format: YYYY-DR-XXXXXX-XX (e.g. 2025-DR-012345-01)' ) case_type = fields.Selection([ ('modification', 'Child Support Modification'), ('dissolution_children', 'Dissolution of Marriage — With Children'), ('dissolution_no_children', 'Dissolution of Marriage — No Children'), ('paternity', 'Paternity'), ('alimony_modification', 'Alimony Modification'), ('custody_modification', 'Timesharing / Custody Modification'), ], string='Case Type', required=True, tracking=True) circuit = fields.Char( string='Circuit', default='11th', readonly=True, help='11th Judicial Circuit — Miami-Dade County' ) county = fields.Char( string='County', default='Miami-Dade', readonly=True ) division = fields.Selection([ ('1', 'Division 1'), ('2', 'Division 2'), ('3', 'Division 3'), ('4', 'Division 4'), ('5', 'Division 5'), ('6', 'Division 6'), ('7', 'Division 7'), ('8', 'Division 8'), ], string='Court Division', tracking=True) judge_id = fields.Many2one( 'res.partner', string='Assigned Judge', domain=[('is_company', '=', False)] ) mediator_id = fields.Many2one( 'res.partner', string='Mediator' ) # ══════════════════════════════════════════════════════════════════════ # STAGE / STATUS # ══════════════════════════════════════════════════════════════════════ stage = fields.Selection([ ('intake', 'Intake & Qualification'), ('preparation', 'Document Preparation'), ('filed', 'Filed — Awaiting Service'), ('service_complete', 'Service Complete'), ('discovery', 'Discovery'), ('deposition', 'Deposition Stage'), ('mediation', 'Mediation'), ('hearing_scheduled', 'Hearing Scheduled'), ('order_entered', 'Order Entered'), ('closed', 'Closed'), ('referred_out', 'Referred to Attorney'), ], string='Stage', default='intake', tracking=True) active = fields.Boolean(default=True) # ══════════════════════════════════════════════════════════════════════ # PARTIES # ══════════════════════════════════════════════════════════════════════ petitioner_id = fields.Many2one( 'res.partner', string='Petitioner', required=True, tracking=True ) respondent_id = fields.Many2one( 'res.partner', string='Respondent', tracking=True ) petitioner_attorney_id = fields.Many2one( 'res.partner', string='Petitioner Attorney', help='Leave blank if petitioner is pro se' ) respondent_attorney_id = fields.Many2one( 'res.partner', string='Respondent Attorney', tracking=True, help='If respondent has counsel, pro se petitioner faces significant disadvantage' ) respondent_has_counsel = fields.Boolean( string='Respondent Has Counsel', compute='_compute_respondent_has_counsel', store=True ) party_ids = fields.One2many( 'fl.party', 'case_id', string='Party Details' ) # ══════════════════════════════════════════════════════════════════════ # CHILDREN # ══════════════════════════════════════════════════════════════════════ child_ids = fields.One2many( 'fl.child', 'case_id', string='Children' ) children_count = fields.Integer( string='Number of Children', compute='_compute_children_count', store=True ) has_minor_children = fields.Boolean( string='Has Minor Children', compute='_compute_has_minor_children', store=True ) # ══════════════════════════════════════════════════════════════════════ # KEY DATES # ══════════════════════════════════════════════════════════════════════ filing_date = fields.Date( string='Filing Date', tracking=True ) service_date = fields.Date( string='Service Date', tracking=True ) last_order_date = fields.Date( string='Date of Last Order', tracking=True, help='Required for modification cases — date of the existing order being modified' ) marriage_date = fields.Date(string='Date of Marriage') separation_date = fields.Date(string='Date of Separation') years_of_marriage = fields.Float( string='Years of Marriage', compute='_compute_years_of_marriage', store=True ) # ══════════════════════════════════════════════════════════════════════ # RESIDENCY (FL 61.021) # ══════════════════════════════════════════════════════════════════════ petitioner_fl_resident_since = fields.Date( string='Petitioner FL Resident Since', help='FL 61.021: Must be FL resident for 6 months before filing' ) residency_requirement_met = fields.Boolean( string='Residency Requirement Met', compute='_compute_residency_met', store=True ) residency_warning = fields.Char( string='Residency Warning', compute='_compute_residency_met' ) # ══════════════════════════════════════════════════════════════════════ # SAFETY FLAGS # ══════════════════════════════════════════════════════════════════════ domestic_violence_flag = fields.Boolean( string='Domestic Violence Present', tracking=True, help='FL 741.30 / FL 61.13(2)(c). ' 'Affects mediation (separate rooms required), timesharing, ' 'and ALL court proceedings. Forces attorney referral.' ) dv_injunction_active = fields.Boolean( string='Active Injunction (FL 741.30)' ) dv_safety_note = fields.Html( string='Safety Information', compute='_compute_dv_safety_note' ) # ══════════════════════════════════════════════════════════════════════ # EXISTING ORDER (MODIFICATION CASES) # ══════════════════════════════════════════════════════════════════════ current_order_amount = fields.Float( string='Current Monthly Support Order ($)', tracking=True, help='Amount in the existing court order that is being modified' ) current_order_per_child = fields.Boolean( string='Amount is Per-Child (not total)', help='Check if the existing order states an amount per child rather than total' ) current_order_total = fields.Float( string='Current Order Total ($)', compute='_compute_current_order_total', store=True ) # ══════════════════════════════════════════════════════════════════════ # CHILD SUPPORT CALCULATION (FL 61.30) # ══════════════════════════════════════════════════════════════════════ support_calc_id = fields.Many2one( 'fl.support.calculation', string='Active Support Calculation', ondelete='cascade' ) calculated_support = fields.Float( string='Calculated Support ($)', related='support_calc_id.total_support_obligation', store=True ) support_difference = fields.Float( string='Difference from Current Order ($)', compute='_compute_support_difference', store=True ) support_difference_pct = fields.Float( string='Difference %', compute='_compute_support_difference', store=True ) # ══════════════════════════════════════════════════════════════════════ # MODIFICATION THRESHOLD TEST (FL 61.30(1)(b)) # ══════════════════════════════════════════════════════════════════════ threshold_met = fields.Boolean( string='Modification Threshold Met', compute='_compute_threshold', store=True, help='FL 61.30(1)(b): Modification qualifies when change is ' 'both ≥ $50 AND ≥ 15% of the current order' ) threshold_result = fields.Html( string='Threshold Analysis', compute='_compute_threshold' ) substantial_change_basis = fields.Selection([ ('income_decrease_petitioner', 'Petitioner Income Decreased'), ('income_decrease_respondent', 'Respondent Income Decreased'), ('income_increase_petitioner', 'Petitioner Income Increased'), ('income_increase_respondent', 'Respondent Income Increased'), ('timesharing_change', 'Timesharing Schedule Changed'), ('child_needs_change', "Child's Needs Changed"), ('child_emancipation', 'Child Reaching 18 / Emancipation'), ('other', 'Other Substantial Change'), ], string='Basis for Modification (FL 61.14)') substantial_change_detail = fields.Text( string='Describe the Substantial Change' ) # ══════════════════════════════════════════════════════════════════════ # TIMESHARING # ══════════════════════════════════════════════════════════════════════ timesharing_changed = fields.Boolean( string='Timesharing Schedule Has Changed' ) petitioner_overnights = fields.Integer( string='Petitioner Overnights / Year', help='Number of overnights per year the petitioner has with the children' ) respondent_overnights = fields.Integer( string='Respondent Overnights / Year', compute='_compute_respondent_overnights', store=True ) petitioner_timesharing_pct = fields.Float( string='Petitioner Timesharing %', compute='_compute_timesharing_pct', store=True ) substantial_timesharing_applies = fields.Boolean( string='Substantial Timesharing Adjustment Applies', compute='_compute_timesharing_pct', store=True, help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)' ) # ══════════════════════════════════════════════════════════════════════ # FEE WAIVER (FL 57.082) # ══════════════════════════════════════════════════════════════════════ fee_waiver_eligible = fields.Boolean( string='Fee Waiver Potentially Eligible', compute='_compute_fee_waiver_eligibility', store=True, help='Based on petitioner income vs 200% FPL (FL 57.082)' ) fee_waiver_id = fields.Many2one( 'fl.fee.waiver', string='Fee Waiver Application' ) # ══════════════════════════════════════════════════════════════════════ # PARENTING CLASS (FL 61.21) # ══════════════════════════════════════════════════════════════════════ parenting_class_required = fields.Boolean( string='Parenting Class Required', compute='_compute_parenting_class_required', store=True, help='FL 61.21: Mandatory when minor children are involved' ) petitioner_parenting_class_done = fields.Boolean( string='Petitioner Parenting Class Complete' ) petitioner_parenting_class_date = fields.Date( string='Petitioner Completion Date' ) respondent_parenting_class_done = fields.Boolean( string='Respondent Parenting Class Complete' ) respondent_parenting_class_date = fields.Date( string='Respondent Completion Date' ) parenting_class_warning = fields.Char( string='Parenting Class Status', compute='_compute_parenting_class_warning' ) # ══════════════════════════════════════════════════════════════════════ # DEADLINES & CALENDAR # ══════════════════════════════════════════════════════════════════════ deadline_ids = fields.One2many( 'fl.deadline', 'case_id', string='Deadlines' ) next_deadline = fields.Date( string='Next Deadline', compute='_compute_next_deadline', store=True ) next_deadline_label = fields.Char( string='Next Deadline Description', compute='_compute_next_deadline' ) overdue_deadline_count = fields.Integer( string='Overdue Deadlines', compute='_compute_next_deadline' ) # ══════════════════════════════════════════════════════════════════════ # HEARINGS # ══════════════════════════════════════════════════════════════════════ hearing_ids = fields.One2many( 'fl.hearing', 'case_id', string='Hearings' ) next_hearing_date = fields.Datetime( string='Next Hearing', compute='_compute_next_hearing', store=True ) discovery_cutoff_date = fields.Date( string='Discovery Cutoff', compute='_compute_discovery_cutoff', store=True, help='30 days before next hearing date' ) # ══════════════════════════════════════════════════════════════════════ # DEPOSITIONS & DISCOVERY # ══════════════════════════════════════════════════════════════════════ income_disputed = fields.Boolean( string='Income Figures Disputed', tracking=True, help='If True, deposition workflow is activated' ) deposition_ids = fields.One2many( 'fl.deposition', 'case_id', string='Depositions' ) discovery_ids = fields.One2many( 'fl.discovery', 'case_id', string='Discovery Items' ) # ══════════════════════════════════════════════════════════════════════ # DOCUMENTS # ══════════════════════════════════════════════════════════════════════ document_ids = fields.One2many( 'fl.document', 'case_id', string='Case Documents' ) # ══════════════════════════════════════════════════════════════════════ # PROJECT / TASK INTEGRATION # ══════════════════════════════════════════════════════════════════════ project_id = fields.Many2one( 'project.project', string='Case Project', help='Auto-created project for case task management' ) task_count = fields.Integer( string='Tasks', compute='_compute_task_count' ) # ══════════════════════════════════════════════════════════════════════ # CASE LAW & AI ANALYSIS # ══════════════════════════════════════════════════════════════════════ caselaw_ids = fields.Many2many( 'fl.caselaw', string='Applicable Case Law' ) analysis_ids = fields.One2many( 'fl.analysis', 'case_id', string='AI Analyses' ) latest_analysis_id = fields.Many2one( 'fl.analysis', string='Latest Analysis', compute='_compute_latest_analysis', store=True ) attorney_referral_flag = fields.Boolean( string='Attorney Referral Recommended', compute='_compute_attorney_referral_flag', store=True ) ai_plain_english = fields.Text( string='AI Summary (English)', related='latest_analysis_id.plain_english_summary' ) ai_plain_english_es = fields.Text( string='AI Summary (Spanish)', related='latest_analysis_id.plain_english_summary_es' ) # ══════════════════════════════════════════════════════════════════════ # EXPENSES # ══════════════════════════════════════════════════════════════════════ expense_ids = fields.One2many( 'hr.expense', 'fl_case_id', string='Case Expenses' ) total_expenses = fields.Float( string='Total Case Expenses ($)', compute='_compute_total_expenses', store=True ) # ══════════════════════════════════════════════════════════════════════ # POST-ORDER # ══════════════════════════════════════════════════════════════════════ new_order_amount = fields.Float( string='New Order Amount ($)', tracking=True ) new_order_date = fields.Date( string='New Order Date', tracking=True ) income_withholding_id = fields.Many2one( 'fl.income.withholding', string='Income Withholding Order' ) retroactivity_date = fields.Date( string='Retroactivity Date', compute='_compute_retroactivity_date', store=True, help='FL 61.30(17): Modification is retroactive to filing date ONLY. ' 'Cannot seek support modification for periods before filing.' ) respondent_answered = fields.Boolean( string='Respondent Filed Answer', tracking=True ) # ══════════════════════════════════════════════════════════════════════ # COMPUTED METHODS # ══════════════════════════════════════════════════════════════════════ @api.depends('respondent_attorney_id') def _compute_respondent_has_counsel(self): for rec in self: rec.respondent_has_counsel = bool(rec.respondent_attorney_id) @api.depends('child_ids') def _compute_children_count(self): for rec in self: rec.children_count = len(rec.child_ids) @api.depends('child_ids.emancipated', 'child_ids.date_of_birth') def _compute_has_minor_children(self): for rec in self: rec.has_minor_children = any( not c.emancipated for c in rec.child_ids ) @api.depends('marriage_date', 'separation_date', 'filing_date') def _compute_years_of_marriage(self): for rec in self: end_date = rec.separation_date or rec.filing_date if rec.marriage_date and end_date: delta = relativedelta(end_date, rec.marriage_date) rec.years_of_marriage = round( delta.years + delta.months / 12 + delta.days / 365, 2 ) else: rec.years_of_marriage = 0.0 @api.depends('petitioner_fl_resident_since', 'filing_date') def _compute_residency_met(self): for rec in self: if rec.petitioner_fl_resident_since and rec.filing_date: required_date = rec.petitioner_fl_resident_since + relativedelta(months=6) rec.residency_requirement_met = rec.filing_date >= required_date if not rec.residency_requirement_met: months_short = relativedelta(required_date, rec.filing_date) rec.residency_warning = ( f'⚠️ FL 61.021: Petitioner must be a FL resident for 6 months ' f'before filing. Currently {months_short.months} month(s) short.' ) else: rec.residency_warning = '✅ Residency requirement met (FL 61.021)' elif rec.petitioner_fl_resident_since and not rec.filing_date: rec.residency_requirement_met = False rec.residency_warning = 'Enter filing date to verify residency requirement' else: rec.residency_requirement_met = False rec.residency_warning = '⚠️ Enter FL residency start date to verify FL 61.021' @api.depends('domestic_violence_flag', 'dv_injunction_active') def _compute_dv_safety_note(self): for rec in self: if rec.domestic_violence_flag: injunction_note = ( '
Active injunction on file.' if rec.dv_injunction_active else '' ) rec.dv_safety_note = ( '
' '

' '⚠️ DOMESTIC VIOLENCE SAFETY NOTICE

' '

Domestic violence has been flagged in this case. ' 'This affects your rights and safety throughout this proceeding.

' '' f'{injunction_note}' '

Resources:
' 'Legal Services of Greater Miami: (305) 576-0080
' 'Safespace Miami: (305) 536-5565
' 'National DV Hotline: 1-800-799-7233

' '
' ) else: rec.dv_safety_note = '' @api.depends('current_order_amount', 'current_order_per_child', 'children_count') def _compute_current_order_total(self): for rec in self: if rec.current_order_per_child and rec.children_count: rec.current_order_total = ( rec.current_order_amount * rec.children_count ) else: rec.current_order_total = rec.current_order_amount @api.depends('calculated_support', 'current_order_total') def _compute_support_difference(self): for rec in self: current = rec.current_order_total or 0.0 proposed = rec.calculated_support or 0.0 rec.support_difference = proposed - current if current > 0: rec.support_difference_pct = ( abs(proposed - current) / current ) * 100 else: rec.support_difference_pct = 0.0 @api.depends('support_difference', 'support_difference_pct', 'case_type') def _compute_threshold(self): """ FL 61.30(1)(b): Modification qualifies when BOTH: - |difference| >= $50 AND - |difference| / current_amount >= 15% """ for rec in self: if rec.case_type != 'modification': rec.threshold_met = False rec.threshold_result = '' continue diff = abs(rec.support_difference) pct = rec.support_difference_pct current = rec.current_order_total or 0.0 dollar_test = diff >= 50.0 pct_test = pct >= 15.0 if current == 0: rec.threshold_met = False rec.threshold_result = ( '⚠️ Enter current order amount ' 'to run threshold test' ) continue color_dollar = '#28a745' if dollar_test else '#dc3545' color_pct = '#28a745' if pct_test else '#dc3545' direction = 'increase' if rec.support_difference > 0 else 'decrease' rec.threshold_met = dollar_test and pct_test overall_color = '#28a745' if rec.threshold_met else '#dc3545' overall_icon = '✅' if rec.threshold_met else '❌' overall_label = 'QUALIFIES FOR MODIFICATION' if rec.threshold_met else 'DOES NOT QUALIFY' rec.threshold_result = ( f'
' f'{overall_icon} {overall_label}' f'

' f'Current order: ${current:,.2f}/mo
' f'Proposed: ${rec.calculated_support:,.2f}/mo ' f'({direction} of ${diff:,.2f})

' f'' f'{"✅" if dollar_test else "❌"} $50 test: ' f'${diff:,.2f} {"≥" if dollar_test else "<"} $50
' f'' f'{"✅" if pct_test else "❌"} 15% test: ' f'{pct:.1f}% {"≥" if pct_test else "<"} 15%' f'
' ) @api.depends('petitioner_overnights') def _compute_respondent_overnights(self): for rec in self: if rec.petitioner_overnights: rec.respondent_overnights = 365 - rec.petitioner_overnights else: rec.respondent_overnights = 0 @api.depends('petitioner_overnights') def _compute_timesharing_pct(self): for rec in self: if rec.petitioner_overnights: pct = rec.petitioner_overnights / 365 rec.petitioner_timesharing_pct = round(pct * 100, 2) # FL 61.30(11)(b): substantial = either parent > 73 overnights (20%) pet_substantial = rec.petitioner_overnights > 73 resp_substantial = (365 - rec.petitioner_overnights) > 73 rec.substantial_timesharing_applies = pet_substantial or resp_substantial else: rec.petitioner_timesharing_pct = 0.0 rec.substantial_timesharing_applies = False @api.depends('party_ids.effective_monthly_income', 'party_ids.role') def _compute_fee_waiver_eligibility(self): """ Quick eligibility check: petitioner income < 200% FPL (FL 57.082). Full determination via fl.fee.waiver model. 2025: 1-person 200% FPL = $30,120/yr = $2,510/mo """ FPL_200PCT_1PERSON_MONTHLY = 2510.0 for rec in self: petitioner_party = rec.party_ids.filtered( lambda p: p.role == 'petitioner' ) if petitioner_party: income = petitioner_party[0].effective_monthly_income or 0.0 rec.fee_waiver_eligible = income <= FPL_200PCT_1PERSON_MONTHLY else: rec.fee_waiver_eligible = False @api.depends('has_minor_children') def _compute_parenting_class_required(self): for rec in self: rec.parenting_class_required = rec.has_minor_children @api.depends( 'parenting_class_required', 'petitioner_parenting_class_done', 'respondent_parenting_class_done', ) def _compute_parenting_class_warning(self): for rec in self: if not rec.parenting_class_required: rec.parenting_class_warning = 'Not required' continue pet_done = rec.petitioner_parenting_class_done resp_done = rec.respondent_parenting_class_done if pet_done and resp_done: rec.parenting_class_warning = '✅ Both parents completed' elif pet_done: rec.parenting_class_warning = '⚠️ Respondent parenting class pending' elif resp_done: rec.parenting_class_warning = '⚠️ Petitioner parenting class pending' else: rec.parenting_class_warning = '❌ Neither parent has completed parenting class' @api.depends('deadline_ids.due_date', 'deadline_ids.completed', 'deadline_ids.waived') def _compute_next_deadline(self): today = fields.Date.today() for rec in self: pending = rec.deadline_ids.filtered( lambda d: not d.completed and not d.waived ) overdue = pending.filtered(lambda d: d.due_date < today) upcoming = pending.filtered( lambda d: d.due_date >= today ).sorted('due_date') rec.overdue_deadline_count = len(overdue) if upcoming: next_dl = upcoming[0] rec.next_deadline = next_dl.due_date rec.next_deadline_label = next_dl.name elif overdue: oldest = overdue.sorted('due_date')[0] rec.next_deadline = oldest.due_date rec.next_deadline_label = f'OVERDUE: {oldest.name}' else: rec.next_deadline = False rec.next_deadline_label = 'No pending deadlines' @api.depends('hearing_ids.hearing_date') def _compute_next_hearing(self): now = fields.Datetime.now() for rec in self: future = rec.hearing_ids.filtered( lambda h: h.hearing_date and h.hearing_date > now and h.state not in ('cancelled',) ).sorted('hearing_date') rec.next_hearing_date = future[0].hearing_date if future else False @api.depends('next_hearing_date') def _compute_discovery_cutoff(self): for rec in self: if rec.next_hearing_date: rec.discovery_cutoff_date = ( rec.next_hearing_date.date() - relativedelta(days=30) ) else: rec.discovery_cutoff_date = False def _compute_task_count(self): for rec in self: if rec.project_id: rec.task_count = self.env['project.task'].search_count([ ('project_id', '=', rec.project_id.id) ]) else: rec.task_count = 0 @api.depends('analysis_ids') def _compute_latest_analysis(self): for rec in self: latest = rec.analysis_ids.sorted('create_date', reverse=True) rec.latest_analysis_id = latest[0] if latest else False @api.depends('latest_analysis_id.attorney_referral_flag', 'domestic_violence_flag', 'respondent_has_counsel') def _compute_attorney_referral_flag(self): for rec in self: rec.attorney_referral_flag = ( rec.domestic_violence_flag or rec.respondent_has_counsel or (rec.latest_analysis_id and rec.latest_analysis_id.attorney_referral_flag) ) @api.depends('expense_ids.total_amount') def _compute_total_expenses(self): for rec in self: rec.total_expenses = sum(rec.expense_ids.mapped('total_amount')) @api.depends('filing_date') def _compute_retroactivity_date(self): """ FL 61.30(17): Modification is retroactive to the filing date ONLY. """ for rec in self: rec.retroactivity_date = rec.filing_date # ══════════════════════════════════════════════════════════════════════ # CRUD OVERRIDES # ══════════════════════════════════════════════════════════════════════ @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code( 'fl.case' ) or _('New') records = super().create(vals_list) for record in records: # 1. Create Odoo project for task management record._create_case_project() # 2. Generate deadlines if filing date already set if record.filing_date: self.env['fl.deadline'].generate_deadlines_for_case(record) # 3. Check fee waiver eligibility (quick check) record._check_fee_waiver_eligibility() # 4. Handle DV flag if record.domestic_violence_flag: record._handle_dv_flag() return records def write(self, vals): result = super().write(vals) # Recalculate service-anchored deadlines when service_date is set/changed if 'service_date' in vals: for rec in self: if rec.service_date: self.env['fl.deadline'].recalculate_service_deadlines(rec) # Handle DV flag being set if vals.get('domestic_violence_flag'): for rec in self: if rec.domestic_violence_flag: rec._handle_dv_flag() # Generate income withholding order when new_order_amount is set if vals.get('new_order_amount') and vals.get('new_order_date'): for rec in self: rec._check_generate_income_withholding() return result # ══════════════════════════════════════════════════════════════════════ # WORKFLOW METHODS # ══════════════════════════════════════════════════════════════════════ def _create_case_project(self): """Create a linked Odoo project for case task management.""" project = self.env['project.project'].create({ 'name': f'Case: {self.name} — {self.petitioner_id.name}', 'partner_id': self.petitioner_id.id, 'description': ( f'Family law case project for {self.petitioner_id.name}. ' f'Case type: {dict(self._fields["case_type"].selection).get(self.case_type, "")}' ), }) self.project_id = project def _check_fee_waiver_eligibility(self): """Post a chatter note if petitioner appears to qualify for fee waiver.""" if self.fee_waiver_eligible and not self.fee_waiver_id: self.message_post( body=( '💡 Fee Waiver Opportunity: Based on the income information ' 'provided, this petitioner may qualify for a civil indigent fee ' 'waiver (FL 57.082). This would waive filing fees, service fees, ' 'and potentially mediator fees. ' 'Consider creating a Fee Waiver application from the case form.' ), subtype_xmlid='mail.mt_note', ) def _handle_dv_flag(self): """ Handle domestic violence flag being set on a case. Per spec: Force attorney referral, post safety resources, require separate mediation rooms, disable direct contact. """ self.message_post( body=( '
' '

' '🚨 SAFETY ALERT — DOMESTIC VIOLENCE FLAGGED

' '' 'Resources:
' 'Legal Services of Greater Miami: (305) 576-0080
' 'Safespace Miami: (305) 536-5565
' 'National DV Hotline: 1-800-799-7233
' 'TTY: 1-800-787-3224' '
' ), subtype_xmlid='mail.mt_note', ) def _check_generate_income_withholding(self): """ FL 61.1301: Auto-generate income withholding order when new order amount is set. Mandatory unless good cause or written agreement. """ if self.income_withholding_id: return # Already exists iwo = self.env['fl.income.withholding'].create({ 'case_id': self.id, 'obligor_id': self.respondent_id.id, 'obligee_id': self.petitioner_id.id, 'monthly_support_amount': self.new_order_amount, }) self.income_withholding_id = iwo self.message_post( body=( f'⚖️ Income Withholding Order created (FL 61.1301).
' f'Amount: ${self.new_order_amount:,.2f}/mo.
' f'Income withholding is MANDATORY under FL 61.1301 unless good cause ' f'is shown or both parties agree in writing to an alternative payment method.' ), subtype_xmlid='mail.mt_note', ) def trigger_ai_analysis(self): """ Trigger AI case analysis via Ollama. Stub for Phase 1 — full implementation in Phase 5. """ self.message_post( body='🤖 AI analysis queued. Full AI analysis will be available in Phase 5.', subtype_xmlid='mail.mt_note', ) # ══════════════════════════════════════════════════════════════════════ # ACTIONS # ══════════════════════════════════════════════════════════════════════ def action_view_tasks(self): return { 'type': 'ir.actions.act_window', 'name': 'Case Tasks', 'res_model': 'project.task', 'view_mode': 'tree,form', 'domain': [('project_id', '=', self.project_id.id)], 'context': {'default_project_id': self.project_id.id}, } def action_open_support_calculator(self): if not self.support_calc_id: calc = self.env['fl.support.calculation'].create({ 'case_id': self.id, 'calculation_type': 'proposed', }) self.support_calc_id = calc return { 'type': 'ir.actions.act_window', 'name': 'FL 61.30 Support Calculator', 'res_model': 'fl.support.calculation', 'res_id': self.support_calc_id.id, 'view_mode': 'form', 'target': 'current', } def action_create_fee_waiver(self): waiver = self.env['fl.fee.waiver'].create({ 'case_id': self.id, 'party_id': self.petitioner_id.id, }) self.fee_waiver_id = waiver return { 'type': 'ir.actions.act_window', 'name': 'Fee Waiver Application', 'res_model': 'fl.fee.waiver', 'res_id': waiver.id, 'view_mode': 'form', 'target': 'current', } def action_run_ai_analysis(self): return { 'type': 'ir.actions.act_window', 'name': 'Run AI Analysis', 'res_model': 'fl.analysis.wizard', 'view_mode': 'form', 'target': 'new', 'context': {'default_case_id': self.id}, }