Files
goodie/addons/google_address/models/google_address.go
Marc 0ed29fe2fd 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>
2026-03-31 01:45:09 +02:00

211 lines
5.8 KiB
Go

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"`
}