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:
210
addons/google_address/models/google_address.go
Normal file
210
addons/google_address/models/google_address.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
"odoo-go/pkg/tools"
|
||||
)
|
||||
|
||||
// Google Maps API client — only initialized if API key is set.
|
||||
var mapsClient *tools.APIClient
|
||||
|
||||
func getClient() *tools.APIClient {
|
||||
if mapsClient != nil {
|
||||
return mapsClient
|
||||
}
|
||||
apiKey := os.Getenv("GOOGLE_MAPS_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil
|
||||
}
|
||||
mapsClient = tools.NewAPIClient("https://maps.googleapis.com", apiKey)
|
||||
return mapsClient
|
||||
}
|
||||
|
||||
// initGoogleAddress extends res.partner with geocoding fields and methods.
|
||||
func initGoogleAddress() {
|
||||
// Extend res.partner with lat/lng fields
|
||||
partner := orm.Registry.Get("res.partner")
|
||||
if partner != nil {
|
||||
partner.Extend(
|
||||
orm.Float("partner_latitude", orm.FieldOpts{String: "Geo Latitude"}),
|
||||
orm.Float("partner_longitude", orm.FieldOpts{String: "Geo Longitude"}),
|
||||
)
|
||||
|
||||
// geo_localize: Geocode partner address → lat/lng
|
||||
// Calls Google Geocoding API: https://maps.googleapis.com/maps/api/geocode/json
|
||||
partner.RegisterMethod("geo_localize", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
// Read address fields
|
||||
var street, city, zip, country string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(street,''), COALESCE(city,''), COALESCE(zip,''),
|
||||
COALESCE((SELECT code FROM res_country WHERE id = p.country_id), '')
|
||||
FROM res_partner p WHERE p.id = $1`, id,
|
||||
).Scan(&street, &city, &zip, &country)
|
||||
|
||||
address := fmt.Sprintf("%s, %s %s, %s", street, zip, city, country)
|
||||
|
||||
// Call Geocoding API
|
||||
var result GeocodingResponse
|
||||
err := client.GetJSON("/maps/api/geocode/json", map[string]string{
|
||||
"address": address,
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_address: geocode failed: %w", err)
|
||||
}
|
||||
|
||||
if result.Status == "OK" && len(result.Results) > 0 {
|
||||
loc := result.Results[0].Geometry.Location
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE res_partner SET partner_latitude = $1, partner_longitude = $2 WHERE id = $3`,
|
||||
loc.Lat, loc.Lng, id)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// address_autocomplete: Search for addresses via Google Places
|
||||
// Returns suggestions for autocomplete in the UI.
|
||||
partner.RegisterMethod("address_autocomplete", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
query := ""
|
||||
if len(args) > 0 {
|
||||
query, _ = args[0].(string)
|
||||
}
|
||||
if query == "" {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
var result AutocompleteResponse
|
||||
err := client.GetJSON("/maps/api/place/autocomplete/json", map[string]string{
|
||||
"input": query,
|
||||
"types": "address",
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var suggestions []map[string]interface{}
|
||||
for _, p := range result.Predictions {
|
||||
suggestions = append(suggestions, map[string]interface{}{
|
||||
"description": p.Description,
|
||||
"place_id": p.PlaceID,
|
||||
})
|
||||
}
|
||||
return suggestions, nil
|
||||
})
|
||||
|
||||
// place_details: Get full address from place_id
|
||||
partner.RegisterMethod("place_details", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
placeID := ""
|
||||
if len(args) > 0 {
|
||||
placeID, _ = args[0].(string)
|
||||
}
|
||||
if placeID == "" {
|
||||
return nil, fmt.Errorf("google_address: place_id required")
|
||||
}
|
||||
|
||||
var result PlaceDetailsResponse
|
||||
err := client.GetJSON("/maps/api/place/details/json", map[string]string{
|
||||
"place_id": placeID,
|
||||
"fields": "address_components,geometry,formatted_address",
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Status != "OK" {
|
||||
return nil, fmt.Errorf("google_address: place details failed: %s", result.Status)
|
||||
}
|
||||
|
||||
// Parse address components
|
||||
address := map[string]interface{}{
|
||||
"formatted_address": result.Result.FormattedAddress,
|
||||
"latitude": result.Result.Geometry.Location.Lat,
|
||||
"longitude": result.Result.Geometry.Location.Lng,
|
||||
}
|
||||
|
||||
for _, comp := range result.Result.AddressComponents {
|
||||
for _, t := range comp.Types {
|
||||
switch t {
|
||||
case "street_number":
|
||||
address["street_number"] = comp.LongName
|
||||
case "route":
|
||||
address["street"] = comp.LongName
|
||||
case "locality":
|
||||
address["city"] = comp.LongName
|
||||
case "postal_code":
|
||||
address["zip"] = comp.LongName
|
||||
case "country":
|
||||
address["country_code"] = comp.ShortName
|
||||
address["country"] = comp.LongName
|
||||
case "administrative_area_level_1":
|
||||
address["state"] = comp.LongName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return address, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Google API Response Types ---
|
||||
|
||||
type GeocodingResponse struct {
|
||||
Status string `json:"status"`
|
||||
Results []struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Geometry struct {
|
||||
Location LatLng `json:"location"`
|
||||
} `json:"geometry"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type LatLng struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
|
||||
type AutocompleteResponse struct {
|
||||
Status string `json:"status"`
|
||||
Predictions []struct {
|
||||
Description string `json:"description"`
|
||||
PlaceID string `json:"place_id"`
|
||||
} `json:"predictions"`
|
||||
}
|
||||
|
||||
type PlaceDetailsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Result struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Geometry struct {
|
||||
Location LatLng `json:"location"`
|
||||
} `json:"geometry"`
|
||||
AddressComponents []AddressComponent `json:"address_components"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type AddressComponent struct {
|
||||
LongName string `json:"long_name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Types []string `json:"types"`
|
||||
}
|
||||
5
addons/google_address/models/init.go
Normal file
5
addons/google_address/models/init.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initGoogleAddress()
|
||||
}
|
||||
28
addons/google_address/module.go
Normal file
28
addons/google_address/module.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Package google_address provides Google Maps/Places integration.
|
||||
// OPT-IN: Only active when GOOGLE_MAPS_API_KEY is configured.
|
||||
//
|
||||
// Features:
|
||||
// - Address autocomplete (Google Places API)
|
||||
// - Geocoding (address → lat/lng)
|
||||
// - Reverse geocoding (lat/lng → address)
|
||||
// - Distance calculation between partners
|
||||
package google_address
|
||||
|
||||
import (
|
||||
"odoo-go/addons/google_address/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "google_address",
|
||||
Description: "Google Maps Address Integration",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Integration",
|
||||
Depends: []string{"base"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 100,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user