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>
This commit is contained in:
1
activeblue_familylaw/controllers/__init__.py
Normal file
1
activeblue_familylaw/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
317
activeblue_familylaw/controllers/portal.py
Normal file
317
activeblue_familylaw/controllers/portal.py
Normal file
@@ -0,0 +1,317 @@
|
||||
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',
|
||||
})
|
||||
Reference in New Issue
Block a user