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