diff --git a/activeblue_familylaw/__manifest__.py b/activeblue_familylaw/__manifest__.py
index f3acc14..4af9932 100644
--- a/activeblue_familylaw/__manifest__.py
+++ b/activeblue_familylaw/__manifest__.py
@@ -38,6 +38,7 @@
'data/fl_deadline_rules.xml',
'data/mail_templates.xml',
'data/fl_caselaw_data.xml',
+ 'data/case_task_templates.xml',
# Views — backend (actions before menus so menuitem refs resolve)
'views/fl_case_views.xml',
'views/fl_party_views.xml',
@@ -51,6 +52,7 @@
'views/fl_analysis_views.xml',
'views/fl_fee_waiver_views.xml',
'views/fl_statute_views.xml',
+ 'views/fl_wizard_views.xml',
'views/menu_views.xml',
# Phase 4 — QWeb PDF Reports
'report/report_financial_affidavit_short.xml',
diff --git a/activeblue_familylaw/data/case_task_templates.xml b/activeblue_familylaw/data/case_task_templates.xml
new file mode 100644
index 0000000..003d562
--- /dev/null
+++ b/activeblue_familylaw/data/case_task_templates.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+ activeblue_familylaw.task_templates_version
+ 7.0
+
+
+
+
+ activeblue_familylaw.parenting_class_reminder_days
+ 30
+
+
+
+
+ activeblue_familylaw.discovery_response_days
+ 30
+
+
+
+
+ activeblue_familylaw.mandatory_disclosure_days
+ 45
+
+
+
+
+ activeblue_familylaw.answer_deadline_days
+ 20
+
+
+
+
+ activeblue_familylaw.ai_reanalysis_lockout_hours
+ 24
+
+
+
+
+ activeblue_familylaw.ollama_url
+ http://192.168.2.10:11434/api/generate
+
+
+
+
+ activeblue_familylaw.ollama_model
+ llama3.1
+
+
+
+
diff --git a/activeblue_familylaw/models/fl_case.py b/activeblue_familylaw/models/fl_case.py
index 2e61292..611819b 100644
--- a/activeblue_familylaw/models/fl_case.py
+++ b/activeblue_familylaw/models/fl_case.py
@@ -808,6 +808,99 @@ class FlCase(models.Model):
# WORKFLOW METHODS
# ══════════════════════════════════════════════════════════════════════
+ # Standard task templates keyed by case_type.
+ # Each entry: (name, description, sequence)
+ _CASE_TASK_TEMPLATES = {
+ 'modification': [
+ ('Gather Financial Documents',
+ 'Collect last 3 years tax returns, pay stubs, bank statements '
+ '(FL 12.285 mandatory disclosure — 45 days from service)',
+ 10),
+ ('Complete Financial Affidavit',
+ 'Complete FL-12.902(b) Short Form or FL-12.902(c) Long Form '
+ 'financial affidavit (required for support proceedings)',
+ 20),
+ ('Calculate New Support Amount',
+ 'Run FL 61.30 child support calculation using updated income figures. '
+ 'Verify modification threshold: 15% AND $50 difference (FL 61.30(1)(b))',
+ 30),
+ ('File Supplemental Petition',
+ 'File FL-12.905 Supplemental Petition to Modify Child Support '
+ 'with the clerk and pay filing fee (or submit fee waiver)',
+ 40),
+ ('Serve Respondent',
+ 'Serve Respondent with Summons + Petition. '
+ 'Start 20-day answer deadline clock (FL 12.285, Rule 1.070)',
+ 50),
+ ('Track Answer Deadline',
+ 'Monitor 20-day answer deadline. If no response, prepare '
+ 'FL 12.922 default motion packet after deadline passes.',
+ 60),
+ ('Schedule / Attend Mediation',
+ 'Attend court-ordered mediation (required in most family cases). '
+ 'Confirm separate rooms if DV flag is set (FL 44.102)',
+ 70),
+ ('Attend Final Hearing',
+ 'Appear at final hearing with all documents. '
+ 'Bring original + 2 copies of all filed pleadings.',
+ 80),
+ ],
+ 'dissolution_children': [
+ ('Gather Financial Documents', 'FL 12.285 mandatory disclosure package', 10),
+ ('Complete Financial Affidavit (Long Form)',
+ 'FL-12.902(c) required when income > $50,000/year', 20),
+ ('Calculate Child Support', 'FL 61.30 worksheet (FL-12.902(e))', 30),
+ ('Draft Parenting Plan', 'FL-12.995(a) Parenting Plan — timesharing schedule', 40),
+ ('File Petition for Dissolution', 'FL-12.901(b)(1) with all required attachments', 50),
+ ('Serve Respondent', 'Serve Summons + Petition; track 20-day answer deadline', 60),
+ ('Attend Parenting Class',
+ 'FL 61.21 — both parties must complete before final hearing', 70),
+ ('Schedule Mediation', 'Court-ordered mediation (FL 44.102)', 80),
+ ('Attend Final Hearing', 'Bring parenting plan, support worksheet, all exhibits', 90),
+ ],
+ 'dissolution_no_children': [
+ ('Gather Financial Documents', 'FL 12.285 mandatory disclosure', 10),
+ ('Complete Financial Affidavit', 'FL-12.902(b) Short Form', 20),
+ ('Identify and Value Marital Assets', 'Real property, accounts, retirement funds', 30),
+ ('File Petition for Dissolution', 'FL-12.901(b)(2) — no minor children', 40),
+ ('Serve Respondent', 'Serve Summons + Petition; track 20-day answer deadline', 50),
+ ('Schedule Mediation', 'Property division mediation if contested', 60),
+ ('Attend Final Hearing', 'Bring financial affidavit, marital settlement agreement', 70),
+ ],
+ 'paternity': [
+ ('Gather Birth Records', 'Obtain certified birth certificate', 10),
+ ('Complete Financial Affidavit', 'FL-12.902(b) required for support', 20),
+ ('Calculate Child Support', 'FL 61.30 worksheet', 30),
+ ('File Petition to Determine Paternity', 'FL-12.983(a)', 40),
+ ('Serve Respondent', 'Summons + Petition; 20-day deadline', 50),
+ ('Draft Parenting Plan', 'FL-12.995(a) if timesharing is requested', 60),
+ ('Attend Parenting Class', 'FL 61.21 — required before final hearing', 70),
+ ('Attend Final Hearing', 'Bring all documents and parenting plan', 80),
+ ],
+ 'alimony_modification': [
+ ('Gather Financial Documents', 'Tax returns, pay stubs — show substantial change', 10),
+ ('Complete Financial Affidavit (Long Form)', 'FL-12.902(c) required', 20),
+ ('Document Change in Circumstances',
+ 'Document the substantial change in circumstances required for modification', 30),
+ ('File Supplemental Petition', 'FL-12.905 Supplemental Petition to Modify Alimony', 40),
+ ('Serve Respondent', 'Summons + Petition; track 20-day answer deadline', 50),
+ ('Schedule Mediation', 'FL 44.102 mediation', 60),
+ ('Attend Final Hearing', 'Bring all financial documents and exhibits', 70),
+ ],
+ 'custody_modification': [
+ ('Document Changed Circumstances',
+ 'Substantial change must affect child welfare — document thoroughly', 10),
+ ('Gather Supporting Evidence', 'School records, medical records, witness statements', 20),
+ ('Complete Financial Affidavit', 'FL-12.902(b) if support change is involved', 30),
+ ('Draft Proposed Parenting Plan', 'FL-12.995(a) — proposed new timesharing', 40),
+ ('File Supplemental Petition', 'FL-12.905 Supplemental Petition to Modify Custody', 50),
+ ('Serve Respondent', 'Summons + Petition; track 20-day answer deadline', 60),
+ ('Attend Parenting Class', 'FL 61.21 if not already completed', 70),
+ ('Schedule Mediation', 'Court-ordered mediation (FL 44.102)', 80),
+ ('Attend Final Hearing', 'Bring parenting plan, evidence, exhibits', 90),
+ ],
+ }
+
def _create_case_project(self):
"""Create a linked Odoo project for case task management."""
project = self.env['project.project'].create({
@@ -819,6 +912,26 @@ class FlCase(models.Model):
),
})
self.project_id = project
+ self._generate_case_tasks()
+
+ def _generate_case_tasks(self):
+ """
+ Create standard project tasks for this case based on its case_type.
+ Templates are defined in _CASE_TASK_TEMPLATES above.
+ """
+ if not self.project_id:
+ return
+ templates = self._CASE_TASK_TEMPLATES.get(self.case_type, [])
+ task_vals_list = []
+ for name, description, sequence in templates:
+ task_vals_list.append({
+ 'name': name,
+ 'description': description,
+ 'project_id': self.project_id.id,
+ 'sequence': sequence,
+ })
+ if task_vals_list:
+ self.env['project.task'].create(task_vals_list)
def _check_fee_waiver_eligibility(self):
"""Post a chatter note if petitioner appears to qualify for fee waiver."""
diff --git a/activeblue_familylaw/views/fl_wizard_views.xml b/activeblue_familylaw/views/fl_wizard_views.xml
new file mode 100644
index 0000000..d2565b7
--- /dev/null
+++ b/activeblue_familylaw/views/fl_wizard_views.xml
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+ fl.intake.wizard.form
+ fl.intake.wizard
+
+
+
+
+
+
+ New Case Intake
+ fl.intake.wizard
+ form
+ new
+
+
+
+
+
+ fl.generate.packet.wizard.form
+ fl.generate.packet.wizard
+
+
+
+
+
+
+ Generate Filing Packet
+ fl.generate.packet.wizard
+ form
+ new
+
+ form
+ {'active_id': active_id}
+
+
+
+
+
+ fl.analysis.wizard.form
+ fl.analysis.wizard
+
+
+
+
+
+
+ Run AI Analysis
+ fl.analysis.wizard
+ form
+ new
+
+ form
+
+
+
+
diff --git a/activeblue_familylaw/wizard/fl_analysis_wizard.py b/activeblue_familylaw/wizard/fl_analysis_wizard.py
index b02d53c..a439ac7 100644
--- a/activeblue_familylaw/wizard/fl_analysis_wizard.py
+++ b/activeblue_familylaw/wizard/fl_analysis_wizard.py
@@ -1,30 +1,135 @@
-from odoo import fields, models
+import logging
+from datetime import datetime, timedelta
+
+from odoo import api, fields, models, _
+
+_logger = logging.getLogger(__name__)
class FlAnalysisWizard(models.TransientModel):
"""
- Trigger AI analysis on a case.
- Phase 7 — full implementation.
- Phase 1: Stub.
+ Phase 7 — Full AI analysis trigger wizard.
+
+ Checks for a recent analysis (<24 h) before running a new one unless
+ force_reanalysis is True. Opens the resulting fl.analysis form on success.
"""
_name = 'fl.analysis.wizard'
- _description = 'Trigger AI Analysis Wizard'
+ _description = 'Trigger AI Case Analysis'
case_id = fields.Many2one(
- 'fl.case', string='Case', required=True
+ 'fl.case', string='Case', required=True,
+ default=lambda self: self.env.context.get('active_id'),
)
force_reanalysis = fields.Boolean(
string='Force Re-analysis',
- help='Run a new analysis even if a recent one exists'
+ help='Run a new analysis even if one was completed in the last 24 hours'
)
+ # Computed info fields shown in the form
+ recent_analysis_id = fields.Many2one(
+ 'fl.analysis', string='Most Recent Analysis',
+ compute='_compute_recent_analysis',
+ )
+ recent_analysis_age_label = fields.Char(
+ string='Last Analysis', compute='_compute_recent_analysis'
+ )
+ has_recent = fields.Boolean(compute='_compute_recent_analysis')
+
+ @api.depends('case_id')
+ def _compute_recent_analysis(self):
+ for rec in self:
+ if not rec.case_id:
+ rec.recent_analysis_id = False
+ rec.recent_analysis_age_label = ''
+ rec.has_recent = False
+ continue
+
+ latest = self.env['fl.analysis'].search(
+ [('case_id', '=', rec.case_id.id), ('state', '=', 'complete')],
+ order='create_date desc',
+ limit=1,
+ )
+ rec.recent_analysis_id = latest
+ if latest:
+ age = datetime.now() - latest.create_date
+ hours = int(age.total_seconds() // 3600)
+ minutes = int((age.total_seconds() % 3600) // 60)
+ if hours == 0:
+ rec.recent_analysis_age_label = f'{minutes} minute(s) ago'
+ elif hours < 24:
+ rec.recent_analysis_age_label = f'{hours} hour(s) ago'
+ else:
+ days = age.days
+ rec.recent_analysis_age_label = f'{days} day(s) ago'
+ rec.has_recent = age < timedelta(hours=24)
+ else:
+ rec.recent_analysis_age_label = 'No analysis yet'
+ rec.has_recent = False
+
def action_run_analysis(self):
- self.env['fl.ai.engine'].analyze_case(self.case_id.id)
+ """
+ Run the AI analysis pipeline via fl.ai.engine.
+ If a recent analysis exists and force_reanalysis is False,
+ just open the existing one.
+ """
+ self.ensure_one()
+
+ if self.has_recent and not self.force_reanalysis:
+ # Surface the existing analysis instead
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('AI Analysis'),
+ 'res_model': 'fl.analysis',
+ 'res_id': self.recent_analysis_id.id,
+ 'view_mode': 'form',
+ 'target': 'new',
+ }
+
+ # Previous analyses remain in history — new one will appear first
+ # (fl.analysis is ordered by create_date desc)
+
+ # Post start note
+ self.case_id.message_post(
+ body='🤖 AI analysis started by %s…' % self.env.user.name,
+ subtype_xmlid='mail.mt_note',
+ )
+
+ try:
+ analysis = self.env['fl.ai.engine'].analyze_case(self.case_id.id)
+ except Exception as e:
+ _logger.error('Analysis wizard: engine error for case %s: %s', self.case_id.id, e)
+ self.case_id.message_post(
+ body=f'❌ AI analysis failed: {e}',
+ subtype_xmlid='mail.mt_note',
+ )
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('Case'),
+ 'res_model': 'fl.case',
+ 'res_id': self.case_id.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
+
return {
'type': 'ir.actions.act_window',
- 'name': 'Case',
- 'res_model': 'fl.case',
- 'res_id': self.case_id.id,
+ 'name': _('AI Analysis Result'),
+ 'res_model': 'fl.analysis',
+ 'res_id': analysis.id,
'view_mode': 'form',
- 'target': 'current',
+ 'target': 'new',
+ }
+
+ def action_view_recent(self):
+ """Open the most recent analysis form view."""
+ self.ensure_one()
+ if not self.recent_analysis_id:
+ return False
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('AI Analysis'),
+ 'res_model': 'fl.analysis',
+ 'res_id': self.recent_analysis_id.id,
+ 'view_mode': 'form',
+ 'target': 'new',
}
diff --git a/activeblue_familylaw/wizard/fl_generate_packet_wizard.py b/activeblue_familylaw/wizard/fl_generate_packet_wizard.py
index eb3830c..90eefb8 100644
--- a/activeblue_familylaw/wizard/fl_generate_packet_wizard.py
+++ b/activeblue_familylaw/wizard/fl_generate_packet_wizard.py
@@ -1,20 +1,88 @@
-from odoo import fields, models
+import base64
+import logging
+
+from odoo import fields, models, _
+
+_logger = logging.getLogger(__name__)
+
+
+# Report XML IDs and their binding models
+# (xml_id, binding_model, label, needs_sub_record)
+# needs_sub_record: which related record to use as the report target
+_REPORT_DEFS = {
+ # fl.case bound
+ 'mandatory_disclosure': (
+ 'activeblue_familylaw.action_report_mandatory_disclosure',
+ 'fl.case', 'Certificate of Mandatory Disclosure (FL-12.932)', None
+ ),
+ 'motion_to_modify': (
+ 'activeblue_familylaw.action_report_motion_to_modify',
+ 'fl.case', 'Motion to Modify Child Support', None
+ ),
+ 'default_motion': (
+ 'activeblue_familylaw.action_report_default_motion',
+ 'fl.case', 'Default Judgment Packet (FL 12.922)', None
+ ),
+ 'parenting_plan': (
+ 'activeblue_familylaw.action_report_parenting_plan',
+ 'fl.case', 'Parenting Plan (FL-12.995(a))', None
+ ),
+ # fl.party bound — petitioner
+ 'financial_affidavit_short': (
+ 'activeblue_familylaw.action_report_financial_affidavit_short',
+ 'fl.party', 'Financial Affidavit — Short Form (FL-12.902(b))', 'petitioner'
+ ),
+ 'financial_affidavit_long': (
+ 'activeblue_familylaw.action_report_financial_affidavit_long',
+ 'fl.party', 'Financial Affidavit — Long Form (FL-12.902(c))', 'petitioner'
+ ),
+ 'notice_ssn': (
+ 'activeblue_familylaw.action_report_notice_ssn',
+ 'fl.party', 'Notice of Social Security Number (FL-12.930(a))', 'petitioner'
+ ),
+ # fl.support.calculation bound
+ 'support_worksheet': (
+ 'activeblue_familylaw.action_report_child_support_worksheet',
+ 'fl.support.calculation', 'Child Support Worksheet (FL-12.902(e))', 'support_calc'
+ ),
+ # fl.fee.waiver bound
+ 'fee_waiver': (
+ 'activeblue_familylaw.action_report_fee_waiver',
+ 'fl.fee.waiver', 'Fee Waiver Application (FL 57.082)', 'fee_waiver'
+ ),
+ # fl.income.withholding bound
+ 'income_withholding': (
+ 'activeblue_familylaw.action_report_income_withholding',
+ 'fl.income.withholding', 'Income Withholding Order (FL 61.1301)', 'income_withholding'
+ ),
+}
class FlGeneratePacketWizard(models.TransientModel):
"""
- Generate full filing packet for a case.
- Phase 7 — full implementation with batch PDF generation.
- Phase 1: Stub.
+ Phase 7 — Generate a filing packet of selected PDF reports for a case.
+
+ Renders each selected report, creates ir.attachment records on the case,
+ posts a chatter summary, and returns to the case form.
"""
_name = 'fl.generate.packet.wizard'
_description = 'Generate Filing Packet Wizard'
case_id = fields.Many2one(
- 'fl.case', string='Case', required=True
+ 'fl.case', string='Case', required=True,
+ default=lambda self: self.env.context.get('active_id'),
)
- include_financial_affidavit = fields.Boolean(
- string='Financial Affidavit', default=True
+
+ # ── Document selection checkboxes ────────────────────────────────────────
+
+ include_mandatory_disclosure = fields.Boolean(
+ string='Certificate of Mandatory Disclosure (FL-12.932)', default=True
+ )
+ include_financial_affidavit_short = fields.Boolean(
+ string='Financial Affidavit — Short Form (FL-12.902(b))', default=False
+ )
+ include_financial_affidavit_long = fields.Boolean(
+ string='Financial Affidavit — Long Form (FL-12.902(c))', default=True
)
include_support_worksheet = fields.Boolean(
string='Child Support Worksheet (FL-12.902(e))', default=True
@@ -25,32 +93,162 @@ class FlGeneratePacketWizard(models.TransientModel):
include_notice_ssn = fields.Boolean(
string='Notice of Social Security Number (FL-12.930(a))', default=True
)
- include_mandatory_disclosure = fields.Boolean(
- string='Certificate of Mandatory Disclosure (FL-12.932)', default=True
- )
include_fee_waiver = fields.Boolean(
- string='Fee Waiver Application (if eligible)', default=False
+ string='Fee Waiver Application (FL 57.082)', default=False
)
- language = fields.Selection([
- ('en', 'English'),
- ('es', 'Spanish / Español'),
- ('both', 'Bilingual'),
- ], string='Document Language', default='en')
+ include_income_withholding = fields.Boolean(
+ string='Income Withholding Order (FL 61.1301)', default=False
+ )
+ include_default_motion = fields.Boolean(
+ string='Default Judgment Packet (FL 12.922)', default=False
+ )
+ include_parenting_plan = fields.Boolean(
+ string='Parenting Plan (FL-12.995(a))', default=False
+ )
+
+ # ── Options ──────────────────────────────────────────────────────────────
+
+ attach_to_case = fields.Boolean(
+ string='Attach generated PDFs to case chatter',
+ default=True
+ )
+
+ # ── Helpers ──────────────────────────────────────────────────────────────
+
+ def _get_selected_keys(self):
+ """Return list of _REPORT_DEFS keys that the user has selected."""
+ mapping = {
+ 'mandatory_disclosure': self.include_mandatory_disclosure,
+ 'financial_affidavit_short': self.include_financial_affidavit_short,
+ 'financial_affidavit_long': self.include_financial_affidavit_long,
+ 'support_worksheet': self.include_support_worksheet,
+ 'motion_to_modify': self.include_motion_to_modify,
+ 'notice_ssn': self.include_notice_ssn,
+ 'fee_waiver': self.include_fee_waiver,
+ 'income_withholding': self.include_income_withholding,
+ 'default_motion': self.include_default_motion,
+ 'parenting_plan': self.include_parenting_plan,
+ }
+ return [k for k, selected in mapping.items() if selected]
+
+ def _resolve_record_id(self, key, case):
+ """
+ Return the (record_id, model) tuple to pass to _render_qweb_pdf.
+ Returns (None, None) if the required sub-record doesn't exist.
+ """
+ xml_id, model, label, sub = _REPORT_DEFS[key]
+ if sub is None:
+ return case.id, model
+ if sub == 'petitioner':
+ if not case.petitioner_id:
+ _logger.warning('Packet: no petitioner_id on case %s', case.id)
+ return None, None
+ return case.petitioner_id.id, model
+ if sub == 'support_calc':
+ calc = case.support_calc_ids[:1] if case.support_calc_ids else None
+ if not calc:
+ _logger.warning('Packet: no support calculation on case %s', case.id)
+ return None, None
+ return calc.id, model
+ if sub == 'fee_waiver':
+ fwv = case.fee_waiver_id
+ if not fwv:
+ _logger.warning('Packet: no fee_waiver_id on case %s', case.id)
+ return None, None
+ return fwv.id, model
+ if sub == 'income_withholding':
+ iwo = self.env['fl.income.withholding'].search(
+ [('case_id', '=', case.id)], limit=1
+ )
+ if not iwo:
+ _logger.warning('Packet: no income withholding on case %s', case.id)
+ return None, None
+ return iwo.id, model
+ return None, None
+
+ # ── Main action ──────────────────────────────────────────────────────────
def action_generate(self):
"""
- Phase 7 — generate all selected documents as PDFs.
- Phase 1: Stub — posts a note and returns to case.
+ Render each selected report PDF, create attachments on the case,
+ post a chatter summary, and return to the case form.
"""
- self.case_id.message_post(
- body='📄 Filing packet generation will be available in Phase 4 (Document Templates).',
- subtype_xmlid='mail.mt_note',
- )
+ self.ensure_one()
+ case = self.case_id
+ selected_keys = self._get_selected_keys()
+
+ if not selected_keys:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Nothing selected'),
+ 'message': _('Please select at least one document to generate.'),
+ 'type': 'warning',
+ 'sticky': False,
+ },
+ }
+
+ generated = []
+ failed = []
+ attachment_ids = []
+
+ for key in selected_keys:
+ xml_id, model, label, sub = _REPORT_DEFS[key]
+ record_id, binding_model = self._resolve_record_id(key, case)
+ if record_id is None:
+ failed.append(f'{label} — required record not found on case')
+ continue
+
+ try:
+ report_action = self.env.ref(xml_id)
+ pdf_content, _mime = report_action._render_qweb_pdf([record_id])
+ safe_name = label.replace('/', '-').replace(':', '').strip()
+ filename = f'{case.name} — {safe_name}.pdf'
+
+ attachment = self.env['ir.attachment'].create({
+ 'name': filename,
+ 'type': 'binary',
+ 'datas': base64.b64encode(pdf_content),
+ 'res_model': 'fl.case',
+ 'res_id': case.id,
+ 'mimetype': 'application/pdf',
+ })
+ attachment_ids.append(attachment.id)
+ generated.append(label)
+ _logger.info('Packet: generated %s for case %s', label, case.id)
+
+ except Exception as e:
+ _logger.error(
+ 'Packet: failed to generate %s for case %s: %s', label, case.id, e
+ )
+ failed.append(f'{label} — {e}')
+
+ # Post chatter summary
+ if self.attach_to_case and (generated or failed):
+ lines = []
+ if generated:
+ lines.append('✅ Documents generated:
')
+ for doc in generated:
+ lines.append(f'- {doc}
')
+ lines.append('
')
+ if failed:
+ lines.append('⚠ Failed:')
+ for doc in failed:
+ lines.append(f'- {doc}
')
+ lines.append('
')
+
+ case.message_post(
+ body=''.join(lines),
+ attachment_ids=attachment_ids,
+ subtype_xmlid='mail.mt_note',
+ )
+
return {
'type': 'ir.actions.act_window',
- 'name': 'Case',
+ 'name': _('Case'),
'res_model': 'fl.case',
- 'res_id': self.case_id.id,
+ 'res_id': case.id,
'view_mode': 'form',
'target': 'current',
}
diff --git a/activeblue_familylaw/wizard/fl_intake_wizard.py b/activeblue_familylaw/wizard/fl_intake_wizard.py
index 04ce913..30d9e4f 100644
--- a/activeblue_familylaw/wizard/fl_intake_wizard.py
+++ b/activeblue_familylaw/wizard/fl_intake_wizard.py
@@ -1,15 +1,24 @@
-from odoo import api, fields, models
+import logging
+from datetime import date
+
+from odoo import api, fields, models, _
+
+_logger = logging.getLogger(__name__)
class FlIntakeWizard(models.TransientModel):
"""
- Guided case creation wizard.
- Phase 7 — full multi-step intake form.
- Phase 1: Stub with basic fields.
+ Phase 7 — Full guided case creation wizard for paralegals/attorneys.
+
+ Creates fl.party records (petitioner + optional respondent),
+ fl.case record with all intake fields, triggers fee waiver check
+ and AI analysis, then opens the new case form.
"""
_name = 'fl.intake.wizard'
_description = 'Family Law Case Intake Wizard'
+ # ── Step 1: Case identity ────────────────────────────────────────────────
+
case_type = fields.Selection([
('modification', 'Child Support Modification'),
('dissolution_children', 'Dissolution of Marriage — With Children'),
@@ -17,38 +26,239 @@ class FlIntakeWizard(models.TransientModel):
('paternity', 'Paternity'),
('alimony_modification', 'Alimony Modification'),
('custody_modification', 'Timesharing / Custody Modification'),
- ], string='Case Type', required=True)
+ ], string='Case Type', required=True, default='modification')
- petitioner_id = fields.Many2one(
- 'res.partner', string='Petitioner', required=True
+ court_case_number = fields.Char(
+ string='Court Case Number',
+ help='Leave blank if not yet assigned'
)
- respondent_id = fields.Many2one(
- 'res.partner', string='Respondent'
+
+ # ── Step 2: Parties ──────────────────────────────────────────────────────
+
+ # Petitioner
+ petitioner_name = fields.Char(string='Petitioner Full Name', required=True)
+ petitioner_email = fields.Char(string='Petitioner Email')
+ petitioner_phone = fields.Char(string='Petitioner Phone')
+ petitioner_address = fields.Char(string='Petitioner Street Address')
+ petitioner_city = fields.Char(string='Petitioner City')
+ petitioner_fl_resident_since = fields.Date(
+ string='FL Resident Since',
+ help='FL 61.021: Petitioner must be a FL resident for 6 months before filing'
+ )
+
+ # Respondent
+ respondent_name = fields.Char(string='Respondent Full Name')
+ respondent_has_counsel = fields.Boolean(
+ string='Respondent Has Legal Counsel?',
+ help='Affects attorney referral recommendation'
+ )
+
+ # ── Step 3: Children & Safety ────────────────────────────────────────────
+
+ num_children = fields.Integer(
+ string='Number of Minor Children', default=1,
+ help='Children under 18 covered by this case'
)
domestic_violence_flag = fields.Boolean(
- string='Is there a history of domestic violence?',
- help='Your answer affects mediation and safety procedures'
+ string='History of Domestic Violence?',
+ help='FL 44.102: Requires separate mediation rooms. '
+ 'Attorney referral is strongly recommended.'
)
- has_minor_children = fields.Boolean(
- string='Are there minor children involved?'
- )
- petitioner_fl_resident_since = fields.Date(
- string='When did the petitioner become a FL resident?',
- help='FL 61.021: Must be 6 months before filing'
+ income_imputation_concern = fields.Boolean(
+ string='Income Imputation Concern?',
+ help='Is one party voluntarily unemployed or underemployed?'
)
+ # ── Step 4: Financial Information ────────────────────────────────────────
+
+ petitioner_monthly_gross = fields.Float(
+ string='Petitioner Monthly Gross Income', digits=(10, 2)
+ )
+ respondent_monthly_gross = fields.Float(
+ string='Respondent Monthly Gross Income', digits=(10, 2)
+ )
+ current_order_amount = fields.Float(
+ string='Current Support Order Amount ($/month)',
+ help='0 if no existing order',
+ digits=(10, 2)
+ )
+ household_size = fields.Integer(
+ string='Household Size (petitioner)', default=3,
+ help='Used for fee waiver eligibility (FL 57.082)'
+ )
+
+ # ── Step 5: Options ──────────────────────────────────────────────────────
+
+ fee_waiver_request = fields.Boolean(
+ string='Request Fee Waiver? (FL 57.082)',
+ help='System will check eligibility based on income'
+ )
+ run_ai_analysis = fields.Boolean(
+ string='Run AI Case Analysis?',
+ default=True,
+ help='Automatically analyze the case using Ollama AI engine'
+ )
+ notes = fields.Text(string='Additional Notes / Case Summary')
+
+ # ── Computed helpers ─────────────────────────────────────────────────────
+
+ residency_warning = fields.Char(
+ string='Residency Status',
+ compute='_compute_residency_warning'
+ )
+
+ @api.depends('petitioner_fl_resident_since')
+ def _compute_residency_warning(self):
+ for rec in self:
+ if rec.petitioner_fl_resident_since:
+ days = (date.today() - rec.petitioner_fl_resident_since).days
+ if days < 180:
+ remaining = 180 - days
+ rec.residency_warning = (
+ f'⚠ Only {days} days as FL resident — '
+ f'{remaining} more days needed (FL 61.021)'
+ )
+ else:
+ rec.residency_warning = f'✓ {days} days — Residency requirement met (FL 61.021)'
+ else:
+ rec.residency_warning = ''
+
+ # ── Action ───────────────────────────────────────────────────────────────
+
def action_create_case(self):
- """Create the fl.case record from intake data."""
- case = self.env['fl.case'].create({
+ """
+ Create partner → party → case chain from wizard data.
+ Mirrors the portal intake_submit logic but for authenticated users.
+ """
+ self.ensure_one()
+
+ petitioner_name = (self.petitioner_name or '').strip()
+ respondent_name = (self.respondent_name or '').strip()
+
+ # --- Petitioner partner ---
+ domain = []
+ if self.petitioner_email:
+ domain = [('email', '=', self.petitioner_email)]
+ else:
+ domain = [('name', '=', petitioner_name)]
+
+ petitioner_partner = self.env['res.partner'].search(domain, limit=1)
+ if not petitioner_partner:
+ vals = {
+ 'name': petitioner_name,
+ 'is_company': False,
+ }
+ if self.petitioner_email:
+ vals['email'] = self.petitioner_email
+ if self.petitioner_phone:
+ vals['phone'] = self.petitioner_phone
+ if self.petitioner_address:
+ vals['street'] = self.petitioner_address
+ if self.petitioner_city:
+ vals['city'] = self.petitioner_city
+ try:
+ vals['state_id'] = self.env.ref('base.state_us_10').id # Florida
+ vals['country_id'] = self.env.ref('base.us').id
+ except Exception:
+ pass
+ petitioner_partner = self.env['res.partner'].create(vals)
+
+ # --- Respondent partner ---
+ respondent_partner = False
+ if respondent_name:
+ respondent_partner = self.env['res.partner'].search(
+ [('name', '=', respondent_name)], limit=1
+ )
+ if not respondent_partner:
+ respondent_partner = self.env['res.partner'].create({
+ 'name': respondent_name,
+ 'is_company': False,
+ })
+
+ # --- Petitioner fl.party ---
+ petitioner_party = self.env['fl.party'].search(
+ [('partner_id', '=', petitioner_partner.id)], limit=1
+ )
+ if not petitioner_party:
+ party_vals = {
+ 'name': petitioner_name,
+ 'partner_id': petitioner_partner.id,
+ 'employment_status': 'employed',
+ 'monthly_gross_income': self.petitioner_monthly_gross,
+ }
+ if self.petitioner_email:
+ party_vals['email'] = self.petitioner_email
+ petitioner_party = self.env['fl.party'].create(party_vals)
+ elif self.petitioner_monthly_gross:
+ petitioner_party.write({'monthly_gross_income': self.petitioner_monthly_gross})
+
+ # --- Respondent fl.party ---
+ respondent_party = False
+ if respondent_partner:
+ respondent_party = self.env['fl.party'].search(
+ [('partner_id', '=', respondent_partner.id)], limit=1
+ )
+ if not respondent_party:
+ respondent_party = self.env['fl.party'].create({
+ 'name': respondent_name,
+ 'partner_id': respondent_partner.id,
+ 'monthly_gross_income': self.respondent_monthly_gross,
+ })
+ elif self.respondent_monthly_gross:
+ respondent_party.write({'monthly_gross_income': self.respondent_monthly_gross})
+
+ # --- Residency check ---
+ filing_date = date.today()
+ residency_ok = True
+ if self.petitioner_fl_resident_since:
+ days = (filing_date - self.petitioner_fl_resident_since).days
+ residency_ok = days >= 180
+
+ # --- Create fl.case ---
+ case_vals = {
'case_type': self.case_type,
- 'petitioner_id': self.petitioner_id.id,
- 'respondent_id': self.respondent_id.id if self.respondent_id else False,
+ 'petitioner_id': petitioner_party.id,
+ 'respondent_id': respondent_party.id if respondent_party else False,
+ 'court_case_number': self.court_case_number or False,
'domestic_violence_flag': self.domestic_violence_flag,
- 'petitioner_fl_resident_since': self.petitioner_fl_resident_since,
- })
+ 'respondent_has_counsel': self.respondent_has_counsel,
+ 'current_order_amount': self.current_order_amount,
+ 'filing_date': filing_date,
+ 'residency_requirement_met': residency_ok,
+ 'household_size': self.household_size,
+ }
+ if self.petitioner_fl_resident_since:
+ case_vals['petitioner_fl_resident_since'] = self.petitioner_fl_resident_since
+ if self.notes:
+ case_vals['description'] = self.notes
+
+ case = self.env['fl.case'].create(case_vals)
+
+ # --- Fee waiver check ---
+ if self.fee_waiver_request:
+ fwv = self.env['fl.fee.waiver'].search(
+ [('case_id', '=', case.id)], limit=1
+ )
+ if not fwv:
+ # Create a fee waiver record manually
+ self.env['fl.fee.waiver'].create({'case_id': case.id})
+
+ # --- AI analysis ---
+ if self.run_ai_analysis:
+ try:
+ self.env['fl.ai.engine'].analyze_case(case.id)
+ except Exception as e:
+ _logger.warning(
+ 'Intake wizard: AI analysis failed for case %s: %s', case.id, e
+ )
+ case.message_post(
+ body=f'⚠ AI analysis could not run automatically: {e}',
+ subtype_xmlid='mail.mt_note',
+ )
+
return {
'type': 'ir.actions.act_window',
- 'name': 'New Case',
+ 'name': _('New Case'),
'res_model': 'fl.case',
'res_id': case.id,
'view_mode': 'form',