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:
Marc
2026-04-12 18:41:57 +02:00
parent 2c7c1e6c88
commit 66383adf06
87 changed files with 14696 additions and 654 deletions

View File

@@ -2,6 +2,8 @@ package orm
import (
"fmt"
"regexp"
"strconv"
"strings"
)
@@ -152,6 +154,8 @@ func (dc *DomainCompiler) JoinSQL() string {
return " " + strings.Join(parts, " ")
}
// compileNodes compiles domain nodes in Polish (prefix) notation.
// Returns the SQL string and the number of nodes consumed from the domain starting at pos.
func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
if pos >= len(domain) {
return "TRUE", nil
@@ -167,7 +171,8 @@ func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
if err != nil {
return "", err
}
right, err := dc.compileNodes(domain, pos+2)
leftSize := nodeSize(domain, pos+1)
right, err := dc.compileNodes(domain, pos+1+leftSize)
if err != nil {
return "", err
}
@@ -178,7 +183,8 @@ func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
if err != nil {
return "", err
}
right, err := dc.compileNodes(domain, pos+2)
leftSize := nodeSize(domain, pos+1)
right, err := dc.compileNodes(domain, pos+1+leftSize)
if err != nil {
return "", err
}
@@ -196,8 +202,6 @@ func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
return dc.compileCondition(n)
case domainGroup:
// domainGroup wraps a sub-domain as a single node.
// Compile it recursively as a full domain.
subSQL, _, err := dc.compileDomainGroup(Domain(n))
if err != nil {
return "", err
@@ -208,6 +212,28 @@ func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
return "", fmt.Errorf("unexpected domain node at position %d: %v", pos, node)
}
// nodeSize returns the number of domain nodes consumed by the subtree at pos.
// Operators (&, |) consume 1 + left subtree + right subtree.
// NOT consumes 1 + inner subtree. Leaf nodes consume 1.
func nodeSize(domain Domain, pos int) int {
if pos >= len(domain) {
return 0
}
switch n := domain[pos].(type) {
case Operator:
_ = n
switch domain[pos].(Operator) {
case OpAnd, OpOr:
leftSize := nodeSize(domain, pos+1)
rightSize := nodeSize(domain, pos+1+leftSize)
return 1 + leftSize + rightSize
case OpNot:
return 1 + nodeSize(domain, pos+1)
}
}
return 1 // Condition or domainGroup = 1 node
}
// compileDomainGroup compiles a sub-domain that was wrapped via domainGroup.
// It reuses the same DomainCompiler (sharing params and joins) so parameter
// indices stay consistent with the outer query.
@@ -227,14 +253,12 @@ func (dc *DomainCompiler) compileCondition(c Condition) (string, error) {
return "", fmt.Errorf("invalid operator: %q", c.Operator)
}
// Handle dot notation (e.g., "partner_id.name")
// Handle dot notation (e.g., "partner_id.name", "partner_id.country_id.code")
// by generating LEFT JOINs through the M2O relational chain.
parts := strings.Split(c.Field, ".")
column := parts[0]
// TODO: Handle JOINs for dot notation paths
// For now, only support direct fields
if len(parts) > 1 {
// Placeholder for JOIN resolution
return dc.compileJoinedCondition(parts, c.Operator, c.Value)
}
@@ -285,7 +309,7 @@ func (dc *DomainCompiler) compileJoinedCondition(fieldPath []string, operator st
dc.joins = append(dc.joins, joinClause{
table: comodel.Table(),
alias: alias,
on: fmt.Sprintf("%s.%q = %q.\"id\"", currentAlias, f.Column(), alias),
on: fmt.Sprintf("%q.%q = %q.\"id\"", currentAlias, f.Column(), alias),
})
currentModel = comodel
@@ -293,8 +317,12 @@ func (dc *DomainCompiler) compileJoinedCondition(fieldPath []string, operator st
}
// The last segment is the actual field to filter on
leafField := fieldPath[len(fieldPath)-1]
qualifiedColumn := fmt.Sprintf("%s.%q", currentAlias, leafField)
leafFieldName := fieldPath[len(fieldPath)-1]
leafCol := leafFieldName
if lf := currentModel.GetField(leafFieldName); lf != nil {
leafCol = lf.Column()
}
qualifiedColumn := fmt.Sprintf("%q.%q", currentAlias, leafCol)
return dc.compileQualifiedCondition(qualifiedColumn, operator, value)
}
@@ -528,13 +556,8 @@ func (dc *DomainCompiler) compileAnyOp(column string, value Value, negate bool)
// Rebase parameter indices: shift them by the current param count
baseIdx := len(dc.params)
dc.params = append(dc.params, subParams...)
rebased := subWhere
// Replace $N with $(N+baseIdx) in the sub-where clause
for i := len(subParams); i >= 1; i-- {
old := fmt.Sprintf("$%d", i)
new := fmt.Sprintf("$%d", i+baseIdx)
rebased = strings.ReplaceAll(rebased, old, new)
}
// Replace $N with $(N+baseIdx) using regex to avoid $1 matching $10
rebased := rebaseParams(subWhere, baseIdx)
// Determine the join condition based on field type
var joinCond string
@@ -676,3 +699,14 @@ func wrapLikeValue(value Value) Value {
}
return "%" + s + "%"
}
// rebaseParams shifts $N placeholders in a SQL string by baseIdx.
// Uses regex to avoid $1 matching inside $10.
var paramRegex = regexp.MustCompile(`\$(\d+)`)
func rebaseParams(sql string, baseIdx int) string {
return paramRegex.ReplaceAllStringFunc(sql, func(match string) string {
n, _ := strconv.Atoi(match[1:])
return fmt.Sprintf("$%d", n+baseIdx)
})
}