Set up the foundational infrastructure so that every future Odoo module change is:
Zero guessing at model names. Zero ignored errors. Zero manual log scanning.
struxio-platform-main-29652199.dev.odoo.comSTRUXIO-ai/struxio-app on GitHub/Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_App/documents.facet, documents.folder) because we had no v19 reference source to verify against.cd /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/
# Community source (public)
git clone --depth 1 --branch 19.0 https://github.com/odoo/odoo.git odoo_v19_reference
# Verify
ls odoo_v19_reference/addons/documents/models/
ls odoo_v19_reference/addons/sale_subscription/models/
ls odoo_v19_reference/addons/crm/models/
echo "✅ Community v19 reference cloned"
Enterprise source is not on public GitHub. Extract from Odoo.sh build container:
# Method A: SSH into the Odoo.sh build and extract enterprise addons
# Find the SSH URL from Odoo.sh → Builds → main → Connect dropdown → SSH
# It looks like: ssh <user>@<buildname>.odoo.com
# Extract enterprise module source for key modules we depend on
ssh <ODOO_SH_SSH> 'tar czf - \
/home/odoo/src/enterprise/documents \
/home/odoo/src/enterprise/quality_control \
/home/odoo/src/enterprise/knowledge \
/home/odoo/src/enterprise/sale_subscription \
/home/odoo/src/enterprise/sign \
/home/odoo/src/enterprise/approvals \
/home/odoo/src/enterprise/documents_spreadsheet \
2>/dev/null' > /tmp/enterprise_modules.tar.gz
mkdir -p /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_enterprise_reference
tar xzf /tmp/enterprise_modules.tar.gz -C /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_enterprise_reference/ --strip-components=4
rm /tmp/enterprise_modules.tar.gz
# Verify
ls /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_enterprise_reference/documents/models/
echo "✅ Enterprise v19 reference extracted"
Method B (fallback): If SSH extraction fails, use Odoo.sh Shell:
# In Odoo.sh Shell, list all models in the documents module
models = env['ir.model'].search([('model','like','document%')])
for m in models:
fields = env['ir.model.fields'].search([('model_id','=',m.id)])
print(f"\n=== {m.model} ({m.name}) ===")
for f in fields[:20]:
print(f" {f.name}: {f.ttype} {'(required)' if f.required else ''}")
A script Roo Code runs BEFORE writing any Odoo XML. Saves it as a Roo Code skill.
Create file: STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py
#!/usr/bin/env python3
"""
Odoo v19 Model & Field Verifier
Run before writing ANY Odoo XML data files.
Verifies that model names and field names exist in the actual v19 source.
Usage:
python3 verify_models.py <xml_file>
python3 verify_models.py --check-model documents.folder
python3 verify_models.py --check-field product.template recurring_invoice
python3 verify_models.py --list-models documents
python3 verify_models.py --list-fields documents.document
"""
import argparse
import glob
import os
import re
import sys
import xml.etree.ElementTree as ET
# Reference paths
COMMUNITY_PATH = os.path.expanduser(
"/Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_reference/addons"
)
ENTERPRISE_PATH = os.path.expanduser(
"/Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_enterprise_reference"
)
def find_model_definition(model_name):
"""Search v19 source for a model definition by _name."""
pattern = f'_name\\s*=\\s*["\']({re.escape(model_name)})["\']'
for base_path in [COMMUNITY_PATH, ENTERPRISE_PATH]:
if not os.path.exists(base_path):
continue
for py_file in glob.glob(f"{base_path}/**/*.py", recursive=True):
try:
with open(py_file, 'r', errors='ignore') as f:
content = f.read()
if re.search(pattern, content):
return py_file
except Exception:
continue
return None
def find_field_in_model(model_name, field_name):
"""Search for a field definition in a model's source."""
model_file = find_model_definition(model_name)
if not model_file:
return None, "Model not found"
# Also check inherited models in the same directory
model_dir = os.path.dirname(model_file)
for py_file in glob.glob(f"{model_dir}/*.py"):
try:
with open(py_file, 'r', errors='ignore') as f:
content = f.read()
# Look for field definition
pattern = f'{field_name}\\s*=\\s*fields\\.'
if re.search(pattern, content):
# Extract field type
match = re.search(f'{field_name}\\s*=\\s*fields\\.(\\w+)', content)
field_type = match.group(1) if match else "Unknown"
return py_file, field_type
except Exception:
continue
return None, "Field not found"
def list_models(module_name):
"""List all models defined in a module."""
models = []
for base_path in [COMMUNITY_PATH, ENTERPRISE_PATH]:
module_path = os.path.join(base_path, module_name)
if not os.path.exists(module_path):
continue
for py_file in glob.glob(f"{module_path}/**/*.py", recursive=True):
try:
with open(py_file, 'r', errors='ignore') as f:
content = f.read()
for match in re.finditer(r'_name\s*=\s*["\']([^"\']+)["\']', content):
models.append((match.group(1), py_file))
except Exception:
continue
return models
def list_fields(model_name):
"""List all fields defined in a model."""
model_file = find_model_definition(model_name)
if not model_file:
return []
fields_found = []
model_dir = os.path.dirname(model_file)
for py_file in glob.glob(f"{model_dir}/*.py"):
try:
with open(py_file, 'r', errors='ignore') as f:
content = f.read()
for match in re.finditer(r'(\w+)\s*=\s*fields\.(\w+)\(', content):
fields_found.append((match.group(1), match.group(2), py_file))
except Exception:
continue
return fields_found
def validate_xml_file(xml_path):
"""Validate all model and field references in an Odoo XML data file."""
errors = []
warnings = []
try:
tree = ET.parse(xml_path)
except ET.ParseError as e:
errors.append(f"XML parse error: {e}")
return errors, warnings
root = tree.getroot()
for record in root.iter('record'):
model = record.get('model')
if not model:
continue
# Check model exists
model_file = find_model_definition(model)
if not model_file:
errors.append(f"Model '{model}' NOT FOUND in v19 source (record id='{record.get('id')}')")
continue
# Check fields exist
for field in record.findall('field'):
field_name = field.get('name')
if not field_name:
continue
# Skip common meta-fields that are always present
if field_name in ('name', 'id', 'create_date', 'write_date',
'create_uid', 'write_uid', 'active', 'sequence',
'display_name', 'company_id'):
continue
field_file, field_type = find_field_in_model(model, field_name)
if not field_file:
# Check if it's a relational field reference
ref = field.get('ref')
if ref and field_name.endswith('_id'):
# Likely a Many2one ref — check the base field name
pass
else:
errors.append(
f"Field '{field_name}' NOT FOUND on model '{model}' "
f"(record id='{record.get('id')}')"
)
# Check for v20 deprecation patterns
with open(xml_path, 'r') as f:
content = f.read()
if 'xmlrpc' in content.lower():
warnings.append("v20 DEPRECATION: XML-RPC references found — use JSON-RPC instead")
return errors, warnings
def main():
parser = argparse.ArgumentParser(description='Odoo v19 Model & Field Verifier')
parser.add_argument('xml_file', nargs='?', help='XML file to validate')
parser.add_argument('--check-model', help='Check if a model exists')
parser.add_argument('--check-field', nargs=2, metavar=('MODEL', 'FIELD'),
help='Check if a field exists on a model')
parser.add_argument('--list-models', help='List all models in a module')
parser.add_argument('--list-fields', help='List all fields on a model')
args = parser.parse_args()
if args.check_model:
result = find_model_definition(args.check_model)
if result:
print(f"✅ Model '{args.check_model}' found in: {result}")
else:
print(f"❌ Model '{args.check_model}' NOT FOUND in v19 source")
sys.exit(1)
elif args.check_field:
model, field = args.check_field
result, info = find_field_in_model(model, field)
if result:
print(f"✅ Field '{field}' on '{model}': type={info}, file={result}")
else:
print(f"❌ Field '{field}' NOT FOUND on model '{model}': {info}")
sys.exit(1)
elif args.list_models:
models = list_models(args.list_models)
if models:
print(f"Models in '{args.list_models}':")
for name, path in models:
print(f" {name} → {os.path.basename(path)}")
else:
print(f"No models found in module '{args.list_models}'")
elif args.list_fields:
fields = list_fields(args.list_fields)
if fields:
print(f"Fields on '{args.list_fields}':")
for name, ftype, path in sorted(fields):
print(f" {name}: {ftype}")
else:
print(f"No fields found for model '{args.list_fields}'")
elif args.xml_file:
print(f"Validating: {args.xml_file}")
errors, warnings = validate_xml_file(args.xml_file)
if warnings:
print(f"\n⚠️ WARNINGS ({len(warnings)}):")
for w in warnings:
print(f" {w}")
if errors:
print(f"\n❌ ERRORS ({len(errors)}):")
for e in errors:
print(f" {e}")
sys.exit(1)
else:
print(f"\n✅ All models and fields verified against v19 source")
else:
parser.print_help()
if __name__ == '__main__':
main()
Create file: STRUXIO_Logic/skills/odoo_sh_monitor/monitor_build.sh
#!/usr/bin/env bash
# Odoo.sh Build Monitor
# Waits for build to complete, fetches logs, analyzes errors.
#
# Usage: ./monitor_build.sh [--timeout 600]
#
# Requires: ODOOSH_SSH set to the SSH target
# or GITHUB_TOKEN + GITHUB_REPO for commit status polling
set -euo pipefail
TIMEOUT=${1:-600} # Default 10 minutes
POLL_INTERVAL=30
REPO="${GITHUB_REPO:-STRUXIO-ai/struxio-app}"
COMMIT_SHA=$(cd /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_App && git rev-parse HEAD)
echo "══════════════════════════════════════════════"
echo "ODOO.SH BUILD MONITOR"
echo "══════════════════════════════════════════════"
echo "Commit: ${COMMIT_SHA:0:8}"
echo "Repo: $REPO"
echo "Timeout: ${TIMEOUT}s"
echo ""
# Phase 1: Wait for build
elapsed=0
build_status="pending"
while [ "$elapsed" -lt "$TIMEOUT" ]; do
# Try GitHub commit status API first
if [ -n "${GITHUB_TOKEN:-}" ]; then
build_status=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$REPO/commits/$COMMIT_SHA/status" \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('state','pending'))" 2>/dev/null || echo "pending")
fi
echo "[$(date +%H:%M:%S)] Build status: $build_status (${elapsed}s / ${TIMEOUT}s)"
if [ "$build_status" = "success" ]; then
echo ""
echo "✅ BUILD PASSED"
break
elif [ "$build_status" = "failure" ] || [ "$build_status" = "error" ]; then
echo ""
echo "⚠️ BUILD COMPLETED WITH ISSUES — Fetching logs..."
break
fi
sleep "$POLL_INTERVAL"
elapsed=$((elapsed + POLL_INTERVAL))
done
if [ "$elapsed" -ge "$TIMEOUT" ]; then
echo "⏰ TIMEOUT — Build did not complete within ${TIMEOUT}s"
echo "Check manually: https://odoo.sh/project/struxio-platform/builds"
exit 1
fi
# Phase 2: Fetch and analyze logs via SSH
if [ -n "${ODOOSH_SSH:-}" ]; then
echo ""
echo "── Fetching install.log ──"
INSTALL_LOG=$(ssh "$ODOOSH_SSH" 'cat /home/odoo/.local/log/install.log 2>/dev/null | tail -200' 2>/dev/null || echo "SSH failed")
echo "── Fetching odoo.log ──"
ODOO_LOG=$(ssh "$ODOOSH_SSH" 'cat /var/log/odoo/odoo.log 2>/dev/null | tail -200' 2>/dev/null || echo "SSH failed")
# Phase 3: Analyze
echo ""
echo "══════════════════════════════════════════════"
echo "LOG ANALYSIS"
echo "══════════════════════════════════════════════"
# Check for our module errors
OUR_ERRORS=$(echo "$INSTALL_LOG" | grep -i "struxio_iso19650" | grep -i "error\|critical\|keyerror\|parseerror" || true)
if [ -n "$OUR_ERRORS" ]; then
echo ""
echo "🔴 OUR MODULE ERRORS:"
echo "$OUR_ERRORS"
fi
# Check for KeyErrors (wrong model/field names)
KEY_ERRORS=$(echo "$INSTALL_LOG" | grep "KeyError" || true)
if [ -n "$KEY_ERRORS" ]; then
echo ""
echo "🔴 MODEL/FIELD ERRORS:"
echo "$KEY_ERRORS"
fi
# Check for deprecation warnings
DEPRECATIONS=$(echo "$ODOO_LOG" | grep -i "deprecated\|removal in Odoo 20" || true)
if [ -n "$DEPRECATIONS" ]; then
echo ""
echo "🟡 v20 DEPRECATION WARNINGS:"
echo "$DEPRECATIONS" | head -5
fi
# Check for known platform bugs
KNOWN_BUGS=$(echo "$INSTALL_LOG" "$ODOO_LOG" | grep -c "FileNotFoundError.*filestore\|could not serialize access.*stock_warehouse\|No API key set for provider" || true)
if [ "$KNOWN_BUGS" -gt 0 ]; then
echo ""
echo "🟠 KNOWN PLATFORM BUGS: $KNOWN_BUGS occurrences (GitHub #247488)"
fi
# Check if our module loaded
MODULE_LOADED=$(echo "$INSTALL_LOG" | grep "loading struxio_iso19650" || true)
if [ -n "$MODULE_LOADED" ]; then
echo ""
echo "✅ Module struxio_iso19650 loaded successfully"
else
echo ""
echo "❌ Module struxio_iso19650 did NOT load"
fi
# Overall verdict
if [ -z "$OUR_ERRORS" ] && [ -z "$KEY_ERRORS" ] && [ -n "$MODULE_LOADED" ]; then
echo ""
echo "══════════════════════════════════════════════"
echo "✅ VERDICT: BUILD CLEAN — Ready for functional testing"
echo "══════════════════════════════════════════════"
exit 0
else
echo ""
echo "══════════════════════════════════════════════"
echo "❌ VERDICT: ERRORS FOUND — Fix required"
echo "══════════════════════════════════════════════"
exit 1
fi
else
echo "⚠️ ODOOSH_SSH not set — cannot fetch logs"
echo "Set it: export ODOOSH_SSH=<user>@<build>.odoo.com"
echo "Find it on Odoo.sh → Builds → Connect → SSH"
exit 1
fi
Create file: STRUXIO_Logic/skills/odoo_development/SKILL.md
# SKILL: Odoo v19 Module Development
## MANDATORY Pre-Flight for ANY Odoo XML/Python Change
Before writing ANY Odoo data file, model, or view:
### Step 1: Verify models exist
```bash
python3 /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py \
--list-models <module_name>
python3 /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py \
--check-field <model.name> <field_name>
python3 /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py \
path/to/data_file.xml
export GITHUB_TOKEN=<token>
bash /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_sh_monitor/monitor_build.sh
/Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_reference/addons//Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/odoo_v19_enterprise_reference//Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py/Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_sh_monitor/monitor_build.sh
### Task 6: Create v20 Compatibility Checklist
Create file: `STRUXIO_OS/03_tickets/sprints/S001_base_and_mvp1/v20_compatibility_checklist.md`
```markdown
---
title: "v20 Compatibility Checklist"
type: checklist
status: active
last_updated: "2026-03-16"
---
# Odoo v20 Compatibility Checklist
Track all deprecation warnings and ensure we use replacement APIs.
| Warning | Source | v19 API | v20 Replacement | Status |
|---|---|---|---|---|
| xmlrpc endpoints deprecated | odoo.log | `/xmlrpc/2/common`, `/xmlrpc/2/object` | `/jsonrpc` (JSON-RPC 2.0) | TODO |
| | | | | |
## Rules
- Every deprecation warning from Odoo logs gets added here
- Use replacement API from day one where possible
- Review before every sprint closes
Ensure the reference sources don't accidentally get committed:
Add to /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/.gitignore (workspace level):
odoo_v19_reference/
odoo_v19_enterprise_reference/
odoo_v19_reference/ exists with full community addonsodoo_v19_enterprise_reference/ exists with Documents, Quality, Knowledge, Subscriptions, Sign, Approvalsverify_models.py --list-models documents returns actual v19 model namesverify_models.py --check-model documents.facet returns ❌ NOT FOUNDverify_models.py --check-model documents.document returns ✅ FOUND monitor_build.sh successfully polls and reports build status# Verify the exact models that broke our build
cd /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/
python3 verify_models.py --check-model documents.facet
# Expected: ❌ NOT FOUND
python3 verify_models.py --check-model documents.folder
# Expected: ❌ NOT FOUND (or ✅ if it was renamed, not removed)
python3 verify_models.py --list-models documents
# Expected: List of actual v19 document models
python3 verify_models.py --check-field product.template recurring_invoice
# Expected: Shows whether this field exists or was renamed