Sale (1177→2321 LOC): - Quotation templates (apply to order, option lines) - Sales reports (by month, product, customer, salesperson, category) - Advance payment wizard (delivered/percentage/fixed modes) - SO cancel wizard, discount wizard - action_quotation_sent, action_lock/unlock, preview_quotation - Line computes: invoice_status, price_reduce, untaxed_amount - Partner extension: sale_order_total Purchase (478→1424 LOC): - Purchase reports (by month, category, bill status, receipt analysis) - Receipt creation from PO (action_create_picking) - 3-way matching: action_view_picking, action_view_invoice - button_approve, button_done, action_rfq_send - Line computes: price_subtotal/total with tax, product onchange - Partner extension: purchase_order_count/total Project (218→1161 LOC): - Project updates (status tracking: on_track/at_risk/off_track) - Milestones (deadline, reached tracking, task count) - Timesheet integration (account.analytic.line extension) - Timesheet reports (by project, employee, task, week) - Task recurrence model - Task: planned/effective/remaining hours, progress, subtask hours - Project: allocated/remaining hours, profitability actions ORM Tests (102 tests, 0→1257 LOC): - domain_test.go: 32 tests (compile, operators, AND/OR/NOT, null) - field_test.go: 15 tests (IsCopyable, SQLType, IsRelational, IsStored) - model_test.go: 21 tests (NewModel, AddFields, RegisterMethod, ExtendModel) - domain_parse_test.go: 21 tests (parse Python domain strings) - sanitize_test.go: 13 tests (false→nil, type conversions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
446 lines
10 KiB
Go
446 lines
10 KiB
Go
package orm
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
)
|
|
|
|
func TestDomainCompileEmpty(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
where, params, err := dc.Compile(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "TRUE" {
|
|
t.Errorf("expected TRUE, got %s", where)
|
|
}
|
|
if len(params) != 0 {
|
|
t.Errorf("expected 0 params, got %d", len(params))
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileSimpleLeaf(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "=", "test")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" = $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if len(params) != 1 || params[0] != "test" {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileNullCheck(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "=", nil)}
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" IS NULL` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileFalseCheck(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("active", "=", false)}
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"active" IS NULL` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileNotEqualNull(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "!=", nil)}
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" IS NOT NULL` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileIn(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("id", "in", []int64{1, 2, 3})}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"id" IN ($1, $2, $3)` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if len(params) != 3 {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileEmptyIn(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("id", "in", []int64{})}
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "FALSE" {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileNotIn(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("id", "not in", []int64{1, 2})}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"id" NOT IN ($1, $2)` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if len(params) != 2 {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileEmptyNotIn(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("id", "not in", []int64{})}
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "TRUE" {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileLike(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "ilike", "test")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" ILIKE $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if params[0] != "%test%" {
|
|
t.Errorf("expected %%test%%, got %v", params[0])
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileLikeWithWildcard(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "ilike", "test%")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" ILIKE $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
// Value already contains wildcard, should not be wrapped again
|
|
if params[0] != "test%" {
|
|
t.Errorf("expected test%%, got %v", params[0])
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileNotLike(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "not like", "foo")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" NOT LIKE $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if params[0] != "%foo%" {
|
|
t.Errorf("expected %%foo%%, got %v", params[0])
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileExactLike(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "=like", "test")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" LIKE $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
// =like does NOT auto-wrap
|
|
if params[0] != "test" {
|
|
t.Errorf("expected test, got %v", params[0])
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileExactIlike(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("name", "=ilike", "Test")}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"name" ILIKE $1` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if params[0] != "Test" {
|
|
t.Errorf("expected Test, got %v", params[0])
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileAnd(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := And(Leaf("a", "=", 1), Leaf("b", "=", 2))
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := `("a" = $1 AND "b" = $2)`
|
|
if where != expected {
|
|
t.Errorf("expected %s, got %s", expected, where)
|
|
}
|
|
if len(params) != 2 {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileOr(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Or(Leaf("a", "=", 1), Leaf("b", "=", 2))
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := `("a" = $1 OR "b" = $2)`
|
|
if where != expected {
|
|
t.Errorf("expected %s, got %s", expected, where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileNot(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Not(Leaf("active", "=", true))
|
|
where, _, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := `(NOT "active" = $1)`
|
|
if where != expected {
|
|
t.Errorf("expected %s, got %s", expected, where)
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileInvalidOperator(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("a", "INVALID", 1)}
|
|
_, _, err := dc.Compile(domain)
|
|
if err == nil {
|
|
t.Error("expected error for invalid operator")
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileComparison(t *testing.T) {
|
|
ops := []string{"<", ">", "<=", ">="}
|
|
for _, op := range ops {
|
|
t.Run(op, func(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("age", op, 18)}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expected := fmt.Sprintf(`"age" %s $1`, op)
|
|
if where != expected {
|
|
t.Errorf("expected %s, got %s", expected, where)
|
|
}
|
|
if len(params) != 1 || params[0] != 18 {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDomainCompileInStrings(t *testing.T) {
|
|
dc := &DomainCompiler{model: &Model{table: "test"}}
|
|
domain := Domain{Leaf("state", "in", []string{"draft", "sent"})}
|
|
where, params, err := dc.Compile(domain)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != `"state" IN ($1, $2)` {
|
|
t.Errorf("got %s", where)
|
|
}
|
|
if len(params) != 2 {
|
|
t.Errorf("params: %v", params)
|
|
}
|
|
}
|
|
|
|
func TestAndEmpty(t *testing.T) {
|
|
d := And()
|
|
if d != nil {
|
|
t.Errorf("expected nil, got %v", d)
|
|
}
|
|
}
|
|
|
|
func TestAndSingle(t *testing.T) {
|
|
d := And(Leaf("a", "=", 1))
|
|
if len(d) != 1 {
|
|
t.Errorf("expected 1 node, got %d", len(d))
|
|
}
|
|
}
|
|
|
|
func TestOrEmpty(t *testing.T) {
|
|
d := Or()
|
|
if d != nil {
|
|
t.Errorf("expected nil, got %v", d)
|
|
}
|
|
}
|
|
|
|
func TestOrSingle(t *testing.T) {
|
|
d := Or(Leaf("a", "=", 1))
|
|
if len(d) != 1 {
|
|
t.Errorf("expected 1 node, got %d", len(d))
|
|
}
|
|
}
|
|
|
|
func TestOrMultiple(t *testing.T) {
|
|
d := Or(Leaf("a", "=", 1), Leaf("b", "=", 2), Leaf("c", "=", 3))
|
|
// Should have 2 OR operators + 3 leaves = 5 nodes
|
|
if len(d) != 5 {
|
|
t.Errorf("expected 5 nodes, got %d", len(d))
|
|
}
|
|
}
|
|
|
|
func TestAndMultiple(t *testing.T) {
|
|
d := And(Leaf("a", "=", 1), Leaf("b", "=", 2), Leaf("c", "=", 3))
|
|
// Should have 2 AND operators + 3 leaves = 5 nodes
|
|
if len(d) != 5 {
|
|
t.Errorf("expected 5 nodes, got %d", len(d))
|
|
}
|
|
}
|
|
|
|
func TestNotDomain(t *testing.T) {
|
|
d := Not(Leaf("active", "=", true))
|
|
if len(d) != 2 {
|
|
t.Errorf("expected 2 nodes, got %d", len(d))
|
|
}
|
|
if d[0] != OpNot {
|
|
t.Errorf("expected OpNot, got %v", d[0])
|
|
}
|
|
}
|
|
|
|
func TestLeafCreation(t *testing.T) {
|
|
c := Leaf("name", "=", "test")
|
|
if c.Field != "name" {
|
|
t.Errorf("field: %s", c.Field)
|
|
}
|
|
if c.Operator != "=" {
|
|
t.Errorf("operator: %s", c.Operator)
|
|
}
|
|
if c.Value != "test" {
|
|
t.Errorf("value: %v", c.Value)
|
|
}
|
|
}
|
|
|
|
func TestWrapLikeValue(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input Value
|
|
want Value
|
|
}{
|
|
{"plain string", "test", "%test%"},
|
|
{"already has %", "test%", "test%"},
|
|
{"already has _", "test_val", "test_val"},
|
|
{"non-string", 42, 42},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := wrapLikeValue(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("wrapLikeValue(%v) = %v, want %v", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSlice(t *testing.T) {
|
|
t.Run("[]int64", func(t *testing.T) {
|
|
result := normalizeSlice([]int64{1, 2, 3})
|
|
if len(result) != 3 {
|
|
t.Errorf("expected 3, got %d", len(result))
|
|
}
|
|
})
|
|
t.Run("[]string", func(t *testing.T) {
|
|
result := normalizeSlice([]string{"a", "b"})
|
|
if len(result) != 2 {
|
|
t.Errorf("expected 2, got %d", len(result))
|
|
}
|
|
})
|
|
t.Run("[]int", func(t *testing.T) {
|
|
result := normalizeSlice([]int{1, 2})
|
|
if len(result) != 2 {
|
|
t.Errorf("expected 2, got %d", len(result))
|
|
}
|
|
})
|
|
t.Run("[]float64", func(t *testing.T) {
|
|
result := normalizeSlice([]float64{1.5, 2.5})
|
|
if len(result) != 2 {
|
|
t.Errorf("expected 2, got %d", len(result))
|
|
}
|
|
})
|
|
t.Run("non-slice", func(t *testing.T) {
|
|
result := normalizeSlice("not a slice")
|
|
if result != nil {
|
|
t.Errorf("expected nil, got %v", result)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestToInt64Slice(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
val Value
|
|
want []int64
|
|
}{
|
|
{"int64", int64(5), []int64{5}},
|
|
{"int", int(3), []int64{3}},
|
|
{"int32", int32(7), []int64{7}},
|
|
{"float64", float64(9), []int64{9}},
|
|
{"[]int64", []int64{1, 2}, []int64{1, 2}},
|
|
{"[]int", []int{3, 4}, []int64{3, 4}},
|
|
{"string", "bad", nil},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := toInt64Slice(tt.val)
|
|
if tt.want == nil {
|
|
if got != nil {
|
|
t.Errorf("expected nil, got %v", got)
|
|
}
|
|
return
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("len: got %d, want %d", len(got), len(tt.want))
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.want[i] {
|
|
t.Errorf("index %d: got %d, want %d", i, got[i], tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|