package server import ( "encoding/csv" "encoding/json" "fmt" "net/http" "odoo-go/pkg/orm" ) // handleExportCSV exports records as CSV. // Mirrors: odoo/addons/web/controllers/export.py ExportController func (s *Server) handleExportCSV(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req JSONRPCRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.writeJSONRPC(w, nil, nil, &RPCError{Code: -32700, Message: "Parse error"}) return } var params struct { Data struct { Model string `json:"model"` Fields []exportField `json:"fields"` Domain []interface{} `json:"domain"` IDs []float64 `json:"ids"` } `json:"data"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: "Invalid params"}) return } // Extract UID from session uid := int64(1) companyID := int64(1) if sess := GetSession(r); sess != nil { uid = sess.UID companyID = sess.CompanyID } env, err := orm.NewEnvironment(r.Context(), orm.EnvConfig{ Pool: s.pool, UID: uid, CompanyID: companyID, }) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return } defer env.Close() rs := env.Model(params.Data.Model) // Determine which record IDs to export var ids []int64 if len(params.Data.IDs) > 0 { for _, id := range params.Data.IDs { ids = append(ids, int64(id)) } } else { // Search with domain domain := parseDomain([]interface{}{params.Data.Domain}) found, err := rs.Search(domain, orm.SearchOpts{Limit: 10000}) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } ids = found.IDs() } if len(ids) == 0 { w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", params.Data.Model)) return } // Extract field names var fieldNames []string var headers []string for _, f := range params.Data.Fields { fieldNames = append(fieldNames, f.Name) label := f.Label if label == "" { label = f.Name } headers = append(headers, label) } // Read records records, err := rs.Browse(ids...).Read(fieldNames) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := env.Commit(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Write CSV w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", params.Data.Model)) writer := csv.NewWriter(w) defer writer.Flush() // Header row writer.Write(headers) // Data rows for _, rec := range records { row := make([]string, len(fieldNames)) for i, fname := range fieldNames { row[i] = formatCSVValue(rec[fname]) } writer.Write(row) } } // exportField describes a field in an export request. type exportField struct { Name string `json:"name"` Label string `json:"label"` } // formatCSVValue converts a field value to a CSV string. func formatCSVValue(v interface{}) string { if v == nil || v == false { return "" } switch val := v.(type) { case string: return val case bool: if val { return "True" } return "False" case []interface{}: // M2O: [id, "name"] → "name" if len(val) == 2 { if name, ok := val[1].(string); ok { return name } } return fmt.Sprintf("%v", val) default: return fmt.Sprintf("%v", val) } }