// Report endpoint — serves HTML reports. // Mirrors: odoo/addons/web/controllers/report.py ReportController package server import ( "fmt" "html/template" "net/http" "strconv" "strings" "odoo-go/pkg/orm" ) // handleReport serves HTML reports. // Route: /report/html// // Mirrors: odoo/addons/web/controllers/report.py report_routes() func (s *Server) handleReport(w http.ResponseWriter, r *http.Request) { // Parse URL: /report/html/account.report_invoice/1,2,3 parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") // Expected: ["report", "html", "", ""] if len(parts) < 4 { http.Error(w, "Invalid report URL. Expected: /report/html//", http.StatusBadRequest) return } reportName := parts[2] idsStr := parts[3] // Parse record IDs var ids []int64 for _, s := range strings.Split(idsStr, ",") { s = strings.TrimSpace(s) if s == "" { continue } id, err := strconv.ParseInt(s, 10, 64) if err != nil { http.Error(w, fmt.Sprintf("Invalid ID %q: %v", s, err), http.StatusBadRequest) return } ids = append(ids, id) } if len(ids) == 0 { http.Error(w, "No record IDs provided", http.StatusBadRequest) return } // Get 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() // Determine model from report name modelName := resolveReportModel(reportName) if modelName == "" { http.Error(w, "Unknown report: "+reportName, http.StatusNotFound) return } // Read records rs := env.Model(modelName).Browse(ids...) records, err := rs.Read(nil) 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 } // Render HTML report html := renderHTMLReport(reportName, modelName, records) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } // resolveReportModel maps a report name to the ORM model it operates on. // Mirrors: odoo ir.actions.report → model field. func resolveReportModel(reportName string) string { mapping := map[string]string{ "account.report_invoice": "account.move", "sale.report_saleorder": "sale.order", "stock.report_picking": "stock.picking", "purchase.report_purchaseorder": "purchase.order", "contacts.report_partner": "res.partner", } return mapping[reportName] } // renderHTMLReport generates a basic HTML report for the given records. // This is a minimal implementation — Odoo uses QWeb templates for real reports. func renderHTMLReport(reportName, modelName string, records []orm.Values) string { var b strings.Builder b.WriteString(` Report: `) b.WriteString(htmlEscape(reportName)) b.WriteString(`

`) b.WriteString(htmlEscape(reportName)) b.WriteString(`

Model: `) b.WriteString(htmlEscape(modelName)) b.WriteString(fmt.Sprintf(` | Records: %d`, len(records))) b.WriteString(`
`) for _, rec := range records { b.WriteString(`
`) // Use "name" or "display_name" as section title if available title := "" if v, ok := rec["display_name"]; ok { title = fmt.Sprintf("%v", v) } else if v, ok := rec["name"]; ok { title = fmt.Sprintf("%v", v) } if title != "" { b.WriteString(fmt.Sprintf("

%s

", htmlEscape(title))) } b.WriteString("") b.WriteString("") for key, val := range rec { if key == "id" { continue } valStr := fmt.Sprintf("%v", val) if valStr == "" { valStr = "" } b.WriteString(fmt.Sprintf("", htmlEscape(key), htmlEscape(valStr))) } b.WriteString("
FieldValue
%s%s
") b.WriteString("
") } b.WriteString("") return b.String() } // htmlEscape escapes HTML special characters. func htmlEscape(s string) string { return template.HTMLEscapeString(s) }