Files
famlaw/activeblue_familylaw/wizard/fl_generate_packet_wizard.py
Carlos Garcia 26f58952b4 Phase 7: full wizards, auto-generated case tasks, config parameters
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 <noreply@anthropic.com>
2026-05-06 23:49:10 -05:00

255 lines
10 KiB
Python

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):
"""
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,
default=lambda self: self.env.context.get('active_id'),
)
# ── 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
)
include_motion_to_modify = fields.Boolean(
string='Motion to Modify Child Support', default=True
)
include_notice_ssn = fields.Boolean(
string='Notice of Social Security Number (FL-12.930(a))', default=True
)
include_fee_waiver = fields.Boolean(
string='Fee Waiver Application (FL 57.082)', default=False
)
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):
"""
Render each selected report PDF, create attachments on the case,
post a chatter summary, and return to the case form.
"""
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('<b>✅ Documents generated:</b><ul>')
for doc in generated:
lines.append(f'<li>{doc}</li>')
lines.append('</ul>')
if failed:
lines.append('<b>⚠ Failed:</b><ul>')
for doc in failed:
lines.append(f'<li>{doc}</li>')
lines.append('</ul>')
case.message_post(
body=''.join(lines),
attachment_ids=attachment_ids,
subtype_xmlid='mail.mt_note',
)
return {
'type': 'ir.actions.act_window',
'name': _('Case'),
'res_model': 'fl.case',
'res_id': case.id,
'view_mode': 'form',
'target': 'current',
}