feat: Portal, Email Inbound, Discuss + module improvements
- Portal: /my/* routes, signup, password reset, portal user support - Email Inbound: IMAP polling (go-imap/v2), thread matching - Discuss: mail.channel, long-polling bus, DM, unread count - Cron: ir.cron runner (goroutine scheduler) - Bank Import, CSV/Excel Import - Automation (ir.actions.server) - Fetchmail service - HR Payroll model - Various fixes across account, sale, stock, purchase, crm, hr, project Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -73,6 +74,14 @@ func (s *Server) handleWebClient(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if post-setup wizard is needed (first login, company not configured)
|
||||
if s.isPostSetupNeeded() {
|
||||
if sess := GetSession(r); sess != nil {
|
||||
http.Redirect(w, r, "/web/setup/wizard", http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
sess := GetSession(r)
|
||||
if sess == nil {
|
||||
@@ -141,7 +150,7 @@ func (s *Server) handleWebClient(w http.ResponseWriter, r *http.Request) {
|
||||
%s
|
||||
<script>
|
||||
var odoo = {
|
||||
csrf_token: "dummy",
|
||||
csrf_token: "%s",
|
||||
debug: "assets",
|
||||
__session_info__: %s,
|
||||
reloadMenus: function() {
|
||||
@@ -178,12 +187,18 @@ func (s *Server) handleWebClient(w http.ResponseWriter, r *http.Request) {
|
||||
%s</head>
|
||||
<body class="o_web_client">
|
||||
</body>
|
||||
</html>`, linkTags.String(), sessionInfoJSON, scriptTags.String())
|
||||
</html>`, linkTags.String(), sess.CSRFToken, sessionInfoJSON, scriptTags.String())
|
||||
}
|
||||
|
||||
// buildSessionInfo constructs the session_info JSON object expected by the webclient.
|
||||
// Mirrors: odoo/addons/web/models/ir_http.py session_info()
|
||||
func (s *Server) buildSessionInfo(sess *Session) map[string]interface{} {
|
||||
// Build allowed_company_ids from session (populated at login)
|
||||
allowedIDs := sess.AllowedCompanyIDs
|
||||
if len(allowedIDs) == 0 {
|
||||
allowedIDs = []int64{sess.CompanyID}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"session_id": sess.ID,
|
||||
"uid": sess.UID,
|
||||
@@ -194,7 +209,7 @@ func (s *Server) buildSessionInfo(sess *Session) map[string]interface{} {
|
||||
"user_context": map[string]interface{}{
|
||||
"lang": "en_US",
|
||||
"tz": "UTC",
|
||||
"allowed_company_ids": []int64{sess.CompanyID},
|
||||
"allowed_company_ids": allowedIDs,
|
||||
},
|
||||
"db": s.config.DBName,
|
||||
"registry_hash": fmt.Sprintf("odoo-go-%d", time.Now().Unix()),
|
||||
@@ -213,7 +228,7 @@ func (s *Server) buildSessionInfo(sess *Session) map[string]interface{} {
|
||||
"current_menu": 1,
|
||||
"support_url": "",
|
||||
"notification_type": "email",
|
||||
"display_switch_company_menu": false,
|
||||
"display_switch_company_menu": len(allowedIDs) > 1,
|
||||
"test_mode": false,
|
||||
"show_effect": true,
|
||||
"currencies": map[string]interface{}{
|
||||
@@ -226,20 +241,7 @@ func (s *Server) buildSessionInfo(sess *Session) map[string]interface{} {
|
||||
"lang": "en_US",
|
||||
"debug": "assets",
|
||||
},
|
||||
"user_companies": map[string]interface{}{
|
||||
"current_company": sess.CompanyID,
|
||||
"allowed_companies": map[string]interface{}{
|
||||
fmt.Sprintf("%d", sess.CompanyID): map[string]interface{}{
|
||||
"id": sess.CompanyID,
|
||||
"name": "My Company",
|
||||
"sequence": 10,
|
||||
"child_ids": []int64{},
|
||||
"parent_id": false,
|
||||
"currency_id": 1,
|
||||
},
|
||||
},
|
||||
"disallowed_ancestor_companies": map[string]interface{}{},
|
||||
},
|
||||
"user_companies": s.buildUserCompanies(sess.CompanyID, allowedIDs),
|
||||
"user_settings": map[string]interface{}{
|
||||
"id": 1,
|
||||
"user_id": map[string]interface{}{"id": sess.UID, "display_name": sess.Login},
|
||||
@@ -365,3 +367,105 @@ func (s *Server) handleTranslations(w http.ResponseWriter, r *http.Request) {
|
||||
"multi_lang": multiLang,
|
||||
})
|
||||
}
|
||||
|
||||
// buildUserCompanies queries company data and builds the user_companies dict
|
||||
// for the session_info response. Mirrors: odoo/addons/web/models/ir_http.py
|
||||
func (s *Server) buildUserCompanies(currentCompanyID int64, allowedIDs []int64) map[string]interface{} {
|
||||
allowedCompanies := make(map[string]interface{})
|
||||
|
||||
// Batch query all companies at once
|
||||
rows, err := s.pool.Query(context.Background(),
|
||||
`SELECT id, COALESCE(name, 'Company'), COALESCE(currency_id, 1)
|
||||
FROM res_company WHERE id = ANY($1)`, allowedIDs)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var cid, currencyID int64
|
||||
var name string
|
||||
if rows.Scan(&cid, &name, ¤cyID) == nil {
|
||||
allowedCompanies[fmt.Sprintf("%d", cid)] = map[string]interface{}{
|
||||
"id": cid,
|
||||
"name": name,
|
||||
"sequence": 10,
|
||||
"child_ids": []int64{},
|
||||
"parent_id": false,
|
||||
"currency_id": currencyID,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for any IDs not found in DB
|
||||
for _, cid := range allowedIDs {
|
||||
key := fmt.Sprintf("%d", cid)
|
||||
if _, exists := allowedCompanies[key]; !exists {
|
||||
allowedCompanies[key] = map[string]interface{}{
|
||||
"id": cid, "name": fmt.Sprintf("Company %d", cid),
|
||||
"sequence": 10, "child_ids": []int64{}, "parent_id": false, "currency_id": int64(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"current_company": currentCompanyID,
|
||||
"allowed_companies": allowedCompanies,
|
||||
"disallowed_ancestor_companies": map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
// handleSwitchCompany switches the active company for the current session.
|
||||
func (s *Server) handleSwitchCompany(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
sess := GetSession(r)
|
||||
if sess == nil {
|
||||
s.writeJSONRPC(w, nil, nil, &RPCError{Code: 100, Message: "Not authenticated"})
|
||||
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 {
|
||||
CompanyID int64 `json:"company_id"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil || params.CompanyID == 0 {
|
||||
s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: -32602, Message: "Invalid company_id"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate company is in allowed list
|
||||
allowed := false
|
||||
for _, cid := range sess.AllowedCompanyIDs {
|
||||
if cid == params.CompanyID {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
s.writeJSONRPC(w, req.ID, nil, &RPCError{Code: 403, Message: "Company not in allowed list"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update session
|
||||
sess.CompanyID = params.CompanyID
|
||||
|
||||
// Persist to DB
|
||||
if s.sessions.pool != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
s.sessions.pool.Exec(ctx,
|
||||
`UPDATE sessions SET company_id = $1 WHERE id = $2`, params.CompanyID, sess.ID)
|
||||
}
|
||||
|
||||
s.writeJSONRPC(w, req.ID, map[string]interface{}{
|
||||
"company_id": params.CompanyID,
|
||||
"result": true,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user