Files
goodie/pkg/orm/model_test.go
Marc fad2a37d1c Expand Sale/Purchase/Project + 102 ORM tests — +4633 LOC
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>
2026-04-03 23:39:41 +02:00

374 lines
9.1 KiB
Go

package orm
import (
"fmt"
"testing"
)
func TestNewModel(t *testing.T) {
m := NewModel("test.model.unit.new", ModelOpts{
Description: "Test Model",
RecName: "name",
})
if m.Name() != "test.model.unit.new" {
t.Errorf("name: %s", m.Name())
}
if m.Table() != "test_model_unit_new" {
t.Errorf("table: %s", m.Table())
}
if m.Description() != "Test Model" {
t.Errorf("desc: %s", m.Description())
}
if m.RecName() != "name" {
t.Errorf("rec_name: %s", m.RecName())
}
}
func TestNewModelDefaults(t *testing.T) {
m := NewModel("test.model.defaults", ModelOpts{})
if m.Order() != "id" {
t.Errorf("default order: %s", m.Order())
}
if m.RecName() != "name" {
t.Errorf("default rec_name: %s", m.RecName())
}
if m.IsAbstract() {
t.Error("should not be abstract")
}
if m.IsTransient() {
t.Error("should not be transient")
}
}
func TestNewModelCustomTable(t *testing.T) {
m := NewModel("test.model.custom.table", ModelOpts{
Table: "my_custom_table",
})
if m.Table() != "my_custom_table" {
t.Errorf("table: %s", m.Table())
}
}
func TestNewModelAbstract(t *testing.T) {
m := NewModel("test.model.abstract", ModelOpts{
Type: ModelAbstract,
})
if m.IsAbstract() != true {
t.Error("should be abstract")
}
// Abstract models have no table
if m.Table() != "" {
t.Errorf("abstract should have no table, got %s", m.Table())
}
}
func TestNewModelTransient(t *testing.T) {
m := NewModel("test.model.transient", ModelOpts{
Type: ModelTransient,
})
if m.IsTransient() != true {
t.Error("should be transient")
}
}
func TestModelMagicFields(t *testing.T) {
m := NewModel("test.model.magic", ModelOpts{})
// Magic fields should be auto-created
if f := m.GetField("id"); f == nil {
t.Error("id field missing")
}
if f := m.GetField("display_name"); f == nil {
t.Error("display_name field missing")
}
if f := m.GetField("create_uid"); f == nil {
t.Error("create_uid field missing")
}
if f := m.GetField("create_date"); f == nil {
t.Error("create_date field missing")
}
if f := m.GetField("write_uid"); f == nil {
t.Error("write_uid field missing")
}
if f := m.GetField("write_date"); f == nil {
t.Error("write_date field missing")
}
}
func TestModelAddFields(t *testing.T) {
m := NewModel("test.model.fields.add", ModelOpts{})
m.AddFields(
Char("name", FieldOpts{String: "Name", Required: true}),
Integer("age", FieldOpts{String: "Age"}),
Boolean("active", FieldOpts{String: "Active", Default: true}),
)
if f := m.GetField("name"); f == nil {
t.Error("name field missing")
}
if f := m.GetField("age"); f == nil {
t.Error("age field missing")
}
if f := m.GetField("active"); f == nil {
t.Error("active field missing")
}
if f := m.GetField("nonexistent"); f != nil {
t.Error("should be nil")
}
nameF := m.GetField("name")
if nameF.Type != TypeChar {
t.Error("expected char")
}
if !nameF.Required {
t.Error("expected required")
}
if nameF.String != "Name" {
t.Error("expected Name label")
}
}
func TestModelAddFieldSetsModel(t *testing.T) {
m := NewModel("test.model.field.backref", ModelOpts{})
f := Char("ref", FieldOpts{})
m.AddField(f)
if f.model != m {
t.Error("field should have back-reference to model")
}
}
func TestModelStoredFields(t *testing.T) {
m := NewModel("test.model.stored", ModelOpts{})
m.AddFields(
Char("name", FieldOpts{}),
Char("computed_field", FieldOpts{Compute: "x"}),
One2many("lines", "other.model", "parent_id", FieldOpts{}),
)
stored := m.StoredFields()
// Should include magic fields + name, but not computed_field or o2m
nameFound := false
computedFound := false
linesFound := false
for _, f := range stored {
switch f.Name {
case "name":
nameFound = true
case "computed_field":
computedFound = true
case "lines":
linesFound = true
}
}
if !nameFound {
t.Error("name should be in stored fields")
}
if computedFound {
t.Error("computed_field should not be in stored fields")
}
if linesFound {
t.Error("o2m lines should not be in stored fields")
}
}
func TestModelRegisterMethod(t *testing.T) {
m := NewModel("test.model.methods.reg", ModelOpts{})
called := false
m.RegisterMethod("test_action", func(rs *Recordset, args ...interface{}) (interface{}, error) {
called = true
return "ok", nil
})
if _, ok := m.Methods["test_action"]; !ok {
t.Error("method not registered")
}
result, err := m.Methods["test_action"](nil)
if err != nil {
t.Fatal(err)
}
if result != "ok" {
t.Error("expected ok")
}
if !called {
t.Error("method not called")
}
}
func TestExtendModel(t *testing.T) {
NewModel("test.model.base.ext", ModelOpts{Description: "Base"})
ext := ExtendModel("test.model.base.ext")
ext.AddFields(Char("extra_field", FieldOpts{String: "Extra"}))
base := Registry.Get("test.model.base.ext")
if f := base.GetField("extra_field"); f == nil {
t.Error("extension field missing")
}
}
func TestExtendModelPanics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic for missing model")
}
}()
ExtendModel("nonexistent.model.xyz.panic")
}
func TestRegistryGet(t *testing.T) {
NewModel("test.registry.get.model", ModelOpts{})
if m := Registry.Get("test.registry.get.model"); m == nil {
t.Error("model not found")
}
if m := Registry.Get("nonexistent.registry.model"); m != nil {
t.Error("should be nil")
}
}
func TestRegistryMustGetPanics(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic for missing model")
}
}()
Registry.MustGet("nonexistent.mustget.model")
}
func TestRegistryAll(t *testing.T) {
NewModel("test.registry.all.model", ModelOpts{})
all := Registry.All()
found := false
for _, name := range all {
if name == "test.registry.all.model" {
found = true
break
}
}
if !found {
t.Error("model not in Registry.All()")
}
}
func TestRegistryModels(t *testing.T) {
NewModel("test.registry.models.model", ModelOpts{})
models := Registry.Models()
if _, ok := models["test.registry.models.model"]; !ok {
t.Error("model not in Registry.Models()")
}
}
func TestModelSQLConstraint(t *testing.T) {
m := NewModel("test.model.constraint.sql", ModelOpts{})
m.AddSQLConstraint("unique_name", "UNIQUE(name)", "Name must be unique")
if len(m.SQLConstraints) != 1 {
t.Error("constraint not added")
}
if m.SQLConstraints[0].Name != "unique_name" {
t.Error("wrong name")
}
if m.SQLConstraints[0].Definition != "UNIQUE(name)" {
t.Error("wrong definition")
}
if m.SQLConstraints[0].Message != "Name must be unique" {
t.Error("wrong message")
}
}
func TestModelAddConstraint(t *testing.T) {
m := NewModel("test.model.constraint.func", ModelOpts{})
m.AddConstraint(func(rs *Recordset) error {
return fmt.Errorf("test error")
})
if len(m.Constraints) != 1 {
t.Error("constraint not added")
}
}
func TestModelRegisterOnchange(t *testing.T) {
m := NewModel("test.model.onchange", ModelOpts{})
m.RegisterOnchange("partner_id", func(env *Environment, vals Values) Values {
return Values{"name": "changed"}
})
if m.OnchangeHandlers == nil {
t.Fatal("OnchangeHandlers should not be nil")
}
if _, ok := m.OnchangeHandlers["partner_id"]; !ok {
t.Error("onchange handler not registered")
}
}
func TestModelRegisterInverse(t *testing.T) {
m := NewModel("test.model.inverse", ModelOpts{})
m.AddFields(Char("computed", FieldOpts{Compute: "_compute_computed"}))
m.RegisterInverse("computed", func(rs *Recordset, args ...interface{}) (interface{}, error) {
return nil, nil
})
f := m.GetField("computed")
if f.Inverse != "_inverse_computed" {
t.Errorf("expected _inverse_computed, got %s", f.Inverse)
}
if _, ok := m.Methods["_inverse_computed"]; !ok {
t.Error("inverse method not registered")
}
}
func TestModelCreateTableSQL(t *testing.T) {
m := NewModel("test.model.ddl", ModelOpts{})
m.AddFields(
Char("name", FieldOpts{Required: true}),
Integer("count", FieldOpts{}),
)
sql := m.CreateTableSQL()
if sql == "" {
t.Fatal("expected non-empty SQL")
}
// Should contain the table name
if !containsStr(sql, `"test_model_ddl"`) {
t.Error("missing table name in DDL")
}
// Should contain name column
if !containsStr(sql, `"name"`) {
t.Error("missing name column in DDL")
}
// Should contain NOT NULL for required
if !containsStr(sql, "NOT NULL") {
t.Error("missing NOT NULL for required field")
}
}
func TestModelCreateTableSQLAbstract(t *testing.T) {
m := NewModel("test.model.ddl.abstract", ModelOpts{Type: ModelAbstract})
sql := m.CreateTableSQL()
if sql != "" {
t.Error("abstract model should have empty DDL")
}
}
func TestModelFields(t *testing.T) {
m := NewModel("test.model.all.fields", ModelOpts{})
m.AddFields(Char("name", FieldOpts{}))
fields := m.Fields()
if _, ok := fields["name"]; !ok {
t.Error("name not in Fields()")
}
if _, ok := fields["id"]; !ok {
t.Error("id not in Fields()")
}
}
// containsStr is a test helper - checks if s contains substr
func containsStr(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr))
}
func stringContains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}