Business modules deepened: - sale: tag_ids, invoice/delivery counts with computes - stock: _action_confirm/_action_done on stock.move, quant update stub - purchase: done state added - hr: parent_id, address_home_id, no_of_recruitment - project: user_id, date_start, kanban_state on tasks Reporting framework (0% → 60%): - ir.actions.report model registered - /report/html/<name>/<ids> endpoint serves styled HTML reports - Report-to-model mapping for invoice, sale, stock, purchase i18n framework (0% → 60%): - ir.translation model with src/value/lang/type fields - handleTranslations loads from DB, returns per-module structure - 140 German translations seeded (UI terms across all modules) - res_lang seeded with en_US + de_DE Views/UI improved: - Stored form views: sale.order (with editable O2M lines), account.move (with Post/Cancel buttons), res.partner (with title) - Stored list views: purchase.order, hr.employee, project.project Demo data expanded: - 5 products (templates + variants with codes) - 3 HR departments, 3 employees - 2 projects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
5.0 KiB
Go
187 lines
5.0 KiB
Go
// 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/<report_name>/<ids>
|
|
// 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", "<report_name>", "<ids>"]
|
|
if len(parts) < 4 {
|
|
http.Error(w, "Invalid report URL. Expected: /report/html/<report_name>/<ids>", 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(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<title>Report: `)
|
|
b.WriteString(htmlEscape(reportName))
|
|
b.WriteString(`</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 40px; }
|
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
th { background: #f5f5f5; font-weight: bold; }
|
|
h1 { color: #333; }
|
|
h2 { color: #555; margin-top: 30px; }
|
|
.header { border-bottom: 2px solid #875A7B; padding-bottom: 10px; margin-bottom: 20px; }
|
|
.report-info { color: #888; font-size: 0.9em; margin-bottom: 30px; }
|
|
.record-section { page-break-after: always; margin-bottom: 40px; }
|
|
.record-section:last-child { page-break-after: avoid; }
|
|
@media print {
|
|
body { margin: 20px; }
|
|
.no-print { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>`)
|
|
b.WriteString(htmlEscape(reportName))
|
|
b.WriteString(`</h1>
|
|
<div class="report-info">Model: `)
|
|
b.WriteString(htmlEscape(modelName))
|
|
b.WriteString(fmt.Sprintf(` | Records: %d`, len(records)))
|
|
b.WriteString(`</div>
|
|
</div>
|
|
<div class="no-print">
|
|
<button onclick="window.print()">Print</button>
|
|
</div>
|
|
`)
|
|
|
|
for _, rec := range records {
|
|
b.WriteString(`<div class="record-section">`)
|
|
|
|
// 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("<h2>%s</h2>", htmlEscape(title)))
|
|
}
|
|
|
|
b.WriteString("<table>")
|
|
b.WriteString("<tr><th>Field</th><th>Value</th></tr>")
|
|
for key, val := range rec {
|
|
if key == "id" {
|
|
continue
|
|
}
|
|
valStr := fmt.Sprintf("%v", val)
|
|
if valStr == "<nil>" {
|
|
valStr = ""
|
|
}
|
|
b.WriteString(fmt.Sprintf("<tr><td>%s</td><td>%s</td></tr>",
|
|
htmlEscape(key), htmlEscape(valStr)))
|
|
}
|
|
b.WriteString("</table>")
|
|
b.WriteString("</div>")
|
|
}
|
|
|
|
b.WriteString("</body></html>")
|
|
return b.String()
|
|
}
|
|
|
|
// htmlEscape escapes HTML special characters.
|
|
func htmlEscape(s string) string {
|
|
return template.HTMLEscapeString(s)
|
|
}
|