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:
@@ -20,7 +20,6 @@ func initResPartner() {
|
|||||||
// -- Identity --
|
// -- Identity --
|
||||||
m.AddFields(
|
m.AddFields(
|
||||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true}),
|
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.Char("ref", orm.FieldOpts{String: "Reference", Index: true}),
|
||||||
orm.Selection("type", []orm.SelectionItem{
|
orm.Selection("type", []orm.SelectionItem{
|
||||||
{Value: "contact", Label: "Contact"},
|
{Value: "contact", Label: "Contact"},
|
||||||
|
|||||||
@@ -107,6 +107,32 @@ func initPurchaseOrder() {
|
|||||||
orm.Char("origin", orm.FieldOpts{String: "Source Document"}),
|
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(¤cyID)
|
||||||
|
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
|
// button_confirm: draft → purchase
|
||||||
m.RegisterMethod("button_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
m.RegisterMethod("button_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||||
env := rs.Env()
|
env := rs.Env()
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func initSaleOrder() {
|
|||||||
`SELECT COALESCE(SUM(
|
`SELECT COALESCE(SUM(
|
||||||
product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
|
product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
|
||||||
* COALESCE((SELECT t.amount / 100 FROM account_tax t
|
* 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)
|
WHERE rel.sale_order_line_id = sol.id LIMIT 1), 0)
|
||||||
), 0)
|
), 0)
|
||||||
FROM sale_order_line sol WHERE sol.order_id = $1
|
FROM sale_order_line sol WHERE sol.order_id = $1
|
||||||
@@ -156,12 +156,41 @@ func initSaleOrder() {
|
|||||||
m.RegisterCompute("amount_tax", computeSaleAmounts)
|
m.RegisterCompute("amount_tax", computeSaleAmounts)
|
||||||
m.RegisterCompute("amount_total", 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(¤cyID)
|
||||||
|
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 --
|
// -- Sequence Hook --
|
||||||
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
|
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
|
||||||
name, _ := vals["name"].(string)
|
name, _ := vals["name"].(string)
|
||||||
if name == "" || name == "/" {
|
if name == "" || name == "/" {
|
||||||
seq, err := orm.NextByCode(env, "sale.order")
|
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
|
vals["name"] = seq
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
odoo-server
BIN
odoo-server
Binary file not shown.
@@ -129,6 +129,13 @@ func (m *Model) addMagicFields() {
|
|||||||
Readonly: true,
|
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 {
|
if m.logAccess {
|
||||||
m.AddField(Many2one("create_uid", "res.users", FieldOpts{
|
m.AddField(Many2one("create_uid", "res.users", FieldOpts{
|
||||||
String: "Created by",
|
String: "Created by",
|
||||||
|
|||||||
@@ -349,12 +349,17 @@ func (rs *Recordset) Read(fields []string) ([]Values, error) {
|
|||||||
var storedFields []string // Fields that come from the DB query
|
var storedFields []string // Fields that come from the DB query
|
||||||
var m2mFields []string // Many2many fields (from junction table)
|
var m2mFields []string // Many2many fields (from junction table)
|
||||||
var relatedFields []string // Related fields (from joined table)
|
var relatedFields []string // Related fields (from joined table)
|
||||||
|
wantDisplayName := false // Whether display_name was requested
|
||||||
|
|
||||||
for _, name := range fields {
|
for _, name := range fields {
|
||||||
f := m.GetField(name)
|
f := m.GetField(name)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return nil, fmt.Errorf("orm: field %q not found on %s", name, m.name)
|
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 {
|
if f.Type == TypeMany2many {
|
||||||
m2mFields = append(m2mFields, name)
|
m2mFields = append(m2mFields, name)
|
||||||
} else if f.Related != "" && !f.Store {
|
} 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
|
// Build query
|
||||||
var args []interface{}
|
var args []interface{}
|
||||||
idPlaceholders := make([]string, len(rs.ids))
|
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)
|
// Post-fetch: M2M fields (from junction tables)
|
||||||
if len(m2mFields) > 0 && len(rs.ids) > 0 {
|
if len(m2mFields) > 0 && len(rs.ids) > 0 {
|
||||||
for _, fname := range m2mFields {
|
for _, fname := range m2mFields {
|
||||||
|
|||||||
Reference in New Issue
Block a user