Files
famlaw_v2/scripts/validate_module.py
tocmo0nlord 979bbfa14a Step 3: documents + the attorney review gate (Gate 1)
familylaw.document — every piece of work product, attached to a PROCEEDING:
- States: ai_draft -> attorney_review -> approved (+ rejected, filed, sent)
- AI-sourced documents born in ai_draft
- Gate 1 enforced in code: _ensure_approved() blocks file/send unless approved;
  _ensure_attorney() restricts approve/reject/file/send to the attorney group
- approved_by_id / approved_date stamped on approval; cleared on reject
- mail.thread audit on every transition
- proceeding.document_ids One2many; case_id derived (stored) from proceeding

Views: document list/form/search + menu; inline documents tab on proceeding form.
Security + manifest + menu updated.

Also folds in a correctness fix for Steps 2-3 view bindings: replaced the invalid
`view_id="..."` attribute on x2many fields with canonical inline <list> subviews
(case notebook tabs + proceeding documents tab). Avoids load-order/attr issues.

Added scripts/validate_module.py: static gate (py compile, xml well-formed,
forbidden-construct scan, button->method mapping, manifest+ACL integrity).

Tests (familylaw_step3): 16 tests — born-draft, Gate 1 (no file/send unapproved
from draft or review), attorney-only approve/reject/file/send, full happy path,
reject clears approval + resubmit, audit, proceeding linkage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:58:48 +00:00

114 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""Static validation for the activeblue_familylaw Odoo 18 module.
Not a substitute for running the Odoo test suite — it catches the classes of error
that are cheap to find without a running Odoo/DB:
* Python files compile
* XML files are well-formed
* no Odoo-18-forbidden constructs in views (attrs=, states=, <tree>)
* every type="object" button name maps to a real method in the model layer
* every file listed in __manifest__["data"] exists
* ir.model.access.csv references models that are defined
Run: python3 scripts/validate_module.py
"""
import ast
import csv
import os
import re
import sys
import xml.etree.ElementTree as ET
ROOT = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"activeblue_familylaw_handoff",
"activeblue_familylaw_build",
"activeblue_familylaw",
)
errors = []
ok_count = 0
def ok(_msg):
global ok_count
ok_count += 1
def walk(ext):
for dirpath, _dirs, files in os.walk(ROOT):
for f in files:
if f.endswith(ext):
yield os.path.join(dirpath, f)
# 1. Python compiles + collect method names + model names
methods = set()
model_names = set()
model_tech_names = set() # familylaw.document -> model_familylaw_document
for f in walk(".py"):
src = open(f).read()
try:
tree = ast.parse(src)
except SyntaxError as e:
errors.append(f"PYTHON SYNTAX {f}: {e}")
continue
ok(f)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
methods.add(node.name)
for m in re.finditer(r'_name\s*=\s*["\']([a-z0-9_.]+)["\']', src):
model_names.add(m.group(1))
model_tech_names.add("model_" + m.group(1).replace(".", "_"))
# 2. XML well-formed + forbidden constructs + collect buttons
buttons = []
for f in walk(".xml"):
raw = open(f).read()
try:
root = ET.fromstring(raw)
except ET.ParseError as e:
errors.append(f"XML PARSE {f}: {e}")
continue
ok(f)
# strip XML comments before scanning for forbidden tokens
no_comments = re.sub(r"<!--.*?-->", "", raw, flags=re.DOTALL)
for bad in ("attrs=", "states=", "<tree"):
if bad in no_comments:
errors.append(f"FORBIDDEN '{bad}' in {f}")
for btn in root.iter("button"):
if btn.get("type") == "object" and btn.get("name"):
buttons.append((btn.get("name"), f))
# 3. Buttons map to methods
for name, src in buttons:
if name not in methods:
errors.append(f"BUTTON '{name}' has no method (in {src})")
# 4. Manifest data files exist
manifest_path = os.path.join(ROOT, "__manifest__.py")
manifest = ast.literal_eval(
re.sub(r"^#.*", "", open(manifest_path).read(), flags=re.MULTILINE).strip()
)
for rel in manifest.get("data", []):
if not os.path.exists(os.path.join(ROOT, rel)):
errors.append(f"MANIFEST data file missing: {rel}")
# 5. ir.model.access.csv references defined models
acl = os.path.join(ROOT, "security", "ir.model.access.csv")
if os.path.exists(acl):
with open(acl) as fh:
for row in csv.DictReader(fh):
mid = row["model_id:id"]
# external models (base.*, mail.*) are fine; only check our own
if mid.startswith("model_familylaw_") and mid not in model_tech_names:
errors.append(f"ACL references unknown model: {mid}")
print(f"Checked OK: {ok_count} files | models: {len(model_names)} | "
f"buttons: {len(buttons)}")
if errors:
print("\n".join("FAIL: " + e for e in errors))
sys.exit(1)
print("ALL STATIC CHECKS PASSED")