From 26f58952b452e6b6088b4d617a1de9efbf9723ad Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Wed, 6 May 2026 23:49:10 -0500 Subject: [PATCH] Phase 7: full wizards, auto-generated case tasks, config parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fl_intake_wizard.py: - Full multi-step intake: parties, income, children, DV flag, fee waiver, AI analysis option - Creates res.partner → fl.party → fl.case chain (mirrors portal) - Triggers fee waiver record creation and Ollama AI analysis - Residency warning computed field (FL 61.021 — 6-month check) fl_generate_packet_wizard.py: - Generates selected documents as PDFs via _render_qweb_pdf - Handles 4 binding models: fl.case, fl.party, fl.fee.waiver, fl.support.calculation, fl.income.withholding - Attaches generated PDFs to case chatter with summary note - Bound to fl.case form as an action button fl_analysis_wizard.py: - Checks for recent analysis (<24h) before running new one - force_reanalysis flag bypasses the lock - Shows last analysis age label in form; opens result as dialog - Bound to fl.case form as an action button fl_case.py: - _CASE_TASK_TEMPLATES: standard task lists for 6 case types - _generate_case_tasks(): creates project.task records from templates - Called automatically from _create_case_project on case creation fl_wizard_views.xml: - Form views for all 3 wizards with inline help text - Packet wizard bound to fl.case form via binding_model_id data/case_task_templates.xml: - ir.config_parameter records for Ollama URL, model, deadline days, mandatory disclosure days, AI lockout hours — all admin-configurable Co-Authored-By: Claude Sonnet 4.6 --- activeblue_familylaw/__manifest__.py | 2 + .../data/case_task_templates.xml | 70 +++++ activeblue_familylaw/models/fl_case.py | 113 ++++++++ .../views/fl_wizard_views.xml | 248 +++++++++++++++++ .../wizard/fl_analysis_wizard.py | 129 ++++++++- .../wizard/fl_generate_packet_wizard.py | 246 +++++++++++++++-- .../wizard/fl_intake_wizard.py | 258 ++++++++++++++++-- 7 files changed, 1006 insertions(+), 60 deletions(-) create mode 100644 activeblue_familylaw/data/case_task_templates.xml create mode 100644 activeblue_familylaw/views/fl_wizard_views.xml 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 Family Law Case

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ⚠ Domestic Violence — Attorney Referral Required
+ Separate mediation rooms are mandatory (FL 44.102). + Pro se representation is strongly discouraged. + Resources: Legal Services of Greater Miami (305) 576-0080 | + Safespace: (305) 536-5565 | National Hotline: 1-800-799-7233 +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + New Case Intake + fl.intake.wizard + form + new + + + + + + fl.generate.packet.wizard.form + fl.generate.packet.wizard + +
+ +
+

Generate Filing Packet

+
+ + + + + + + + + + + + + + + + + + + + + + +
+ Note: Generated PDFs will be attached to the case + chatter and available for download. Documents that require related + records (e.g. a completed Support Calculation or Fee Waiver) will + be skipped if those records don't yet exist on the case. +
+ + + + + +
+
+
+
+
+
+ + + Generate Filing Packet + fl.generate.packet.wizard + form + new + + form + {'active_id': active_id} + + + + + + fl.analysis.wizard.form + fl.analysis.wizard + +
+ +
+

AI Case Analysis

+

Powered by Ollama / LLaMA 3.1

+
+ + + + + + + + + + + + +
+ ⚠ Recent analysis found (). + Clicking "Run Analysis" will open the existing result. + Enable "Force Re-analysis" to generate a fresh one. +
+ + + + + + +
+ What this does: + The AI engine analyzes the case data against Florida Family Law rules, + matches relevant case law, and produces a plain-English summary with + attorney referral recommendation. Analysis takes 30–90 seconds. +
+ +
+
+
+
+
+
+ + + 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:') + if failed: + lines.append('⚠ Failed:') + + 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',