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>
114 lines
3.6 KiB
Python
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")
|