package server import ( "encoding/csv" "encoding/json" "fmt" "net/http" "github.com/xuri/excelize/v2" "odoo-go/pkg/orm" ) // exportField describes a field in an export request. type exportField struct { Name string `json:"name"` Label string `json:"label"` } // exportData holds the parsed and fetched data for an export operation. type exportData struct { Model string FieldNames []string Headers []string Records []orm.Values } // parseExportRequest parses the common request/params/env/search logic shared by CSV and XLSX export. func (s *Server) parseExportRequest(w http.ResponseWriter, r *http.Request) (*exportData, error) { 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 nil, err } 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 nil, err } 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 nil, err } defer env.Close() rs := env.Model(params.Data.Model) var ids []int64 if len(params.Data.IDs) > 0 { for _, id := range params.Data.IDs { ids = append(ids, int64(id)) } } else { 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 nil, err } ids = found.IDs() } 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) } var records []orm.Values if len(ids) > 0 { records, err = rs.Browse(ids...).Read(fieldNames) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return nil, err } } if err := env.Commit(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return nil, err } return &exportData{ Model: params.Data.Model, FieldNames: fieldNames, Headers: headers, Records: records, }, nil } // 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 } data, err := s.parseExportRequest(w, r) if err != nil { return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", data.Model)) writer := csv.NewWriter(w) defer writer.Flush() writer.Write(data.Headers) for _, rec := range data.Records { row := make([]string, len(data.FieldNames)) for i, fname := range data.FieldNames { row[i] = formatCSVValue(rec[fname]) } writer.Write(row) } } // handleExportXLSX exports records as XLSX (Excel). // Mirrors: odoo/addons/web/controllers/export.py ExportXlsxController func (s *Server) handleExportXLSX(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } data, err := s.parseExportRequest(w, r) if err != nil { return } f := excelize.NewFile() sheet := "Sheet1" headerStyle, _ := f.NewStyle(&excelize.Style{ Font: &excelize.Font{Bold: true}, }) for i, h := range data.Headers { cell, _ := excelize.CoordinatesToCellName(i+1, 1) f.SetCellValue(sheet, cell, h) f.SetCellStyle(sheet, cell, cell, headerStyle) } for rowIdx, rec := range data.Records { for colIdx, fname := range data.FieldNames { cell, _ := excelize.CoordinatesToCellName(colIdx+1, rowIdx+2) f.SetCellValue(sheet, cell, formatCSVValue(rec[fname])) } } w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", data.Model)) f.Write(w) } // 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) } }