title: "TICKET_036: Odoo Development Infrastructure — OCA Tools, v19 Reference, CI/CD, Build Monitor"
type: ticket
status: ready
priority: P0
sprint: S001_base_and_mvp1
assignee: "Sonnet 4.6 (Coder Swarm) + Founder (GitHub/Odoo.sh config)"
estimated_hours: 3
depends_on: []
blocks: [TICKET_035_rewrite]

TICKET_036: Odoo Development Infrastructure

Objective

Set up the foundational infrastructure so that every future Odoo module change is:

  1. Validated LOCALLY against actual v19 source before pushing
  2. Tested AUTOMATICALLY via GitHub Actions CI on every PR/push
  3. Monitored on Odoo.sh with automated log fetching and error analysis
  4. Checked for v20 deprecations before merge

Zero guessing at model names. Zero ignored errors. Zero manual log scanning.

Context

Tasks

Task 1: Clone Odoo v19 Community Source as Reference

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"

Task 2: Get Odoo v19 Enterprise Source as Reference

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 ''}")

Task 3: Create the Model Verification Script

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()

Task 4: Create the Odoo.sh Build Monitor Script

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

Task 5: Create Roo Code Skill File

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>

Step 2: Verify fields exist

python3 /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py \
  --check-field <model.name> <field_name>

Step 3: After writing XML, validate before commit

python3 /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_v19_verify/verify_models.py \
  path/to/data_file.xml

Step 4: After push, monitor build

export GITHUB_TOKEN=<token>
bash /Volumes/CORSAIR/STRUXIO_HardDrive/STRUXIO_Workspace/STRUXIO_Logic/skills/odoo_sh_monitor/monitor_build.sh

RULES

  1. NEVER guess model or field names. Always grep the v19 reference.
  2. NEVER ignore errors or warnings in build logs.
  3. ALWAYS check for v20 deprecation warnings.
  4. ALWAYS validate XML files before committing.
  5. ALWAYS run the build monitor after push and wait for results.
  6. If build fails, fix the root cause — never skip DEV stage.
  7. Report progress to Founder while waiting for builds.
  8. Demo data is a friend — if our changes conflict with it, fix our changes.

Reference Paths


### 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

Task 7: Add .gitignore Entries for Reference Sources

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/

Execution Order

  1. Task 1 — Clone community source (Founder runs on Mac terminal)
  2. Task 2 — Extract enterprise source (Founder runs SSH commands)
  3. Task 3 — Sonnet creates verifier script
  4. Task 4 — Sonnet creates monitor script
  5. Task 5 — Sonnet creates Roo Code skill file
  6. Task 6 — Sonnet creates v20 checklist
  7. Task 7 — Sonnet adds gitignore entries

Acceptance Criteria

Test

# 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