commit 1d52d85a78e0f2f878e05b99e98da14e6068769b Author: Carlos Garcia Date: Mon May 4 18:52:04 2026 -0400 Phase 1: core models, security, seed data, and backend views Implements full Phase 1 of the activeblue_familylaw Odoo 18 module: - 17 Python models (fl.case, fl.party, fl.child, fl.support.calculation, fl.fee.waiver, fl.income.withholding, fl.deadline, fl.hearing, fl.deposition, fl.discovery, fl.document, fl.caselaw, fl.analysis, fl.ai.engine, fl.argument, fl.statute, fl.issue.tag) + hr.expense extension - 3 wizard stubs (intake, analysis, generate-packet) - Security: 4 groups (admin/paralegal/portal-petitioner/portal-respondent) + record rules scoping portal users to their own cases - Seed data: issue tags, FL statutes, FL DCF support schedule, ir.sequence - 13 backend view XML files with FL 61.30 worksheet, fee waiver eligibility banner, DV safety resources, emancipation alerts - Static CSS/JS stubs for Phase 6 portal Co-Authored-By: Claude Sonnet 4.6 diff --git a/activeblue_familylaw/__init__.py b/activeblue_familylaw/__init__.py new file mode 100644 index 0000000..9b42961 --- /dev/null +++ b/activeblue_familylaw/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/activeblue_familylaw/__manifest__.py b/activeblue_familylaw/__manifest__.py new file mode 100644 index 0000000..2108cf8 --- /dev/null +++ b/activeblue_familylaw/__manifest__.py @@ -0,0 +1,63 @@ +{ + 'name': 'ActiveBlue Family Law', + 'version': '18.0.1.0.0', + 'category': 'Legal', + 'summary': 'Florida Family Law Case Management — Pro Se', + 'description': """ + Florida family law case management for pro se litigants. + Covers child support modification, dissolution of marriage, + and paternity cases in Miami-Dade County (11th Circuit). + Includes FL 61.30 child support calculator, document generation, + deadline tracking, and AI-powered case law analysis via Ollama. + """, + 'author': 'Active Blue LLC', + 'website': 'https://avc.activeblue.net', + 'depends': [ + 'base', + 'mail', + 'portal', + 'website', + 'contacts', + 'calendar', + 'project', + 'crm', + 'account', + 'hr_expense', + # 'sign', # Odoo Sign — enable when confirmed installed + # 'queue_job', # OCA queue_job — install from https://github.com/OCA/queue + ], + 'data': [ + # Security + 'security/fl_security.xml', + 'security/ir.model.access.csv', + # Seed data (load before views) + 'data/fl_issue_tags.xml', + 'data/fl_statute_data.xml', + 'data/fl_support_schedule.xml', + 'data/ir_sequence.xml', + # Views — backend (actions before menus so menuitem refs resolve) + 'views/fl_case_views.xml', + 'views/fl_party_views.xml', + 'views/fl_child_views.xml', + 'views/fl_support_views.xml', + 'views/fl_deadline_views.xml', + 'views/fl_hearing_views.xml', + 'views/fl_deposition_views.xml', + 'views/fl_discovery_views.xml', + 'views/fl_caselaw_views.xml', + 'views/fl_analysis_views.xml', + 'views/fl_fee_waiver_views.xml', + 'views/fl_statute_views.xml', + 'views/menu_views.xml', + ], + 'assets': { + 'web.assets_frontend': [ + 'activeblue_familylaw/static/src/css/familylaw_portal.css', + 'activeblue_familylaw/static/src/js/fl_calculator.js', + 'activeblue_familylaw/static/src/js/fl_timeline.js', + ], + }, + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/activeblue_familylaw/data/fl_issue_tags.xml b/activeblue_familylaw/data/fl_issue_tags.xml new file mode 100644 index 0000000..2d00f2c --- /dev/null +++ b/activeblue_familylaw/data/fl_issue_tags.xml @@ -0,0 +1,111 @@ + + + + + + Modification Threshold + Umbral de Modificación + 1 + modification + + + + Income Imputation + Imputación de Ingresos + 2 + all + + + + Self-Employment Income + Ingresos por Trabajo Independiente + 3 + all + + + + Timesharing Deviation + Desviación de Tiempo Compartido + 4 + modification + + + + Domestic Violence + Violencia Doméstica + 9 + all + + + + Fee Waiver / Indigent Status + Exención de Tarifas / Estado Indigente + 6 + all + + + + Default Judgment + Sentencia en Rebeldía + 7 + all + + + + Parenting Class Required + Clase de Crianza Requerida + 5 + all + + + + Residency Requirement + Requisito de Residencia + 8 + all + + + + Post-Order / Income Withholding + Post-Orden / Retención de Ingresos + 10 + all + + + + Lifestyle Inconsistency + Inconsistencia de Estilo de Vida + 3 + all + + + + Child Emancipation + Emancipación de Hijo + 6 + modification + + + + Substantial Change in Circumstances + Cambio Sustancial en Circunstancias + 1 + modification + + + + Alimony (2023 Reform) + Pensión Alimenticia (Reforma 2023) + 2 + dissolution + + + + Attorney Referral Recommended + Se Recomienda Referir a Abogado + 9 + all + + + + diff --git a/activeblue_familylaw/data/fl_statute_data.xml b/activeblue_familylaw/data/fl_statute_data.xml new file mode 100644 index 0000000..4e4fecf --- /dev/null +++ b/activeblue_familylaw/data/fl_statute_data.xml @@ -0,0 +1,242 @@ + + + + + + + + FL 61.30 + Child Support Guidelines + Establishes the guidelines for calculating child support in Florida. Provides the Basic Support Obligation schedule based on combined net monthly income and number of children. Governs adjustments for health insurance, childcare, timesharing, and extraordinary expenses. + child_support + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.30.html + + + + FL 61.14 + Modification of Support, Maintenance, Alimony, or Child Support + Authorizes courts to modify child support and alimony orders when there is a substantial change in circumstances. For child support, requires the change to meet the 15% or $50 threshold under FL 61.30(1)(b). + modification + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.14.html + + + + FL 61.08 + Alimony + Governs alimony awards in dissolution cases. AMENDED 2023 (HB 1409, effective July 1, 2023): Permanent alimony eliminated. Durational alimony capped at length of marriage. Pre-2023 permanent alimony orders may still be modified under new standards. + alimony + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.08.html + + + + FL 61.13 + Support of Children; Parenting and Time-Sharing + Governs parental responsibility, timesharing schedules, and parenting plans. FL 61.13(2)(c) specifically addresses timesharing in domestic violence situations. Best interests of the child standard applies. + timesharing + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.13.html + + + + FL 61.21 + Parenting Course; Requirement + Requires both parents to complete a parenting course when minor children are involved in a dissolution or modification proceeding. Must be completed before final hearing. Miami-Dade approved courses available through the court. + procedure + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.21.html + + + + FL 61.075 + Equitable Distribution of Marital Assets and Liabilities + Governs the distribution of marital assets and liabilities in dissolution proceedings. Presumption of equal distribution. Applies in dissolution cases — not directly applicable to child support modification. + dissolution + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.075.html + + + + FL 61.052 + Dissolution of Marriage + Establishes grounds for dissolution of marriage in Florida. Florida is a no-fault divorce state — irretrievable breakdown of the marriage is sufficient grounds. No showing of fault required. + dissolution + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.052.html + + + + FL 61.021 + Residence Requirement + Requires at least one party to have been a Florida resident for at least 6 months before filing for dissolution of marriage or modification. Must be proven by driver's license, voter registration, or witness testimony. + procedure + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.021.html + + + + FL 61.1301 + Income Deduction Orders + Mandatory income withholding order must be entered with every child support or alimony order. Employer deducts from paycheck and remits to Florida State Disbursement Unit (SDU). Exceptions only for good cause shown or written agreement between parties. + enforcement + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.1301.html + + + + FL 61.30(17) + Child Support Retroactivity + Modified child support is retroactive only to the date the petition for modification was filed. Cannot seek retroactive modification for periods before the filing date. Critical rule often missed by pro se filers. + modification + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0061/Sections/0061.30.html + + + + + + FL 742.10 + Establishment of Paternity + Governs the establishment of paternity in Florida. Methods include voluntary acknowledgment, administrative order, or court proceedings. Once paternity is established, child support and timesharing may be addressed. + paternity + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0742/Sections/0742.10.html + + + + + + FL 741.30 + Domestic Violence Injunctions + Provides for injunctions for protection against domestic violence. An active injunction affects mediation requirements, timesharing, and all court proceedings. Pro se litigants with active DV injunctions should strongly consider legal representation. + domestic_violence + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0741/Sections/0741.30.html + + + + + + FL 44.102 + Referral to Mediation + Authorizes courts to refer civil matters to mediation. In family law cases, mediation is generally required before final hearing. Miami-Dade Family Mediation Unit provides services. + procedure + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0044/Sections/0044.102.html + + + + FL 44.108 + Mediation Confidentiality; Mediator Fees + Governs mediator fees and confidentiality. Miami-Dade County provides county mediators at no cost for qualifying income levels (under 200% FPL). Parties may apply for mediator fee waiver in conjunction with civil indigent status. + procedure + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0044/Sections/0044.108.html + + + + + + FL 57.082 + Determination of Civil Indigent Status + Process for determining civil indigent status for fee waivers. Income threshold: less than 200% of federal poverty level. Application filed with clerk of court. If approved, filing fees and service fees are waived. + fee_waiver + http://www.leg.state.fl.us/statutes/index.cfm?App_mode=Display_Statute&URL=0000-0099/0057/Sections/0057.082.html + + + + + + FL 1.070 + Process — Service of Process + Governs service of process in civil actions. Summons must be served within 120 days of filing. If not served within 120 days, court may dismiss or extend time for good cause. Pro se tip: aim to serve within 30 days of filing. + procedure + http://www.floridabar.org/rules/frcp/ + + + + FL 1.140 + Defenses — Time to Answer + Respondent has 20 days from date of service to file an answer or other responsive pleading. Failure to respond within 20 days may result in a default judgment being entered against the respondent. + procedure + http://www.floridabar.org/rules/frcp/ + + + + FL 1.280 + General Provisions Governing Discovery + Governs the scope and limits of discovery in civil proceedings. Discovery may cover any matter relevant to the subject matter of the pending action. Allows discovery of information reasonably calculated to lead to admissible evidence. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.310 + Depositions Upon Oral Examination + Governs oral depositions. Minimum 10 days notice required (FL 1.310(b)). Maximum 7 hours per deponent per day (FL 1.310(d)). Video recording allowed with notice. Deponent may object to questions on privilege grounds. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.340 + Interrogatories to Parties + Written questions directed to a party. Responding party has 30 days to answer in writing under oath. Limited to 30 interrogatories without court permission. Commonly used to discover income, assets, employment, and expenses. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.350 + Production of Documents and Things + Request for production of documents, electronically stored information, or tangible things. Responding party has 30 days to respond. Used to obtain tax returns, pay stubs, bank statements, business records. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.370 + Requests for Admission + Written requests to admit or deny specific facts. Responding party has 30 days to respond. Facts not denied within 30 days are deemed admitted. Useful for establishing undisputed facts and narrowing issues before hearing. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.351 + Production of Documents Without Deposition (Subpoena) + Allows subpoena of documents from third parties (employers, banks, accountants) without requiring a deposition. Third party has 30 days to respond. Commonly used to subpoena employer payroll records when respondent income is unknown or disputed. + discovery + http://www.floridabar.org/rules/frcp/ + + + + FL 1.380 + Failure to Make Discovery; Sanctions + Motion to Compel filed when opposing party fails to respond to discovery requests or deposition notice. Court may impose sanctions including striking pleadings, entering default, or finding contempt. File within 5 days of deposition no-show. + discovery + http://www.floridabar.org/rules/frcp/ + + + + + + FL 12.285 + Mandatory Disclosure + Both parties must exchange financial information within 45 days of service of petition. Required documents include last 3 years tax returns, last 3 months pay stubs, last 12 months bank statements, and business records if self-employed. Failure may result in sanctions. + disclosure + http://www.floridabar.org/rules/flfr/ + + + + FL 12.922 + Default + Governs the default process in family law cases. If respondent fails to respond within 20 days of service, petitioner may move for default. Motion for Default filed with clerk, followed by Motion for Final Judgment by Default. Respondent may move to set aside default for good cause. + procedure + http://www.floridabar.org/rules/flfr/ + + + + diff --git a/activeblue_familylaw/data/fl_support_schedule.xml b/activeblue_familylaw/data/fl_support_schedule.xml new file mode 100644 index 0000000..c7382eb --- /dev/null +++ b/activeblue_familylaw/data/fl_support_schedule.xml @@ -0,0 +1,263 @@ + + + + + + + + + 800899.991742024-01-01 + 800899.9921032024-01-01 + 800899.9931272024-01-01 + 800899.9941412024-01-01 + 800899.9951532024-01-01 + 800899.9961662024-01-01 + + + 900999.991832024-01-01 + 900999.9921162024-01-01 + 900999.9931432024-01-01 + 900999.9941592024-01-01 + 900999.9951732024-01-01 + 900999.9961872024-01-01 + + + 10001099.991922024-01-01 + 10001099.9921282024-01-01 + 10001099.9931582024-01-01 + 10001099.9941752024-01-01 + 10001099.9951902024-01-01 + 10001099.9962062024-01-01 + + + 11001199.9911022024-01-01 + 11001199.9921422024-01-01 + 11001199.9931752024-01-01 + 11001199.9941942024-01-01 + 11001199.9952112024-01-01 + 11001199.9962282024-01-01 + + + 12001299.9911112024-01-01 + 12001299.9921552024-01-01 + 12001299.9931912024-01-01 + 12001299.9942122024-01-01 + 12001299.9952302024-01-01 + 12001299.9962492024-01-01 + + + 13001399.9911202024-01-01 + 13001399.9921672024-01-01 + 13001399.9932062024-01-01 + 13001399.9942282024-01-01 + 13001399.9952482024-01-01 + 13001399.9962682024-01-01 + + + 14001499.9911292024-01-01 + 14001499.9921802024-01-01 + 14001499.9932212024-01-01 + 14001499.9942452024-01-01 + 14001499.9952662024-01-01 + 14001499.9962882024-01-01 + + + 15001599.9911382024-01-01 + 15001599.9921922024-01-01 + 15001599.9932372024-01-01 + 15001599.9942632024-01-01 + 15001599.9952852024-01-01 + 15001599.9963092024-01-01 + + + 16001799.9911522024-01-01 + 16001799.9922122024-01-01 + 16001799.9932612024-01-01 + 16001799.9942902024-01-01 + 16001799.9953142024-01-01 + 16001799.9963402024-01-01 + + + 18001999.9911702024-01-01 + 18001999.9922372024-01-01 + 18001999.9932922024-01-01 + 18001999.9943242024-01-01 + 18001999.9953522024-01-01 + 18001999.9963812024-01-01 + + + + + 20002199.9911842024-01-01 + 20002199.9922562024-01-01 + 20002199.9933162024-01-01 + 20002199.9943502024-01-01 + 20002199.9953802024-01-01 + 20002199.9964112024-01-01 + + + 22002399.9912022024-01-01 + 22002399.9922812024-01-01 + 22002399.9933472024-01-01 + 22002399.9943852024-01-01 + 22002399.9954182024-01-01 + 22002399.9964522024-01-01 + + + 24002599.9912212024-01-01 + 24002599.9923082024-01-01 + 24002599.9933792024-01-01 + 24002599.9944202024-01-01 + 24002599.9954562024-01-01 + 24002599.9964932024-01-01 + + + 26002799.9912392024-01-01 + 26002799.9923332024-01-01 + 26002799.9934102024-01-01 + 26002799.9944542024-01-01 + 26002799.9954932024-01-01 + 26002799.9965332024-01-01 + + + 28002999.9912572024-01-01 + 28002999.9923582024-01-01 + 28002999.9934412024-01-01 + 28002999.9944892024-01-01 + 28002999.9955312024-01-01 + 28002999.9965742024-01-01 + + + 30003499.9912762024-01-01 + 30003499.9923842024-01-01 + 30003499.9934732024-01-01 + 30003499.9945242024-01-01 + 30003499.9955692024-01-01 + 30003499.9966152024-01-01 + + + 35003999.9913222024-01-01 + 35003999.9924492024-01-01 + 35003999.9935532024-01-01 + 35003999.9946132024-01-01 + 35003999.9956652024-01-01 + 35003999.9967202024-01-01 + + + + + 40004499.9913682024-01-01 + 40004499.9925132024-01-01 + 40004499.9936312024-01-01 + 40004499.9947002024-01-01 + 40004499.9957602024-01-01 + 40004499.9968222024-01-01 + + + 45004999.9914132024-01-01 + 45004999.9925762024-01-01 + 45004999.9937092024-01-01 + 45004999.9947872024-01-01 + 45004999.9958542024-01-01 + 45004999.9969242024-01-01 + + + 50005499.9914592024-01-01 + 50005499.9926412024-01-01 + 50005499.9937892024-01-01 + 50005499.9948752024-01-01 + 50005499.9959502024-01-01 + 50005499.99610272024-01-01 + + + 55005999.9915052024-01-01 + 55005999.9927042024-01-01 + 55005999.9938672024-01-01 + 55005999.9949622024-01-01 + 55005999.99510442024-01-01 + 55005999.99611292024-01-01 + + + 60006499.9915502024-01-01 + 60006499.9927672024-01-01 + 60006499.9939452024-01-01 + 60006499.99410482024-01-01 + 60006499.99511382024-01-01 + 60006499.99612302024-01-01 + + + 65006999.9915962024-01-01 + 65006999.9928312024-01-01 + 65006999.99310232024-01-01 + 65006999.99411342024-01-01 + 65006999.99512312024-01-01 + 65006999.99613312024-01-01 + + + 70007499.9916412024-01-01 + 70007499.9928942024-01-01 + 70007499.99311002024-01-01 + 70007499.99412202024-01-01 + 70007499.99513252024-01-01 + 70007499.99614322024-01-01 + + + 75007999.9916872024-01-01 + 75007999.9929572024-01-01 + 75007999.99311782024-01-01 + 75007999.99413072024-01-01 + 75007999.99514192024-01-01 + 75007999.99615342024-01-01 + + + 80008499.9917322024-01-01 + 80008499.99210212024-01-01 + 80008499.99312562024-01-01 + 80008499.99413932024-01-01 + 80008499.99515132024-01-01 + 80008499.99616352024-01-01 + + + 85008999.9917782024-01-01 + 85008999.99210842024-01-01 + 85008999.99313342024-01-01 + 85008999.99414792024-01-01 + 85008999.99516062024-01-01 + 85008999.99617362024-01-01 + + + 90009499.9918232024-01-01 + 90009499.99211472024-01-01 + 90009499.99314122024-01-01 + 90009499.99415662024-01-01 + 90009499.99517002024-01-01 + 90009499.99618382024-01-01 + + + 95009999.9918692024-01-01 + 95009999.99212112024-01-01 + 95009999.99314902024-01-01 + 95009999.99416522024-01-01 + 95009999.99517942024-01-01 + 95009999.99619392024-01-01 + + + + + + diff --git a/activeblue_familylaw/data/ir_sequence.xml b/activeblue_familylaw/data/ir_sequence.xml new file mode 100644 index 0000000..1a3db74 --- /dev/null +++ b/activeblue_familylaw/data/ir_sequence.xml @@ -0,0 +1,12 @@ + + + + + Family Law Case Reference + fl.case + FL-%(year)s- + 5 + + + + diff --git a/activeblue_familylaw/models/__init__.py b/activeblue_familylaw/models/__init__.py new file mode 100644 index 0000000..725a64a --- /dev/null +++ b/activeblue_familylaw/models/__init__.py @@ -0,0 +1,17 @@ +from . import fl_statute +from . import fl_child +from . import fl_party +from . import fl_support +from . import fl_fee_waiver +from . import fl_income_withholding +from . import fl_expense_case +from . import fl_deadline +from . import fl_hearing +from . import fl_deposition +from . import fl_discovery +from . import fl_document +from . import fl_caselaw +from . import fl_analysis +from . import fl_ai_engine +from . import fl_argument +from . import fl_case diff --git a/activeblue_familylaw/models/fl_ai_engine.py b/activeblue_familylaw/models/fl_ai_engine.py new file mode 100644 index 0000000..63d9bcc --- /dev/null +++ b/activeblue_familylaw/models/fl_ai_engine.py @@ -0,0 +1,78 @@ +import json + +from odoo import models + +OLLAMA_URL = 'http://192.168.2.10:11434/api/generate' +OLLAMA_MODEL = 'llama3.1' + + +class FlAiEngine(models.AbstractModel): + """ + Phase 5 — Full Ollama integration. + Phase 1: Stub service model. + + This is an AbstractModel — not stored in the database. + Used as a service class for AI analysis calls. + """ + _name = 'fl.ai.engine' + _description = 'Family Law AI Analysis Engine (Ollama)' + + def analyze_case(self, case_id): + """ + Phase 5 entry point. + Full workflow: + 1. Rule-based issue tagging + 2. Build case context JSON + 3. Call Ollama (llama3.1) + 4. Parse JSON response + 5. Store fl.analysis record + """ + case = self.env['fl.case'].browse(case_id) + analysis = self.env['fl.analysis'].create({ + 'case_id': case.id, + 'state': 'pending', + 'model_used': OLLAMA_MODEL, + 'plain_english_summary': ( + 'AI analysis not yet implemented. ' + 'Full analysis will be available in Phase 5.' + ), + 'plain_english_summary_es': ( + 'El análisis de IA aún no está implementado. ' + 'El análisis completo estará disponible en la Fase 5.' + ), + 'state': 'complete', + }) + return analysis + + def _call_ollama(self, prompt): + """Call Ollama API and return parsed JSON response.""" + try: + import requests + except ImportError: + raise RuntimeError( + 'requests library not available. ' + 'Install with: pip install requests' + ) + response = requests.post( + OLLAMA_URL, + json={ + 'model': OLLAMA_MODEL, + 'prompt': prompt, + 'stream': False, + 'options': { + 'temperature': 0.1, + 'top_p': 0.9, + 'num_predict': 2000, + }, + }, + timeout=180, + ) + response.raise_for_status() + raw = response.json().get('response', '{}').strip() + # Strip markdown code fences if present + if raw.startswith('```'): + parts = raw.split('```') + raw = parts[1] if len(parts) > 1 else raw + if raw.startswith('json'): + raw = raw[4:] + return json.loads(raw.strip()) diff --git a/activeblue_familylaw/models/fl_analysis.py b/activeblue_familylaw/models/fl_analysis.py new file mode 100644 index 0000000..9e774f9 --- /dev/null +++ b/activeblue_familylaw/models/fl_analysis.py @@ -0,0 +1,79 @@ +from odoo import fields, models + + +class FlAnalysis(models.Model): + """ + Phase 5 — Full Ollama AI analysis implementation. + Phase 1: Stub with fields required by fl_case computed fields. + """ + _name = 'fl.analysis' + _description = 'AI Case Analysis Result' + _order = 'create_date desc' + + case_id = fields.Many2one( + 'fl.case', ondelete='cascade', index=True + ) + analysis_date = fields.Datetime( + string='Analysis Date', + default=fields.Datetime.now + ) + model_used = fields.Char( + string='AI Model', + default='llama3.1' + ) + + # ── Results (referenced by fl_case related fields) ───────────────────── + attorney_referral_flag = fields.Boolean( + string='Attorney Referral Recommended', + default=False + ) + attorney_referral_reason = fields.Text( + string='Attorney Referral Reason' + ) + plain_english_summary = fields.Text( + string='Plain English Summary (EN)', + help='3-5 sentence summary of case analysis — no legal jargon' + ) + plain_english_summary_es = fields.Text( + string='Plain English Summary (ES)', + help='Resumen en español — sin jerga legal' + ) + + # ── Analysis Detail (Phase 5) ────────────────────────────────────────── + petitioner_arguments = fields.Text( + string='Petitioner Arguments (JSON)' + ) + respondent_counterarguments = fields.Text( + string='Respondent Counter-Arguments (JSON)' + ) + procedural_risks = fields.Text( + string='Procedural Risks (JSON)' + ) + matched_caselaw_ids = fields.Many2many( + 'fl.caselaw', + 'fl_analysis_caselaw_rel', + 'analysis_id', 'caselaw_id', + string='Matched Case Law' + ) + confidence_level = fields.Selection([ + ('high', 'High'), + ('medium', 'Medium'), + ('low', 'Low'), + ], string='Confidence Level') + case_complexity = fields.Selection([ + ('simple', 'Simple'), + ('moderate', 'Moderate'), + ('complex', 'Complex'), + ], string='Case Complexity') + raw_response = fields.Text( + string='Raw AI Response', + help='Full JSON response from Ollama — for debugging' + ) + error_message = fields.Text( + string='Error (if analysis failed)' + ) + state = fields.Selection([ + ('pending', 'Pending'), + ('complete', 'Complete'), + ('failed', 'Failed'), + ], string='Status', default='pending') diff --git a/activeblue_familylaw/models/fl_argument.py b/activeblue_familylaw/models/fl_argument.py new file mode 100644 index 0000000..a1cfa81 --- /dev/null +++ b/activeblue_familylaw/models/fl_argument.py @@ -0,0 +1,47 @@ +from odoo import fields, models + + +class FlArgument(models.Model): + """ + Phase 5 — AI-generated legal argument builder. + Phase 1: Stub. + """ + _name = 'fl.argument' + _description = 'Legal Argument' + _order = 'sequence, id' + + case_id = fields.Many2one( + 'fl.case', ondelete='cascade', index=True + ) + sequence = fields.Integer(default=10) + argument_text = fields.Text( + string='Argument', + required=True + ) + argument_type = fields.Selection([ + ('petitioner', 'Petitioner Argument'), + ('respondent', 'Respondent Counter-Argument'), + ('procedural', 'Procedural Point'), + ], string='Type', default='petitioner') + strength = fields.Selection([ + ('strong', 'Strong'), + ('moderate', 'Moderate'), + ('weak', 'Weak'), + ], string='Argument Strength') + caselaw_ids = fields.Many2many( + 'fl.caselaw', + 'fl_argument_caselaw_rel', + 'argument_id', 'caselaw_id', + string='Supporting Case Law' + ) + statute_ids = fields.Many2many( + 'fl.statute', + 'fl_argument_statute_rel', + 'argument_id', 'statute_id', + string='Supporting Statutes' + ) + notes = fields.Text(string='Notes') + ai_generated = fields.Boolean( + string='AI Generated', + default=False + ) diff --git a/activeblue_familylaw/models/fl_case.py b/activeblue_familylaw/models/fl_case.py new file mode 100644 index 0000000..335be58 --- /dev/null +++ b/activeblue_familylaw/models/fl_case.py @@ -0,0 +1,952 @@ +from odoo import _, api, fields, models +from dateutil.relativedelta import relativedelta + + +class FlCase(models.Model): + _name = 'fl.case' + _description = 'Florida Family Law Case' + _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] + _order = 'create_date desc' + _rec_name = 'name' + + # ══════════════════════════════════════════════════════════════════════ + # IDENTITY + # ══════════════════════════════════════════════════════════════════════ + + name = fields.Char( + string='Case Reference', + required=True, + copy=False, + readonly=True, + default=lambda self: _('New'), + help='Auto-generated internal reference (FL-YYYY-NNNNN)' + ) + court_case_number = fields.Char( + string='Court Case Number', + tracking=True, + help='Assigned by Miami-Dade Clerk after filing. ' + 'Format: YYYY-DR-XXXXXX-XX (e.g. 2025-DR-012345-01)' + ) + case_type = fields.Selection([ + ('modification', 'Child Support Modification'), + ('dissolution_children', 'Dissolution of Marriage — With Children'), + ('dissolution_no_children', 'Dissolution of Marriage — No Children'), + ('paternity', 'Paternity'), + ('alimony_modification', 'Alimony Modification'), + ('custody_modification', 'Timesharing / Custody Modification'), + ], string='Case Type', required=True, tracking=True) + + circuit = fields.Char( + string='Circuit', default='11th', readonly=True, + help='11th Judicial Circuit — Miami-Dade County' + ) + county = fields.Char( + string='County', default='Miami-Dade', readonly=True + ) + division = fields.Selection([ + ('1', 'Division 1'), ('2', 'Division 2'), + ('3', 'Division 3'), ('4', 'Division 4'), + ('5', 'Division 5'), ('6', 'Division 6'), + ('7', 'Division 7'), ('8', 'Division 8'), + ], string='Court Division', tracking=True) + judge_id = fields.Many2one( + 'res.partner', string='Assigned Judge', + domain=[('is_company', '=', False)] + ) + mediator_id = fields.Many2one( + 'res.partner', string='Mediator' + ) + + # ══════════════════════════════════════════════════════════════════════ + # STAGE / STATUS + # ══════════════════════════════════════════════════════════════════════ + + stage = fields.Selection([ + ('intake', 'Intake & Qualification'), + ('preparation', 'Document Preparation'), + ('filed', 'Filed — Awaiting Service'), + ('service_complete', 'Service Complete'), + ('discovery', 'Discovery'), + ('deposition', 'Deposition Stage'), + ('mediation', 'Mediation'), + ('hearing_scheduled', 'Hearing Scheduled'), + ('order_entered', 'Order Entered'), + ('closed', 'Closed'), + ('referred_out', 'Referred to Attorney'), + ], string='Stage', default='intake', tracking=True) + active = fields.Boolean(default=True) + + # ══════════════════════════════════════════════════════════════════════ + # PARTIES + # ══════════════════════════════════════════════════════════════════════ + + petitioner_id = fields.Many2one( + 'res.partner', string='Petitioner', + required=True, tracking=True + ) + respondent_id = fields.Many2one( + 'res.partner', string='Respondent', + tracking=True + ) + petitioner_attorney_id = fields.Many2one( + 'res.partner', string='Petitioner Attorney', + help='Leave blank if petitioner is pro se' + ) + respondent_attorney_id = fields.Many2one( + 'res.partner', string='Respondent Attorney', + tracking=True, + help='If respondent has counsel, pro se petitioner faces significant disadvantage' + ) + respondent_has_counsel = fields.Boolean( + string='Respondent Has Counsel', + compute='_compute_respondent_has_counsel', store=True + ) + party_ids = fields.One2many( + 'fl.party', 'case_id', string='Party Details' + ) + + # ══════════════════════════════════════════════════════════════════════ + # CHILDREN + # ══════════════════════════════════════════════════════════════════════ + + child_ids = fields.One2many( + 'fl.child', 'case_id', string='Children' + ) + children_count = fields.Integer( + string='Number of Children', + compute='_compute_children_count', store=True + ) + has_minor_children = fields.Boolean( + string='Has Minor Children', + compute='_compute_has_minor_children', store=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # KEY DATES + # ══════════════════════════════════════════════════════════════════════ + + filing_date = fields.Date( + string='Filing Date', tracking=True + ) + service_date = fields.Date( + string='Service Date', tracking=True + ) + last_order_date = fields.Date( + string='Date of Last Order', + tracking=True, + help='Required for modification cases — date of the existing order being modified' + ) + marriage_date = fields.Date(string='Date of Marriage') + separation_date = fields.Date(string='Date of Separation') + years_of_marriage = fields.Float( + string='Years of Marriage', + compute='_compute_years_of_marriage', store=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # RESIDENCY (FL 61.021) + # ══════════════════════════════════════════════════════════════════════ + + petitioner_fl_resident_since = fields.Date( + string='Petitioner FL Resident Since', + help='FL 61.021: Must be FL resident for 6 months before filing' + ) + residency_requirement_met = fields.Boolean( + string='Residency Requirement Met', + compute='_compute_residency_met', store=True + ) + residency_warning = fields.Char( + string='Residency Warning', + compute='_compute_residency_met' + ) + + # ══════════════════════════════════════════════════════════════════════ + # SAFETY FLAGS + # ══════════════════════════════════════════════════════════════════════ + + domestic_violence_flag = fields.Boolean( + string='Domestic Violence Present', + tracking=True, + help='FL 741.30 / FL 61.13(2)(c). ' + 'Affects mediation (separate rooms required), timesharing, ' + 'and ALL court proceedings. Forces attorney referral.' + ) + dv_injunction_active = fields.Boolean( + string='Active Injunction (FL 741.30)' + ) + dv_safety_note = fields.Html( + string='Safety Information', + compute='_compute_dv_safety_note' + ) + + # ══════════════════════════════════════════════════════════════════════ + # EXISTING ORDER (MODIFICATION CASES) + # ══════════════════════════════════════════════════════════════════════ + + current_order_amount = fields.Float( + string='Current Monthly Support Order ($)', + tracking=True, + help='Amount in the existing court order that is being modified' + ) + current_order_per_child = fields.Boolean( + string='Amount is Per-Child (not total)', + help='Check if the existing order states an amount per child rather than total' + ) + current_order_total = fields.Float( + string='Current Order Total ($)', + compute='_compute_current_order_total', store=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # CHILD SUPPORT CALCULATION (FL 61.30) + # ══════════════════════════════════════════════════════════════════════ + + support_calc_id = fields.Many2one( + 'fl.support.calculation', + string='Active Support Calculation', + ondelete='cascade' + ) + calculated_support = fields.Float( + string='Calculated Support ($)', + related='support_calc_id.total_support_obligation', + store=True + ) + support_difference = fields.Float( + string='Difference from Current Order ($)', + compute='_compute_support_difference', store=True + ) + support_difference_pct = fields.Float( + string='Difference %', + compute='_compute_support_difference', store=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # MODIFICATION THRESHOLD TEST (FL 61.30(1)(b)) + # ══════════════════════════════════════════════════════════════════════ + + threshold_met = fields.Boolean( + string='Modification Threshold Met', + compute='_compute_threshold', store=True, + help='FL 61.30(1)(b): Modification qualifies when change is ' + 'both ≥ $50 AND ≥ 15% of the current order' + ) + threshold_result = fields.Html( + string='Threshold Analysis', + compute='_compute_threshold' + ) + substantial_change_basis = fields.Selection([ + ('income_decrease_petitioner', 'Petitioner Income Decreased'), + ('income_decrease_respondent', 'Respondent Income Decreased'), + ('income_increase_petitioner', 'Petitioner Income Increased'), + ('income_increase_respondent', 'Respondent Income Increased'), + ('timesharing_change', 'Timesharing Schedule Changed'), + ('child_needs_change', "Child's Needs Changed"), + ('child_emancipation', 'Child Reaching 18 / Emancipation'), + ('other', 'Other Substantial Change'), + ], string='Basis for Modification (FL 61.14)') + substantial_change_detail = fields.Text( + string='Describe the Substantial Change' + ) + + # ══════════════════════════════════════════════════════════════════════ + # TIMESHARING + # ══════════════════════════════════════════════════════════════════════ + + timesharing_changed = fields.Boolean( + string='Timesharing Schedule Has Changed' + ) + petitioner_overnights = fields.Integer( + string='Petitioner Overnights / Year', + help='Number of overnights per year the petitioner has with the children' + ) + respondent_overnights = fields.Integer( + string='Respondent Overnights / Year', + compute='_compute_respondent_overnights', store=True + ) + petitioner_timesharing_pct = fields.Float( + string='Petitioner Timesharing %', + compute='_compute_timesharing_pct', store=True + ) + substantial_timesharing_applies = fields.Boolean( + string='Substantial Timesharing Adjustment Applies', + compute='_compute_timesharing_pct', store=True, + help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)' + ) + + # ══════════════════════════════════════════════════════════════════════ + # FEE WAIVER (FL 57.082) + # ══════════════════════════════════════════════════════════════════════ + + fee_waiver_eligible = fields.Boolean( + string='Fee Waiver Potentially Eligible', + compute='_compute_fee_waiver_eligibility', store=True, + help='Based on petitioner income vs 200% FPL (FL 57.082)' + ) + fee_waiver_id = fields.Many2one( + 'fl.fee.waiver', string='Fee Waiver Application' + ) + + # ══════════════════════════════════════════════════════════════════════ + # PARENTING CLASS (FL 61.21) + # ══════════════════════════════════════════════════════════════════════ + + parenting_class_required = fields.Boolean( + string='Parenting Class Required', + compute='_compute_parenting_class_required', store=True, + help='FL 61.21: Mandatory when minor children are involved' + ) + petitioner_parenting_class_done = fields.Boolean( + string='Petitioner Parenting Class Complete' + ) + petitioner_parenting_class_date = fields.Date( + string='Petitioner Completion Date' + ) + respondent_parenting_class_done = fields.Boolean( + string='Respondent Parenting Class Complete' + ) + respondent_parenting_class_date = fields.Date( + string='Respondent Completion Date' + ) + parenting_class_warning = fields.Char( + string='Parenting Class Status', + compute='_compute_parenting_class_warning' + ) + + # ══════════════════════════════════════════════════════════════════════ + # DEADLINES & CALENDAR + # ══════════════════════════════════════════════════════════════════════ + + deadline_ids = fields.One2many( + 'fl.deadline', 'case_id', string='Deadlines' + ) + next_deadline = fields.Date( + string='Next Deadline', + compute='_compute_next_deadline', store=True + ) + next_deadline_label = fields.Char( + string='Next Deadline Description', + compute='_compute_next_deadline' + ) + overdue_deadline_count = fields.Integer( + string='Overdue Deadlines', + compute='_compute_next_deadline' + ) + + # ══════════════════════════════════════════════════════════════════════ + # HEARINGS + # ══════════════════════════════════════════════════════════════════════ + + hearing_ids = fields.One2many( + 'fl.hearing', 'case_id', string='Hearings' + ) + next_hearing_date = fields.Datetime( + string='Next Hearing', + compute='_compute_next_hearing', store=True + ) + discovery_cutoff_date = fields.Date( + string='Discovery Cutoff', + compute='_compute_discovery_cutoff', store=True, + help='30 days before next hearing date' + ) + + # ══════════════════════════════════════════════════════════════════════ + # DEPOSITIONS & DISCOVERY + # ══════════════════════════════════════════════════════════════════════ + + income_disputed = fields.Boolean( + string='Income Figures Disputed', + tracking=True, + help='If True, deposition workflow is activated' + ) + deposition_ids = fields.One2many( + 'fl.deposition', 'case_id', string='Depositions' + ) + discovery_ids = fields.One2many( + 'fl.discovery', 'case_id', string='Discovery Items' + ) + + # ══════════════════════════════════════════════════════════════════════ + # DOCUMENTS + # ══════════════════════════════════════════════════════════════════════ + + document_ids = fields.One2many( + 'fl.document', 'case_id', string='Case Documents' + ) + + # ══════════════════════════════════════════════════════════════════════ + # PROJECT / TASK INTEGRATION + # ══════════════════════════════════════════════════════════════════════ + + project_id = fields.Many2one( + 'project.project', string='Case Project', + help='Auto-created project for case task management' + ) + task_count = fields.Integer( + string='Tasks', compute='_compute_task_count' + ) + + # ══════════════════════════════════════════════════════════════════════ + # CASE LAW & AI ANALYSIS + # ══════════════════════════════════════════════════════════════════════ + + caselaw_ids = fields.Many2many( + 'fl.caselaw', string='Applicable Case Law' + ) + analysis_ids = fields.One2many( + 'fl.analysis', 'case_id', string='AI Analyses' + ) + latest_analysis_id = fields.Many2one( + 'fl.analysis', + string='Latest Analysis', + compute='_compute_latest_analysis', store=True + ) + attorney_referral_flag = fields.Boolean( + string='Attorney Referral Recommended', + compute='_compute_attorney_referral_flag', store=True + ) + ai_plain_english = fields.Text( + string='AI Summary (English)', + related='latest_analysis_id.plain_english_summary' + ) + ai_plain_english_es = fields.Text( + string='AI Summary (Spanish)', + related='latest_analysis_id.plain_english_summary_es' + ) + + # ══════════════════════════════════════════════════════════════════════ + # EXPENSES + # ══════════════════════════════════════════════════════════════════════ + + expense_ids = fields.One2many( + 'hr.expense', 'fl_case_id', string='Case Expenses' + ) + total_expenses = fields.Float( + string='Total Case Expenses ($)', + compute='_compute_total_expenses', store=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # POST-ORDER + # ══════════════════════════════════════════════════════════════════════ + + new_order_amount = fields.Float( + string='New Order Amount ($)', tracking=True + ) + new_order_date = fields.Date( + string='New Order Date', tracking=True + ) + income_withholding_id = fields.Many2one( + 'fl.income.withholding', + string='Income Withholding Order' + ) + retroactivity_date = fields.Date( + string='Retroactivity Date', + compute='_compute_retroactivity_date', store=True, + help='FL 61.30(17): Modification is retroactive to filing date ONLY. ' + 'Cannot seek support modification for periods before filing.' + ) + respondent_answered = fields.Boolean( + string='Respondent Filed Answer', + tracking=True + ) + + # ══════════════════════════════════════════════════════════════════════ + # COMPUTED METHODS + # ══════════════════════════════════════════════════════════════════════ + + @api.depends('respondent_attorney_id') + def _compute_respondent_has_counsel(self): + for rec in self: + rec.respondent_has_counsel = bool(rec.respondent_attorney_id) + + @api.depends('child_ids') + def _compute_children_count(self): + for rec in self: + rec.children_count = len(rec.child_ids) + + @api.depends('child_ids.emancipated', 'child_ids.date_of_birth') + def _compute_has_minor_children(self): + for rec in self: + rec.has_minor_children = any( + not c.emancipated for c in rec.child_ids + ) + + @api.depends('marriage_date', 'separation_date', 'filing_date') + def _compute_years_of_marriage(self): + for rec in self: + end_date = rec.separation_date or rec.filing_date + if rec.marriage_date and end_date: + delta = relativedelta(end_date, rec.marriage_date) + rec.years_of_marriage = round( + delta.years + delta.months / 12 + delta.days / 365, 2 + ) + else: + rec.years_of_marriage = 0.0 + + @api.depends('petitioner_fl_resident_since', 'filing_date') + def _compute_residency_met(self): + for rec in self: + if rec.petitioner_fl_resident_since and rec.filing_date: + required_date = rec.petitioner_fl_resident_since + relativedelta(months=6) + rec.residency_requirement_met = rec.filing_date >= required_date + if not rec.residency_requirement_met: + months_short = relativedelta(required_date, rec.filing_date) + rec.residency_warning = ( + f'⚠️ FL 61.021: Petitioner must be a FL resident for 6 months ' + f'before filing. Currently {months_short.months} month(s) short.' + ) + else: + rec.residency_warning = '✅ Residency requirement met (FL 61.021)' + elif rec.petitioner_fl_resident_since and not rec.filing_date: + rec.residency_requirement_met = False + rec.residency_warning = 'Enter filing date to verify residency requirement' + else: + rec.residency_requirement_met = False + rec.residency_warning = '⚠️ Enter FL residency start date to verify FL 61.021' + + @api.depends('domestic_violence_flag', 'dv_injunction_active') + def _compute_dv_safety_note(self): + for rec in self: + if rec.domestic_violence_flag: + injunction_note = ( + '
Active injunction on file.' + if rec.dv_injunction_active else '' + ) + rec.dv_safety_note = ( + '
' + '

' + '⚠️ DOMESTIC VIOLENCE SAFETY NOTICE

' + '

Domestic violence has been flagged in this case. ' + 'This affects your rights and safety throughout this proceeding.

' + '
    ' + '
  • Mediation: Separate rooms are REQUIRED (FL 44.102)
  • ' + '
  • Representation: Pro se representation is strongly discouraged
  • ' + '
  • Contact: Do not communicate with the opposing party directly
  • ' + '
' + f'{injunction_note}' + '

Resources:
' + 'Legal Services of Greater Miami: (305) 576-0080
' + 'Safespace Miami: (305) 536-5565
' + 'National DV Hotline: 1-800-799-7233

' + '
' + ) + else: + rec.dv_safety_note = '' + + @api.depends('current_order_amount', 'current_order_per_child', 'children_count') + def _compute_current_order_total(self): + for rec in self: + if rec.current_order_per_child and rec.children_count: + rec.current_order_total = ( + rec.current_order_amount * rec.children_count + ) + else: + rec.current_order_total = rec.current_order_amount + + @api.depends('calculated_support', 'current_order_total') + def _compute_support_difference(self): + for rec in self: + current = rec.current_order_total or 0.0 + proposed = rec.calculated_support or 0.0 + rec.support_difference = proposed - current + if current > 0: + rec.support_difference_pct = ( + abs(proposed - current) / current + ) * 100 + else: + rec.support_difference_pct = 0.0 + + @api.depends('support_difference', 'support_difference_pct', 'case_type') + def _compute_threshold(self): + """ + FL 61.30(1)(b): Modification qualifies when BOTH: + - |difference| >= $50 AND + - |difference| / current_amount >= 15% + """ + for rec in self: + if rec.case_type != 'modification': + rec.threshold_met = False + rec.threshold_result = '' + continue + + diff = abs(rec.support_difference) + pct = rec.support_difference_pct + current = rec.current_order_total or 0.0 + + dollar_test = diff >= 50.0 + pct_test = pct >= 15.0 + + if current == 0: + rec.threshold_met = False + rec.threshold_result = ( + '⚠️ Enter current order amount ' + 'to run threshold test' + ) + continue + + color_dollar = '#28a745' if dollar_test else '#dc3545' + color_pct = '#28a745' if pct_test else '#dc3545' + + direction = 'increase' if rec.support_difference > 0 else 'decrease' + + rec.threshold_met = dollar_test and pct_test + overall_color = '#28a745' if rec.threshold_met else '#dc3545' + overall_icon = '✅' if rec.threshold_met else '❌' + overall_label = 'QUALIFIES FOR MODIFICATION' if rec.threshold_met else 'DOES NOT QUALIFY' + + rec.threshold_result = ( + f'
' + f'{overall_icon} {overall_label}' + f'

' + f'Current order: ${current:,.2f}/mo
' + f'Proposed: ${rec.calculated_support:,.2f}/mo ' + f'({direction} of ${diff:,.2f})

' + f'' + f'{"✅" if dollar_test else "❌"} $50 test: ' + f'${diff:,.2f} {"≥" if dollar_test else "<"} $50
' + f'' + f'{"✅" if pct_test else "❌"} 15% test: ' + f'{pct:.1f}% {"≥" if pct_test else "<"} 15%' + f'
' + ) + + @api.depends('petitioner_overnights') + def _compute_respondent_overnights(self): + for rec in self: + if rec.petitioner_overnights: + rec.respondent_overnights = 365 - rec.petitioner_overnights + else: + rec.respondent_overnights = 0 + + @api.depends('petitioner_overnights') + def _compute_timesharing_pct(self): + for rec in self: + if rec.petitioner_overnights: + pct = rec.petitioner_overnights / 365 + rec.petitioner_timesharing_pct = round(pct * 100, 2) + # FL 61.30(11)(b): substantial = either parent > 73 overnights (20%) + pet_substantial = rec.petitioner_overnights > 73 + resp_substantial = (365 - rec.petitioner_overnights) > 73 + rec.substantial_timesharing_applies = pet_substantial or resp_substantial + else: + rec.petitioner_timesharing_pct = 0.0 + rec.substantial_timesharing_applies = False + + @api.depends('party_ids.effective_monthly_income', 'party_ids.role') + def _compute_fee_waiver_eligibility(self): + """ + Quick eligibility check: petitioner income < 200% FPL (FL 57.082). + Full determination via fl.fee.waiver model. + 2025: 1-person 200% FPL = $30,120/yr = $2,510/mo + """ + FPL_200PCT_1PERSON_MONTHLY = 2510.0 + + for rec in self: + petitioner_party = rec.party_ids.filtered( + lambda p: p.role == 'petitioner' + ) + if petitioner_party: + income = petitioner_party[0].effective_monthly_income or 0.0 + rec.fee_waiver_eligible = income <= FPL_200PCT_1PERSON_MONTHLY + else: + rec.fee_waiver_eligible = False + + @api.depends('has_minor_children') + def _compute_parenting_class_required(self): + for rec in self: + rec.parenting_class_required = rec.has_minor_children + + @api.depends( + 'parenting_class_required', + 'petitioner_parenting_class_done', + 'respondent_parenting_class_done', + ) + def _compute_parenting_class_warning(self): + for rec in self: + if not rec.parenting_class_required: + rec.parenting_class_warning = 'Not required' + continue + pet_done = rec.petitioner_parenting_class_done + resp_done = rec.respondent_parenting_class_done + if pet_done and resp_done: + rec.parenting_class_warning = '✅ Both parents completed' + elif pet_done: + rec.parenting_class_warning = '⚠️ Respondent parenting class pending' + elif resp_done: + rec.parenting_class_warning = '⚠️ Petitioner parenting class pending' + else: + rec.parenting_class_warning = '❌ Neither parent has completed parenting class' + + @api.depends('deadline_ids.due_date', 'deadline_ids.completed', 'deadline_ids.waived') + def _compute_next_deadline(self): + today = fields.Date.today() + for rec in self: + pending = rec.deadline_ids.filtered( + lambda d: not d.completed and not d.waived + ) + overdue = pending.filtered(lambda d: d.due_date < today) + upcoming = pending.filtered( + lambda d: d.due_date >= today + ).sorted('due_date') + + rec.overdue_deadline_count = len(overdue) + if upcoming: + next_dl = upcoming[0] + rec.next_deadline = next_dl.due_date + rec.next_deadline_label = next_dl.name + elif overdue: + oldest = overdue.sorted('due_date')[0] + rec.next_deadline = oldest.due_date + rec.next_deadline_label = f'OVERDUE: {oldest.name}' + else: + rec.next_deadline = False + rec.next_deadline_label = 'No pending deadlines' + + @api.depends('hearing_ids.hearing_date') + def _compute_next_hearing(self): + now = fields.Datetime.now() + for rec in self: + future = rec.hearing_ids.filtered( + lambda h: h.hearing_date and h.hearing_date > now + and h.state not in ('cancelled',) + ).sorted('hearing_date') + rec.next_hearing_date = future[0].hearing_date if future else False + + @api.depends('next_hearing_date') + def _compute_discovery_cutoff(self): + for rec in self: + if rec.next_hearing_date: + rec.discovery_cutoff_date = ( + rec.next_hearing_date.date() - relativedelta(days=30) + ) + else: + rec.discovery_cutoff_date = False + + def _compute_task_count(self): + for rec in self: + if rec.project_id: + rec.task_count = self.env['project.task'].search_count([ + ('project_id', '=', rec.project_id.id) + ]) + else: + rec.task_count = 0 + + @api.depends('analysis_ids') + def _compute_latest_analysis(self): + for rec in self: + latest = rec.analysis_ids.sorted('create_date', reverse=True) + rec.latest_analysis_id = latest[0] if latest else False + + @api.depends('latest_analysis_id.attorney_referral_flag', 'domestic_violence_flag', 'respondent_has_counsel') + def _compute_attorney_referral_flag(self): + for rec in self: + rec.attorney_referral_flag = ( + rec.domestic_violence_flag + or rec.respondent_has_counsel + or (rec.latest_analysis_id and rec.latest_analysis_id.attorney_referral_flag) + ) + + @api.depends('expense_ids.total_amount') + def _compute_total_expenses(self): + for rec in self: + rec.total_expenses = sum(rec.expense_ids.mapped('total_amount')) + + @api.depends('filing_date') + def _compute_retroactivity_date(self): + """ + FL 61.30(17): Modification is retroactive to the filing date ONLY. + """ + for rec in self: + rec.retroactivity_date = rec.filing_date + + # ══════════════════════════════════════════════════════════════════════ + # CRUD OVERRIDES + # ══════════════════════════════════════════════════════════════════════ + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'fl.case' + ) or _('New') + records = super().create(vals_list) + for record in records: + # 1. Create Odoo project for task management + record._create_case_project() + # 2. Generate deadlines if filing date already set + if record.filing_date: + self.env['fl.deadline'].generate_deadlines_for_case(record) + # 3. Check fee waiver eligibility (quick check) + record._check_fee_waiver_eligibility() + # 4. Handle DV flag + if record.domestic_violence_flag: + record._handle_dv_flag() + return records + + def write(self, vals): + result = super().write(vals) + # Recalculate service-anchored deadlines when service_date is set/changed + if 'service_date' in vals: + for rec in self: + if rec.service_date: + self.env['fl.deadline'].recalculate_service_deadlines(rec) + # Handle DV flag being set + if vals.get('domestic_violence_flag'): + for rec in self: + if rec.domestic_violence_flag: + rec._handle_dv_flag() + # Generate income withholding order when new_order_amount is set + if vals.get('new_order_amount') and vals.get('new_order_date'): + for rec in self: + rec._check_generate_income_withholding() + return result + + # ══════════════════════════════════════════════════════════════════════ + # WORKFLOW METHODS + # ══════════════════════════════════════════════════════════════════════ + + def _create_case_project(self): + """Create a linked Odoo project for case task management.""" + project = self.env['project.project'].create({ + 'name': f'Case: {self.name} — {self.petitioner_id.name}', + 'partner_id': self.petitioner_id.id, + 'description': ( + f'Family law case project for {self.petitioner_id.name}. ' + f'Case type: {dict(self._fields["case_type"].selection).get(self.case_type, "")}' + ), + }) + self.project_id = project + + def _check_fee_waiver_eligibility(self): + """Post a chatter note if petitioner appears to qualify for fee waiver.""" + if self.fee_waiver_eligible and not self.fee_waiver_id: + self.message_post( + body=( + '💡 Fee Waiver Opportunity: Based on the income information ' + 'provided, this petitioner may qualify for a civil indigent fee ' + 'waiver (FL 57.082). This would waive filing fees, service fees, ' + 'and potentially mediator fees. ' + 'Consider creating a Fee Waiver application from the case form.' + ), + subtype_xmlid='mail.mt_note', + ) + + def _handle_dv_flag(self): + """ + Handle domestic violence flag being set on a case. + Per spec: Force attorney referral, post safety resources, + require separate mediation rooms, disable direct contact. + """ + self.message_post( + body=( + '
' + '

' + '🚨 SAFETY ALERT — DOMESTIC VIOLENCE FLAGGED

' + '
    ' + '
  • Separate mediation rooms are REQUIRED (FL 44.102)
  • ' + '
  • Pro se representation in DV cases is STRONGLY DISCOURAGED
  • ' + '
  • Attorney referral has been flagged on this case
  • ' + '
  • Do NOT allow direct portal contact between parties
  • ' + '
' + 'Resources:
' + 'Legal Services of Greater Miami: (305) 576-0080
' + 'Safespace Miami: (305) 536-5565
' + 'National DV Hotline: 1-800-799-7233
' + 'TTY: 1-800-787-3224' + '
' + ), + subtype_xmlid='mail.mt_note', + ) + + def _check_generate_income_withholding(self): + """ + FL 61.1301: Auto-generate income withholding order when new order amount is set. + Mandatory unless good cause or written agreement. + """ + if self.income_withholding_id: + return # Already exists + iwo = self.env['fl.income.withholding'].create({ + 'case_id': self.id, + 'obligor_id': self.respondent_id.id, + 'obligee_id': self.petitioner_id.id, + 'monthly_support_amount': self.new_order_amount, + }) + self.income_withholding_id = iwo + self.message_post( + body=( + f'⚖️ Income Withholding Order created (FL 61.1301).
' + f'Amount: ${self.new_order_amount:,.2f}/mo.
' + f'Income withholding is MANDATORY under FL 61.1301 unless good cause ' + f'is shown or both parties agree in writing to an alternative payment method.' + ), + subtype_xmlid='mail.mt_note', + ) + + def trigger_ai_analysis(self): + """ + Trigger AI case analysis via Ollama. + Stub for Phase 1 — full implementation in Phase 5. + """ + self.message_post( + body='🤖 AI analysis queued. Full AI analysis will be available in Phase 5.', + subtype_xmlid='mail.mt_note', + ) + + # ══════════════════════════════════════════════════════════════════════ + # ACTIONS + # ══════════════════════════════════════════════════════════════════════ + + def action_view_tasks(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Case Tasks', + 'res_model': 'project.task', + 'view_mode': 'tree,form', + 'domain': [('project_id', '=', self.project_id.id)], + 'context': {'default_project_id': self.project_id.id}, + } + + def action_open_support_calculator(self): + if not self.support_calc_id: + calc = self.env['fl.support.calculation'].create({ + 'case_id': self.id, + 'calculation_type': 'proposed', + }) + self.support_calc_id = calc + return { + 'type': 'ir.actions.act_window', + 'name': 'FL 61.30 Support Calculator', + 'res_model': 'fl.support.calculation', + 'res_id': self.support_calc_id.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_create_fee_waiver(self): + waiver = self.env['fl.fee.waiver'].create({ + 'case_id': self.id, + 'party_id': self.petitioner_id.id, + }) + self.fee_waiver_id = waiver + return { + 'type': 'ir.actions.act_window', + 'name': 'Fee Waiver Application', + 'res_model': 'fl.fee.waiver', + 'res_id': waiver.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_run_ai_analysis(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Run AI Analysis', + 'res_model': 'fl.analysis.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_case_id': self.id}, + } diff --git a/activeblue_familylaw/models/fl_caselaw.py b/activeblue_familylaw/models/fl_caselaw.py new file mode 100644 index 0000000..a2f44e0 --- /dev/null +++ b/activeblue_familylaw/models/fl_caselaw.py @@ -0,0 +1,95 @@ +import urllib.parse + +from odoo import api, fields, models + + +class FlCaselaw(models.Model): + """ + Phase 5 — Full caselaw library with AI integration. + Phase 1: Full model definition (seed data loaded in Phase 5). + """ + _name = 'fl.caselaw' + _description = 'Florida Family Law Case' + _inherit = ['mail.thread'] + _order = 'year desc, short_name' + _rec_name = 'short_name' + + citation = fields.Char( + string='Full Citation', + required=True, + help='e.g. Smith v. Jones, 123 So.3d 456 (Fla. 3d DCA 2019)' + ) + short_name = fields.Char( + string='Short Name', + required=True, + help='e.g. Smith v. Jones' + ) + court = fields.Selection([ + ('fl_supreme', 'Florida Supreme Court'), + ('1st_dca', '1st District Court of Appeal'), + ('2nd_dca', '2nd District Court of Appeal'), + ('3rd_dca', '3rd DCA (Miami-Dade / Monroe)'), + ('4th_dca', '4th District Court of Appeal'), + ('5th_dca', '5th District Court of Appeal'), + ('11th_circuit', '11th Circuit Court (Trial — Miami-Dade)'), + ('other', 'Other'), + ], string='Court', required=True) + year = fields.Integer(string='Year', required=True) + statute_ref_ids = fields.Many2many( + 'fl.statute', + 'fl_caselaw_statute_rel', + 'caselaw_id', 'statute_id', + string='FL Statutes' + ) + issue_tag_ids = fields.Many2many( + 'fl.issue.tag', + 'fl_caselaw_tag_rel', + 'caselaw_id', 'tag_id', + string='Issue Tags' + ) + holding = fields.Text(string='Holding', required=True) + facts_summary = fields.Text(string='Facts Summary') + relevance = fields.Text(string='Why It Matters for Pro Se') + favorable_to = fields.Selection([ + ('petitioner', 'Petitioner / Moving Party'), + ('respondent', 'Respondent'), + ('neutral', 'Neutral / Procedural'), + ('depends', 'Depends on Facts'), + ], string='Generally Favorable To', required=True) + full_text_url = fields.Char(string='Full Opinion URL') + google_scholar_url = fields.Char( + string='Google Scholar URL', + compute='_compute_google_scholar_url' + ) + case_ids = fields.Many2many( + 'fl.case', + 'fl_case_caselaw_rel', + 'caselaw_id', 'case_id', + string='Cases Citing This' + ) + active = fields.Boolean(default=True) + + # Plain English summaries (AI-generated, human-reviewed) + plain_english = fields.Text( + string='Plain English Explanation (EN)', + help='What this case means for a pro se litigant — no legal jargon' + ) + plain_english_es = fields.Text( + string='Plain English Explanation (ES)', + help='Explicación en español para litigantes pro se' + ) + + # Post-2023 flag for alimony cases + superseded_by_2023_reform = fields.Boolean( + string='Superseded by 2023 Alimony Reform (HB 1409)', + help='Pre-2023 permanent alimony cases are superseded by FL 61.08 amendment' + ) + + @api.depends('citation') + def _compute_google_scholar_url(self): + base = 'https://scholar.google.com/scholar?q=' + for rec in self: + if rec.citation: + rec.google_scholar_url = base + urllib.parse.quote(rec.citation) + else: + rec.google_scholar_url = '' diff --git a/activeblue_familylaw/models/fl_child.py b/activeblue_familylaw/models/fl_child.py new file mode 100644 index 0000000..32b628d --- /dev/null +++ b/activeblue_familylaw/models/fl_child.py @@ -0,0 +1,166 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta + + +class FlChild(models.Model): + _name = 'fl.child' + _description = 'Child on Case' + _order = 'date_of_birth' + + case_id = fields.Many2one( + 'fl.case', string='Case', + required=True, ondelete='cascade', index=True + ) + name = fields.Char(string='Full Name', required=True) + date_of_birth = fields.Date(string='Date of Birth', required=True) + age = fields.Integer( + string='Age', compute='_compute_age', store=True + ) + gender = fields.Selection([ + ('m', 'Male'), + ('f', 'Female'), + ('x', 'Non-binary / Other'), + ], string='Gender') + school = fields.Char(string='School') + medical_provider = fields.Char(string='Primary Care Provider') + + # ── Support Per Child ────────────────────────────────────────────────── + support_amount = fields.Float( + string='Support Amount This Child ($)', + help='Per-child support amount from the calculation' + ) + health_insurance_premium = fields.Float( + string='Health Insurance Premium ($)', + help='Monthly cost of health insurance for this child only' + ) + childcare_cost = fields.Float( + string='Work-Related Childcare ($)', + help='FL 61.30(7): Only work or job-search related childcare costs qualify' + ) + extraordinary_expenses = fields.Float( + string='Extraordinary Expenses ($)', + help='FL 61.30(9): Medical or educational expenses beyond ordinary child-rearing costs' + ) + + # ── Emancipation Tracking ────────────────────────────────────────────── + emancipation_date = fields.Date( + string='Emancipation Date', + compute='_compute_emancipation', + store=True, + help='18th birthday (or high school graduation, whichever is later). ' + 'Support terminates on this date.' + ) + approaching_emancipation = fields.Boolean( + string='Approaching Emancipation', + compute='_compute_approaching_emancipation', + store=True, + help='True when emancipation is within 90 days' + ) + emancipation_alert_sent = fields.Boolean( + string='Emancipation Alert Sent', + default=False + ) + emancipated = fields.Boolean( + string='Emancipated', + compute='_compute_emancipated', + store=True + ) + days_until_emancipation = fields.Integer( + string='Days Until Emancipation', + compute='_compute_approaching_emancipation' + ) + + # ── Early Emancipation Factors ───────────────────────────────────────── + married = fields.Boolean( + string='Child is Married', + help='Marriage constitutes emancipation under FL law' + ) + active_military = fields.Boolean( + string='Active Military Service', + help='Active duty military service constitutes emancipation under FL law' + ) + declared_emancipated = fields.Boolean( + string='Court-Declared Emancipated', + help='Court has entered an order declaring this child emancipated' + ) + emancipation_notes = fields.Text( + string='Emancipation Notes', + help='Document basis for any early emancipation determination' + ) + + # ── Computed ────────────────────────────────────────────────────────── + + @api.depends('date_of_birth') + def _compute_age(self): + today = fields.Date.today() + for rec in self: + if rec.date_of_birth: + delta = relativedelta(today, rec.date_of_birth) + rec.age = delta.years + else: + rec.age = 0 + + @api.depends('date_of_birth') + def _compute_emancipation(self): + for rec in self: + if rec.date_of_birth: + rec.emancipation_date = rec.date_of_birth + relativedelta(years=18) + else: + rec.emancipation_date = False + + @api.depends('emancipation_date', 'married', 'active_military', 'declared_emancipated') + def _compute_approaching_emancipation(self): + today = fields.Date.today() + for rec in self: + # Early emancipation factors + if rec.married or rec.active_military or rec.declared_emancipated: + rec.approaching_emancipation = False + rec.days_until_emancipation = 0 + continue + if rec.emancipation_date: + days_remaining = (rec.emancipation_date - today).days + rec.days_until_emancipation = max(days_remaining, 0) + rec.approaching_emancipation = 0 < days_remaining <= 90 + else: + rec.approaching_emancipation = False + rec.days_until_emancipation = 0 + + @api.depends('emancipation_date', 'married', 'active_military', 'declared_emancipated') + def _compute_emancipated(self): + today = fields.Date.today() + for rec in self: + if rec.married or rec.active_military or rec.declared_emancipated: + rec.emancipated = True + elif rec.emancipation_date: + rec.emancipated = today >= rec.emancipation_date + else: + rec.emancipated = False + + # ── Cron ────────────────────────────────────────────────────────────── + + def _cron_emancipation_alerts(self): + """ + Run daily via ir.cron. + Sends chatter alert on cases where a child is approaching emancipation + and no alert has been sent yet. + """ + approaching = self.search([ + ('approaching_emancipation', '=', True), + ('emancipation_alert_sent', '=', False), + ]) + for child in approaching: + days = child.days_until_emancipation + child.case_id.message_post( + body=( + f'⚠️ EMANCIPATION ALERT
' + f'{child.name} will turn 18 on ' + f'{child.emancipation_date} ' + f'({days} days from today).
' + f'Child support for this child terminates on that date.
' + f'Action required: A Motion to Modify should be filed ' + f'at least 60 days before emancipation to address the change ' + f'in support obligation.' + ), + subtype_xmlid='mail.mt_note', + ) + child.emancipation_alert_sent = True diff --git a/activeblue_familylaw/models/fl_deadline.py b/activeblue_familylaw/models/fl_deadline.py new file mode 100644 index 0000000..912864c --- /dev/null +++ b/activeblue_familylaw/models/fl_deadline.py @@ -0,0 +1,248 @@ +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. + """ + _name = 'fl.deadline' + _description = 'Case Deadline' + _inherit = ['mail.thread'] + _order = 'due_date asc' + + case_id = fields.Many2one( + 'fl.case', required=True, ondelete='cascade', index=True + ) + name = fields.Char(string='Deadline', required=True) + deadline_type = fields.Selection([ + ('filing', 'Filing'), + ('service', 'Service of Process'), + ('response', 'Response / Answer'), + ('discovery_open', 'Discovery Opens'), + ('financial_disclosure', 'Financial Disclosure Exchange'), + ('deposition_notice', 'Deposition Notice Deadline'), + ('deposition', 'Deposition'), + ('discovery_cutoff', 'Discovery Cutoff'), + ('parenting_class', 'Parenting Class Completion'), + ('mediation', 'Mediation'), + ('hearing', 'Hearing'), + ('compliance', 'Compliance Deadline'), + ('emancipation', 'Emancipation'), + ('default_motion', 'Motion for Default'), + ('custom', 'Custom'), + ], required=True) + + statute_reference = fields.Char( + string='Statute / Rule', + help='e.g. FL 1.140 — 20 days to answer' + ) + 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' + ) + offset_days = fields.Integer( + string='Offset Days', + help='Days from anchor date to due date' + ) + + completed = fields.Boolean(string='Completed', tracking=True) + completed_date = fields.Date(string='Completion Date') + waived = fields.Boolean(string='Waived') + notes = fields.Text(string='Notes') + + # ── Calendar Integration (Phase 2) ──────────────────────────────────── + calendar_event_id = fields.Many2one( + 'calendar.event', string='Calendar Event' + ) + + # ── 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) + + is_overdue = fields.Boolean( + string='Overdue', + compute='_compute_is_overdue', store=True + ) + days_until_due = fields.Integer( + string='Days Until Due', + compute='_compute_days_until_due' + ) + + @api.depends('due_date', 'completed', 'waived') + def _compute_is_overdue(self): + today = fields.Date.today() + for rec in self: + rec.is_overdue = ( + not rec.completed + and not rec.waived + and bool(rec.due_date) + and rec.due_date < today + ) + + @api.depends('due_date') + def _compute_days_until_due(self): + today = fields.Date.today() + for rec in self: + if rec.due_date: + rec.days_until_due = (rec.due_date - today).days + else: + rec.days_until_due = 0 + + def action_mark_complete(self): + self.completed = True + self.completed_date = fields.Date.today() + + # ── 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. + """ + if not case.filing_date: + return + + # Avoid duplicates on re-run + existing_types = case.deadline_ids.mapped('deadline_type') + + rules = [ + { + 'name': 'Serve Respondent (Target)', + 'deadline_type': 'service', + 'offset_days': 30, + 'anchor': case.filing_date, + 'statute_reference': 'FL 1.070 — 120-day maximum', + }, + ] + + if case.has_minor_children: + rules.append({ + 'name': 'Parenting Class — Petitioner (FL 61.21)', + 'deadline_type': 'parenting_class', + 'offset_days': 45, + 'anchor': case.filing_date, + 'statute_reference': 'FL 61.21', + }) + + for rule in rules: + if rule['deadline_type'] in existing_types: + continue + due = rule['anchor'] + relativedelta(days=rule['offset_days']) + self.create({ + 'case_id': case.id, + 'name': rule['name'], + 'deadline_type': rule['deadline_type'], + 'due_date': due, + 'anchor_date': rule['anchor'], + 'offset_days': rule['offset_days'], + 'statute_reference': rule.get('statute_reference', ''), + }) + + @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. + """ + if not case.service_date: + return + + service_anchored = [ + { + 'name': 'Respondent Answer Deadline', + 'deadline_type': 'response', + 'offset_days': 20, + 'statute_reference': 'FL 1.140 — 20 days to answer', + }, + { + 'name': 'Mandatory Financial Disclosure Exchange', + 'deadline_type': 'financial_disclosure', + 'offset_days': 45, + 'statute_reference': 'FL 12.285 — 45 days', + }, + { + 'name': 'Discovery Opens (Case at Issue)', + 'deadline_type': 'discovery_open', + 'offset_days': 20, + 'statute_reference': 'FL 12.280', + }, + ] + + if case.has_minor_children: + service_anchored.append({ + 'name': 'Parenting Class — Respondent (FL 61.21)', + 'deadline_type': 'parenting_class', + 'offset_days': 60, + 'statute_reference': 'FL 61.21', + }) + + existing = {d.deadline_type: d for d in case.deadline_ids} + + for rule in service_anchored: + due = case.service_date + relativedelta(days=rule['offset_days']) + dl_type = rule['deadline_type'] + if dl_type in existing: + existing[dl_type].write({ + 'due_date': due, + 'anchor_date': case.service_date, + }) + else: + self.create({ + 'case_id': case.id, + 'name': rule['name'], + 'deadline_type': dl_type, + 'due_date': due, + 'anchor_date': case.service_date, + 'offset_days': rule['offset_days'], + 'statute_reference': rule.get('statute_reference', ''), + }) + + def _cron_deadline_alerts(self): + """Run daily — send deadline alerts at 7, 3, 1 days and overdue.""" + today = fields.Date.today() + upcoming = self.search([ + ('completed', '=', False), + ('waived', '=', False), + ('due_date', '>=', today), + ]) + for dl in upcoming: + days = (dl.due_date - today).days + if days == 7 and not dl.alert_7day_sent: + dl._send_deadline_alert('7 days') + dl.alert_7day_sent = True + elif days == 3 and not dl.alert_3day_sent: + dl._send_deadline_alert('3 days') + dl.alert_3day_sent = True + elif days == 1 and not dl.alert_1day_sent: + dl._send_deadline_alert('1 day') + dl.alert_1day_sent = True + + overdue = self.search([ + ('completed', '=', False), + ('waived', '=', False), + ('due_date', '<', today), + ('overdue_alert_sent', '=', False), + ]) + for dl in overdue: + dl._send_deadline_alert('OVERDUE') + dl.overdue_alert_sent = True + + def _send_deadline_alert(self, timing): + icon = '🔴' if timing == 'OVERDUE' else '⏰' + 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"}' + ), + subtype_xmlid='mail.mt_note', + ) diff --git a/activeblue_familylaw/models/fl_deposition.py b/activeblue_familylaw/models/fl_deposition.py new file mode 100644 index 0000000..ab439f4 --- /dev/null +++ b/activeblue_familylaw/models/fl_deposition.py @@ -0,0 +1,55 @@ +from odoo import api, fields, models + + +class FlDeposition(models.Model): + """ + Phase 3 — Full implementation with notice validation, duces tecum, no-show workflow. + Phase 1: Stub with core fields. + """ + _name = 'fl.deposition' + _description = 'Deposition Record' + _inherit = ['mail.thread'] + _order = 'scheduled_date asc' + + case_id = fields.Many2one( + 'fl.case', required=True, ondelete='cascade', index=True + ) + deponent_id = fields.Many2one( + 'res.partner', string='Deponent', required=True + ) + deponent_type = fields.Selection([ + ('opposing_party', 'Opposing Party'), + ('employer', 'Employer'), + ('accountant_cpa', 'Accountant / CPA'), + ('business_partner', 'Business Partner'), + ('vocational_expert', 'Vocational Expert'), + ('witness', 'Witness'), + ('other', 'Other'), + ], string='Deponent Type', required=True) + notice_date = fields.Date( + string='Notice Served Date', + help='FL 1.310(b): Minimum 10 days notice required before deposition' + ) + scheduled_date = fields.Datetime(string='Deposition Date / Time') + location = fields.Char(string='Location / Zoom Link') + state = fields.Selection([ + ('draft', 'Drafting Notice'), + ('noticed', 'Notice Served'), + ('confirmed', 'Confirmed'), + ('completed', 'Completed'), + ('no_show', 'Deponent No-Show'), + ('cancelled', 'Cancelled'), + ('rescheduled', 'Rescheduled'), + ], string='Status', default='draft', tracking=True) + duces_tecum = fields.Boolean( + string='Duces Tecum (Document Production)', + help='Deponent is required to bring documents' + ) + max_duration_hours = fields.Float( + default=7.0, + help='FL 1.310(d): Maximum 7 hours per deponent per day' + ) + income_verified = fields.Boolean(string='Income Figures Verified') + income_verified_amount = fields.Float(string='Verified Income Amount ($)') + key_findings = fields.Text(string='Key Findings') + notes = fields.Text(string='Notes') diff --git a/activeblue_familylaw/models/fl_discovery.py b/activeblue_familylaw/models/fl_discovery.py new file mode 100644 index 0000000..cb6a100 --- /dev/null +++ b/activeblue_familylaw/models/fl_discovery.py @@ -0,0 +1,70 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta + + +class FlDiscovery(models.Model): + """ + Phase 3 — Full implementation. + Phase 1: Stub with core fields. + + Covers all FL discovery methods: + - Interrogatories (FL 1.340) — 30 days to respond + - Request for Production (FL 1.350) — 30 days to respond + - Request for Admissions (FL 1.370) — 30 days to respond + - Subpoena — third party documents (FL 1.351) + - Depositions — tracked in fl.deposition + """ + _name = 'fl.discovery' + _description = 'Discovery Item' + _inherit = ['mail.thread'] + _order = 'served_date asc' + + case_id = fields.Many2one( + 'fl.case', required=True, ondelete='cascade', index=True + ) + discovery_type = fields.Selection([ + ('interrogatories', 'Interrogatories (FL 1.340)'), + ('production', 'Request for Production (FL 1.350)'), + ('admissions', 'Request for Admissions (FL 1.370)'), + ('subpoena', 'Subpoena — Third Party (FL 1.351)'), + ('deposition', 'Deposition (FL 1.310)'), + ], string='Discovery Type', required=True) + directed_to = fields.Selection([ + ('petitioner', 'Petitioner'), + ('respondent', 'Respondent'), + ('third_party', 'Third Party'), + ], string='Directed To', required=True) + third_party_id = fields.Many2one( + 'res.partner', string='Third Party' + ) + description = fields.Char(string='Description / Subject') + served_date = fields.Date(string='Served Date') + response_due_date = fields.Date( + string='Response Due Date', + compute='_compute_response_due', store=True, + help='FL 1.340/1.350/1.370: 30 days to respond' + ) + response_received_date = fields.Date(string='Response Received Date') + response_complete = fields.Boolean(string='Response Complete') + objections_raised = fields.Boolean(string='Objections Raised') + objection_detail = fields.Text(string='Objection Details') + deficiency_notice_sent = fields.Boolean(string='Deficiency Notice Sent') + state = fields.Selection([ + ('draft', 'Drafting'), + ('served', 'Served'), + ('responded', 'Response Received'), + ('deficient', 'Response Deficient'), + ('compelled', 'Motion to Compel Filed'), + ('complete', 'Complete'), + ], string='Status', default='draft', tracking=True) + notes = fields.Text(string='Notes') + + @api.depends('served_date', 'discovery_type') + def _compute_response_due(self): + for rec in self: + if rec.served_date and rec.discovery_type != 'deposition': + rec.response_due_date = ( + rec.served_date + relativedelta(days=30) + ) + else: + rec.response_due_date = False diff --git a/activeblue_familylaw/models/fl_document.py b/activeblue_familylaw/models/fl_document.py new file mode 100644 index 0000000..878b8b3 --- /dev/null +++ b/activeblue_familylaw/models/fl_document.py @@ -0,0 +1,52 @@ +from odoo import fields, models + + +class FlDocument(models.Model): + """ + Phase 4 — Full implementation with QWeb report generation. + Phase 1: Stub with core fields. + """ + _name = 'fl.document' + _description = 'Case Document' + _inherit = ['mail.thread'] + _order = 'create_date desc' + + case_id = fields.Many2one( + 'fl.case', required=True, ondelete='cascade', index=True + ) + name = fields.Char(string='Document Name', required=True) + document_type = fields.Selection([ + ('financial_affidavit_short', 'Financial Affidavit Short (FL-12.902(b))'), + ('financial_affidavit_long', 'Financial Affidavit Long (FL-12.902(c))'), + ('support_worksheet', 'Child Support Worksheet (FL-12.902(e))'), + ('motion_to_modify', 'Motion to Modify Child Support'), + ('notice_deposition', 'Notice of Taking Deposition'), + ('motion_to_compel', 'Motion to Compel (FL 1.380)'), + ('motion_default', 'Motion for Default (FL 12.922)'), + ('income_withholding', 'Income Withholding Order (FL 61.1301)'), + ('parenting_plan', 'Parenting Plan (FL-12.995(a))'), + ('fee_waiver', 'Application for Civil Indigent Status (FL 57.082)'), + ('notice_ssn', 'Notice of SSN (FL-12.930(a))'), + ('mandatory_disclosure', 'Certificate of Mandatory Disclosure (FL-12.932)'), + ('duces_tecum', 'Subpoena Duces Tecum'), + ('other', 'Other Document'), + ], string='Document Type') + state = fields.Selection([ + ('draft', 'Draft'), + ('generated', 'Generated'), + ('signed', 'Signed'), + ('filed', 'Filed with Court'), + ], string='Status', default='draft', tracking=True) + attachment_ids = fields.Many2many( + 'ir.attachment', + 'fl_document_attachment_rel', + 'document_id', 'attachment_id', + string='Files' + ) + notes = fields.Text(string='Notes') + requires_notarization = fields.Boolean( + string='Requires Notarization', + help='Financial affidavits must be notarized before filing' + ) + notarized = fields.Boolean(string='Notarized') + filed_date = fields.Date(string='Filed with Court Date') diff --git a/activeblue_familylaw/models/fl_expense_case.py b/activeblue_familylaw/models/fl_expense_case.py new file mode 100644 index 0000000..b80818f --- /dev/null +++ b/activeblue_familylaw/models/fl_expense_case.py @@ -0,0 +1,27 @@ +from odoo import fields, models + + +class HrExpenseFLCase(models.Model): + """ + Extend hr.expense to link expenses to a family law case. + Enables cost-tracking per case (filing fees, process server, court reporter, etc.) + """ + _inherit = 'hr.expense' + + fl_case_id = fields.Many2one( + 'fl.case', + string='Family Law Case', + index=True, + help='Link this expense to a specific family law case for cost tracking' + ) + expense_category = fields.Selection([ + ('filing_fee', 'Court Filing Fee'), + ('service_fee', 'Service of Process Fee'), + ('court_reporter', 'Court Reporter'), + ('mediation', 'Mediation Fee'), + ('copying', 'Document Copying / Printing'), + ('postage', 'Postage / Certified Mail'), + ('translation', 'Translation / Interpretation'), + ('expert', 'Expert Witness'), + ('other', 'Other Case Expense'), + ], string='Case Expense Type') diff --git a/activeblue_familylaw/models/fl_fee_waiver.py b/activeblue_familylaw/models/fl_fee_waiver.py new file mode 100644 index 0000000..68f86ab --- /dev/null +++ b/activeblue_familylaw/models/fl_fee_waiver.py @@ -0,0 +1,162 @@ +from odoo import api, fields, models + + +class FlFeeWaiver(models.Model): + """ + FL Statute 57.082 — Determination of Civil Indigent Status. + Income threshold: < 200% of federal poverty level. + + 2025 Federal Poverty Level (HHS): + Base (1 person): $15,060/year + Per additional person: $5,380/year + + Update FPL_BASE and FPL_PER_PERSON constants annually. + Source: https://aspe.hhs.gov/topics/poverty-economic-mobility/poverty-guidelines + """ + _name = 'fl.fee.waiver' + _description = 'Fee Waiver / Civil Indigent Status (FL 57.082)' + _inherit = ['mail.thread'] + _order = 'create_date desc' + + # 2025 FPL constants — UPDATE ANNUALLY + FPL_BASE_2025 = 15060 # 1-person household + FPL_PER_PERSON_2025 = 5380 # Each additional person + + case_id = fields.Many2one( + 'fl.case', string='Case', + ondelete='cascade', index=True + ) + party_id = fields.Many2one( + 'res.partner', string='Applicant', + required=True + ) + + # ── Income & Household ───────────────────────────────────────────────── + + household_size = fields.Integer( + string='Household Size', + required=True, + default=1, + help='Include all persons in the household (applicant + dependents + spouse/partner)' + ) + monthly_gross_income = fields.Float( + string='Monthly Gross Income ($)', + required=True, + help='Total gross monthly income from all sources' + ) + annual_gross_income = fields.Float( + string='Annual Gross Income ($)', + compute='_compute_annual', store=True + ) + + # ── FPL Threshold Calculation ────────────────────────────────────────── + + fpl_annual = fields.Float( + string='Federal Poverty Level — Annual ($)', + compute='_compute_threshold', store=True, + help='100% FPL for this household size' + ) + fpl_200pct_threshold = fields.Float( + string='200% FPL Threshold ($)', + compute='_compute_threshold', store=True, + help='200% FPL — income must be below this to qualify' + ) + + eligible = fields.Boolean( + string='Fee Waiver Eligible', + compute='_compute_eligibility', store=True + ) + eligibility_note = fields.Char( + string='Eligibility Result', + compute='_compute_eligibility' + ) + + # ── Miami-Dade Mediator Fee Waiver (FL 44.108) ───────────────────────── + + mediator_fee_waiver_eligible = fields.Boolean( + string='Mediator Fee Waiver Eligible (FL 44.108)', + compute='_compute_mediator_waiver', store=True, + help='Miami-Dade County provides free mediators for qualifying income. ' + 'Same income threshold as civil indigent status (200% FPL).' + ) + + # ── Status ──────────────────────────────────────────────────────────── + + state = fields.Selection([ + ('draft', 'Draft'), + ('submitted', 'Submitted to Clerk'), + ('approved', 'Approved'), + ('denied', 'Denied'), + ], string='Status', default='draft', tracking=True) + + application_date = fields.Date( + string='Application Date', + default=fields.Date.today + ) + approval_date = fields.Date(string='Approval Date') + denial_reason = fields.Text(string='Denial Reason') + expiration_date = fields.Date( + string='Expiration Date', + help='Fee waiver approvals typically expire — check with clerk' + ) + clerk_stamp_attachment_ids = fields.Many2many( + 'ir.attachment', + 'fl_fee_waiver_attachment_rel', + 'waiver_id', 'attachment_id', + string='Clerk-Stamped Copy' + ) + notes = fields.Text(string='Notes') + + # ── Computed ────────────────────────────────────────────────────────── + + @api.depends('monthly_gross_income') + def _compute_annual(self): + for rec in self: + rec.annual_gross_income = (rec.monthly_gross_income or 0.0) * 12 + + @api.depends('household_size') + def _compute_threshold(self): + for rec in self: + size = max(rec.household_size or 1, 1) + fpl = self.FPL_BASE_2025 + (size - 1) * self.FPL_PER_PERSON_2025 + rec.fpl_annual = fpl + rec.fpl_200pct_threshold = fpl * 2 + + @api.depends('annual_gross_income', 'fpl_200pct_threshold') + def _compute_eligibility(self): + for rec in self: + annual = rec.annual_gross_income or 0.0 + threshold = rec.fpl_200pct_threshold or 0.0 + if annual and threshold: + rec.eligible = annual <= threshold + if rec.eligible: + rec.eligibility_note = ( + f'✅ ELIGIBLE — Annual income ${annual:,.0f} ' + f'is below 200% FPL (${threshold:,.0f})' + ) + else: + excess = annual - threshold + rec.eligibility_note = ( + f'❌ NOT ELIGIBLE — Annual income ${annual:,.0f} ' + f'exceeds 200% FPL (${threshold:,.0f}) ' + f'by ${excess:,.0f}' + ) + else: + rec.eligible = False + rec.eligibility_note = 'Enter income and household size to check eligibility' + + @api.depends('eligible') + def _compute_mediator_waiver(self): + # Miami-Dade FL 44.108 — same threshold as civil indigent status + for rec in self: + rec.mediator_fee_waiver_eligible = rec.eligible + + def action_submit(self): + self.state = 'submitted' + + def action_approve(self): + self.state = 'approved' + self.approval_date = fields.Date.today() + + def action_deny(self): + self.state = 'denied' diff --git a/activeblue_familylaw/models/fl_hearing.py b/activeblue_familylaw/models/fl_hearing.py new file mode 100644 index 0000000..e8b378b --- /dev/null +++ b/activeblue_familylaw/models/fl_hearing.py @@ -0,0 +1,47 @@ +from odoo import api, fields, models + + +class FlHearing(models.Model): + """ + Phase 2 — Full implementation. + Phase 1: Stub with fields needed by fl_case computed fields. + """ + _name = 'fl.hearing' + _description = 'Case Hearing' + _inherit = ['mail.thread'] + _order = 'hearing_date asc' + + case_id = fields.Many2one( + 'fl.case', required=True, ondelete='cascade', index=True + ) + name = fields.Char(string='Hearing Description', required=True) + hearing_date = fields.Datetime( + string='Hearing Date / Time', tracking=True + ) + location = fields.Char( + string='Location', + default='Lawson E. Thomas Courthouse Center, 175 NW 1st Ave, Miami, FL 33128' + ) + courtroom = fields.Char(string='Courtroom') + judge_id = fields.Many2one( + 'res.partner', string='Judge', + related='case_id.judge_id', store=True + ) + hearing_type = fields.Selection([ + ('status_conference', 'Status Conference'), + ('motion', 'Motion Hearing'), + ('temporary_relief', 'Temporary Relief Hearing'), + ('mediation', 'Mediation'), + ('final', 'Final Hearing'), + ('contempt', 'Contempt Hearing'), + ('other', 'Other'), + ], string='Hearing Type', default='final') + state = fields.Selection([ + ('scheduled', 'Scheduled'), + ('completed', 'Completed'), + ('cancelled', 'Cancelled'), + ('continued', 'Continued'), + ], string='Status', default='scheduled', tracking=True) + notes = fields.Text(string='Notes') + outcome = fields.Text(string='Outcome / Result') + order_entered = fields.Boolean(string='Order Entered') diff --git a/activeblue_familylaw/models/fl_income_withholding.py b/activeblue_familylaw/models/fl_income_withholding.py new file mode 100644 index 0000000..0669bf4 --- /dev/null +++ b/activeblue_familylaw/models/fl_income_withholding.py @@ -0,0 +1,165 @@ +from odoo import api, fields, models + + +class FlIncomeWithholding(models.Model): + """ + FL 61.1301 — Income Deduction (Withholding) Order. + + MANDATORY after every child support or alimony order unless: + - Good cause shown, OR + - Written agreement between parties for alternative payment method. + + All payments route through Florida State Disbursement Unit (SDU). + Employer receives copy and deducts from obligor's paycheck. + """ + _name = 'fl.income.withholding' + _description = 'Income Withholding Order (FL 61.1301)' + _inherit = ['mail.thread'] + _order = 'issued_date desc' + + case_id = fields.Many2one( + 'fl.case', string='Case', + ondelete='cascade', index=True + ) + name = fields.Char( + string='Reference', + compute='_compute_name', store=True + ) + + # ── Parties ──────────────────────────────────────────────────────────── + + obligor_id = fields.Many2one( + 'res.partner', + string='Obligor (Person Paying)', + required=True + ) + obligee_id = fields.Many2one( + 'res.partner', + string='Obligee (Person Receiving)', + required=True + ) + employer_id = fields.Many2one( + 'res.partner', + string="Obligor's Employer", + help='Employer who will receive and execute the withholding order' + ) + employer_contact = fields.Char( + string='Employer HR / Payroll Contact' + ) + + # ── Amounts ──────────────────────────────────────────────────────────── + + monthly_support_amount = fields.Float( + string='Monthly Support Amount ($)', + required=True, + tracking=True + ) + arrears_amount = fields.Float( + string='Arrears Amount ($)', + help='Total unpaid support owed prior to this order' + ) + arrears_monthly_payment = fields.Float( + string='Monthly Arrears Payment ($)', + help='Additional monthly amount toward arrears payoff' + ) + total_monthly_withholding = fields.Float( + string='Total Monthly Withholding ($)', + compute='_compute_total', store=True, + help='Support + arrears payment per month' + ) + + # ── Florida State Disbursement Unit ──────────────────────────────────── + # ALL payments must go through FL SDU — never directly to the other party + + fl_sdu_case_number = fields.Char( + string='FL SDU Case Number', + help='Florida State Disbursement Unit case number. ' + 'Obtain from DOR: https://www.floridarevenue.com/childsupport' + ) + fl_sdu_address = fields.Char( + string='FL SDU Payment Address', + default='Florida State Disbursement Unit, PO Box 8500, Tallahassee, FL 32314-8500', + readonly=True + ) + fl_sdu_website = fields.Char( + default='https://www.floridarevenue.com/childsupport', + readonly=True + ) + + # ── Dates & Status ──────────────────────────────────────────────────── + + issued_date = fields.Date( + string='Order Issued Date', + tracking=True + ) + served_on_employer_date = fields.Date( + string='Served on Employer Date' + ) + first_withholding_date = fields.Date( + string='First Withholding Payroll Date', + help='Date of first paycheck with withholding applied' + ) + + state = fields.Selection([ + ('draft', 'Draft'), + ('issued', 'Issued by Court'), + ('served', 'Served on Employer'), + ('active', 'Active — Withholding'), + ('modified', 'Modified'), + ('terminated', 'Terminated'), + ], string='Status', default='draft', tracking=True) + + # ── Exceptions (rare) ───────────────────────────────────────────────── + + good_cause_exception = fields.Boolean( + string='Good Cause Exception', + help='FL 61.1301(1)(a): Court may waive withholding for good cause shown. ' + 'Must be documented in court order. RARE.' + ) + written_agreement = fields.Boolean( + string='Written Agreement Between Parties', + help='Parties may agree in writing to an alternative payment method. ' + 'Agreement must be filed with the court.' + ) + exception_reason = fields.Text( + string='Exception Reason / Agreement Details' + ) + + # ── Notes ───────────────────────────────────────────────────────────── + + notes = fields.Text(string='Notes') + + # ── Computed ────────────────────────────────────────────────────────── + + @api.depends('case_id', 'issued_date') + def _compute_name(self): + for rec in self: + case_ref = rec.case_id.name if rec.case_id else 'NEW' + date_str = str(rec.issued_date) if rec.issued_date else 'Draft' + rec.name = f'IWO — {case_ref} — {date_str}' + + @api.depends('monthly_support_amount', 'arrears_monthly_payment') + def _compute_total(self): + for rec in self: + rec.total_monthly_withholding = ( + (rec.monthly_support_amount or 0.0) + + (rec.arrears_monthly_payment or 0.0) + ) + + # ── State Actions ───────────────────────────────────────────────────── + + def action_issue(self): + self.state = 'issued' + self.issued_date = self.issued_date or fields.Date.today() + + def action_serve(self): + self.state = 'served' + self.served_on_employer_date = ( + self.served_on_employer_date or fields.Date.today() + ) + + def action_activate(self): + self.state = 'active' + + def action_terminate(self): + self.state = 'terminated' diff --git a/activeblue_familylaw/models/fl_party.py b/activeblue_familylaw/models/fl_party.py new file mode 100644 index 0000000..300cf68 --- /dev/null +++ b/activeblue_familylaw/models/fl_party.py @@ -0,0 +1,318 @@ +from odoo import api, fields, models + + +class FlIncomeSource(models.Model): + _name = 'fl.income.source' + _description = 'Party Income Source' + _order = 'monthly_amount desc' + + party_id = fields.Many2one( + 'fl.party', string='Party', + required=True, ondelete='cascade', index=True + ) + source_type = fields.Selection([ + ('wages', 'Wages / Salary (W-2)'), + ('self_employment', 'Self-Employment / Business Income'), + ('rental', 'Rental Income'), + ('investment', 'Investment / Dividend Income'), + ('pension', 'Pension / Retirement'), + ('social_security', 'Social Security'), + ('disability', 'Disability Benefits (SSI/SSDI)'), + ('unemployment', 'Unemployment Compensation'), + ('workers_comp', "Workers' Compensation"), + ('alimony_received', 'Alimony Received'), + ('child_support_received', 'Child Support Received (other case)'), + ('overtime', 'Overtime / Bonuses'), + ('tips', 'Tips / Gratuities'), + ('commission', 'Commissions'), + ('trust', 'Trust / Estate Income'), + ('other', 'Other Income'), + ], string='Source Type', required=True) + description = fields.Char(string='Description / Payer') + monthly_amount = fields.Float(string='Monthly Amount ($)', required=True) + annual_amount = fields.Float( + string='Annual Amount ($)', + compute='_compute_annual', store=True + ) + verified = fields.Boolean( + string='Verified', + help='Income verified via documentation (pay stub, tax return, bank statement)' + ) + verification_document = fields.Char( + string='Verification Document', + help='e.g. "2023 W-2", "Last 3 pay stubs", "2023 1040 Schedule C"' + ) + + @api.depends('monthly_amount') + def _compute_annual(self): + for rec in self: + rec.annual_amount = rec.monthly_amount * 12 + + +class FlParty(models.Model): + _name = 'fl.party' + _description = 'Case Party Details' + _inherit = ['mail.thread'] + _order = 'role, id' + + case_id = fields.Many2one( + 'fl.case', string='Case', + required=True, ondelete='cascade', index=True + ) + partner_id = fields.Many2one( + 'res.partner', string='Contact', + required=True + ) + role = fields.Selection([ + ('petitioner', 'Petitioner'), + ('respondent', 'Respondent'), + ], string='Role', required=True) + display_name_computed = fields.Char( + string='Display Name', + compute='_compute_display_name_field', store=True + ) + + # ── Employment ───────────────────────────────────────────────────────── + + employment_type = fields.Selection([ + ('employed', 'W-2 Employed'), + ('self_employed', 'Self-Employed / Business Owner'), + ('unemployed', 'Unemployed'), + ('underemployed', 'Voluntarily Underemployed'), + ('disabled', 'Disabled — Receiving Benefits'), + ('retired', 'Retired'), + ('student', 'Full-Time Student'), + ('unknown', 'Unknown / To Be Discovered'), + ], string='Employment Type', required=True, default='employed', tracking=True) + + employer_name = fields.Char(string='Employer Name') + employer_address = fields.Char(string='Employer Address') + employer_phone = fields.Char( + string='Employer Phone', + help='Used for deposition notice and employer subpoena' + ) + + # ── Income (FL 61.30(2)) ─────────────────────────────────────────────── + + gross_monthly_income = fields.Float( + string='Gross Monthly Income ($)', + tracking=True + ) + income_source_ids = fields.One2many( + 'fl.income.source', 'party_id', + string='Income Sources' + ) + income_sources_total = fields.Float( + string='Income Sources Total ($)', + compute='_compute_income_sources_total', store=True, + help='Sum of all income source monthly amounts' + ) + + # Statutory deductions to reach Net Monthly Income (FL 61.30(3)) + fed_tax_monthly = fields.Float( + string='Federal Income Tax Withholding ($)', + help='Actual federal tax withheld per month (from pay stub)' + ) + fica_ss_monthly = fields.Float( + string='Social Security Tax (6.2%) ($)', + compute='_compute_fica', store=True + ) + fica_medicare_monthly = fields.Float( + string='Medicare Tax (1.45%) ($)', + compute='_compute_fica', store=True + ) + mandatory_retirement = fields.Float( + string='Mandatory Retirement Contribution ($)', + help='Only mandatory contributions qualify — not voluntary 401k' + ) + mandatory_union_dues = fields.Float(string='Mandatory Union Dues ($)') + health_insurance_self = fields.Float( + string='Health Insurance — Self Only ($)', + help='FL 61.30(3)(e): Self-only portion of health insurance premium. ' + 'Do NOT include the child portion here.' + ) + other_court_ordered_support = fields.Float( + string='Other Court-Ordered Child Support ($)', + help='Court-ordered support for children from other relationships (FL 61.30(3)(g))' + ) + + net_monthly_income = fields.Float( + string='Net Monthly Income ($)', + compute='_compute_net_income', store=True, + tracking=True, + help='Gross income minus all allowable FL 61.30(3) deductions' + ) + + # ── Income Imputation (FL 61.30(2)(b)) ─────────────────────────────── + + income_imputed = fields.Boolean( + string='Income Imputed', + help='Check if this party is voluntarily unemployed/underemployed ' + 'and income should be imputed' + ) + imputed_amount = fields.Float( + string='Imputed Monthly Income ($)', + help='FL 61.30(2)(b): Requires showing BOTH ability AND availability of work. ' + 'Default to FL minimum wage if voluntarily unemployed.' + ) + imputation_basis = fields.Text( + string='Basis for Imputation', + help='FL 61.30(2)(b): Document ability (education, skills, health) ' + 'AND availability (local job market, prior work history). ' + 'Cannot impute income to disabled party without medical evidence.' + ) + fl_minimum_wage_hourly = fields.Float( + string='FL Minimum Wage ($/hr)', + default=13.00, + help='Update annually per Florida minimum wage schedule. ' + '2025: $13.00/hr. Full-time = $13.00 × 40hr × 52wk / 12 = $2,253.33/mo' + ) + fl_minimum_wage_monthly = fields.Float( + string='FL Minimum Wage ($/mo)', + compute='_compute_fl_min_wage_monthly', store=True + ) + + effective_monthly_income = fields.Float( + string='Effective Monthly Income ($)', + compute='_compute_effective_income', store=True, + help='Net monthly income used in support calculation — ' + 'uses imputed amount if income_imputed = True' + ) + + # ── Lifestyle Analysis (Barner v. Barner) ──────────────────────────── + + lifestyle_inconsistency_flag = fields.Boolean( + string='Income/Lifestyle Inconsistency Flagged', + help='Flag when reported income appears inconsistent with ' + 'observed lifestyle (Barner v. Barner). ' + 'Document indicators: vehicles, vacations, residence, spending.' + ) + lifestyle_notes = fields.Text( + string='Lifestyle Analysis Notes', + help='Document specific lifestyle inconsistencies: ' + 'e.g., drives a 2023 BMW but reports $800/mo income; ' + 'took 3 vacations in past year; lives in $3,000/mo apartment.' + ) + + # ── Portal Access ────────────────────────────────────────────────────── + + portal_user_id = fields.Many2one( + 'res.users', string='Portal User Account' + ) + portal_access_granted = fields.Boolean( + string='Portal Access Granted' + ) + portal_invite_sent = fields.Boolean( + string='Portal Invitation Sent' + ) + preferred_language = fields.Selection([ + ('en_US', 'English'), + ('es_MX', 'Spanish / Español'), + ], string='Preferred Language', default='en_US') + + # ── SSN (FL-12.930(a)) ──────────────────────────────────────────────── + # SECURITY: Store last 4 digits only. + # Full SSN is NOT stored in this system. + # User handwrites full SSN on the physical FL-12.930(a) form before filing. + + ssn_last4 = fields.Char( + string='SSN Last 4 Digits', + size=4, + help='Last 4 digits only. Full SSN is not stored — ' + 'user handwrites SSN on physical FL-12.930(a) form.' + ) + ssn_notice_filed = fields.Boolean( + string='Notice of Social Security Number Filed (FL-12.930(a))', + help='Required to be filed with the court' + ) + ssn_handwritten_confirmed = fields.Boolean( + string='I have handwritten my SSN on the FL-12.930(a) form', + help='Checkbox confirms user has added SSN to physical form before filing' + ) + + # ── Service of Process ──────────────────────────────────────────────── + + service_address = fields.Text( + string='Address for Service of Process' + ) + service_method = fields.Selection([ + ('personal', 'Personal Service (Process Server)'), + ('certified_mail', 'Certified Mail (if agreed by both parties)'), + ('publication', 'Service by Publication (last resort — respondent location unknown)'), + ], string='Service Method') + process_server_id = fields.Many2one( + 'res.partner', string='Process Server' + ) + service_by_publication_warning = fields.Boolean( + compute='_compute_service_publication_warning' + ) + + # ── Computed ────────────────────────────────────────────────────────── + + @api.depends('partner_id', 'role') + def _compute_display_name_field(self): + for rec in self: + role_label = dict(rec._fields['role'].selection).get(rec.role, '') + name = rec.partner_id.name or '' + rec.display_name_computed = f'{name} ({role_label})' if role_label else name + + @api.depends('income_source_ids.monthly_amount') + def _compute_income_sources_total(self): + for rec in self: + rec.income_sources_total = sum( + rec.income_source_ids.mapped('monthly_amount') + ) + + @api.depends('gross_monthly_income') + def _compute_fica(self): + for rec in self: + gross = rec.gross_monthly_income or 0.0 + rec.fica_ss_monthly = round(gross * 0.062, 2) + rec.fica_medicare_monthly = round(gross * 0.0145, 2) + + @api.depends( + 'gross_monthly_income', + 'fed_tax_monthly', + 'fica_ss_monthly', + 'fica_medicare_monthly', + 'mandatory_retirement', + 'mandatory_union_dues', + 'health_insurance_self', + 'other_court_ordered_support', + ) + def _compute_net_income(self): + for rec in self: + gross = rec.gross_monthly_income or 0.0 + deductions = ( + (rec.fed_tax_monthly or 0.0) + + (rec.fica_ss_monthly or 0.0) + + (rec.fica_medicare_monthly or 0.0) + + (rec.mandatory_retirement or 0.0) + + (rec.mandatory_union_dues or 0.0) + + (rec.health_insurance_self or 0.0) + + (rec.other_court_ordered_support or 0.0) + ) + rec.net_monthly_income = max(gross - deductions, 0.0) + + @api.depends('fl_minimum_wage_hourly') + def _compute_fl_min_wage_monthly(self): + for rec in self: + # Full-time: 40hr/wk × 52wk / 12 months + rec.fl_minimum_wage_monthly = round( + rec.fl_minimum_wage_hourly * 40 * 52 / 12, 2 + ) + + @api.depends('net_monthly_income', 'income_imputed', 'imputed_amount') + def _compute_effective_income(self): + for rec in self: + if rec.income_imputed and rec.imputed_amount: + rec.effective_monthly_income = rec.imputed_amount + else: + rec.effective_monthly_income = rec.net_monthly_income + + @api.depends('service_method') + def _compute_service_publication_warning(self): + for rec in self: + rec.service_by_publication_warning = ( + rec.service_method == 'publication' + ) diff --git a/activeblue_familylaw/models/fl_statute.py b/activeblue_familylaw/models/fl_statute.py new file mode 100644 index 0000000..65c556c --- /dev/null +++ b/activeblue_familylaw/models/fl_statute.py @@ -0,0 +1,56 @@ +from odoo import fields, models + + +class FlStatute(models.Model): + _name = 'fl.statute' + _description = 'Florida Statute Reference' + _order = 'name' + + name = fields.Char( + string='Statute', + required=True, + help='e.g. FL 61.30' + ) + title = fields.Char(string='Title', required=True) + description = fields.Text(string='Summary') + category = fields.Selection([ + ('child_support', 'Child Support'), + ('modification', 'Modification'), + ('alimony', 'Alimony'), + ('timesharing', 'Timesharing / Parental Responsibility'), + ('dissolution', 'Dissolution of Marriage'), + ('paternity', 'Paternity'), + ('domestic_violence', 'Domestic Violence'), + ('enforcement', 'Enforcement'), + ('disclosure', 'Mandatory Disclosure'), + ('fee_waiver', 'Fee Waiver / Indigent Status'), + ('procedure', 'Procedure'), + ('discovery', 'Discovery'), + ('other', 'Other'), + ], string='Category') + url = fields.Char(string='Official URL') + active = fields.Boolean(default=True) + + _sql_constraints = [ + ('name_uniq', 'unique(name)', 'Statute reference must be unique.'), + ] + + +class FlIssueTag(models.Model): + _name = 'fl.issue.tag' + _description = 'Legal Issue Tag' + _order = 'name' + + name = fields.Char(string='Tag', required=True) + name_es = fields.Char(string='Tag (Spanish)') + color = fields.Integer(string='Color Index', default=0) + case_type = fields.Selection([ + ('modification', 'Modification'), + ('dissolution', 'Dissolution'), + ('paternity', 'Paternity'), + ('all', 'All Cases'), + ], string='Applies To', default='all') + + _sql_constraints = [ + ('name_uniq', 'unique(name)', 'Issue tag name must be unique.'), + ] diff --git a/activeblue_familylaw/models/fl_support.py b/activeblue_familylaw/models/fl_support.py new file mode 100644 index 0000000..f3f026f --- /dev/null +++ b/activeblue_familylaw/models/fl_support.py @@ -0,0 +1,426 @@ +from odoo import api, fields, models + + +class FlSupportScheduleEntry(models.Model): + """ + FL Department of Revenue — Basic Support Obligation Schedule. + Loaded from data/fl_support_schedule.xml. + MUST be updated annually when FL updates the schedule. + Source: https://www.floridarevenue.com/childsupport/guidelines + """ + _name = 'fl.support.schedule.entry' + _description = 'FL Support Obligation Schedule Entry' + _order = 'income_min, children_count' + + income_min = fields.Float( + string='Combined Income — Min ($/mo)', + required=True + ) + income_max = fields.Float( + string='Combined Income — Max ($/mo)', + required=True + ) + children_count = fields.Integer( + string='Number of Children', + required=True + ) + obligation_amount = fields.Float( + string='Basic Obligation ($)', + required=True + ) + effective_date = fields.Date( + string='Schedule Effective Date', + required=True + ) + active = fields.Boolean(default=True) + + def name_get(self): + result = [] + for rec in self: + result.append(( + rec.id, + f'${rec.income_min:,.0f}–${rec.income_max:,.0f} / ' + f'{rec.children_count} child(ren) = ${rec.obligation_amount:,.0f}' + )) + return result + + +class FlSupportCalculation(models.Model): + """ + FL 61.30 Child Support Guidelines Worksheet. + One calculation record per case (current vs. proposed vs. historical). + """ + _name = 'fl.support.calculation' + _description = 'FL 61.30 Child Support Calculation' + _inherit = ['mail.thread'] + _order = 'calculation_date desc' + + case_id = fields.Many2one( + 'fl.case', string='Case', + ondelete='cascade', index=True + ) + calculation_date = fields.Date( + string='Calculation Date', + default=fields.Date.today + ) + calculation_type = fields.Selection([ + ('current', 'Current Order (Baseline)'), + ('proposed', 'Proposed Modification'), + ('historical', 'Historical Reference'), + ], string='Type', default='proposed', required=True) + notes = fields.Text(string='Notes') + + # ── Party Net Incomes ────────────────────────────────────────────────── + + petitioner_net_income = fields.Float( + string='Petitioner Net Monthly Income ($)', + tracking=True + ) + respondent_net_income = fields.Float( + string='Respondent Net Monthly Income ($)', + tracking=True + ) + combined_net_income = fields.Float( + string='Combined Net Monthly Income ($)', + compute='_compute_combined', store=True + ) + petitioner_income_pct = fields.Float( + string='Petitioner Income %', + compute='_compute_income_pcts', store=True + ) + respondent_income_pct = fields.Float( + string='Respondent Income %', + compute='_compute_income_pcts', store=True + ) + + # ── Basic Support Obligation ─────────────────────────────────────────── + + number_of_children = fields.Integer( + string='Number of Children', + related='case_id.children_count', store=True + ) + basic_support_obligation = fields.Float( + string='Basic Support Obligation ($)', + compute='_compute_basic_obligation', store=True, + help='Looked up from FL DCF Schedule based on combined income and number of children' + ) + support_schedule_id = fields.Many2one( + 'fl.support.schedule.entry', + string='Schedule Entry Used', + compute='_compute_basic_obligation', store=True + ) + above_schedule = fields.Boolean( + string='Above Schedule Maximum', + compute='_compute_basic_obligation', store=True, + help='If True, FL 61.30(6) percentage formula was applied' + ) + + # ── Adjustments (FL 61.30(7),(8),(9)) ───────────────────────────────── + + # Health insurance for children (FL 61.30(8)) + child_health_insurance_total = fields.Float( + string='Child Health Insurance Premium Total ($)', + help='Total monthly premium for children only — not self-only portion' + ) + health_insurance_by_petitioner = fields.Float( + string=' Paid by Petitioner ($)' + ) + health_insurance_by_respondent = fields.Float( + string=' Paid by Respondent ($)' + ) + + # Work-related childcare (FL 61.30(7)) + childcare_total = fields.Float( + string='Work-Related Childcare Total ($)', + help='FL 61.30(7): Only work or job-search related childcare qualifies' + ) + childcare_by_petitioner = fields.Float( + string=' Paid by Petitioner ($)' + ) + childcare_by_respondent = fields.Float( + string=' Paid by Respondent ($)' + ) + + # Extraordinary expenses (FL 61.30(9)) + extraordinary_expenses = fields.Float( + string='Extraordinary Medical / Educational Expenses ($)', + help='FL 61.30(9): Expenses beyond ordinary child-rearing costs' + ) + + # ── Adjusted Support Obligation ──────────────────────────────────────── + + adjusted_support_obligation = fields.Float( + string='Adjusted Support Obligation ($)', + compute='_compute_adjusted_obligation', store=True, + help='Basic Obligation + Health Insurance + Childcare + Extraordinary' + ) + + # ── Timesharing Adjustment (FL 61.30(11)(b)) ────────────────────────── + + petitioner_overnights = fields.Integer( + string='Petitioner Overnights / Year', + related='case_id.petitioner_overnights', store=True + ) + respondent_overnights = fields.Integer( + string='Respondent Overnights / Year', + related='case_id.respondent_overnights', store=True + ) + substantial_timesharing = fields.Boolean( + string='Substantial Timesharing Applies', + related='case_id.substantial_timesharing_applies', store=True, + help='FL 61.30(11)(b): Applies if either parent has > 73 overnights/year (20%)' + ) + timesharing_adjustment = fields.Float( + string='Timesharing Adjustment ($)', + compute='_compute_timesharing_adjustment', store=True + ) + + # ── Final Obligations ────────────────────────────────────────────────── + + total_support_obligation = fields.Float( + string='Total Support Obligation ($)', + compute='_compute_total_obligation', store=True + ) + petitioner_obligation = fields.Float( + string='Petitioner Share ($)', + compute='_compute_party_obligations', store=True + ) + respondent_obligation = fields.Float( + string='Respondent Share ($)', + compute='_compute_party_obligations', store=True + ) + net_payment_amount = fields.Float( + string='Net Payment Amount ($)', + compute='_compute_net_payment', store=True, + help='Net amount actually exchanged between parties after credits' + ) + payment_direction = fields.Selection([ + ('petitioner_pays', 'Petitioner Pays Respondent'), + ('respondent_pays', 'Respondent Pays Petitioner'), + ('no_payment', 'No Net Payment'), + ], string='Payment Direction', + compute='_compute_net_payment', store=True + ) + + # ── Deviation (FL 61.30(1)(a)) ──────────────────────────────────────── + + deviation_requested = fields.Boolean( + string='Deviation Requested', + help='FL 61.30(1)(a): Court may deviate from guidelines with written findings' + ) + deviation_reason = fields.Text( + string='Deviation Reason', + help='Valid reasons: extraordinary medical needs, independent child income, ' + 'seasonal income variations, shared physical custody arrangements' + ) + deviation_amount = fields.Float( + string='Deviation Amount ($)', + help='Positive = above guidelines; Negative = below guidelines' + ) + final_amount_with_deviation = fields.Float( + string='Final Amount With Deviation ($)', + compute='_compute_final_with_deviation', store=True + ) + + # ── Summary ─────────────────────────────────────────────────────────── + + calculation_summary = fields.Text( + string='Calculation Summary', + compute='_compute_summary' + ) + + # ══════════════════════════════════════════════════════════════════════ + # COMPUTED METHODS + # ══════════════════════════════════════════════════════════════════════ + + @api.depends('petitioner_net_income', 'respondent_net_income') + def _compute_combined(self): + for rec in self: + rec.combined_net_income = ( + (rec.petitioner_net_income or 0.0) + + (rec.respondent_net_income or 0.0) + ) + + @api.depends('combined_net_income', 'petitioner_net_income', 'respondent_net_income') + def _compute_income_pcts(self): + for rec in self: + combined = rec.combined_net_income or 0.0 + if combined > 0: + rec.petitioner_income_pct = ( + rec.petitioner_net_income or 0.0 + ) / combined + rec.respondent_income_pct = ( + rec.respondent_net_income or 0.0 + ) / combined + else: + rec.petitioner_income_pct = 0.5 + rec.respondent_income_pct = 0.5 + + @api.depends('combined_net_income', 'number_of_children') + def _compute_basic_obligation(self): + """ + Look up Basic Support Obligation from FL DCF Schedule. + If combined income exceeds schedule maximum, apply FL 61.30(6) percentages: + 1 child = 5% + 2 children = 7.5% + 3 children = 9.5% + 4 children = 11% + 5 children = 12% + 6+ children = 12.5% + """ + PCT_MAP = {1: 0.05, 2: 0.075, 3: 0.095, 4: 0.11, 5: 0.12, 6: 0.125} + + for rec in self: + if not rec.combined_net_income or not rec.number_of_children: + rec.basic_support_obligation = 0.0 + rec.support_schedule_id = False + rec.above_schedule = False + continue + + children = min(rec.number_of_children, 6) + entry = self.env['fl.support.schedule.entry'].search([ + ('income_min', '<=', rec.combined_net_income), + ('income_max', '>=', rec.combined_net_income), + ('children_count', '=', children), + ('active', '=', True), + ], order='effective_date desc', limit=1) + + if entry: + rec.basic_support_obligation = entry.obligation_amount + rec.support_schedule_id = entry + rec.above_schedule = False + else: + # Above schedule maximum — FL 61.30(6) formula + pct = PCT_MAP.get(children, 0.125) + rec.basic_support_obligation = round( + rec.combined_net_income * pct, 2 + ) + rec.support_schedule_id = False + rec.above_schedule = True + + @api.depends( + 'basic_support_obligation', + 'child_health_insurance_total', + 'childcare_total', + 'extraordinary_expenses', + ) + def _compute_adjusted_obligation(self): + for rec in self: + rec.adjusted_support_obligation = ( + (rec.basic_support_obligation or 0.0) + + (rec.child_health_insurance_total or 0.0) + + (rec.childcare_total or 0.0) + + (rec.extraordinary_expenses or 0.0) + ) + + @api.depends( + 'adjusted_support_obligation', + 'petitioner_overnights', + 'substantial_timesharing', + 'petitioner_income_pct', + 'respondent_income_pct', + ) + def _compute_timesharing_adjustment(self): + """ + FL 61.30(11)(b) Substantial Timesharing Adjustment. + Only applies if either parent has > 73 overnights/year (20%). + + Formula: + 1. Each parent's share = adjusted_obligation × parent's income % + 2. Multiply each share × 1.5 (cross-credit factor) + 3. Multiply each result × other parent's timesharing % + 4. Net payment = |pet_adjusted - resp_adjusted| + """ + for rec in self: + if not rec.substantial_timesharing: + rec.timesharing_adjustment = 0.0 + continue + + pet_nights = rec.petitioner_overnights or 0 + resp_nights = 365 - pet_nights + pet_time_pct = pet_nights / 365 + resp_time_pct = resp_nights / 365 + + adj = rec.adjusted_support_obligation or 0.0 + pet_income_pct = rec.petitioner_income_pct or 0.5 + resp_income_pct = rec.respondent_income_pct or 0.5 + + pet_share = adj * pet_income_pct + resp_share = adj * resp_income_pct + + pet_adjusted = pet_share * 1.5 * resp_time_pct + resp_adjusted = resp_share * 1.5 * pet_time_pct + + rec.timesharing_adjustment = round(abs(pet_adjusted - resp_adjusted), 2) + + @api.depends('adjusted_support_obligation', 'timesharing_adjustment', 'substantial_timesharing') + def _compute_total_obligation(self): + for rec in self: + if rec.substantial_timesharing: + rec.total_support_obligation = rec.timesharing_adjustment + else: + rec.total_support_obligation = rec.adjusted_support_obligation + + @api.depends('total_support_obligation', 'petitioner_income_pct', 'respondent_income_pct') + def _compute_party_obligations(self): + for rec in self: + total = rec.total_support_obligation or 0.0 + rec.petitioner_obligation = round(total * (rec.petitioner_income_pct or 0.5), 2) + rec.respondent_obligation = round(total * (rec.respondent_income_pct or 0.5), 2) + + @api.depends( + 'petitioner_obligation', 'respondent_obligation', + 'health_insurance_by_petitioner', 'health_insurance_by_respondent', + 'childcare_by_petitioner', 'childcare_by_respondent', + ) + def _compute_net_payment(self): + for rec in self: + # Credit parties for amounts they directly pay + pet_credits = ( + (rec.health_insurance_by_petitioner or 0.0) + + (rec.childcare_by_petitioner or 0.0) + ) + resp_credits = ( + (rec.health_insurance_by_respondent or 0.0) + + (rec.childcare_by_respondent or 0.0) + ) + pet_net = (rec.petitioner_obligation or 0.0) - pet_credits + resp_net = (rec.respondent_obligation or 0.0) - resp_credits + net = resp_net - pet_net + + rec.net_payment_amount = abs(net) + if net > 0.01: + rec.payment_direction = 'petitioner_pays' + elif net < -0.01: + rec.payment_direction = 'respondent_pays' + else: + rec.payment_direction = 'no_payment' + + @api.depends('total_support_obligation', 'deviation_requested', 'deviation_amount') + def _compute_final_with_deviation(self): + for rec in self: + if rec.deviation_requested and rec.deviation_amount: + rec.final_amount_with_deviation = ( + rec.total_support_obligation + rec.deviation_amount + ) + else: + rec.final_amount_with_deviation = rec.total_support_obligation + + @api.depends( + 'combined_net_income', 'basic_support_obligation', + 'adjusted_support_obligation', 'net_payment_amount', 'payment_direction' + ) + def _compute_summary(self): + direction_map = { + 'petitioner_pays': 'Petitioner → Respondent', + 'respondent_pays': 'Respondent → Petitioner', + 'no_payment': 'No net payment', + } + for rec in self: + direction = direction_map.get(rec.payment_direction or '', '') + rec.calculation_summary = ( + f'Combined income: ${rec.combined_net_income:,.2f}/mo | ' + f'Basic obligation: ${rec.basic_support_obligation:,.2f} | ' + f'Adjusted: ${rec.adjusted_support_obligation:,.2f} | ' + f'Net payment: ${rec.net_payment_amount:,.2f} — {direction}' + ) diff --git a/activeblue_familylaw/security/fl_security.xml b/activeblue_familylaw/security/fl_security.xml new file mode 100644 index 0000000..2d2921f --- /dev/null +++ b/activeblue_familylaw/security/fl_security.xml @@ -0,0 +1,103 @@ + + + + + + + Family Law + Florida Family Law Case Management + 100 + + + + + + + Family Law / Administrator + + + Full access to all family law models. Can configure AI engine, delete records, manage security. + + + + + Family Law / Paralegal + + + Can read/write cases and generate documents. Cannot delete records or access AI engine config. + + + + + Family Law / Portal Petitioner + + + Portal user — petitioner. Sees own cases only. Cannot see respondent financial data until mandatory disclosure complete. + + + + + Family Law / Portal Respondent + + + Portal user — respondent. Sees own case info. Cannot see petitioner private notes. + + + + + + + FL Case: Petitioner Portal Access + + [('petitioner_id.user_ids', 'in', [user.id])] + + + + + + + + + + FL Case: Respondent Portal Access + + [('respondent_id.user_ids', 'in', [user.id])] + + + + + + + + + + FL Party: Petitioner Portal Access + + [('case_id.petitioner_id.user_ids', 'in', [user.id])] + + + + + + + + + + FL Party: Respondent Portal Access + + [('case_id.respondent_id.user_ids', 'in', [user.id])] + + + + + + + + + diff --git a/activeblue_familylaw/security/ir.model.access.csv b/activeblue_familylaw/security/ir.model.access.csv new file mode 100644 index 0000000..78ed8cd --- /dev/null +++ b/activeblue_familylaw/security/ir.model.access.csv @@ -0,0 +1,84 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +# ── fl.case ────────────────────────────────────────────────────────────────── +access_fl_case_admin,fl.case admin,model_fl_case,group_admin,1,1,1,1 +access_fl_case_paralegal,fl.case paralegal,model_fl_case,group_paralegal,1,1,1,0 +access_fl_case_petitioner,fl.case petitioner portal,model_fl_case,group_portal_petitioner,1,1,0,0 +access_fl_case_respondent,fl.case respondent portal,model_fl_case,group_portal_respondent,1,0,0,0 +# ── fl.party ───────────────────────────────────────────────────────────────── +access_fl_party_admin,fl.party admin,model_fl_party,group_admin,1,1,1,1 +access_fl_party_paralegal,fl.party paralegal,model_fl_party,group_paralegal,1,1,1,0 +access_fl_party_petitioner,fl.party petitioner portal,model_fl_party,group_portal_petitioner,1,1,0,0 +access_fl_party_respondent,fl.party respondent portal,model_fl_party,group_portal_respondent,1,1,0,0 +# ── fl.income.source ───────────────────────────────────────────────────────── +access_fl_income_source_admin,fl.income.source admin,model_fl_income_source,group_admin,1,1,1,1 +access_fl_income_source_paralegal,fl.income.source paralegal,model_fl_income_source,group_paralegal,1,1,1,0 +access_fl_income_source_petitioner,fl.income.source petitioner,model_fl_income_source,group_portal_petitioner,1,1,0,0 +# ── fl.child ───────────────────────────────────────────────────────────────── +access_fl_child_admin,fl.child admin,model_fl_child,group_admin,1,1,1,1 +access_fl_child_paralegal,fl.child paralegal,model_fl_child,group_paralegal,1,1,1,0 +access_fl_child_petitioner,fl.child petitioner portal,model_fl_child,group_portal_petitioner,1,1,0,0 +access_fl_child_respondent,fl.child respondent portal,model_fl_child,group_portal_respondent,1,0,0,0 +# ── fl.support.calculation ─────────────────────────────────────────────────── +access_fl_support_calc_admin,fl.support.calculation admin,model_fl_support_calculation,group_admin,1,1,1,1 +access_fl_support_calc_paralegal,fl.support.calculation paralegal,model_fl_support_calculation,group_paralegal,1,1,1,0 +access_fl_support_calc_petitioner,fl.support.calculation petitioner,model_fl_support_calculation,group_portal_petitioner,1,1,1,0 +access_fl_support_calc_respondent,fl.support.calculation respondent,model_fl_support_calculation,group_portal_respondent,1,0,0,0 +# ── fl.support.schedule.entry ──────────────────────────────────────────────── +access_fl_support_schedule_admin,fl.support.schedule.entry admin,model_fl_support_schedule_entry,group_admin,1,1,1,1 +access_fl_support_schedule_paralegal,fl.support.schedule.entry paralegal,model_fl_support_schedule_entry,group_paralegal,1,0,0,0 +access_fl_support_schedule_petitioner,fl.support.schedule.entry petitioner,model_fl_support_schedule_entry,group_portal_petitioner,1,0,0,0 +# ── fl.statute ─────────────────────────────────────────────────────────────── +access_fl_statute_admin,fl.statute admin,model_fl_statute,group_admin,1,1,1,1 +access_fl_statute_paralegal,fl.statute paralegal,model_fl_statute,group_paralegal,1,0,0,0 +access_fl_statute_petitioner,fl.statute petitioner,model_fl_statute,group_portal_petitioner,1,0,0,0 +# ── fl.issue.tag ───────────────────────────────────────────────────────────── +access_fl_issue_tag_admin,fl.issue.tag admin,model_fl_issue_tag,group_admin,1,1,1,1 +access_fl_issue_tag_paralegal,fl.issue.tag paralegal,model_fl_issue_tag,group_paralegal,1,0,0,0 +access_fl_issue_tag_petitioner,fl.issue.tag petitioner,model_fl_issue_tag,group_portal_petitioner,1,0,0,0 +# ── fl.deadline ────────────────────────────────────────────────────────────── +access_fl_deadline_admin,fl.deadline admin,model_fl_deadline,group_admin,1,1,1,1 +access_fl_deadline_paralegal,fl.deadline paralegal,model_fl_deadline,group_paralegal,1,1,1,0 +access_fl_deadline_petitioner,fl.deadline petitioner,model_fl_deadline,group_portal_petitioner,1,0,0,0 +# ── fl.hearing ─────────────────────────────────────────────────────────────── +access_fl_hearing_admin,fl.hearing admin,model_fl_hearing,group_admin,1,1,1,1 +access_fl_hearing_paralegal,fl.hearing paralegal,model_fl_hearing,group_paralegal,1,1,1,0 +access_fl_hearing_petitioner,fl.hearing petitioner,model_fl_hearing,group_portal_petitioner,1,0,0,0 +# ── fl.deposition ──────────────────────────────────────────────────────────── +access_fl_deposition_admin,fl.deposition admin,model_fl_deposition,group_admin,1,1,1,1 +access_fl_deposition_paralegal,fl.deposition paralegal,model_fl_deposition,group_paralegal,1,1,1,0 +access_fl_deposition_petitioner,fl.deposition petitioner,model_fl_deposition,group_portal_petitioner,1,0,0,0 +# ── fl.discovery ───────────────────────────────────────────────────────────── +access_fl_discovery_admin,fl.discovery admin,model_fl_discovery,group_admin,1,1,1,1 +access_fl_discovery_paralegal,fl.discovery paralegal,model_fl_discovery,group_paralegal,1,1,1,0 +access_fl_discovery_petitioner,fl.discovery petitioner,model_fl_discovery,group_portal_petitioner,1,0,0,0 +# ── fl.document ────────────────────────────────────────────────────────────── +access_fl_document_admin,fl.document admin,model_fl_document,group_admin,1,1,1,1 +access_fl_document_paralegal,fl.document paralegal,model_fl_document,group_paralegal,1,1,1,0 +access_fl_document_petitioner,fl.document petitioner,model_fl_document,group_portal_petitioner,1,0,0,0 +# ── fl.caselaw ─────────────────────────────────────────────────────────────── +access_fl_caselaw_admin,fl.caselaw admin,model_fl_caselaw,group_admin,1,1,1,1 +access_fl_caselaw_paralegal,fl.caselaw paralegal,model_fl_caselaw,group_paralegal,1,1,0,0 +access_fl_caselaw_petitioner,fl.caselaw petitioner,model_fl_caselaw,group_portal_petitioner,1,0,0,0 +# ── fl.analysis ────────────────────────────────────────────────────────────── +access_fl_analysis_admin,fl.analysis admin,model_fl_analysis,group_admin,1,1,1,1 +access_fl_analysis_paralegal,fl.analysis paralegal,model_fl_analysis,group_paralegal,1,0,0,0 +access_fl_analysis_petitioner,fl.analysis petitioner,model_fl_analysis,group_portal_petitioner,1,0,0,0 +# ── fl.argument ────────────────────────────────────────────────────────────── +access_fl_argument_admin,fl.argument admin,model_fl_argument,group_admin,1,1,1,1 +access_fl_argument_paralegal,fl.argument paralegal,model_fl_argument,group_paralegal,1,1,0,0 +# ── fl.fee.waiver ──────────────────────────────────────────────────────────── +access_fl_fee_waiver_admin,fl.fee.waiver admin,model_fl_fee_waiver,group_admin,1,1,1,1 +access_fl_fee_waiver_paralegal,fl.fee.waiver paralegal,model_fl_fee_waiver,group_paralegal,1,1,1,0 +access_fl_fee_waiver_petitioner,fl.fee.waiver petitioner,model_fl_fee_waiver,group_portal_petitioner,1,1,1,0 +# ── fl.income.withholding ───────────────────────────────────────────────────── +access_fl_income_withholding_admin,fl.income.withholding admin,model_fl_income_withholding,group_admin,1,1,1,1 +access_fl_income_withholding_paralegal,fl.income.withholding paralegal,model_fl_income_withholding,group_paralegal,1,1,1,0 +access_fl_income_withholding_petitioner,fl.income.withholding petitioner,model_fl_income_withholding,group_portal_petitioner,1,0,0,0 +# ── fl.intake.wizard ───────────────────────────────────────────────────────── +access_fl_intake_wizard_admin,fl.intake.wizard admin,model_fl_intake_wizard,group_admin,1,1,1,1 +access_fl_intake_wizard_paralegal,fl.intake.wizard paralegal,model_fl_intake_wizard,group_paralegal,1,1,1,1 +# ── fl.analysis.wizard ─────────────────────────────────────────────────────── +access_fl_analysis_wizard_admin,fl.analysis.wizard admin,model_fl_analysis_wizard,group_admin,1,1,1,1 +# ── fl.generate.packet.wizard ──────────────────────────────────────────────── +access_fl_generate_packet_wizard_admin,fl.generate.packet.wizard admin,model_fl_generate_packet_wizard,group_admin,1,1,1,1 +access_fl_generate_packet_wizard_paralegal,fl.generate.packet.wizard paralegal,model_fl_generate_packet_wizard,group_paralegal,1,1,1,1 diff --git a/activeblue_familylaw/static/src/css/familylaw_portal.css b/activeblue_familylaw/static/src/css/familylaw_portal.css new file mode 100644 index 0000000..1106e71 --- /dev/null +++ b/activeblue_familylaw/static/src/css/familylaw_portal.css @@ -0,0 +1,56 @@ +/* ActiveBlue Family Law — Portal CSS + Phase 6: Full portal styling + Phase 1: Stub +*/ + +/* Attorney referral banner */ +.fl-attorney-referral-banner { + background: #f8d7da; + border: 2px solid #dc3545; + border-radius: 6px; + padding: 16px; + margin-bottom: 20px; + color: #721c24; + font-weight: bold; +} + +/* DV safety banner */ +.fl-dv-safety-banner { + background: #fff3cd; + border: 2px solid #ffc107; + border-radius: 6px; + padding: 16px; + margin-bottom: 20px; +} + +/* Deadline urgency colors */ +.fl-deadline-overdue { + color: #dc3545; + font-weight: bold; +} +.fl-deadline-urgent { + color: #fd7e14; + font-weight: bold; +} +.fl-deadline-ok { + color: #28a745; +} + +/* Support calculator */ +.fl-calculator-result { + background: #d4edda; + border: 1px solid #28a745; + border-radius: 6px; + padding: 20px; + text-align: center; + font-size: 1.4em; + font-weight: bold; + color: #155724; + margin: 20px 0; +} + +/* Bilingual toggle */ +.fl-lang-toggle { + float: right; + margin-bottom: 10px; +} diff --git a/activeblue_familylaw/static/src/js/fl_calculator.js b/activeblue_familylaw/static/src/js/fl_calculator.js new file mode 100644 index 0000000..7643031 --- /dev/null +++ b/activeblue_familylaw/static/src/js/fl_calculator.js @@ -0,0 +1,9 @@ +/** @odoo-module **/ +/** + * ActiveBlue Family Law — FL 61.30 Interactive Calculator Widget + * Phase 6: Full interactive implementation + * Phase 1: Stub + */ + +// Placeholder — full FL 61.30 interactive widget implemented in Phase 6 +console.log('[ActiveBlue FamilyLaw] Calculator widget loaded (Phase 1 stub)'); diff --git a/activeblue_familylaw/static/src/js/fl_timeline.js b/activeblue_familylaw/static/src/js/fl_timeline.js new file mode 100644 index 0000000..87fe191 --- /dev/null +++ b/activeblue_familylaw/static/src/js/fl_timeline.js @@ -0,0 +1,9 @@ +/** @odoo-module **/ +/** + * ActiveBlue Family Law — Visual Timeline Widget + * Phase 6: Full visual timeline + * Phase 1: Stub + */ + +// Placeholder — visual deadline timeline implemented in Phase 6 +console.log('[ActiveBlue FamilyLaw] Timeline widget loaded (Phase 1 stub)'); diff --git a/activeblue_familylaw/views/fl_analysis_views.xml b/activeblue_familylaw/views/fl_analysis_views.xml new file mode 100644 index 0000000..aa6cd38 --- /dev/null +++ b/activeblue_familylaw/views/fl_analysis_views.xml @@ -0,0 +1,57 @@ + + + + + + fl.analysis.tree + fl.analysis + + + + + + + + + + + + + + + fl.analysis.form + fl.analysis + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
diff --git a/activeblue_familylaw/views/fl_case_views.xml b/activeblue_familylaw/views/fl_case_views.xml new file mode 100644 index 0000000..3fa9561 --- /dev/null +++ b/activeblue_familylaw/views/fl_case_views.xml @@ -0,0 +1,388 @@ + + + + + + + fl.case.form + fl.case + +
+
+ +
+ + + + + + + + +
+ + +
+ +
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +