Fix P0-P2 bugs: sale.order M2M, display_name, DefaultGet

P0: Fix sale.order creation (was completely broken)
- Corrected M2M junction table name from sale_order_line_account_tax_rel
  to account_tax_sale_order_line_rel (ORM sorts alphabetically)
- Added fallback in BeforeCreate if sequence generation fails

P1: Add display_name as magic field on ALL models
- Added to addMagicFields() in pkg/orm/model.go (like Python BaseModel)
- Computed on-the-fly in Read() from recName field, no DB column
- Removed explicit display_name from res.partner (now auto-inherited)

P2: Add DefaultGet hooks for sale.order and purchase.order
- Sets company_id, currency_id, date_order/date_planned from environment
- Follows same pattern as account.move's DefaultGet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-01 02:28:49 +02:00
parent 1aa9351054
commit 70649c4b4e
6 changed files with 107 additions and 3 deletions

View File

@@ -20,7 +20,6 @@ func initResPartner() {
// -- Identity --
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true}),
orm.Char("display_name", orm.FieldOpts{String: "Display Name", Compute: "_compute_display_name", Store: true}),
orm.Char("ref", orm.FieldOpts{String: "Reference", Index: true}),
orm.Selection("type", []orm.SelectionItem{
{Value: "contact", Label: "Contact"},

View File

@@ -107,6 +107,32 @@ func initPurchaseOrder() {
orm.Char("origin", orm.FieldOpts{String: "Source Document"}),
)
// -- DefaultGet: Provide dynamic defaults for new records --
// Mirrors: odoo/addons/purchase/models/purchase_order.py PurchaseOrder.default_get()
// Supplies company_id, currency_id, date_order when creating a new PO.
m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
vals := make(orm.Values)
// Default company from the current user's session
companyID := env.CompanyID()
if companyID > 0 {
vals["company_id"] = companyID
}
// Default currency from the company
var currencyID int64
err := env.Tx().QueryRow(env.Ctx(),
`SELECT currency_id FROM res_company WHERE id = $1`, companyID).Scan(&currencyID)
if err == nil && currencyID > 0 {
vals["currency_id"] = currencyID
}
// Default date_order = now
vals["date_order"] = time.Now().Format("2006-01-02 15:04:05")
return vals
}
// button_confirm: draft → purchase
m.RegisterMethod("button_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
env := rs.Env()

View File

@@ -135,7 +135,7 @@ func initSaleOrder() {
`SELECT COALESCE(SUM(
product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
* COALESCE((SELECT t.amount / 100 FROM account_tax t
JOIN sale_order_line_account_tax_rel rel ON rel.account_tax_id = t.id
JOIN account_tax_sale_order_line_rel rel ON rel.account_tax_id = t.id
WHERE rel.sale_order_line_id = sol.id LIMIT 1), 0)
), 0)
FROM sale_order_line sol WHERE sol.order_id = $1
@@ -156,12 +156,41 @@ func initSaleOrder() {
m.RegisterCompute("amount_tax", computeSaleAmounts)
m.RegisterCompute("amount_total", computeSaleAmounts)
// -- DefaultGet: Provide dynamic defaults for new records --
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.default_get()
// Supplies company_id, currency_id, date_order when creating a new quotation.
m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
vals := make(orm.Values)
// Default company from the current user's session
companyID := env.CompanyID()
if companyID > 0 {
vals["company_id"] = companyID
}
// Default currency from the company
var currencyID int64
err := env.Tx().QueryRow(env.Ctx(),
`SELECT currency_id FROM res_company WHERE id = $1`, companyID).Scan(&currencyID)
if err == nil && currencyID > 0 {
vals["currency_id"] = currencyID
}
// Default date_order = now
vals["date_order"] = time.Now().Format("2006-01-02 15:04:05")
return vals
}
// -- Sequence Hook --
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
name, _ := vals["name"].(string)
if name == "" || name == "/" {
seq, err := orm.NextByCode(env, "sale.order")
if err == nil {
if err != nil {
// Fallback: generate a simple name like purchase.order does
vals["name"] = fmt.Sprintf("SO/%d", time.Now().UnixNano()%100000)
} else {
vals["name"] = seq
}
}

Binary file not shown.

View File

@@ -129,6 +129,13 @@ func (m *Model) addMagicFields() {
Readonly: true,
}))
// display_name: computed on-the-fly from rec_name, not stored in DB.
// Mirrors: odoo/orm/models.py BaseModel.display_name (computed field on ALL models)
m.AddField(Char("display_name", FieldOpts{
String: "Display Name",
Compute: "_compute_display_name",
}))
if m.logAccess {
m.AddField(Many2one("create_uid", "res.users", FieldOpts{
String: "Created by",

View File

@@ -349,12 +349,17 @@ func (rs *Recordset) Read(fields []string) ([]Values, error) {
var storedFields []string // Fields that come from the DB query
var m2mFields []string // Many2many fields (from junction table)
var relatedFields []string // Related fields (from joined table)
wantDisplayName := false // Whether display_name was requested
for _, name := range fields {
f := m.GetField(name)
if f == nil {
return nil, fmt.Errorf("orm: field %q not found on %s", name, m.name)
}
if name == "display_name" && f.Compute != "" && !f.Store {
wantDisplayName = true
continue
}
if f.Type == TypeMany2many {
m2mFields = append(m2mFields, name)
} else if f.Related != "" && !f.Store {
@@ -365,6 +370,25 @@ func (rs *Recordset) Read(fields []string) ([]Values, error) {
}
}
// If display_name is requested, ensure the rec_name field is fetched from DB
recName := m.recName
if wantDisplayName {
recNameAlreadyFetched := false
for _, sf := range storedFields {
if sf == recName {
recNameAlreadyFetched = true
break
}
}
if !recNameAlreadyFetched {
recF := m.GetField(recName)
if recF != nil && recF.IsStored() {
columns = append(columns, fmt.Sprintf("%q", recF.Column()))
storedFields = append(storedFields, recName)
}
}
}
// Build query
var args []interface{}
idPlaceholders := make([]string, len(rs.ids))
@@ -426,6 +450,25 @@ func (rs *Recordset) Read(fields []string) ([]Values, error) {
}
}
// Post-fetch: compute display_name from the rec_name field value.
// Mirrors: odoo/orm/models.py BaseModel._compute_display_name()
if wantDisplayName {
for _, rec := range results {
if nameVal, ok := rec[recName]; ok {
switch v := nameVal.(type) {
case string:
rec["display_name"] = v
case nil:
rec["display_name"] = ""
default:
rec["display_name"] = fmt.Sprintf("%v", v)
}
} else {
rec["display_name"] = ""
}
}
}
// Post-fetch: M2M fields (from junction tables)
if len(m2mFields) > 0 && len(rs.ids) > 0 {
for _, fname := range m2mFields {