package server import ( "encoding/csv" "encoding/json" "fmt" "io" "log" "net/http" "strconv" "strings" "odoo-go/pkg/orm" ) // handleImportCSV imports records from a CSV file into any model. // Accepts JSON body with: model, fields (mapping), csv_data (raw CSV string). // Mirrors: odoo/addons/base_import/controllers/main.py ImportController func (s *Server) handleImportCSV(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 { Model string `json:"model"` Fields []importFieldMap `json:"fields"` CSVData string `json:"csv_data"` HasHeader bool `json:"has_header"` DryRun bool `json:"dry_run"` } if err := json.Unmarshal(req.Params, ¶ms); err != nil { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: "Invalid params"}) return } if params.Model == "" || len(params.Fields) == 0 || params.CSVData == "" { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: "model, fields, and csv_data are required"}) return } // Verify model exists m := orm.Registry.Get(params.Model) if m == nil { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: fmt.Sprintf("Unknown model: %s", params.Model)}) return } // Parse CSV reader := csv.NewReader(strings.NewReader(params.CSVData)) reader.LazyQuotes = true reader.TrimLeadingSpace = true var allRows [][]string for { row, err := reader.Read() if err == io.EOF { break } if err != nil { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: fmt.Sprintf("CSV parse error: %v", err)}) return } allRows = append(allRows, row) } if len(allRows) == 0 { s.writeJSONRPC(w, req.ID, map[string]interface{}{"ids": []int64{}, "count": 0}, nil) return } // Skip header row if present dataRows := allRows if params.HasHeader && len(allRows) > 1 { dataRows = allRows[1:] } // Build field mapping: CSV column index → ORM field name type colMapping struct { colIndex int fieldName string fieldType orm.FieldType } var mappings []colMapping for _, fm := range params.Fields { if fm.FieldName == "" || fm.ColumnIndex < 0 { continue } f := m.GetField(fm.FieldName) if f == nil { continue // skip unknown fields } mappings = append(mappings, colMapping{ colIndex: fm.ColumnIndex, fieldName: fm.FieldName, fieldType: f.Type, }) } if len(mappings) == 0 { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: "No valid field mappings"}) return } 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 { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32603, Message: "Internal error"}) return } defer env.Close() rs := env.Model(params.Model) var createdIDs []int64 var errors []importError for rowIdx, row := range dataRows { vals := make(orm.Values) for _, cm := range mappings { if cm.colIndex >= len(row) { continue } raw := strings.TrimSpace(row[cm.colIndex]) if raw == "" { continue } vals[cm.fieldName] = coerceImportValue(raw, cm.fieldType) } if len(vals) == 0 { continue } if params.DryRun { continue // validate only, don't create } rec, err := rs.Create(vals) if err != nil { errors = append(errors, importError{ Row: rowIdx + 1, Message: err.Error(), }) log.Printf("import: row %d error: %v", rowIdx+1, err) continue } createdIDs = append(createdIDs, rec.ID()) } if err := env.Commit(); err != nil { s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32603, Message: fmt.Sprintf("Commit error: %v", err)}) return } result := map[string]interface{}{ "ids": createdIDs, "count": len(createdIDs), "errors": errors, "dry_run": params.DryRun, } s.writeJSONRPC(w, req.ID, result, nil) } // importFieldMap maps a CSV column to an ORM field. type importFieldMap struct { ColumnIndex int `json:"column_index"` FieldName string `json:"field_name"` } // importError describes a per-row import error. type importError struct { Row int `json:"row"` Message string `json:"message"` } // coerceImportValue converts a raw CSV string to the appropriate Go type for ORM Create. func coerceImportValue(raw string, ft orm.FieldType) interface{} { switch ft { case orm.TypeInteger: v, err := strconv.ParseInt(raw, 10, 64) if err != nil { return nil } return v case orm.TypeFloat, orm.TypeMonetary: // Handle comma as decimal separator raw = strings.ReplaceAll(raw, ",", ".") v, err := strconv.ParseFloat(raw, 64) if err != nil { return nil } return v case orm.TypeBoolean: lower := strings.ToLower(raw) return lower == "true" || lower == "1" || lower == "yes" || lower == "ja" case orm.TypeMany2one: // Try as integer ID first, then as name_search later v, err := strconv.ParseInt(raw, 10, 64) if err != nil { return raw // pass as string, ORM may handle name_create } return v default: return raw } }