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>
331 lines
8.1 KiB
Go
331 lines
8.1 KiB
Go
package orm
|
|
|
|
import "testing"
|
|
|
|
func TestFieldIsCopyable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
field Field
|
|
want bool
|
|
}{
|
|
{"regular char", Field{Name: "name", Type: TypeChar}, true},
|
|
{"id field", Field{Name: "id", Type: TypeInteger}, false},
|
|
{"create_uid", Field{Name: "create_uid", Type: TypeMany2one}, false},
|
|
{"write_uid", Field{Name: "write_uid", Type: TypeMany2one}, false},
|
|
{"create_date", Field{Name: "create_date", Type: TypeDatetime}, false},
|
|
{"write_date", Field{Name: "write_date", Type: TypeDatetime}, false},
|
|
{"password", Field{Name: "password", Type: TypeChar}, false},
|
|
{"computed non-stored", Field{Name: "total", Type: TypeFloat, Compute: "x"}, false},
|
|
{"computed stored", Field{Name: "total", Type: TypeFloat, Compute: "x", Store: true}, true},
|
|
{"o2m", Field{Name: "lines", Type: TypeOne2many}, false},
|
|
{"m2o", Field{Name: "partner_id", Type: TypeMany2one}, true},
|
|
{"boolean", Field{Name: "active", Type: TypeBoolean}, true},
|
|
{"explicit copy true", Field{Name: "ref", Type: TypeChar, Copy: true}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.field.IsCopyable()
|
|
if got != tt.want {
|
|
t.Errorf("IsCopyable() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldSQLType(t *testing.T) {
|
|
tests := []struct {
|
|
typ FieldType
|
|
want string
|
|
}{
|
|
{TypeChar, "varchar"},
|
|
{TypeText, "text"},
|
|
{TypeHTML, "text"},
|
|
{TypeInteger, "int4"},
|
|
{TypeFloat, "numeric"},
|
|
{TypeMonetary, "numeric"},
|
|
{TypeBoolean, "bool"},
|
|
{TypeDate, "date"},
|
|
{TypeDatetime, "timestamp without time zone"},
|
|
{TypeMany2one, "int4"},
|
|
{TypeOne2many, ""},
|
|
{TypeMany2many, ""},
|
|
{TypeJson, "jsonb"},
|
|
{TypeProperties, "jsonb"},
|
|
{TypeBinary, "bytea"},
|
|
{TypeSelection, "varchar"},
|
|
{TypeReference, "varchar"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.typ.String(), func(t *testing.T) {
|
|
got := tt.typ.SQLType()
|
|
if got != tt.want {
|
|
t.Errorf("SQLType() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldSQLTypeWithSize(t *testing.T) {
|
|
f := &Field{Type: TypeChar, Size: 64}
|
|
got := f.SQLType()
|
|
if got != "varchar(64)" {
|
|
t.Errorf("expected varchar(64), got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestFieldTypeString(t *testing.T) {
|
|
if TypeChar.String() != "char" {
|
|
t.Error("expected char")
|
|
}
|
|
if TypeMany2one.String() != "many2one" {
|
|
t.Error("expected many2one")
|
|
}
|
|
if TypeBoolean.String() != "boolean" {
|
|
t.Error("expected boolean")
|
|
}
|
|
if TypeText.String() != "text" {
|
|
t.Error("expected text")
|
|
}
|
|
if TypeInteger.String() != "integer" {
|
|
t.Error("expected integer")
|
|
}
|
|
if TypeFloat.String() != "float" {
|
|
t.Error("expected float")
|
|
}
|
|
}
|
|
|
|
func TestFieldTypeIsRelational(t *testing.T) {
|
|
if !TypeMany2one.IsRelational() {
|
|
t.Error("m2o should be relational")
|
|
}
|
|
if !TypeOne2many.IsRelational() {
|
|
t.Error("o2m should be relational")
|
|
}
|
|
if !TypeMany2many.IsRelational() {
|
|
t.Error("m2m should be relational")
|
|
}
|
|
if TypeChar.IsRelational() {
|
|
t.Error("char should not be relational")
|
|
}
|
|
if TypeInteger.IsRelational() {
|
|
t.Error("integer should not be relational")
|
|
}
|
|
if TypeBoolean.IsRelational() {
|
|
t.Error("boolean should not be relational")
|
|
}
|
|
}
|
|
|
|
func TestFieldTypeIsStored(t *testing.T) {
|
|
if !TypeChar.IsStored() {
|
|
t.Error("char should be stored")
|
|
}
|
|
if !TypeMany2one.IsStored() {
|
|
t.Error("m2o should be stored")
|
|
}
|
|
if TypeOne2many.IsStored() {
|
|
t.Error("o2m should not be stored")
|
|
}
|
|
if TypeMany2many.IsStored() {
|
|
t.Error("m2m should not be stored")
|
|
}
|
|
if !TypeBoolean.IsStored() {
|
|
t.Error("boolean should be stored")
|
|
}
|
|
if !TypeInteger.IsStored() {
|
|
t.Error("integer should be stored")
|
|
}
|
|
}
|
|
|
|
func TestFieldIsStored(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
f Field
|
|
want bool
|
|
}{
|
|
{"plain char", Field{Type: TypeChar}, true},
|
|
{"computed not stored", Field{Type: TypeChar, Compute: "x"}, false},
|
|
{"computed stored", Field{Type: TypeChar, Compute: "x", Store: true}, true},
|
|
{"related not stored", Field{Type: TypeChar, Related: "partner_id.name"}, false},
|
|
{"related stored", Field{Type: TypeChar, Related: "partner_id.name", Store: true}, true},
|
|
{"o2m", Field{Type: TypeOne2many}, false},
|
|
{"m2m", Field{Type: TypeMany2many}, false},
|
|
{"m2o", Field{Type: TypeMany2one}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.f.IsStored()
|
|
if got != tt.want {
|
|
t.Errorf("IsStored() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFieldColumn(t *testing.T) {
|
|
f := &Field{Name: "partner_id", column: "partner_id"}
|
|
if f.Column() != "partner_id" {
|
|
t.Errorf("expected partner_id, got %s", f.Column())
|
|
}
|
|
|
|
f2 := &Field{Name: "custom", column: "custom_col"}
|
|
if f2.Column() != "custom_col" {
|
|
t.Errorf("expected custom_col, got %s", f2.Column())
|
|
}
|
|
|
|
// When column is empty, falls back to Name
|
|
f3 := &Field{Name: "fallback"}
|
|
if f3.Column() != "fallback" {
|
|
t.Errorf("expected fallback, got %s", f3.Column())
|
|
}
|
|
}
|
|
|
|
func TestFieldConstructors(t *testing.T) {
|
|
t.Run("Char", func(t *testing.T) {
|
|
f := Char("name", FieldOpts{String: "Name", Required: true})
|
|
if f.Type != TypeChar {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
if f.Name != "name" {
|
|
t.Errorf("name: %s", f.Name)
|
|
}
|
|
if !f.Required {
|
|
t.Error("expected required")
|
|
}
|
|
if f.String != "Name" {
|
|
t.Errorf("string: %s", f.String)
|
|
}
|
|
})
|
|
|
|
t.Run("Integer", func(t *testing.T) {
|
|
f := Integer("count", FieldOpts{})
|
|
if f.Type != TypeInteger {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Boolean", func(t *testing.T) {
|
|
f := Boolean("active", FieldOpts{Default: true})
|
|
if f.Type != TypeBoolean {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
if f.Default != true {
|
|
t.Error("expected default true")
|
|
}
|
|
})
|
|
|
|
t.Run("Many2one", func(t *testing.T) {
|
|
f := Many2one("partner_id", "res.partner", FieldOpts{String: "Partner"})
|
|
if f.Type != TypeMany2one {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
if f.Comodel != "res.partner" {
|
|
t.Errorf("comodel: %s", f.Comodel)
|
|
}
|
|
if !f.Index {
|
|
t.Error("M2O should be auto-indexed")
|
|
}
|
|
if f.OnDelete != OnDeleteSetNull {
|
|
t.Errorf("expected set null, got %s", f.OnDelete)
|
|
}
|
|
})
|
|
|
|
t.Run("One2many", func(t *testing.T) {
|
|
f := One2many("line_ids", "sale.order.line", "order_id", FieldOpts{})
|
|
if f.Type != TypeOne2many {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
if f.InverseField != "order_id" {
|
|
t.Errorf("inverse: %s", f.InverseField)
|
|
}
|
|
})
|
|
|
|
t.Run("Many2many", func(t *testing.T) {
|
|
f := Many2many("tag_ids", "res.partner.tag", FieldOpts{})
|
|
if f.Type != TypeMany2many {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
if f.Comodel != "res.partner.tag" {
|
|
t.Errorf("comodel: %s", f.Comodel)
|
|
}
|
|
})
|
|
|
|
t.Run("Text", func(t *testing.T) {
|
|
f := Text("description", FieldOpts{})
|
|
if f.Type != TypeText {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Float", func(t *testing.T) {
|
|
f := Float("amount", FieldOpts{})
|
|
if f.Type != TypeFloat {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Date", func(t *testing.T) {
|
|
f := Date("birthday", FieldOpts{})
|
|
if f.Type != TypeDate {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Datetime", func(t *testing.T) {
|
|
f := Datetime("created", FieldOpts{})
|
|
if f.Type != TypeDatetime {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Binary", func(t *testing.T) {
|
|
f := Binary("image", FieldOpts{})
|
|
if f.Type != TypeBinary {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("Json", func(t *testing.T) {
|
|
f := Json("data", FieldOpts{})
|
|
if f.Type != TypeJson {
|
|
t.Errorf("type: %s", f.Type)
|
|
}
|
|
})
|
|
|
|
t.Run("default label from name", func(t *testing.T) {
|
|
f := Char("my_field", FieldOpts{})
|
|
if f.String != "my_field" {
|
|
t.Errorf("expected my_field as default label, got %s", f.String)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFieldResolveDefault(t *testing.T) {
|
|
t.Run("nil default", func(t *testing.T) {
|
|
f := &Field{Default: nil}
|
|
if f.ResolveDefault() != nil {
|
|
t.Error("expected nil")
|
|
}
|
|
})
|
|
|
|
t.Run("string default", func(t *testing.T) {
|
|
f := &Field{Default: "hello"}
|
|
if f.ResolveDefault() != "hello" {
|
|
t.Error("expected hello")
|
|
}
|
|
})
|
|
|
|
t.Run("bool default", func(t *testing.T) {
|
|
f := &Field{Default: true}
|
|
if f.ResolveDefault() != true {
|
|
t.Error("expected true")
|
|
}
|
|
})
|
|
|
|
t.Run("int default", func(t *testing.T) {
|
|
f := &Field{Default: 42}
|
|
if f.ResolveDefault() != 42 {
|
|
t.Error("expected 42")
|
|
}
|
|
})
|
|
}
|