package models import ( "fmt" "os" "odoo-go/pkg/orm" "odoo-go/pkg/tools" ) var calendarClient *tools.APIClient func getCalendarClient() *tools.APIClient { if calendarClient != nil { return calendarClient } apiKey := os.Getenv("GOOGLE_CALENDAR_API_KEY") if apiKey == "" { return nil } calendarClient = tools.NewAPIClient("https://www.googleapis.com", apiKey) return calendarClient } // initCalendarEvent registers the calendar.event model. // Mirrors: odoo/addons/calendar/models/calendar_event.py func initCalendarEvent() { m := orm.NewModel("calendar.event", orm.ModelOpts{ Description: "Calendar Event", Order: "start desc", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Meeting Subject", Required: true}), orm.Datetime("start", orm.FieldOpts{String: "Start", Required: true}), orm.Datetime("stop", orm.FieldOpts{String: "Stop", Required: true}), orm.Boolean("allday", orm.FieldOpts{String: "All Day"}), orm.Text("description", orm.FieldOpts{String: "Description"}), orm.Char("location", orm.FieldOpts{String: "Location"}), orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Organizer"}), orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Contact"}), orm.Many2many("attendee_ids", "res.partner", orm.FieldOpts{String: "Attendees"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Selection("state", []orm.SelectionItem{ {Value: "draft", Label: "Unconfirmed"}, {Value: "open", Label: "Confirmed"}, {Value: "done", Label: "Done"}, {Value: "cancel", Label: "Cancelled"}, }, orm.FieldOpts{String: "Status", Default: "draft"}), // Google sync fields orm.Char("google_event_id", orm.FieldOpts{String: "Google Event ID", Index: true}), orm.Char("google_calendar_id", orm.FieldOpts{String: "Google Calendar ID"}), orm.Datetime("google_synced_at", orm.FieldOpts{String: "Last Synced"}), ) } // initGoogleCalendarSync registers sync methods. func initGoogleCalendarSync() { event := orm.Registry.Get("calendar.event") if event == nil { return } // push_to_google: Create/update event in Google Calendar event.RegisterMethod("push_to_google", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { client := getCalendarClient() if client == nil { return nil, fmt.Errorf("google_calendar: GOOGLE_CALENDAR_API_KEY not configured") } env := rs.Env() calendarID := "primary" if len(args) > 0 { if cid, ok := args[0].(string); ok && cid != "" { calendarID = cid } } for _, id := range rs.IDs() { var name, description, location, googleEventID string var start, stop string env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(name,''), COALESCE(description,''), COALESCE(location,''), COALESCE(google_event_id,''), COALESCE(start::text,''), COALESCE(stop::text,'') FROM calendar_event WHERE id = $1`, id, ).Scan(&name, &description, &location, &googleEventID, &start, &stop) eventBody := map[string]interface{}{ "summary": name, "description": description, "location": location, "start": map[string]string{ "dateTime": start, "timeZone": "Europe/Berlin", }, "end": map[string]string{ "dateTime": stop, "timeZone": "Europe/Berlin", }, } if googleEventID != "" { // Update existing var result map[string]interface{} err := client.PostJSON( fmt.Sprintf("/calendar/v3/calendars/%s/events/%s", calendarID, googleEventID), nil, eventBody, &result, ) if err != nil { return nil, fmt.Errorf("google_calendar: update event %d: %w", id, err) } } else { // Create new var result struct { ID string `json:"id"` } err := client.PostJSON( fmt.Sprintf("/calendar/v3/calendars/%s/events", calendarID), nil, eventBody, &result, ) if err != nil { return nil, fmt.Errorf("google_calendar: create event %d: %w", id, err) } // Store Google event ID env.Tx().Exec(env.Ctx(), `UPDATE calendar_event SET google_event_id = $1, google_synced_at = NOW() WHERE id = $2`, result.ID, id) } } return true, nil }) // pull_from_google: Fetch events from Google Calendar event.RegisterMethod("pull_from_google", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { client := getCalendarClient() if client == nil { return nil, fmt.Errorf("google_calendar: GOOGLE_CALENDAR_API_KEY not configured") } calendarID := "primary" if len(args) > 0 { if cid, ok := args[0].(string); ok && cid != "" { calendarID = cid } } var result GoogleEventsResponse err := client.GetJSON( fmt.Sprintf("/calendar/v3/calendars/%s/events", calendarID), map[string]string{ "maxResults": "50", "singleEvents": "true", "orderBy": "startTime", "timeMin": "2026-01-01T00:00:00Z", }, &result, ) if err != nil { return nil, fmt.Errorf("google_calendar: fetch events: %w", err) } env := rs.Env() imported := 0 for _, ge := range result.Items { // Check if already synced var existing int env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM calendar_event WHERE google_event_id = $1`, ge.ID, ).Scan(&existing) if existing > 0 { continue // Already imported } startTime := ge.Start.DateTime if startTime == "" { startTime = ge.Start.Date } endTime := ge.End.DateTime if endTime == "" { endTime = ge.End.Date } eventRS := env.Model("calendar.event") _, err := eventRS.Create(orm.Values{ "name": ge.Summary, "description": ge.Description, "location": ge.Location, "start": startTime, "stop": endTime, "google_event_id": ge.ID, "google_synced_at": "now", "state": "open", }) if err == nil { imported++ } } return map[string]interface{}{ "imported": imported, "total": len(result.Items), }, nil }) } // --- Google Calendar API Response Types --- type GoogleEventsResponse struct { Items []GoogleEvent `json:"items"` } type GoogleEvent struct { ID string `json:"id"` Summary string `json:"summary"` Description string `json:"description"` Location string `json:"location"` Start struct { DateTime string `json:"dateTime"` Date string `json:"date"` } `json:"start"` End struct { DateTime string `json:"dateTime"` Date string `json:"date"` } `json:"end"` }