Files
famlaw/activeblue_familylaw/controllers/portal.py
Carlos Garcia 6dc2144db7 Phase 6: portal, website intake, calculator, and case law library
- controllers/portal.py: FamilyLawPortal with 8 routes (cases list,
  case detail, calculator pre-fill, caselaw library, deadline complete
  AJAX, public intake landing/form/submit)
- views/portal_case_templates.xml: portal home card, case list, full
  case detail with timeline widget, AI summary, DV safety banner
- views/portal_calculator_templates.xml: FL 61.30 interactive calculator
- views/portal_caselaw_templates.xml: searchable case law library (EN/ES)
- views/website_intake_templates.xml: public 4-step intake form with DV
  quick-exit, fee waiver, and intake confirmation page
- static/src/css/familylaw_portal.css: full portal/website CSS (EN/ES
  lang toggle, deadline card color coding, timeline, AI summary box)
- static/src/js/fl_calculator.js: FL 61.30 schedule lookup, above-
  schedule formula, FL 61.30(11)(b) substantial timesharing calculation
- static/src/js/fl_timeline.js: deadline timeline widget with filter
  buttons and mark-complete AJAX
- __init__.py: import controllers package
- __manifest__.py: add Phase 6 portal view files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:42:37 -05:00

318 lines
16 KiB
Python

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/<int:case_id> — 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_id> — case detail
# ──────────────────────────────────────────────────────────────────────────
@http.route('/my/cases/<int:case_id>', 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',
})