Files
famlaw_v2/scripts/validate_module.py
tocmo0nlord 935394620b Rename module to activeblue_familylaw_v2 (coexist with legacy prod module)
Production already has a DIFFERENT, earlier module installed as `activeblue_familylaw`
(models fl.*, real data). Renamed this build's technical name to
`activeblue_familylaw_v2` so it installs ALONGSIDE the legacy app instead of replacing
it. Models (familylaw.*) and test tags (familylaw_step<N>) are unchanged — only the
module name and its group XML IDs change.

Changes:
- Folder activeblue_familylaw -> activeblue_familylaw_v2 (git mv)
- All 44 dotted refs activeblue_familylaw. -> activeblue_familylaw_v2.
  (security group XML IDs in views/python; test patch targets odoo.addons.*)
- Manifest display name -> "Active Blue Family Law v2"; root menu -> "Family Law (v2)"
- scripts/validate_module.py ROOT path; BUILD_STATUS.md run commands + coexistence
  note; START_HERE.md pointer

Verified in live Odoo 18:
- Fresh install + full suite: 200 tests, 0 failed, 0 errors.
- COEXISTENCE on a clone of prod db1: installing _v2 alongside the installed legacy
  activeblue_familylaw left the legacy untouched (still installed 18.0.1.0.0, fl.*
  models registered, fl_caselaw 25 rows intact) while adding the 30 familylaw.* models.
  Exit 0, no errors. Clone dropped; prod DBs untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:23:34 +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_v2",
)
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")