import json import logging from datetime import date, timedelta from odoo import http from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager _logger = logging.getLogger(__name__) class FamilyLawPortal(CustomerPortal): """ Phase 6 — Portal controller for fl.case, fl.caselaw, calculator, and intake. Routes: GET /my/cases — list of petitioner/respondent cases GET /my/cases/ — case detail with deadlines/timeline GET /my/cases/calculator — FL 61.30 interactive calculator GET /my/cases/caselaw — FL case law library (searchable) POST /family-law/deadline/complete — AJAX: mark deadline complete GET /family-law/intake — public landing page GET /family-law/intake/start — multi-step intake form POST /family-law/intake/submit — process intake form → create case """ # ────────────────────────────────────────────────────────────────────────── # Portal home — add case_count for the "My Cases" card # ────────────────────────────────────────────────────────────────────────── def _prepare_home_portal_values(self, counters): values = super()._prepare_home_portal_values(counters) if 'case_count' in counters: partner = request.env.user.partner_id cases = request.env['fl.case'].search([ '|', ('petitioner_id.user_ids', 'in', request.env.user.ids), ('respondent_id.user_ids', 'in', request.env.user.ids), ]) values['case_count'] = len(cases) return values # ────────────────────────────────────────────────────────────────────────── # /my/cases — case list # ────────────────────────────────────────────────────────────────────────── @http.route('/my/cases', type='http', auth='user', website=True) def portal_my_cases(self, **kwargs): cases = request.env['fl.case'].search([ '|', ('petitioner_id.user_ids', 'in', request.env.user.ids), ('respondent_id.user_ids', 'in', request.env.user.ids), ], order='create_date desc') return request.render('activeblue_familylaw.portal_my_cases', { 'cases': cases, 'page_name': 'cases', }) # ────────────────────────────────────────────────────────────────────────── # /my/cases/ — case detail # ────────────────────────────────────────────────────────────────────────── @http.route('/my/cases/', type='http', auth='user', website=True) def portal_case_detail(self, case_id, **kwargs): # Access check: user must be petitioner or respondent case = request.env['fl.case'].search([ ('id', '=', case_id), '|', ('petitioner_id.user_ids', 'in', request.env.user.ids), ('respondent_id.user_ids', 'in', request.env.user.ids), ], limit=1) if not case: return request.render('website.403') latest_analysis = case.analysis_ids[:1] if case.analysis_ids else None return request.render('activeblue_familylaw.portal_case_detail', { 'case': case, 'latest_analysis': latest_analysis, 'page_name': 'case_detail', }) # ────────────────────────────────────────────────────────────────────────── # /my/cases/calculator — FL 61.30 calculator # ────────────────────────────────────────────────────────────────────────── @http.route('/my/cases/calculator', type='http', auth='public', website=True) def portal_calculator(self, case_id=None, **kwargs): """ Pre-fill calculator from case data if case_id is provided and user is authenticated. """ prefill_petitioner = None prefill_respondent = None if case_id and request.env.user and request.env.user.id != request.env.ref('base.public_user').id: case = request.env['fl.case'].search([ ('id', '=', int(case_id)), '|', ('petitioner_id.user_ids', 'in', request.env.user.ids), ('respondent_id.user_ids', 'in', request.env.user.ids), ], limit=1) if case: prefill_petitioner = case.petitioner_net_income prefill_respondent = case.respondent_net_income return request.render('activeblue_familylaw.portal_calculator', { 'prefill_petitioner': prefill_petitioner, 'prefill_respondent': prefill_respondent, 'page_name': 'calculator', }) # ────────────────────────────────────────────────────────────────────────── # /my/cases/caselaw — case law library # ────────────────────────────────────────────────────────────────────────── @http.route('/my/cases/caselaw', type='http', auth='public', website=True) def portal_caselaw(self, tag=None, court=None, **kwargs): domain = [('active', '=', True)] if tag: domain += [('issue_tag_ids.name', '=', tag)] if court: domain += [('court', '=', court)] cases = request.env['fl.caselaw'].search(domain, order='year desc, short_name') return request.render('activeblue_familylaw.portal_caselaw', { 'cases': cases, 'page_name': 'caselaw', }) # ────────────────────────────────────────────────────────────────────────── # AJAX: /family-law/deadline/complete # ────────────────────────────────────────────────────────────────────────── @http.route('/family-law/deadline/complete', type='json', auth='user', methods=['POST'], csrf=True) def deadline_complete(self, deadline_id=None, **kwargs): if not deadline_id: return {'success': False, 'error': 'Missing deadline_id'} deadline = request.env['fl.deadline'].search([ ('id', '=', deadline_id), '|', ('case_id.petitioner_id.user_ids', 'in', request.env.user.ids), ('case_id.respondent_id.user_ids', 'in', request.env.user.ids), ], limit=1) if not deadline: return {'success': False, 'error': 'Deadline not found or access denied'} try: deadline.action_complete() return {'success': True} except Exception as e: _logger.error("Portal deadline complete error: %s", e) return {'success': False, 'error': str(e)} # ────────────────────────────────────────────────────────────────────────── # Public: /family-law/intake — landing page # ────────────────────────────────────────────────────────────────────────── @http.route('/family-law/intake', type='http', auth='public', website=True) def intake_landing(self, **kwargs): return request.render('activeblue_familylaw.website_intake_landing', { 'page_name': 'intake', }) # ────────────────────────────────────────────────────────────────────────── # Public: /family-law/intake/start — multi-step form # ────────────────────────────────────────────────────────────────────────── @http.route('/family-law/intake/start', type='http', auth='public', website=True) def intake_start(self, case_type=None, **kwargs): return request.render('activeblue_familylaw.website_intake_form', { 'case_type': case_type or 'child_support', 'page_name': 'intake_form', }) # ────────────────────────────────────────────────────────────────────────── # Public: POST /family-law/intake/submit — process intake → create case # ────────────────────────────────────────────────────────────────────────── @http.route('/family-law/intake/submit', type='http', auth='public', website=True, methods=['POST'], csrf=True) def intake_submit(self, **post): """ Process intake form submission. Creates or finds partner records, creates fl.case, triggers fee waiver check and optional AI analysis, then redirects to confirmation page. """ try: petitioner_name = (post.get('petitioner_name') or '').strip() petitioner_email = (post.get('petitioner_email') or '').strip() respondent_name = (post.get('respondent_name') or '').strip() case_type = post.get('case_type') or 'child_support' num_children = int(post.get('num_children') or 1) court_case_number = (post.get('court_case_number') or '').strip() or False domestic_violence = post.get('domestic_violence') == 'yes' respondent_has_counsel = post.get('respondent_has_counsel') == 'yes' income_concern = post.get('income_imputation_concern') == 'yes' petitioner_income = float(post.get('petitioner_income') or 0) respondent_income = float(post.get('respondent_income') or 0) current_order = float(post.get('current_order_amount') or 0) household_size = int(post.get('household_size') or 3) fee_waiver_request = post.get('fee_waiver_request') == 'yes' notes = (post.get('notes') or '').strip() fl_resident_since = post.get('fl_resident_since') or False # Use sudo for partner/case creation (public user has limited rights) env = request.env['fl.case'].sudo() # Find or create petitioner partner petitioner = request.env['res.partner'].sudo().search( [('email', '=', petitioner_email)], limit=1 ) if not petitioner: petitioner = request.env['res.partner'].sudo().create({ 'name': petitioner_name, 'email': petitioner_email, 'phone': post.get('petitioner_phone') or False, 'street': post.get('petitioner_address') or False, 'city': post.get('petitioner_city') or False, 'state_id': request.env.ref('base.state_us_10').id, # FL 'country_id': request.env.ref('base.us').id, }) # Find or create respondent partner respondent = False if respondent_name: respondent = request.env['res.partner'].sudo().search( [('name', '=', respondent_name)], limit=1 ) if not respondent: respondent = request.env['res.partner'].sudo().create({ 'name': respondent_name, }) # Build fl.party records (minimal) petitioner_party = request.env['fl.party'].sudo().search( [('partner_id', '=', petitioner.id)], limit=1 ) if not petitioner_party: petitioner_party = request.env['fl.party'].sudo().create({ 'name': petitioner_name, 'email': petitioner_email, 'employment_status': 'employed', 'monthly_gross_income': petitioner_income, }) respondent_party = False if respondent: respondent_party = request.env['fl.party'].sudo().search( [('partner_id', '=', respondent.id)], limit=1 ) if not respondent_party: respondent_party = request.env['fl.party'].sudo().create({ 'name': respondent_name, 'monthly_gross_income': respondent_income, }) # Parse FL resident since date filing_date = date.today() residency_ok = True if fl_resident_since: try: res_date = date.fromisoformat(fl_resident_since) residency_ok = (filing_date - res_date).days >= 180 except ValueError: pass # Create the case case_vals = { 'case_type': case_type, 'petitioner_id': petitioner_party.id, 'respondent_id': respondent_party.id if respondent_party else False, 'court_case_number': court_case_number, 'domestic_violence_flag': domestic_violence, 'respondent_has_counsel': respondent_has_counsel, 'current_order_amount': current_order, 'filing_date': filing_date, 'residency_requirement_met': residency_ok, 'household_size': household_size, } if notes: case_vals['description'] = notes case = request.env['fl.case'].sudo().create(case_vals) # Fee waiver check attorney_referral = domestic_violence or respondent_has_counsel fee_waiver_eligible = False if fee_waiver_request: fpl = request.env['fl.fee.waiver'].sudo().search([('case_id', '=', case.id)], limit=1) if fpl: fee_waiver_eligible = fpl.is_eligible # Trigger AI analysis (async-ish: do it now, portal will show result) try: request.env['fl.ai.engine'].sudo().analyze_case(case.id) except Exception as e: _logger.warning("Portal intake: AI analysis failed for case %s: %s", case.id, e) return request.render('activeblue_familylaw.website_intake_confirm', { 'case_name': case.name, 'petitioner_email': petitioner_email, 'fee_waiver_eligible': fee_waiver_eligible, 'attorney_referral': attorney_referral, 'page_name': 'intake_confirm', }) except Exception as exc: _logger.error("Intake submission error: %s", exc, exc_info=True) return request.render('activeblue_familylaw.website_intake_form', { 'case_type': post.get('case_type', 'child_support'), 'error': str(exc), 'page_name': 'intake_form', })