From 7c865c8c22a8023f4dad94195819312f5abaa69e Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Mon, 4 May 2026 23:16:24 -0400 Subject: [PATCH] Phase 2: Deadlines + Calendar integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- activeblue_familylaw/__manifest__.py | 2 + .../data/fl_deadline_rules.xml | 49 +++ activeblue_familylaw/data/mail_templates.xml | 298 +++++++++++++ activeblue_familylaw/models/fl_deadline.py | 393 +++++++++++++++--- activeblue_familylaw/models/fl_hearing.py | 293 ++++++++++++- .../views/fl_deadline_views.xml | 157 ++++++- .../views/fl_hearing_views.xml | 166 +++++++- 7 files changed, 1272 insertions(+), 86 deletions(-) create mode 100644 activeblue_familylaw/data/fl_deadline_rules.xml create mode 100644 activeblue_familylaw/data/mail_templates.xml diff --git a/activeblue_familylaw/__manifest__.py b/activeblue_familylaw/__manifest__.py index 2108cf8..24a9921 100644 --- a/activeblue_familylaw/__manifest__.py +++ b/activeblue_familylaw/__manifest__.py @@ -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', diff --git a/activeblue_familylaw/data/fl_deadline_rules.xml b/activeblue_familylaw/data/fl_deadline_rules.xml new file mode 100644 index 0000000..734c382 --- /dev/null +++ b/activeblue_familylaw/data/fl_deadline_rules.xml @@ -0,0 +1,49 @@ + + + + + + + + + FL Family Law: Daily Deadline Alerts + + code + model._cron_deadline_alerts() + 1 + days + -1 + True + + + + + + FL Family Law: Check Default Judgment Triggers (FL 12.922) + + code + model._cron_check_default_judgment() + 1 + days + -1 + True + + + + + + FL Family Law: Child Emancipation Approaching Alerts + + code + model._cron_emancipation_alerts() + 1 + days + -1 + True + + + + + diff --git a/activeblue_familylaw/data/mail_templates.xml b/activeblue_familylaw/data/mail_templates.xml new file mode 100644 index 0000000..4b993f7 --- /dev/null +++ b/activeblue_familylaw/data/mail_templates.xml @@ -0,0 +1,298 @@ + + + + + + + FL: Deadline Alert — Upcoming + + ⏰ Deadline Alert: ${object.name} — Due ${object.due_date} + +
+

⏰ Upcoming Deadline Alert

+

ActiveBlue Family Law Case Management

+
+
+

You have an upcoming deadline on your family law case.

+ + + + + + + + + + + + + + + + + + + + + +
Case:${object.case_id.name}
Deadline:${object.name}
Due Date:${object.due_date}
Days Remaining:${object.days_until_due} days
Statute:${object.statute_reference or 'N/A'}
+ % if object.notes: +
+ Notes: ${object.notes} +
+ % endif +
+ Action Required: Please complete this deadline by the due date. + Missed deadlines in family law cases can result in default judgments + or dismissal of your case. +
+
+
+ DISCLAIMER: 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.

+ Legal Services of Greater Miami: (305) 576-0080 | + Florida Courts Self-Help: flcourts.gov +
+ + ]]>
+ +
+ + + + FL: Deadline Alert — OVERDUE + + 🔴 OVERDUE DEADLINE: ${object.name} — Case ${object.case_id.name} + +
+

🔴 OVERDUE DEADLINE — URGENT ACTION REQUIRED

+

ActiveBlue Family Law Case Management

+
+
+

+ A deadline on your family law case is PAST DUE. Immediate action may be required. +

+ + + + + + + + + + + + + + + + + +
Case:${object.case_id.name}
Deadline:${object.name}
Was Due:${object.due_date}
Statute:${object.statute_reference or 'N/A'}
+
+ ⚠️ WARNING: Missing court deadlines can result in: +
    +
  • Default judgment entered against you
  • +
  • Dismissal of your case
  • +
  • Contempt of court findings
  • +
  • Loss of your legal rights
  • +
+ Contact the court clerk immediately to understand your options. +
+
+
+ DISCLAIMER: This is an automated alert. This is NOT legal advice. + Consult a licensed Florida family law attorney immediately regarding missed deadlines.

+ Emergency Legal Help:
+ Legal Services of Greater Miami: (305) 576-0080
+ Florida Courts Self-Help: flcourts.gov
+ Miami-Dade Law Library: law.miami.edu/library +
+ + ]]>
+ +
+ + + + FL: Portal Welcome — New Case (EN) + + Welcome to Your Family Law Case Portal — Case ${object.name} + +
+

Welcome to Your Family Law Case Portal

+

ActiveBlue Family Law — Miami-Dade 11th Circuit

+
+
+

Dear ${object.petitioner_id.name},

+

+ Your family law case has been created in our system. You can now + track your case, deadlines, and documents through the portal. +

+
+ Your Case Reference: + + ${object.name} + +
+ + + + + + % if object.filing_date: + + + + + % endif +
Case Type:${object.case_type}
Filing Date:${object.filing_date}
+

What You Can Do in the Portal:

+
    +
  • 📋 View your case status and upcoming deadlines
  • +
  • 📄 Download and generate court forms
  • +
  • 💰 Use the FL 61.30 child support calculator
  • +
  • 📅 Track the visual timeline of your case
  • +
  • 💬 Communicate through the secure case messaging system
  • +
+
+ ⚠️ Important Reminder: + This system provides information and tools to assist you, but + does not provide legal advice. Family law matters + are complex. We strongly recommend consulting with a licensed + Florida family law attorney. +
+

Free Legal Resources:

+
    +
  • Legal Services of Greater Miami: (305) 576-0080
  • +
  • Florida Courts Self-Help: flcourts.gov
  • +
  • Miami-Dade Bar Lawyer Referral: (305) 371-2444
  • +
  • FL Courts e-Filing Portal: myflcourtaccess.com
  • +
+
+
+ ActiveBlue Family Law Case Management — Miami-Dade County, Florida
+ DISCLAIMER: This software is not a substitute for legal advice. + Verify all information with the court before filing. +
+ + ]]>
+ +
+ + + + FL: Portal Welcome — New Case (ES) + + Bienvenido a su Portal de Caso de Derecho Familiar — Caso ${object.name} + +
+

Bienvenido a su Portal de Caso de Derecho Familiar

+

ActiveBlue Family Law — Circuito 11 de Miami-Dade

+
+
+

Estimado/a ${object.petitioner_id.name},

+

+ Su caso de derecho familiar ha sido creado en nuestro sistema. Ahora puede + seguir su caso, plazos y documentos a través del portal. +

+
+ Número de Referencia de su Caso: + + ${object.name} + +
+

¿Qué puede hacer en el Portal?

+
    +
  • 📋 Ver el estado de su caso y los próximos plazos
  • +
  • 📄 Descargar y generar formularios del tribunal
  • +
  • 💰 Usar la calculadora de manutención de hijos FL 61.30
  • +
  • 📅 Ver el cronograma visual de su caso
  • +
  • 💬 Comunicarse a través del sistema de mensajería segura
  • +
+
+ ⚠️ Aviso Importante: + Este sistema proporciona información y herramientas para ayudarle, pero + no proporciona asesoramiento legal. Los asuntos de derecho familiar + son complejos. Recomendamos consultar con un abogado certificado de Florida. +
+

Recursos Legales Gratuitos:

+
    +
  • Servicios Legales del Gran Miami: (305) 576-0080
  • +
  • Autoayuda de los Tribunales de Florida: flcourts.gov
  • +
  • Colegio de Abogados de Miami-Dade — Referidos: (305) 371-2444
  • +
+
+
+ ActiveBlue Family Law — Condado de Miami-Dade, Florida
+ AVISO LEGAL: Este software no reemplaza el asesoramiento legal. + Verifique toda la información con el tribunal antes de presentar documentos. +
+ + ]]>
+ +
+ + + + FL: Default Judgment Window Alert (FL 12.922) + + ⚖️ ACTION REQUIRED: File Motion for Default — Case ${object.name} + +
+

⚖️ Default Judgment Window — Action Required

+

FL 12.922 — Motion for Default

+
+
+

+ The respondent in your case did not file an answer within the required + 20-day window after service. You may now request a Clerk's Default. +

+
+ Case: ${object.name}
+ Petitioner: ${object.petitioner_id.name}
+ Respondent: ${object.respondent_id.name if object.respondent_id else 'N/A'} +
+

Steps to Request a Default:

+
    +
  1. Download FL-12.922 (Motion for Default) from the Florida Courts website
  2. +
  3. Complete the form with your case number from Miami-Dade Clerk
  4. +
  5. File at Miami-Dade Clerk's Office OR via the e-Filing portal
  6. +
  7. After the Clerk enters the default, file a Motion for Final Judgment
  8. +
  9. Request a final hearing date from the court
  10. +
+
+ ⚠️ Note: 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. +
+
+
+ DISCLAIMER: This is NOT legal advice. Consult a licensed + Florida family law attorney for guidance on default proceedings.
+ Legal Services of Greater Miami: (305) 576-0080 +
+ + ]]>
+ +
+ +
+
diff --git a/activeblue_familylaw/models/fl_deadline.py b/activeblue_familylaw/models/fl_deadline.py index 912864c..e274f98 100644 --- a/activeblue_familylaw/models/fl_deadline.py +++ b/activeblue_familylaw/models/fl_deadline.py @@ -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=( + '⚖️ DEFAULT JUDGMENT WINDOW OPEN (FL 12.922)
' + 'Respondent did not file an answer by the Day 20 deadline ' + '({}).

' + 'Action Required: File a Clerk\'s Default (FL-12.922) with the ' + 'Miami-Dade Clerk within 5 days (by {}).

' + 'Steps:
' + '1. Download FL-12.922 from Florida Courts website
' + '2. File at Miami-Dade Clerk\'s office or via e-Filing portal
' + '3. After default is entered, file Motion for Final Judgment

' + 'Statute: FL 12.922 / FL 1.500' + ).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} Deadline Alert — {timing}
' - f'{self.name} is due on {self.due_date}.
' - f'Statute: {self.statute_reference or "N/A"}' + '{icon} Deadline Alert — {urgency}
' + '{name}
' + 'Due: {due_date}
' + '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', ) diff --git a/activeblue_familylaw/models/fl_hearing.py b/activeblue_familylaw/models/fl_hearing.py index e8b378b..070d37e 100644 --- a/activeblue_familylaw/models/fl_hearing.py +++ b/activeblue_familylaw/models/fl_hearing.py @@ -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=( + '✅ Hearing Completed
' + '{} — {}
' + 'Update the Outcome field with the judge\'s ruling.' + ).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=( + '❌ Hearing Cancelled
' + '{} — {} 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=( + '⏸️ Hearing Continued
' + '{} — {} has been continued (rescheduled by the court).
' + 'Action required: 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', + ) diff --git a/activeblue_familylaw/views/fl_deadline_views.xml b/activeblue_familylaw/views/fl_deadline_views.xml index 1b28746..67793e8 100644 --- a/activeblue_familylaw/views/fl_deadline_views.xml +++ b/activeblue_familylaw/views/fl_deadline_views.xml @@ -2,65 +2,198 @@ + fl.deadline.tree fl.deadline + decoration-warning="days_until_due <= 7 and days_until_due >= 0 and not completed and not waived" + decoration-success="completed == True" + decoration-muted="waived == True"> - - + + +