Odoo ERP ported to Go — complete backend + original OWL frontend
Full port of Odoo's ERP system from Python to Go, with the original Odoo JavaScript frontend (OWL framework) running against the Go server. Backend (10,691 LoC Go): - Custom ORM: CRUD, domains→SQL with JOINs, computed fields, sequences - 93 models across 14 modules (base, account, sale, stock, purchase, hr, project, crm, fleet, product, l10n_de, google_address/translate/calendar) - Auth with bcrypt + session cookies - Setup wizard (company, SKR03 chart, admin, demo data) - Double-entry bookkeeping constraint - Sale→Invoice workflow (confirm SO → generate invoice → post) - SKR03 chart of accounts (110 accounts) + German taxes (USt/VSt) - Record rules (multi-company filter) - Google integrations as opt-in modules (Maps, Translate, Calendar) Frontend: - Odoo's original OWL webclient (503 JS modules, 378 XML templates) - JS transpiled via Odoo's js_transpiler (ES modules → odoo.define) - SCSS compiled to CSS (675KB) via dart-sass - XML templates compiled to registerTemplate() JS calls - Static file serving from Odoo source addons - Login page, session management, menu navigation - Contacts list view renders with real data from PostgreSQL Infrastructure: - 14MB single binary (CGO_ENABLED=0) - Docker Compose (Go server + PostgreSQL 16) - Zero phone-home (no outbound calls to odoo.com) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
tools/compile_templates.py
Normal file
102
tools/compile_templates.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Compile Odoo XML templates to a JS bundle with registerTemplate() calls.
|
||||
Mirrors: odoo/addons/base/models/assetsbundle.py generate_xml_bundle()
|
||||
|
||||
Usage:
|
||||
python3 tools/compile_templates.py <odoo_source_path> <output_file>
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from lxml import etree
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: compile_templates.py <odoo_source_path> <output_file>")
|
||||
sys.exit(1)
|
||||
|
||||
odoo_path = sys.argv[1]
|
||||
output_file = sys.argv[2]
|
||||
|
||||
# Read XML file list
|
||||
bundle_file = os.path.join(os.path.dirname(__file__), '..', 'pkg', 'server', 'assets_xml.txt')
|
||||
with open(bundle_file) as f:
|
||||
xml_urls = [line.strip() for line in f if line.strip()]
|
||||
|
||||
addons_dirs = [
|
||||
os.path.join(odoo_path, 'addons'),
|
||||
os.path.join(odoo_path, 'odoo', 'addons'),
|
||||
]
|
||||
|
||||
content = []
|
||||
count = 0
|
||||
errors = 0
|
||||
|
||||
for url_path in xml_urls:
|
||||
rel_path = url_path.lstrip('/')
|
||||
source_file = None
|
||||
for addons_dir in addons_dirs:
|
||||
candidate = os.path.join(addons_dir, rel_path)
|
||||
if os.path.isfile(candidate):
|
||||
source_file = candidate
|
||||
break
|
||||
|
||||
if not source_file:
|
||||
continue
|
||||
|
||||
try:
|
||||
tree = etree.parse(source_file)
|
||||
root = tree.getroot()
|
||||
|
||||
# Process each <templates> block
|
||||
if root.tag == 'templates':
|
||||
templates_el = root
|
||||
else:
|
||||
templates_el = root
|
||||
|
||||
for template in templates_el.iter():
|
||||
t_name = template.get('t-name')
|
||||
if not t_name:
|
||||
continue
|
||||
|
||||
inherit_from = template.get('t-inherit')
|
||||
template.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
||||
xml_string = etree.tostring(template, encoding='unicode')
|
||||
# Escape for JS template literal
|
||||
xml_string = xml_string.replace('\\', '\\\\').replace('`', '\\`').replace('${', '\\${')
|
||||
|
||||
# Templates with a t-name are always registered as primary templates.
|
||||
# If they have t-inherit, the templates.js _getTemplate function
|
||||
# handles the inheritance (cloning parent + applying xpath modifications).
|
||||
# registerTemplateExtension is ONLY for anonymous patches without t-name.
|
||||
content.append(f'registerTemplate("{t_name}", `{url_path}`, `{xml_string}`);')
|
||||
count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR parsing {url_path}: {e}")
|
||||
errors += 1
|
||||
|
||||
# Wrap in odoo.define with new-format module name so the loader resolves it
|
||||
js_output = f'''odoo.define("@web/bundle_xml", ["@web/core/templates"], function(require) {{
|
||||
"use strict";
|
||||
const {{ checkPrimaryTemplateParents, registerTemplate, registerTemplateExtension }} = require("@web/core/templates");
|
||||
|
||||
{chr(10).join(content)}
|
||||
}});
|
||||
|
||||
// Trigger the module in case the loader hasn't started it yet
|
||||
if (odoo.loader && odoo.loader.modules.has("@web/bundle_xml") === false) {{
|
||||
odoo.loader.startModule("@web/bundle_xml");
|
||||
}}
|
||||
'''
|
||||
|
||||
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(js_output)
|
||||
|
||||
print(f"Done: {count} templates compiled, {errors} errors")
|
||||
print(f"Output: {output_file}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
120
tools/transpile_assets.py
Normal file
120
tools/transpile_assets.py
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Transpile Odoo ES module JS files to odoo.define() format.
|
||||
Uses Odoo's built-in js_transpiler.py to convert:
|
||||
import { X } from "@web/core/foo" → odoo.define("@web/...", [...], function(require) {...})
|
||||
|
||||
Usage:
|
||||
python3 tools/transpile_assets.py <odoo_source_path> <output_dir>
|
||||
|
||||
Example:
|
||||
python3 tools/transpile_assets.py ../odoo build/js
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: transpile_assets.py <odoo_source_path> <output_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
odoo_path = sys.argv[1]
|
||||
output_dir = sys.argv[2]
|
||||
|
||||
# Mock odoo.tools.misc.OrderedSet before importing transpiler
|
||||
import types
|
||||
odoo_mock = types.ModuleType('odoo')
|
||||
odoo_tools_mock = types.ModuleType('odoo.tools')
|
||||
odoo_misc_mock = types.ModuleType('odoo.tools.misc')
|
||||
|
||||
class OrderedSet(dict):
|
||||
"""Minimal OrderedSet replacement using dict keys."""
|
||||
def __init__(self, iterable=None):
|
||||
super().__init__()
|
||||
if iterable:
|
||||
for item in iterable:
|
||||
self[item] = None
|
||||
def add(self, item):
|
||||
self[item] = None
|
||||
def __iter__(self):
|
||||
return iter(self.keys())
|
||||
def __contains__(self, item):
|
||||
return dict.__contains__(self, item)
|
||||
|
||||
odoo_misc_mock.OrderedSet = OrderedSet
|
||||
odoo_tools_mock.misc = odoo_misc_mock
|
||||
odoo_mock.tools = odoo_tools_mock
|
||||
sys.modules['odoo'] = odoo_mock
|
||||
sys.modules['odoo.tools'] = odoo_tools_mock
|
||||
sys.modules['odoo.tools.misc'] = odoo_misc_mock
|
||||
|
||||
# Import the transpiler directly
|
||||
import importlib.util
|
||||
transpiler_path = os.path.join(odoo_path, 'odoo', 'tools', 'js_transpiler.py')
|
||||
spec = importlib.util.spec_from_file_location("js_transpiler", transpiler_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
transpile_javascript = mod.transpile_javascript
|
||||
is_odoo_module = mod.is_odoo_module
|
||||
|
||||
# Read the JS bundle file list
|
||||
bundle_file = os.path.join(os.path.dirname(__file__), '..', 'pkg', 'server', 'assets_js.txt')
|
||||
with open(bundle_file) as f:
|
||||
js_files = [line.strip() for line in f if line.strip()]
|
||||
|
||||
addons_dirs = [
|
||||
os.path.join(odoo_path, 'addons'),
|
||||
os.path.join(odoo_path, 'odoo', 'addons'),
|
||||
]
|
||||
|
||||
transpiled = 0
|
||||
copied = 0
|
||||
errors = 0
|
||||
|
||||
for url_path in js_files:
|
||||
# url_path is like /web/static/src/env.js
|
||||
# Find the real file
|
||||
rel_path = url_path.lstrip('/')
|
||||
source_file = None
|
||||
for addons_dir in addons_dirs:
|
||||
candidate = os.path.join(addons_dir, rel_path)
|
||||
if os.path.isfile(candidate):
|
||||
source_file = candidate
|
||||
break
|
||||
|
||||
if not source_file:
|
||||
print(f" SKIP (not found): {url_path}")
|
||||
continue
|
||||
|
||||
# Output path
|
||||
out_file = os.path.join(output_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(out_file), exist_ok=True)
|
||||
|
||||
# Read source
|
||||
with open(source_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check if it's an odoo module (has import/export)
|
||||
if is_odoo_module(url_path, content):
|
||||
try:
|
||||
result = transpile_javascript(url_path, content)
|
||||
with open(out_file, 'w', encoding='utf-8') as f:
|
||||
f.write(result)
|
||||
transpiled += 1
|
||||
except Exception as e:
|
||||
print(f" ERROR transpiling {url_path}: {e}")
|
||||
# Copy as-is on error
|
||||
shutil.copy2(source_file, out_file)
|
||||
errors += 1
|
||||
else:
|
||||
# Not an ES module — copy as-is (e.g., libraries, legacy code)
|
||||
shutil.copy2(source_file, out_file)
|
||||
copied += 1
|
||||
|
||||
print(f"\nDone: {transpiled} transpiled, {copied} copied as-is, {errors} errors")
|
||||
print(f"Output: {output_dir}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user