Phase 2: Deadlines + Calendar integration

Full fl_deadline.py:
- Calendar event creation/update on every deadline (all-day events)
- _cron_check_default_judgment: FL 12.922 — if respondent misses Day 20
  answer deadline, auto-creates Motion for Default deadline (5 days),
  project task, and urgent chatter alert
- _cron_deadline_alerts: 7/3/1-day and overdue chatter alerts
- Complete service-anchored deadline set: response (Day 20), financial
  disclosure + mandatory disclosure cert (Day 45), discovery opens (Day 20),
  respondent parenting class (Day 60), 120-day service max
- context-flag pattern (_no_calendar_sync) to prevent recursive write loops

Full fl_hearing.py:
- Calendar event sync (show_as=busy, confidential)
- Pre-hearing checklist computed fields: parenting class (FL 61.21),
  discovery cutoff (hearing -30 days), financial disclosure status
- Workflow buttons: Mark Completed, Mark Continued, Cancel
- _create_hearing_deadline: creates fl.deadline record for each hearing

New data/fl_deadline_rules.xml:
- ir.cron: Daily Deadline Alerts (fl_deadline._cron_deadline_alerts)
- ir.cron: Default Judgment Check (fl_deadline._cron_check_default_judgment)
- ir.cron: Emancipation Alerts (fl_child._cron_emancipation_alerts)

New data/mail_templates.xml:
- Deadline Alert Upcoming (EN)
- Deadline Alert OVERDUE (EN)
- Portal Welcome (EN + ES bilingual)
- Default Judgment Window Alert (EN)

Enhanced views: deadline + hearing now have calendar view, search view
with filters (overdue, due this week, by type), and group-by options.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Garcia
2026-05-04 23:16:24 -04:00
parent 1d52d85a78
commit 7c865c8c22
7 changed files with 1272 additions and 86 deletions

View File

@@ -35,6 +35,8 @@
'data/fl_statute_data.xml',
'data/fl_support_schedule.xml',
'data/ir_sequence.xml',
'data/fl_deadline_rules.xml',
'data/mail_templates.xml',
# Views — backend (actions before menus so menuitem refs resolve)
'views/fl_case_views.xml',
'views/fl_party_views.xml',

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ══════════════════════════════════════════════════════
SCHEDULED ACTIONS (ir.cron)
══════════════════════════════════════════════════════ -->
<!-- Daily: Send deadline alerts at 7, 3, 1 days and on overdue -->
<record id="cron_fl_deadline_alerts" model="ir.cron">
<field name="name">FL Family Law: Daily Deadline Alerts</field>
<field name="model_id" ref="model_fl_deadline"/>
<field name="state">code</field>
<field name="code">model._cron_deadline_alerts()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
<field name="user_id" ref="base.user_root"/>
</record>
<!-- Daily: Check for default judgment triggers (FL 12.922) -->
<record id="cron_fl_default_judgment" model="ir.cron">
<field name="name">FL Family Law: Check Default Judgment Triggers (FL 12.922)</field>
<field name="model_id" ref="model_fl_deadline"/>
<field name="state">code</field>
<field name="code">model._cron_check_default_judgment()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
<field name="user_id" ref="base.user_root"/>
</record>
<!-- Daily: Check child emancipation approaching (90-day window) -->
<record id="cron_fl_emancipation_alerts" model="ir.cron">
<field name="name">FL Family Law: Child Emancipation Approaching Alerts</field>
<field name="model_id" ref="model_fl_child"/>
<field name="state">code</field>
<field name="code">model._cron_emancipation_alerts()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
<field name="user_id" ref="base.user_root"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ══════════════════════════════════════════════════════
DEADLINE ALERT — UPCOMING (English)
══════════════════════════════════════════════════════ -->
<record id="mail_template_deadline_upcoming" model="mail.template">
<field name="name">FL: Deadline Alert — Upcoming</field>
<field name="model_id" ref="model_fl_deadline"/>
<field name="subject">⏰ Deadline Alert: ${object.name} — Due ${object.due_date}</field>
<field name="body_html"><![CDATA[
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #003366; color: #fff; padding: 16px; border-radius: 6px 6px 0 0;">
<h2 style="margin: 0;">⏰ Upcoming Deadline Alert</h2>
<p style="margin: 4px 0 0 0; font-size: 0.9em;">ActiveBlue Family Law Case Management</p>
</div>
<div style="padding: 20px; background: #f8f9fa; border: 1px solid #dee2e6;">
<p>You have an upcoming deadline on your family law case.</p>
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<tr>
<td style="padding: 8px; font-weight: bold; width: 40%;">Case:</td>
<td style="padding: 8px;">${object.case_id.name}</td>
</tr>
<tr style="background: #fff;">
<td style="padding: 8px; font-weight: bold;">Deadline:</td>
<td style="padding: 8px;">${object.name}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Due Date:</td>
<td style="padding: 8px; color: #dc3545; font-weight: bold;">${object.due_date}</td>
</tr>
<tr style="background: #fff;">
<td style="padding: 8px; font-weight: bold;">Days Remaining:</td>
<td style="padding: 8px;">${object.days_until_due} days</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Statute:</td>
<td style="padding: 8px;">${object.statute_reference or 'N/A'}</td>
</tr>
</table>
% if object.notes:
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 12px; border-radius: 4px; margin-top: 12px;">
<strong>Notes:</strong> ${object.notes}
</div>
% endif
<div style="background: #d4edda; border: 1px solid #28a745; padding: 12px; border-radius: 4px; margin-top: 16px;">
<strong>Action Required:</strong> Please complete this deadline by the due date.
Missed deadlines in family law cases can result in default judgments
or dismissal of your case.
</div>
</div>
<div style="padding: 12px; background: #e9ecef; border-radius: 0 0 6px 6px; font-size: 0.8em; color: #6c757d;">
<strong>DISCLAIMER:</strong> This is an automated reminder from ActiveBlue Family Law
case management software. This is NOT legal advice. If you have questions about
this deadline, consult a licensed Florida family law attorney.<br/><br/>
Legal Services of Greater Miami: (305) 576-0080 |
Florida Courts Self-Help: <a href="https://www.flcourts.gov">flcourts.gov</a>
</div>
</div>
]]></field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ══════════════════════════════════════════════════════
DEADLINE ALERT — OVERDUE (English)
══════════════════════════════════════════════════════ -->
<record id="mail_template_deadline_overdue" model="mail.template">
<field name="name">FL: Deadline Alert — OVERDUE</field>
<field name="model_id" ref="model_fl_deadline"/>
<field name="subject">🔴 OVERDUE DEADLINE: ${object.name} — Case ${object.case_id.name}</field>
<field name="body_html"><![CDATA[
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #dc3545; color: #fff; padding: 16px; border-radius: 6px 6px 0 0;">
<h2 style="margin: 0;">🔴 OVERDUE DEADLINE — URGENT ACTION REQUIRED</h2>
<p style="margin: 4px 0 0 0; font-size: 0.9em;">ActiveBlue Family Law Case Management</p>
</div>
<div style="padding: 20px; background: #f8d7da; border: 1px solid #f5c6cb;">
<p style="font-size: 1.1em; font-weight: bold; color: #721c24;">
A deadline on your family law case is PAST DUE. Immediate action may be required.
</p>
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<tr>
<td style="padding: 8px; font-weight: bold; width: 40%;">Case:</td>
<td style="padding: 8px;">${object.case_id.name}</td>
</tr>
<tr style="background: #fff;">
<td style="padding: 8px; font-weight: bold;">Deadline:</td>
<td style="padding: 8px; color: #dc3545; font-weight: bold;">${object.name}</td>
</tr>
<tr>
<td style="padding: 8px; font-weight: bold;">Was Due:</td>
<td style="padding: 8px; color: #dc3545; font-weight: bold;">${object.due_date}</td>
</tr>
<tr style="background: #fff;">
<td style="padding: 8px; font-weight: bold;">Statute:</td>
<td style="padding: 8px;">${object.statute_reference or 'N/A'}</td>
</tr>
</table>
<div style="background: #f8d7da; border: 2px solid #dc3545; padding: 16px; border-radius: 4px; margin-top: 12px;">
<strong>⚠️ WARNING:</strong> Missing court deadlines can result in:
<ul>
<li>Default judgment entered against you</li>
<li>Dismissal of your case</li>
<li>Contempt of court findings</li>
<li>Loss of your legal rights</li>
</ul>
Contact the court clerk immediately to understand your options.
</div>
</div>
<div style="padding: 12px; background: #e9ecef; border-radius: 0 0 6px 6px; font-size: 0.8em; color: #6c757d;">
<strong>DISCLAIMER:</strong> This is an automated alert. This is NOT legal advice.
Consult a licensed Florida family law attorney immediately regarding missed deadlines.<br/><br/>
<strong>Emergency Legal Help:</strong><br/>
Legal Services of Greater Miami: <strong>(305) 576-0080</strong><br/>
Florida Courts Self-Help: <a href="https://www.flcourts.gov">flcourts.gov</a><br/>
Miami-Dade Law Library: <a href="https://www.law.miami.edu/library">law.miami.edu/library</a>
</div>
</div>
]]></field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ══════════════════════════════════════════════════════
PORTAL WELCOME EMAIL (English)
══════════════════════════════════════════════════════ -->
<record id="mail_template_portal_welcome" model="mail.template">
<field name="name">FL: Portal Welcome — New Case (EN)</field>
<field name="model_id" ref="model_fl_case"/>
<field name="subject">Welcome to Your Family Law Case Portal — Case ${object.name}</field>
<field name="body_html"><![CDATA[
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #003366; color: #fff; padding: 20px; border-radius: 6px 6px 0 0;">
<h2 style="margin: 0;">Welcome to Your Family Law Case Portal</h2>
<p style="margin: 8px 0 0 0;">ActiveBlue Family Law — Miami-Dade 11th Circuit</p>
</div>
<div style="padding: 24px; background: #fff; border: 1px solid #dee2e6;">
<p>Dear ${object.petitioner_id.name},</p>
<p>
Your family law case has been created in our system. You can now
track your case, deadlines, and documents through the portal.
</p>
<div style="background: #d4edda; border: 1px solid #28a745; padding: 16px; border-radius: 6px; margin: 16px 0;">
<strong>Your Case Reference:</strong>
<span style="font-size: 1.3em; font-weight: bold; color: #003366; display: block; margin-top: 4px;">
${object.name}
</span>
</div>
<table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
<tr>
<td style="padding: 8px; font-weight: bold; width: 40%; background: #f8f9fa;">Case Type:</td>
<td style="padding: 8px;">${object.case_type}</td>
</tr>
% if object.filing_date:
<tr>
<td style="padding: 8px; font-weight: bold; background: #f8f9fa;">Filing Date:</td>
<td style="padding: 8px;">${object.filing_date}</td>
</tr>
% endif
</table>
<h3>What You Can Do in the Portal:</h3>
<ul>
<li>📋 View your case status and upcoming deadlines</li>
<li>📄 Download and generate court forms</li>
<li>💰 Use the FL 61.30 child support calculator</li>
<li>📅 Track the visual timeline of your case</li>
<li>💬 Communicate through the secure case messaging system</li>
</ul>
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 16px; border-radius: 6px; margin: 16px 0;">
<strong>⚠️ Important Reminder:</strong>
This system provides information and tools to assist you, but
<strong>does not provide legal advice</strong>. Family law matters
are complex. We strongly recommend consulting with a licensed
Florida family law attorney.
</div>
<h3>Free Legal Resources:</h3>
<ul>
<li>Legal Services of Greater Miami: <strong>(305) 576-0080</strong></li>
<li>Florida Courts Self-Help: <a href="https://www.flcourts.gov">flcourts.gov</a></li>
<li>Miami-Dade Bar Lawyer Referral: <strong>(305) 371-2444</strong></li>
<li>FL Courts e-Filing Portal: <a href="https://www.myflcourtaccess.com">myflcourtaccess.com</a></li>
</ul>
</div>
<div style="padding: 12px; background: #e9ecef; border-radius: 0 0 6px 6px; font-size: 0.8em; color: #6c757d;">
ActiveBlue Family Law Case Management — Miami-Dade County, Florida<br/>
<strong>DISCLAIMER:</strong> This software is not a substitute for legal advice.
Verify all information with the court before filing.
</div>
</div>
]]></field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ══════════════════════════════════════════════════════
PORTAL WELCOME EMAIL (Spanish / Español)
══════════════════════════════════════════════════════ -->
<record id="mail_template_portal_welcome_es" model="mail.template">
<field name="name">FL: Portal Welcome — New Case (ES)</field>
<field name="model_id" ref="model_fl_case"/>
<field name="subject">Bienvenido a su Portal de Caso de Derecho Familiar — Caso ${object.name}</field>
<field name="body_html"><![CDATA[
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #003366; color: #fff; padding: 20px; border-radius: 6px 6px 0 0;">
<h2 style="margin: 0;">Bienvenido a su Portal de Caso de Derecho Familiar</h2>
<p style="margin: 8px 0 0 0;">ActiveBlue Family Law — Circuito 11 de Miami-Dade</p>
</div>
<div style="padding: 24px; background: #fff; border: 1px solid #dee2e6;">
<p>Estimado/a ${object.petitioner_id.name},</p>
<p>
Su caso de derecho familiar ha sido creado en nuestro sistema. Ahora puede
seguir su caso, plazos y documentos a través del portal.
</p>
<div style="background: #d4edda; border: 1px solid #28a745; padding: 16px; border-radius: 6px; margin: 16px 0;">
<strong>Número de Referencia de su Caso:</strong>
<span style="font-size: 1.3em; font-weight: bold; color: #003366; display: block; margin-top: 4px;">
${object.name}
</span>
</div>
<h3>¿Qué puede hacer en el Portal?</h3>
<ul>
<li>📋 Ver el estado de su caso y los próximos plazos</li>
<li>📄 Descargar y generar formularios del tribunal</li>
<li>💰 Usar la calculadora de manutención de hijos FL 61.30</li>
<li>📅 Ver el cronograma visual de su caso</li>
<li>💬 Comunicarse a través del sistema de mensajería segura</li>
</ul>
<div style="background: #fff3cd; border: 1px solid #ffc107; padding: 16px; border-radius: 6px; margin: 16px 0;">
<strong>⚠️ Aviso Importante:</strong>
Este sistema proporciona información y herramientas para ayudarle, pero
<strong>no proporciona asesoramiento legal</strong>. Los asuntos de derecho familiar
son complejos. Recomendamos consultar con un abogado certificado de Florida.
</div>
<h3>Recursos Legales Gratuitos:</h3>
<ul>
<li>Servicios Legales del Gran Miami: <strong>(305) 576-0080</strong></li>
<li>Autoayuda de los Tribunales de Florida: <a href="https://www.flcourts.gov">flcourts.gov</a></li>
<li>Colegio de Abogados de Miami-Dade — Referidos: <strong>(305) 371-2444</strong></li>
</ul>
</div>
<div style="padding: 12px; background: #e9ecef; border-radius: 0 0 6px 6px; font-size: 0.8em; color: #6c757d;">
ActiveBlue Family Law — Condado de Miami-Dade, Florida<br/>
<strong>AVISO LEGAL:</strong> Este software no reemplaza el asesoramiento legal.
Verifique toda la información con el tribunal antes de presentar documentos.
</div>
</div>
]]></field>
<field name="auto_delete" eval="True"/>
</record>
<!-- ══════════════════════════════════════════════════════
DEFAULT JUDGMENT ALERT (English)
══════════════════════════════════════════════════════ -->
<record id="mail_template_default_judgment" model="mail.template">
<field name="name">FL: Default Judgment Window Alert (FL 12.922)</field>
<field name="model_id" ref="model_fl_case"/>
<field name="subject">⚖️ ACTION REQUIRED: File Motion for Default — Case ${object.name}</field>
<field name="body_html"><![CDATA[
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #856404; color: #fff; padding: 20px; border-radius: 6px 6px 0 0;">
<h2 style="margin: 0;">⚖️ Default Judgment Window — Action Required</h2>
<p style="margin: 8px 0 0 0;">FL 12.922 — Motion for Default</p>
</div>
<div style="padding: 24px; background: #fff3cd; border: 1px solid #ffc107;">
<p>
The respondent in your case did not file an answer within the required
20-day window after service. You may now request a <strong>Clerk's Default</strong>.
</p>
<div style="background: #fff; border: 1px solid #dee2e6; padding: 16px; border-radius: 6px; margin: 16px 0;">
<strong>Case:</strong> ${object.name}<br/>
<strong>Petitioner:</strong> ${object.petitioner_id.name}<br/>
<strong>Respondent:</strong> ${object.respondent_id.name if object.respondent_id else 'N/A'}
</div>
<h3>Steps to Request a Default:</h3>
<ol>
<li>Download <strong>FL-12.922</strong> (Motion for Default) from the Florida Courts website</li>
<li>Complete the form with your case number from Miami-Dade Clerk</li>
<li>File at Miami-Dade Clerk's Office OR via the e-Filing portal</li>
<li>After the Clerk enters the default, file a <strong>Motion for Final Judgment</strong></li>
<li>Request a final hearing date from the court</li>
</ol>
<div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 12px; border-radius: 4px;">
<strong>⚠️ Note:</strong> A default does not automatically mean you win.
You must still present evidence at a final hearing.
The respondent may still contest the default by filing a motion to vacate.
</div>
</div>
<div style="padding: 12px; background: #e9ecef; border-radius: 0 0 6px 6px; font-size: 0.8em; color: #6c757d;">
<strong>DISCLAIMER:</strong> This is NOT legal advice. Consult a licensed
Florida family law attorney for guidance on default proceedings.<br/>
Legal Services of Greater Miami: <strong>(305) 576-0080</strong>
</div>
</div>
]]></field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

View File

@@ -1,29 +1,31 @@
from datetime import date, datetime, time
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
class FlDeadline(models.Model):
"""
Phase 2 — Full implementation with calendar integration and cron alerts.
Phase 1: Core fields and generate_deadlines_for_case stub.
Phase 2 — Full implementation with calendar integration, default judgment
workflow, and complete FL procedural deadline generation engine.
"""
_name = 'fl.deadline'
_description = 'Case Deadline'
_inherit = ['mail.thread']
_order = 'due_date asc'
_rec_name = 'name'
case_id = fields.Many2one(
'fl.case', required=True, ondelete='cascade', index=True
'fl.case', required=True, ondelete='cascade', index=True,
string='Case'
)
name = fields.Char(string='Deadline', required=True)
deadline_type = fields.Selection([
('filing', 'Filing'),
('service', 'Service of Process'),
('service', 'Service of Process (Target)'),
('service_max', 'Service Deadline — 120-day Maximum'),
('response', 'Response / Answer'),
('discovery_open', 'Discovery Opens'),
('financial_disclosure', 'Financial Disclosure Exchange'),
('mandatory_disclosure_cert', 'Mandatory Disclosure Certificate'),
('deposition_notice', 'Deposition Notice Deadline'),
('deposition', 'Deposition'),
('discovery_cutoff', 'Discovery Cutoff'),
@@ -32,49 +34,56 @@ class FlDeadline(models.Model):
('hearing', 'Hearing'),
('compliance', 'Compliance Deadline'),
('emancipation', 'Emancipation'),
('default_motion', 'Motion for Default'),
('default_motion', 'Motion for Default — Watch Period'),
('default_motion_file', 'File Motion for Default (FL 12.922)'),
('custom', 'Custom'),
], required=True)
], required=True, string='Type')
statute_reference = fields.Char(
string='Statute / Rule',
help='e.g. FL 1.140 — 20 days to answer'
help='e.g. FL 1.140 — 20 days to answer after service'
)
due_date = fields.Date(string='Due Date', required=True, tracking=True)
anchor_date = fields.Date(
string='Anchor Date',
help='Date this deadline is calculated from'
help='Date this deadline is calculated from (filing date, service date, etc.)'
)
offset_days = fields.Integer(
string='Offset Days',
help='Days from anchor date to due date'
help='Number of days from anchor date to this deadline'
)
completed = fields.Boolean(string='Completed', tracking=True)
completed_date = fields.Date(string='Completion Date')
waived = fields.Boolean(string='Waived')
waived = fields.Boolean(string='Waived / Not Applicable')
notes = fields.Text(string='Notes')
# ── Calendar Integration (Phase 2) ────────────────────────────────────
# ── Calendar Integration ──────────────────────────────────────────────────
calendar_event_id = fields.Many2one(
'calendar.event', string='Calendar Event'
'calendar.event', string='Calendar Event', ondelete='set null'
)
# ── Alerts ────────────────────────────────────────────────────────────
alert_7day_sent = fields.Boolean(default=False)
alert_3day_sent = fields.Boolean(default=False)
alert_1day_sent = fields.Boolean(default=False)
overdue_alert_sent = fields.Boolean(default=False)
# ── Alert Tracking ────────────────────────────────────────────────────────
alert_7day_sent = fields.Boolean(default=False, string='7-day Alert Sent')
alert_3day_sent = fields.Boolean(default=False, string='3-day Alert Sent')
alert_1day_sent = fields.Boolean(default=False, string='1-day Alert Sent')
overdue_alert_sent = fields.Boolean(default=False, string='Overdue Alert Sent')
# ── Computed Status ───────────────────────────────────────────────────────
is_overdue = fields.Boolean(
string='Overdue',
compute='_compute_is_overdue', store=True
compute='_compute_is_overdue',
store=True
)
days_until_due = fields.Integer(
string='Days Until Due',
compute='_compute_days_until_due'
)
# ══════════════════════════════════════════════════════════════════════════
# COMPUTED FIELDS
# ══════════════════════════════════════════════════════════════════════════
@api.depends('due_date', 'completed', 'waived')
def _compute_is_overdue(self):
today = fields.Date.today()
@@ -95,45 +104,167 @@ class FlDeadline(models.Model):
else:
rec.days_until_due = 0
def action_mark_complete(self):
self.completed = True
self.completed_date = fields.Date.today()
# ══════════════════════════════════════════════════════════════════════════
# CRUD — calendar event lifecycle
# ══════════════════════════════════════════════════════════════════════════
# ── Generation Engine ─────────────────────────────────────────────────
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for rec in records:
rec.with_context(_no_calendar_sync=True)._create_or_update_calendar_event()
return records
def write(self, vals):
result = super().write(vals)
if not self.env.context.get('_no_calendar_sync'):
sync_fields = {'due_date', 'name', 'completed', 'waived', 'deadline_type'}
if sync_fields.intersection(vals.keys()):
for rec in self:
rec.with_context(_no_calendar_sync=True)._create_or_update_calendar_event()
return result
# ══════════════════════════════════════════════════════════════════════════
# CALENDAR INTEGRATION
# ══════════════════════════════════════════════════════════════════════════
def _create_or_update_calendar_event(self):
"""Create or update a calendar.event linked to this deadline."""
if not self.due_date or self.waived:
# Archive the calendar event if waived or no date
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
self.write({'calendar_event_id': False})
return
if self.completed:
# Archive if completed
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
return
# Build attendee list
partners = self.env['res.partner']
if self.case_id.petitioner_id:
partners |= self.case_id.petitioner_id
if self.env.user.partner_id:
partners |= self.env.user.partner_id
# All-day event: start at 08:00, stop at 09:00
start_dt = fields.Datetime.from_string(
'{} 08:00:00'.format(self.due_date)
)
stop_dt = fields.Datetime.from_string(
'{} 09:00:00'.format(self.due_date)
)
type_label = dict(self._fields['deadline_type'].selection).get(
self.deadline_type, ''
)
event_name = '[{}] {} ({})'.format(
self.case_id.name, self.name, type_label
)
description = (
'FL Case Deadline\n'
'Case: {}\n'
'Type: {}\n'
'Statute: {}\n'
'Status: Pending'
).format(
self.case_id.name,
type_label,
self.statute_reference or 'N/A',
)
if self.calendar_event_id:
self.calendar_event_id.write({
'name': event_name,
'start': start_dt,
'stop': stop_dt,
'description': description,
'active': True,
})
else:
event = self.env['calendar.event'].sudo().create({
'name': event_name,
'start': start_dt,
'stop': stop_dt,
'allday': True,
'description': description,
'partner_ids': [(6, 0, partners.ids)],
'show_as': 'free',
'privacy': 'confidential',
})
# Use sudo write to avoid recursion on calendar_event_id
self.write({'calendar_event_id': event.id})
# ══════════════════════════════════════════════════════════════════════════
# WORKFLOW ACTIONS
# ══════════════════════════════════════════════════════════════════════════
def action_mark_complete(self):
"""Mark deadline as completed and archive the calendar event."""
self.write({
'completed': True,
'completed_date': fields.Date.today(),
})
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
def action_mark_waived(self):
"""Mark deadline as not applicable for this case."""
self.write({'waived': True})
# ══════════════════════════════════════════════════════════════════════════
# DEADLINE GENERATION ENGINE
# ══════════════════════════════════════════════════════════════════════════
@api.model
def generate_deadlines_for_case(self, case):
"""
Auto-generate procedural deadlines from filing_date.
Phase 1: Core deadlines only.
Phase 2: Full calendar events + alert setup.
Auto-generate procedural deadlines anchored to filing_date.
Called on case create when filing_date is set, and whenever
filing_date changes.
"""
if not case.filing_date:
return
# Avoid duplicates on re-run
existing_types = case.deadline_ids.mapped('deadline_type')
existing_types = set(case.deadline_ids.mapped('deadline_type'))
rules = [
# ── Filing-date anchored deadlines ────────────────────────────────────
filing_rules = [
{
'name': 'Serve Respondent (Target)',
'name': 'Serve Respondent Target Date (FL 1.070)',
'deadline_type': 'service',
'offset_days': 30,
'anchor': case.filing_date,
'statute_reference': 'FL 1.070 — 120-day maximum',
'statute_reference': 'FL 1.070 — Aim to serve within 30 days of filing',
},
{
'name': 'Serve Respondent — MAXIMUM Deadline (FL 1.070)',
'deadline_type': 'service_max',
'offset_days': 120,
'anchor': case.filing_date,
'statute_reference': (
'FL 1.070 — Case may be dismissed if not served within 120 days'
),
},
]
# Parenting class from filing (petitioner side — FL 61.21)
if case.has_minor_children:
rules.append({
'name': 'Parenting Class — Petitioner (FL 61.21)',
filing_rules.append({
'name': 'Parenting Class — Petitioner Must Complete (FL 61.21)',
'deadline_type': 'parenting_class',
'offset_days': 45,
'anchor': case.filing_date,
'statute_reference': 'FL 61.21',
'statute_reference': (
'FL 61.21 — Both parents must complete parenting class '
'before final hearing when minor children involved'
),
})
for rule in rules:
for rule in filing_rules:
if rule['deadline_type'] in existing_types:
continue
due = rule['anchor'] + relativedelta(days=rule['offset_days'])
@@ -150,47 +281,72 @@ class FlDeadline(models.Model):
@api.model
def recalculate_service_deadlines(self, case):
"""
Recalculate deadlines anchored to service_date when it is set.
Called from fl_case.write when service_date changes.
Create or update deadlines anchored to service_date.
Called from fl_case.write when service_date is set or changed.
All FL procedural deadlines run from the date of service.
"""
if not case.service_date:
return
service_anchored = [
service_rules = [
# FL 1.140: Respondent has 20 days to answer after service
{
'name': 'Respondent Answer Deadline',
'name': 'Respondent Answer Deadline (FL 1.140)',
'deadline_type': 'response',
'offset_days': 20,
'statute_reference': 'FL 1.140 — 20 days to answer',
'statute_reference': 'FL 1.140 — 20 days to serve answer after service',
},
# FL 12.285: Mandatory financial disclosure within 45 days of service
{
'name': 'Mandatory Financial Disclosure Exchange',
'name': 'Mandatory Financial Disclosure Exchange (FL 12.285)',
'deadline_type': 'financial_disclosure',
'offset_days': 45,
'statute_reference': 'FL 12.285 — 45 days',
'statute_reference': (
'FL 12.285 — Exchange mandatory disclosure documents within '
'45 days of service: last 3 years tax returns, paystubs, '
'bank statements, financial affidavit'
),
},
# FL 12.932: Certificate of mandatory disclosure
{
'name': 'Discovery Opens (Case at Issue)',
'name': 'File Certificate of Mandatory Disclosure (FL 12.932)',
'deadline_type': 'mandatory_disclosure_cert',
'offset_days': 45,
'statute_reference': (
'FL 12.932 — Certificate confirming disclosure documents exchanged'
),
},
# FL 12.280: Discovery opens once case is at issue
{
'name': 'Discovery Opens — Case at Issue (FL 12.280)',
'deadline_type': 'discovery_open',
'offset_days': 20,
'statute_reference': 'FL 12.280',
'statute_reference': (
'FL 12.280 — Discovery may commence after respondent answers '
'(or after Day 20 if no answer filed)'
),
},
]
# Respondent parenting class runs from service date
if case.has_minor_children:
service_anchored.append({
'name': 'Parenting Class — Respondent (FL 61.21)',
service_rules.append({
'name': 'Parenting Class — Respondent Must Complete (FL 61.21)',
'deadline_type': 'parenting_class',
'offset_days': 60,
'statute_reference': 'FL 61.21',
'statute_reference': (
'FL 61.21 — Respondent must complete parenting class '
'before final hearing'
),
})
existing = {d.deadline_type: d for d in case.deadline_ids}
for rule in service_anchored:
for rule in service_rules:
due = case.service_date + relativedelta(days=rule['offset_days'])
dl_type = rule['deadline_type']
if dl_type in existing:
# Update existing deadline
existing[dl_type].write({
'due_date': due,
'anchor_date': case.service_date,
@@ -206,9 +362,18 @@ class FlDeadline(models.Model):
'statute_reference': rule.get('statute_reference', ''),
})
# ══════════════════════════════════════════════════════════════════════════
# CRON: Daily Deadline Alerts
# ══════════════════════════════════════════════════════════════════════════
def _cron_deadline_alerts(self):
"""Run daily — send deadline alerts at 7, 3, 1 days and overdue."""
"""
Run daily via ir.cron.
Posts deadline alerts to case chatter at 7, 3, 1 days before
due date and once when a deadline becomes overdue.
"""
today = fields.Date.today()
upcoming = self.search([
('completed', '=', False),
('waived', '=', False),
@@ -236,13 +401,133 @@ class FlDeadline(models.Model):
dl._send_deadline_alert('OVERDUE')
dl.overdue_alert_sent = True
# ══════════════════════════════════════════════════════════════════════════
# CRON: Default Judgment Workflow (FL 12.922)
# ══════════════════════════════════════════════════════════════════════════
def _cron_check_default_judgment(self):
"""
Run daily via ir.cron.
FL 12.922 / FL 1.500: If respondent has not filed an answer by Day 20
after service, trigger the default judgment workflow:
1. Create 'File Motion for Default' deadline (5 days from today)
2. Create project task for petitioner
3. Post urgent alert to case chatter
Only triggers once per case (checks for existing default_motion_file
deadline to avoid duplicates).
"""
today = fields.Date.today()
# Find ALL overdue response deadlines
overdue_responses = self.search([
('deadline_type', '=', 'response'),
('completed', '=', False),
('waived', '=', False),
('due_date', '<', today),
])
for dl in overdue_responses:
case = dl.case_id
# If respondent has now answered, mark deadline complete
if case.respondent_answered:
dl.action_mark_complete()
continue
# Check if we already created the default motion deadline
existing_default = self.search([
('case_id', '=', case.id),
('deadline_type', '=', 'default_motion_file'),
], limit=1)
if existing_default:
continue
# Create the "File Motion for Default" deadline (5 days from today)
default_due = today + relativedelta(days=5)
self.create({
'case_id': case.id,
'name': 'File Motion for Default — Respondent Did Not Answer (FL 12.922)',
'deadline_type': 'default_motion_file',
'due_date': default_due,
'anchor_date': today,
'offset_days': 5,
'statute_reference': (
'FL 12.922 / FL 1.500 — Motion for Default. '
'Respondent failed to respond within 20 days of service.'
),
'notes': (
'File Clerk\'s Default (FL-12.922) with Miami-Dade Clerk.\n'
'After clerk enters default, file Motion for Final Judgment.\n'
'Form available at: https://www.flcourts.gov/Resources-Services/'
'Court-Improvement/Family-Courts/Family-Law-Self-Help-Information'
),
})
# Create a project task to remind the petitioner
if case.project_id:
self.env['project.task'].create({
'name': 'FILE: Motion for Default (FL 12.922) — URGENT',
'project_id': case.project_id.id,
'description': (
'Respondent did not file an answer by the Day 20 deadline '
'({}). You may now file a Clerk\'s Default.\n\n'
'Steps:\n'
'1. Download FL-12.922 from Florida Courts website\n'
'2. Complete the form with your case number\n'
'3. File at Miami-Dade Clerk\'s office OR via e-Filing portal\n'
'4. After clerk enters default, file Motion for Final Judgment\n\n'
'Deadline to file: {}\n'
'Statute: FL 12.922, FL 1.500'
).format(dl.due_date, default_due),
'date_deadline': default_due,
})
# Post urgent chatter alert
case.message_post(
body=(
'<b>⚖️ DEFAULT JUDGMENT WINDOW OPEN (FL 12.922)</b><br/>'
'Respondent did not file an answer by the Day 20 deadline '
'(<b>{}</b>).<br/><br/>'
'<b>Action Required:</b> File a Clerk\'s Default (FL-12.922) with the '
'Miami-Dade Clerk within <b>5 days (by {})</b>.<br/><br/>'
'<b>Steps:</b><br/>'
'1. Download FL-12.922 from Florida Courts website<br/>'
'2. File at Miami-Dade Clerk\'s office or via e-Filing portal<br/>'
'3. After default is entered, file Motion for Final Judgment<br/><br/>'
'<i>Statute: FL 12.922 / FL 1.500</i>'
).format(dl.due_date, default_due),
subtype_xmlid='mail.mt_note',
)
# ══════════════════════════════════════════════════════════════════════════
# ALERT HELPER
# ══════════════════════════════════════════════════════════════════════════
def _send_deadline_alert(self, timing):
icon = '🔴' if timing == 'OVERDUE' else ''
"""Post a deadline alert message to the case chatter."""
if timing == 'OVERDUE':
icon = '🔴'
urgency = 'OVERDUE'
elif timing == '1 day':
icon = '🚨'
urgency = 'Due Tomorrow'
else:
icon = ''
urgency = 'Due in {}'.format(timing)
self.case_id.message_post(
body=(
f'{icon} <b>Deadline Alert — {timing}</b><br/>'
f'<b>{self.name}</b> is due on <b>{self.due_date}</b>.<br/>'
f'Statute: {self.statute_reference or "N/A"}'
'{icon} <b>Deadline Alert — {urgency}</b><br/>'
'<b>{name}</b><br/>'
'Due: <b>{due_date}</b><br/>'
'Statute: {statute}'
).format(
icon=icon,
urgency=urgency,
name=self.name,
due_date=self.due_date,
statute=self.statute_reference or 'N/A',
),
subtype_xmlid='mail.mt_note',
)

View File

@@ -1,28 +1,37 @@
from datetime import timedelta
from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
class FlHearing(models.Model):
"""
Phase 2 — Full implementation.
Phase 1: Stub with fields needed by fl_case computed fields.
Phase 2 — Full implementation with calendar integration, workflow buttons,
and pre-hearing checklist for Florida family law hearings.
"""
_name = 'fl.hearing'
_description = 'Case Hearing'
_inherit = ['mail.thread']
_order = 'hearing_date asc'
_rec_name = 'name'
case_id = fields.Many2one(
'fl.case', required=True, ondelete='cascade', index=True
'fl.case', required=True, ondelete='cascade', index=True,
string='Case'
)
name = fields.Char(string='Hearing Description', required=True)
hearing_date = fields.Datetime(
string='Hearing Date / Time', tracking=True
)
duration_hours = fields.Float(
string='Duration (hours)', default=1.0,
help='Expected hearing duration. Default 1 hour.'
)
location = fields.Char(
string='Location',
default='Lawson E. Thomas Courthouse Center, 175 NW 1st Ave, Miami, FL 33128'
)
courtroom = fields.Char(string='Courtroom')
courtroom = fields.Char(string='Courtroom / Floor')
judge_id = fields.Many2one(
'res.partner', string='Judge',
related='case_id.judge_id', store=True
@@ -35,13 +44,283 @@ class FlHearing(models.Model):
('final', 'Final Hearing'),
('contempt', 'Contempt Hearing'),
('other', 'Other'),
], string='Hearing Type', default='final')
], string='Hearing Type', default='final', required=True, tracking=True)
state = fields.Selection([
('scheduled', 'Scheduled'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
('continued', 'Continued'),
], string='Status', default='scheduled', tracking=True)
notes = fields.Text(string='Notes')
notes = fields.Text(string='Pre-Hearing Notes / Preparation')
outcome = fields.Text(string='Outcome / Result')
order_entered = fields.Boolean(string='Order Entered')
order_entered = fields.Boolean(string='Order Entered After Hearing')
continued_date = fields.Datetime(string='New Date (if Continued)')
# ── Calendar Integration ──────────────────────────────────────────────────
calendar_event_id = fields.Many2one(
'calendar.event', string='Calendar Event', ondelete='set null'
)
# ── Pre-Hearing Checklist ─────────────────────────────────────────────────
parenting_class_warning = fields.Char(
string='Parenting Class Status',
compute='_compute_parenting_class_warning',
help='FL 61.21 — Both parents must complete before final hearing'
)
discovery_cutoff_warning = fields.Char(
string='Discovery Cutoff',
compute='_compute_discovery_cutoff_warning',
help='Discovery cutoff is 30 days before hearing date'
)
financial_disclosure_warning = fields.Char(
string='Financial Disclosure Status',
compute='_compute_financial_disclosure_warning',
)
# ══════════════════════════════════════════════════════════════════════════
# COMPUTED: Pre-Hearing Checks
# ══════════════════════════════════════════════════════════════════════════
@api.depends(
'case_id.petitioner_parenting_class_done',
'case_id.respondent_parenting_class_done',
'case_id.parenting_class_required',
)
def _compute_parenting_class_warning(self):
for rec in self:
case = rec.case_id
if not case.parenting_class_required:
rec.parenting_class_warning = '✅ Not required (no minor children)'
elif (case.petitioner_parenting_class_done
and case.respondent_parenting_class_done):
rec.parenting_class_warning = '✅ Both parties completed (FL 61.21)'
else:
missing = []
if not case.petitioner_parenting_class_done:
missing.append('Petitioner')
if not case.respondent_parenting_class_done:
missing.append('Respondent')
rec.parenting_class_warning = (
'⚠️ FL 61.21: Parenting class NOT completed by: {}. '
'Final hearing may not proceed without completion.'
).format(', '.join(missing))
@api.depends('hearing_date')
def _compute_discovery_cutoff_warning(self):
today = fields.Date.today()
for rec in self:
if not rec.hearing_date:
rec.discovery_cutoff_warning = '⚪ Set hearing date to see discovery cutoff'
continue
cutoff = rec.hearing_date.date() - relativedelta(days=30)
if today > cutoff:
rec.discovery_cutoff_warning = (
'🔴 Discovery cutoff PASSED ({}) — 30 days before hearing. '
'No new discovery requests may be served.'
).format(cutoff)
else:
days_left = (cutoff - today).days
rec.discovery_cutoff_warning = (
'✅ Discovery cutoff: {} ({} days remaining)'
).format(cutoff, days_left)
@api.depends('case_id.deadline_ids.completed', 'case_id.deadline_ids.deadline_type')
def _compute_financial_disclosure_warning(self):
for rec in self:
disc_deadline = rec.case_id.deadline_ids.filtered(
lambda d: d.deadline_type == 'financial_disclosure'
)
if not disc_deadline:
rec.financial_disclosure_warning = '⚪ No financial disclosure deadline set'
elif any(d.completed for d in disc_deadline):
rec.financial_disclosure_warning = '✅ Financial disclosure exchanged (FL 12.285)'
elif any(d.is_overdue for d in disc_deadline):
rec.financial_disclosure_warning = (
'🔴 OVERDUE: Financial disclosure not yet exchanged (FL 12.285)'
)
else:
due = min(d.due_date for d in disc_deadline)
rec.financial_disclosure_warning = (
'⏳ Financial disclosure due: {} (FL 12.285)'
).format(due)
# ══════════════════════════════════════════════════════════════════════════
# CRUD — calendar event lifecycle
# ══════════════════════════════════════════════════════════════════════════
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for rec in records:
if rec.hearing_date:
rec.with_context(_no_hearing_sync=True)._create_or_update_calendar_event()
rec._create_hearing_deadline()
return records
def write(self, vals):
result = super().write(vals)
if not self.env.context.get('_no_hearing_sync'):
sync_fields = {'hearing_date', 'name', 'state', 'duration_hours', 'location', 'courtroom'}
if sync_fields.intersection(vals.keys()):
for rec in self:
rec.with_context(_no_hearing_sync=True)._create_or_update_calendar_event()
return result
# ══════════════════════════════════════════════════════════════════════════
# CALENDAR INTEGRATION
# ══════════════════════════════════════════════════════════════════════════
def _create_or_update_calendar_event(self):
"""Create or sync a calendar.event for this hearing."""
if not self.hearing_date:
return
if self.state in ('cancelled',):
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
return
# Build attendee list
partners = self.env['res.partner']
if self.case_id.petitioner_id:
partners |= self.case_id.petitioner_id
if self.env.user.partner_id:
partners |= self.env.user.partner_id
stop_dt = self.hearing_date + timedelta(hours=self.duration_hours or 1.0)
type_label = dict(self._fields['hearing_type'].selection).get(
self.hearing_type, 'Hearing'
)
event_name = '[{}] {}{}'.format(
self.case_id.name, type_label, self.name
)
description = (
'Florida Family Law Hearing\n'
'Case: {}\n'
'Type: {}\n'
'Location: {}\n'
'Courtroom: {}\n'
'Judge: {}\n'
'Status: {}'
).format(
self.case_id.name,
type_label,
self.location or 'TBD',
self.courtroom or 'TBD',
self.judge_id.name if self.judge_id else 'TBD',
dict(self._fields['state'].selection).get(self.state, ''),
)
if self.calendar_event_id:
self.calendar_event_id.write({
'name': event_name,
'start': self.hearing_date,
'stop': stop_dt,
'description': description,
'active': self.state != 'cancelled',
'show_as': 'busy',
})
else:
event = self.env['calendar.event'].sudo().create({
'name': event_name,
'start': self.hearing_date,
'stop': stop_dt,
'description': description,
'partner_ids': [(6, 0, partners.ids)],
'show_as': 'busy',
'privacy': 'confidential',
})
self.write({'calendar_event_id': event.id})
def _create_hearing_deadline(self):
"""
Create an fl.deadline 'hearing' record for this hearing
so it appears in the case deadline tracker.
"""
if not self.hearing_date:
return
existing = self.env['fl.deadline'].search([
('case_id', '=', self.case_id.id),
('deadline_type', '=', 'hearing'),
], limit=1)
if not existing:
self.env['fl.deadline'].create({
'case_id': self.case_id.id,
'name': '{} Hearing — {}'.format(
dict(self._fields['hearing_type'].selection).get(
self.hearing_type, 'Hearing'
),
self.name,
),
'deadline_type': 'hearing',
'due_date': self.hearing_date.date(),
'statute_reference': 'Florida Rules of Civil Procedure',
'notes': (
'Location: {}\n'
'Courtroom: {}'
).format(self.location or '', self.courtroom or 'TBD'),
})
# ══════════════════════════════════════════════════════════════════════════
# WORKFLOW BUTTONS
# ══════════════════════════════════════════════════════════════════════════
def action_mark_completed(self):
"""Mark hearing as completed and archive calendar event."""
self.write({'state': 'completed'})
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
# Update linked hearing deadline
self.env['fl.deadline'].search([
('case_id', '=', self.case_id.id),
('deadline_type', '=', 'hearing'),
]).write({'completed': True, 'completed_date': fields.Date.today()})
self.case_id.message_post(
body=(
'✅ <b>Hearing Completed</b><br/>'
'{}{}<br/>'
'<i>Update the Outcome field with the judge\'s ruling.</i>'
).format(
dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''),
self.name,
),
subtype_xmlid='mail.mt_note',
)
def action_cancel(self):
"""Cancel this hearing and archive calendar event."""
self.write({'state': 'cancelled'})
if self.calendar_event_id:
self.calendar_event_id.write({'active': False})
self.case_id.message_post(
body=(
'❌ <b>Hearing Cancelled</b><br/>'
'{}{} has been cancelled.'
).format(
dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''),
self.name,
),
subtype_xmlid='mail.mt_note',
)
def action_mark_continued(self):
"""
Mark hearing as continued (rescheduled by court).
User must enter the new hearing date separately.
"""
self.write({'state': 'continued'})
self.case_id.message_post(
body=(
'⏸️ <b>Hearing Continued</b><br/>'
'{}{} has been continued (rescheduled by the court).<br/>'
'<b>Action required:</b> Contact the court for the new hearing date '
'and create a new hearing record.'
).format(
dict(self._fields['hearing_type'].selection).get(self.hearing_type, ''),
self.name,
),
subtype_xmlid='mail.mt_note',
)

View File

@@ -2,65 +2,198 @@
<odoo>
<data>
<!-- ══════════════════════════════════════════════════════
TREE VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_deadline_tree" model="ir.ui.view">
<field name="name">fl.deadline.tree</field>
<field name="model">fl.deadline</field>
<field name="arch" type="xml">
<tree string="Deadlines"
decoration-danger="is_overdue == True"
decoration-warning="days_until_due &lt;= 7 and days_until_due &gt;= 0 and completed == False"
decoration-muted="completed == True or waived == True">
decoration-warning="days_until_due &lt;= 7 and days_until_due &gt;= 0 and not completed and not waived"
decoration-success="completed == True"
decoration-muted="waived == True">
<field name="case_id"/>
<field name="name"/>
<field name="deadline_type"/>
<field name="due_date"/>
<field name="days_until_due"/>
<field name="statute_reference"/>
<field name="days_until_due" string="Days Left"
attrs="{'invisible': [('completed', '=', True)]}"/>
<field name="statute_reference" optional="show"/>
<field name="completed"/>
<field name="waived"/>
<field name="is_overdue" readonly="1"/>
<button name="action_mark_complete" string="✔ Complete"
type="object" icon="fa-check"
attrs="{'invisible': ['|', ('completed', '=', True), ('waived', '=', True)]}"/>
</tree>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
FORM VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_deadline_form" model="ir.ui.view">
<field name="name">fl.deadline.form</field>
<field name="model">fl.deadline</field>
<field name="arch" type="xml">
<form string="Deadline">
<header>
<button name="action_mark_complete" string="Mark Complete"
type="object" class="oe_highlight"
attrs="{'invisible': ['|', ('completed', '=', True), ('waived', '=', True)]}"/>
<button name="action_mark_waived" string="Mark Waived / N/A"
type="object"
attrs="{'invisible': ['|', ('completed', '=', True), ('waived', '=', True)]}"/>
</header>
<sheet>
<!-- Overdue alert banner -->
<div class="alert alert-danger" role="alert"
attrs="{'invisible': [('is_overdue', '=', False)]}">
<strong>🔴 OVERDUE</strong> — This deadline has passed.
Missed court deadlines can result in default judgment
or case dismissal. Take action immediately.
</div>
<!-- 7-day warning banner -->
<div class="alert alert-warning" role="alert"
attrs="{'invisible': ['|', ('is_overdue', '=', True), ('days_until_due', '&gt;', 7), ('completed', '=', True)]}">
<strong>⏰ Due in ${days_until_due} days</strong>
This deadline is coming up soon.
</div>
<!-- Completed banner -->
<div class="alert alert-success" role="alert"
attrs="{'invisible': [('completed', '=', False)]}">
<strong>✅ Completed</strong> on
<field name="completed_date" readonly="1" nolabel="1"/>
</div>
<group>
<group>
<field name="case_id"/>
<field name="name"/>
<field name="deadline_type"/>
<field name="due_date"/>
<field name="statute_reference"/>
</group>
<group>
<field name="due_date"/>
<field name="anchor_date"/>
<field name="offset_days"/>
<field name="days_until_due" readonly="1"/>
<field name="is_overdue" readonly="1"/>
</group>
</group>
<group string="Status">
<group>
<field name="completed"/>
<field name="completed_date"
attrs="{'invisible': [('completed', '=', False)]}"/>
<field name="waived"/>
attrs="{'invisible': [('completed', '=', False)], 'required': [('completed', '=', True)]}"/>
</group>
<field name="notes"/>
<group>
<field name="waived"/>
<field name="calendar_event_id" readonly="1"
attrs="{'invisible': [('calendar_event_id', '=', False)]}"/>
</group>
</group>
<group string="Alerts Sent" groups="activeblue_familylaw.group_admin">
<field name="alert_7day_sent" readonly="1"/>
<field name="alert_3day_sent" readonly="1"/>
<field name="alert_1day_sent" readonly="1"/>
<field name="overdue_alert_sent" readonly="1"/>
</group>
<field name="notes" placeholder="Notes, instructions, or action steps for this deadline..."/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
CALENDAR VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_deadline_calendar" model="ir.ui.view">
<field name="name">fl.deadline.calendar</field>
<field name="model">fl.deadline</field>
<field name="arch" type="xml">
<calendar string="Deadline Calendar"
date_start="due_date"
color="deadline_type"
mode="month"
quick_create="False">
<field name="name"/>
<field name="case_id"/>
<field name="deadline_type"/>
<field name="is_overdue"/>
</calendar>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
SEARCH VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_deadline_search" model="ir.ui.view">
<field name="name">fl.deadline.search</field>
<field name="model">fl.deadline</field>
<field name="arch" type="xml">
<search string="Search Deadlines">
<field name="name" string="Deadline"/>
<field name="case_id" string="Case"/>
<field name="deadline_type"/>
<!-- Quick filters -->
<filter string="Overdue" name="filter_overdue"
domain="[('is_overdue', '=', True)]"
help="Show only past-due deadlines"/>
<filter string="Due This Week" name="filter_this_week"
domain="[('due_date', '&lt;=', (context_today() + relativedelta(days=7)).strftime('%Y-%m-%d')), ('due_date', '&gt;=', context_today().strftime('%Y-%m-%d')), ('completed', '=', False)]"/>
<filter string="Pending" name="filter_pending"
domain="[('completed', '=', False), ('waived', '=', False)]"/>
<filter string="Completed" name="filter_completed"
domain="[('completed', '=', True)]"/>
<separator/>
<filter string="Service" name="type_service"
domain="[('deadline_type', 'in', ['service', 'service_max'])]"/>
<filter string="Responses" name="type_response"
domain="[('deadline_type', '=', 'response')]"/>
<filter string="Financial Disclosure" name="type_disclosure"
domain="[('deadline_type', '=', 'financial_disclosure')]"/>
<filter string="Hearings" name="type_hearing"
domain="[('deadline_type', '=', 'hearing')]"/>
<filter string="Default Motion" name="type_default"
domain="[('deadline_type', 'in', ['default_motion', 'default_motion_file'])]"/>
<!-- Group By -->
<separator/>
<group string="Group By">
<filter string="Case" name="group_case"
context="{'group_by': 'case_id'}"/>
<filter string="Type" name="group_type"
context="{'group_by': 'deadline_type'}"/>
<filter string="Due Month" name="group_month"
context="{'group_by': 'due_date:month'}"/>
</group>
</search>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
ACTIONS
══════════════════════════════════════════════════════ -->
<record id="action_fl_deadline_list" model="ir.actions.act_window">
<field name="name">Case Deadlines</field>
<field name="res_model">fl.deadline</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree,calendar,form</field>
<field name="search_view_id" ref="view_fl_deadline_search"/>
<field name="domain">[('completed', '=', False), ('waived', '=', False)]</field>
<field name="context">{'search_default_filter_pending': 1, 'search_default_group_case': 1}</field>
</record>
<!-- All deadlines including completed (for admin) -->
<record id="action_fl_deadline_all" model="ir.actions.act_window">
<field name="name">All Deadlines</field>
<field name="res_model">fl.deadline</field>
<field name="view_mode">tree,calendar,form</field>
<field name="search_view_id" ref="view_fl_deadline_search"/>
<field name="context">{'search_default_group_case': 1}</field>
</record>

View File

@@ -2,59 +2,199 @@
<odoo>
<data>
<!-- ══════════════════════════════════════════════════════
TREE VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_hearing_tree" model="ir.ui.view">
<field name="name">fl.hearing.tree</field>
<field name="model">fl.hearing</field>
<field name="arch" type="xml">
<tree string="Hearings">
<tree string="Hearings"
decoration-success="state == 'completed'"
decoration-danger="state == 'cancelled'"
decoration-warning="state == 'continued'"
decoration-info="state == 'scheduled'">
<field name="case_id"/>
<field name="name"/>
<field name="hearing_type"/>
<field name="hearing_date"/>
<field name="location"/>
<field name="courtroom"/>
<field name="state"/>
<field name="order_entered"/>
<field name="location" optional="show"/>
<field name="courtroom" optional="show"/>
<field name="state" widget="badge"
decoration-success="state == 'completed'"
decoration-danger="state == 'cancelled'"
decoration-warning="state == 'continued'"
decoration-info="state == 'scheduled'"/>
<field name="order_entered" optional="show"/>
</tree>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
FORM VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_hearing_form" model="ir.ui.view">
<field name="name">fl.hearing.form</field>
<field name="model">fl.hearing</field>
<field name="arch" type="xml">
<form string="Hearing">
<header>
<field name="state" widget="statusbar"/>
<button name="action_mark_completed" string="Mark Completed"
type="object" class="oe_highlight"
attrs="{'invisible': [('state', '!=', 'scheduled')]}"/>
<button name="action_mark_continued" string="Mark Continued"
type="object"
attrs="{'invisible': [('state', '!=', 'scheduled')]}"/>
<button name="action_cancel" string="Cancel Hearing"
type="object" confirm="Cancel this hearing? This cannot be undone."
attrs="{'invisible': [('state', 'not in', ['scheduled', 'continued'])]}"/>
<field name="state" widget="statusbar"
statusbar_visible="scheduled,completed,cancelled,continued"/>
</header>
<sheet>
<!-- Outcome section — shown after completion -->
<div class="alert alert-success" role="alert"
attrs="{'invisible': [('state', '!=', 'completed')]}">
<strong>✅ Hearing Completed</strong>
<span attrs="{'invisible': [('outcome', '=', False)]}">
<field name="outcome" readonly="1" nolabel="1"/>
</span>
<span attrs="{'invisible': [('outcome', '!=', False)]}">
— Update the Outcome field below with the judge's ruling.
</span>
</div>
<!-- Continued notice -->
<div class="alert alert-warning" role="alert"
attrs="{'invisible': [('state', '!=', 'continued')]}">
<strong>⏸️ Hearing Continued</strong> — Contact the court for
the rescheduled date and create a new hearing record.
</div>
<!-- Cancelled notice -->
<div class="alert alert-danger" role="alert"
attrs="{'invisible': [('state', '!=', 'cancelled')]}">
<strong>❌ Hearing Cancelled</strong>
</div>
<group>
<group>
<group string="Hearing Details">
<field name="case_id"/>
<field name="name"/>
<field name="hearing_type"/>
<field name="hearing_date"/>
<field name="duration_hours"/>
</group>
<group>
<group string="Location">
<field name="location"/>
<field name="courtroom"/>
<field name="judge_id"/>
<field name="calendar_event_id" readonly="1"
attrs="{'invisible': [('calendar_event_id', '=', False)]}"/>
</group>
</group>
<group string="Outcome">
<!-- Pre-Hearing Checklist -->
<group string="Pre-Hearing Checklist (FL Requirements)">
<div class="o_field_widget">
<div class="row">
<div class="col-md-6">
<strong>Parenting Class (FL 61.21):</strong><br/>
<field name="parenting_class_warning" readonly="1" nolabel="1"/>
</div>
<div class="col-md-6">
<strong>Discovery Cutoff:</strong><br/>
<field name="discovery_cutoff_warning" readonly="1" nolabel="1"/>
</div>
</div>
<div class="row mt-2">
<div class="col-md-6">
<strong>Financial Disclosure (FL 12.285):</strong><br/>
<field name="financial_disclosure_warning" readonly="1" nolabel="1"/>
</div>
</div>
</div>
</group>
<!-- Outcome (visible after hearing) -->
<group string="Outcome"
attrs="{'invisible': [('state', 'not in', ['completed', 'continued'])]}">
<field name="order_entered"/>
<field name="outcome"/>
<field name="outcome" placeholder="Describe the judge's ruling, any orders entered, next steps..."/>
</group>
<field name="notes"/>
<field name="notes"
placeholder="Pre-hearing preparation notes: exhibits to bring, arguments to make, questions for the judge..."/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
CALENDAR VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_hearing_calendar" model="ir.ui.view">
<field name="name">fl.hearing.calendar</field>
<field name="model">fl.hearing</field>
<field name="arch" type="xml">
<calendar string="Hearing Calendar"
date_start="hearing_date"
date_stop="hearing_date"
color="hearing_type"
mode="month"
quick_create="False">
<field name="name"/>
<field name="case_id"/>
<field name="hearing_type"/>
<field name="state"/>
<field name="location"/>
</calendar>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
SEARCH VIEW
══════════════════════════════════════════════════════ -->
<record id="view_fl_hearing_search" model="ir.ui.view">
<field name="name">fl.hearing.search</field>
<field name="model">fl.hearing</field>
<field name="arch" type="xml">
<search string="Search Hearings">
<field name="name"/>
<field name="case_id"/>
<field name="hearing_type"/>
<filter string="Scheduled" name="filter_scheduled"
domain="[('state', '=', 'scheduled')]"/>
<filter string="Upcoming (30 days)" name="filter_upcoming"
domain="[('hearing_date', '&gt;=', context_today().strftime('%Y-%m-%d')), ('hearing_date', '&lt;=', (context_today() + relativedelta(days=30)).strftime('%Y-%m-%d')), ('state', '=', 'scheduled')]"/>
<filter string="Completed" name="filter_completed"
domain="[('state', '=', 'completed')]"/>
<filter string="Final Hearings" name="type_final"
domain="[('hearing_type', '=', 'final')]"/>
<group string="Group By">
<filter string="Case" name="group_case"
context="{'group_by': 'case_id'}"/>
<filter string="Type" name="group_type"
context="{'group_by': 'hearing_type'}"/>
<filter string="Status" name="group_state"
context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<!-- ══════════════════════════════════════════════════════
ACTIONS
══════════════════════════════════════════════════════ -->
<record id="action_fl_hearing_list" model="ir.actions.act_window">
<field name="name">Hearings</field>
<field name="res_model">fl.hearing</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">tree,calendar,form</field>
<field name="search_view_id" ref="view_fl_hearing_search"/>
<field name="domain">[('state', '=', 'scheduled')]</field>
<field name="context">{'search_default_filter_scheduled': 1}</field>
</record>
</data>