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,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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user