package models import ( "fmt" "odoo-go/pkg/orm" ) // initSaleOrderTemplate registers sale.order.template and sale.order.template.line. // Mirrors: odoo/addons/sale_management/models/sale_order_template.py // // class SaleOrderTemplate(models.Model): // _name = 'sale.order.template' // _description = 'Quotation Template' func initSaleOrderTemplate() { m := orm.NewModel("sale.order.template", orm.ModelOpts{ Description: "Quotation Template", Order: "sequence, id", }) // -- Identity -- m.AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), orm.Integer("number_of_days", orm.FieldOpts{String: "Validity (Days)", Default: 30}), orm.Text("note", orm.FieldOpts{String: "Terms and Conditions", Translate: true}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}), orm.Many2one("mail_template_id", "mail.template", orm.FieldOpts{String: "Confirmation Mail"}), ) // -- Lines -- m.AddFields( orm.One2many("sale_order_template_line_ids", "sale.order.template.line", "sale_order_template_id", orm.FieldOpts{ String: "Lines", }), orm.One2many("sale_order_template_option_ids", "sale.order.template.option", "sale_order_template_id", orm.FieldOpts{ String: "Optional Products", }), ) // -- Computed: line_count -- m.AddFields( orm.Integer("line_count", orm.FieldOpts{ String: "Line Count", Compute: "_compute_line_count", }), ) m.RegisterCompute("line_count", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() templateID := rs.IDs()[0] var count int err := env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM sale_order_template_line WHERE sale_order_template_id = $1`, templateID).Scan(&count) if err != nil { count = 0 } return orm.Values{"line_count": count}, nil }) // action_apply_to_order: Apply this template to a sale order. // Mirrors: odoo/addons/sale_management/models/sale_order.py SaleOrder._onchange_sale_order_template_id() // Copies template lines into the SO as order lines, and copies the template note. m.RegisterMethod("action_apply_to_order", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("order_id required") } env := rs.Env() templateID := rs.IDs()[0] orderID, _ := args[0].(float64) // Read template lines rows, err := env.Tx().Query(env.Ctx(), `SELECT name, product_id, product_uom_qty, price_unit, discount, sequence FROM sale_order_template_line WHERE sale_order_template_id = $1 ORDER BY sequence`, templateID) if err != nil { return nil, fmt.Errorf("sale_template: read lines: %w", err) } defer rows.Close() lineRS := env.Model("sale.order.line") for rows.Next() { var name string var prodID *int64 var qty, price, disc float64 var seq int if err := rows.Scan(&name, &prodID, &qty, &price, &disc, &seq); err != nil { return nil, fmt.Errorf("sale_template: scan line: %w", err) } vals := orm.Values{ "order_id": int64(orderID), "name": name, "product_uom_qty": qty, "price_unit": price, "discount": disc, "sequence": seq, } if prodID != nil { vals["product_id"] = *prodID } if _, err := lineRS.Create(vals); err != nil { return nil, fmt.Errorf("sale_template: create SO line: %w", err) } } // Copy template note to the SO var note *string env.Tx().QueryRow(env.Ctx(), `SELECT note FROM sale_order_template WHERE id = $1`, templateID).Scan(¬e) if note != nil { env.Tx().Exec(env.Ctx(), `UPDATE sale_order SET note = $1 WHERE id = $2`, *note, int64(orderID)) } // Copy validity_date from number_of_days var numDays int env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(number_of_days, 0) FROM sale_order_template WHERE id = $1`, templateID).Scan(&numDays) if numDays > 0 { env.Tx().Exec(env.Ctx(), `UPDATE sale_order SET validity_date = CURRENT_DATE + $1 WHERE id = $2`, numDays, int64(orderID)) } // Copy template options as sale.order.option records on the SO optRows, err := env.Tx().Query(env.Ctx(), `SELECT COALESCE(name, ''), product_id, COALESCE(quantity, 1), COALESCE(price_unit, 0), COALESCE(discount, 0), COALESCE(sequence, 10) FROM sale_order_template_option WHERE sale_order_template_id = $1 ORDER BY sequence`, templateID) if err == nil { optionModel := orm.Registry.Get("sale.order.option") if optionModel != nil { optionRS := env.Model("sale.order.option") for optRows.Next() { var oName string var oProdID *int64 var oQty, oPrice, oDisc float64 var oSeq int if err := optRows.Scan(&oName, &oProdID, &oQty, &oPrice, &oDisc, &oSeq); err != nil { continue } optVals := orm.Values{ "order_id": int64(orderID), "name": oName, "quantity": oQty, "price_unit": oPrice, "discount": oDisc, "sequence": oSeq, "is_present": false, } if oProdID != nil { optVals["product_id"] = *oProdID } optionRS.Create(optVals) } } optRows.Close() } return true, nil }) // action_open_template: Return an action to open this template's form view. // Mirrors: odoo/addons/sale_management/models/sale_order_template.py m.RegisterMethod("action_open_template", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { templateID := rs.IDs()[0] return map[string]interface{}{ "type": "ir.actions.act_window", "res_model": "sale.order.template", "res_id": templateID, "view_mode": "form", "views": [][]interface{}{{nil, "form"}}, "target": "current", }, nil }) } // initSaleOrderTemplateLine registers sale.order.template.line. // Mirrors: odoo/addons/sale_management/models/sale_order_template.py SaleOrderTemplateLine func initSaleOrderTemplateLine() { m := orm.NewModel("sale.order.template.line", orm.ModelOpts{ Description: "Quotation Template Line", Order: "sequence, id", }) m.AddFields( orm.Many2one("sale_order_template_id", "sale.order.template", orm.FieldOpts{ String: "Template", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product"}), orm.Char("name", orm.FieldOpts{String: "Description", Required: true, Translate: true}), orm.Float("product_uom_qty", orm.FieldOpts{String: "Quantity", Default: 1}), orm.Many2one("product_uom_id", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}), orm.Float("price_unit", orm.FieldOpts{String: "Unit Price"}), orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}), orm.Selection("display_type", []orm.SelectionItem{ {Value: "line_section", Label: "Section"}, {Value: "line_note", Label: "Note"}, }, orm.FieldOpts{String: "Display Type"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), ) // Onchange: product_id → name + price_unit // Mirrors: odoo/addons/sale_management/models/sale_order_template.py SaleOrderTemplateLine._compute_name() m.RegisterOnchange("product_id", func(env *orm.Environment, vals orm.Values) orm.Values { result := make(orm.Values) var productID int64 switch v := vals["product_id"].(type) { case int64: productID = v case float64: productID = int64(v) case map[string]interface{}: if id, ok := v["id"]; ok { switch n := id.(type) { case float64: productID = int64(n) case int64: productID = n } } } if productID <= 0 { return result } var name string var listPrice float64 err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(pt.name, ''), COALESCE(pt.list_price, 0) FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pp.id = $1`, productID, ).Scan(&name, &listPrice) if err != nil { return result } result["name"] = name result["price_unit"] = listPrice return result }) // _compute_price_subtotal: qty * price * (1 - discount/100) m.AddFields( orm.Monetary("price_subtotal", orm.FieldOpts{ String: "Subtotal", Compute: "_compute_price_subtotal", CurrencyField: "currency_id", }), ) m.RegisterCompute("price_subtotal", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() lineID := rs.IDs()[0] var qty, price, discount float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(product_uom_qty, 0), COALESCE(price_unit, 0), COALESCE(discount, 0) FROM sale_order_template_line WHERE id = $1`, lineID, ).Scan(&qty, &price, &discount) return orm.Values{"price_subtotal": qty * price * (1 - discount/100)}, nil }) } // initSaleOrderTemplateOption registers sale.order.template.option. // Mirrors: odoo/addons/sale_management/models/sale_order_template.py SaleOrderTemplateOption func initSaleOrderTemplateOption() { m := orm.NewModel("sale.order.template.option", orm.ModelOpts{ Description: "Quotation Template Option", Order: "sequence, id", }) m.AddFields( orm.Many2one("sale_order_template_id", "sale.order.template", orm.FieldOpts{ String: "Template", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product", Required: true}), orm.Char("name", orm.FieldOpts{String: "Description", Required: true, Translate: true}), orm.Float("quantity", orm.FieldOpts{String: "Quantity", Default: 1}), orm.Many2one("uom_id", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}), orm.Float("price_unit", orm.FieldOpts{String: "Unit Price"}), orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}), ) // Onchange: product_id → name + price_unit m.RegisterOnchange("product_id", func(env *orm.Environment, vals orm.Values) orm.Values { result := make(orm.Values) var productID int64 switch v := vals["product_id"].(type) { case int64: productID = v case float64: productID = int64(v) case map[string]interface{}: if id, ok := v["id"]; ok { switch n := id.(type) { case float64: productID = int64(n) case int64: productID = n } } } if productID <= 0 { return result } var name string var listPrice float64 err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(pt.name, ''), COALESCE(pt.list_price, 0) FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pp.id = $1`, productID, ).Scan(&name, &listPrice) if err != nil { return result } result["name"] = name result["price_unit"] = listPrice return result }) } // initSaleOrderOption registers sale.order.option — optional products on a specific sale order. // When a template with options is applied to an SO, options are copied here. // The customer or salesperson can then choose to add them as order lines. // Mirrors: odoo/addons/sale_management/models/sale_order_option.py SaleOrderOption func initSaleOrderOption() { m := orm.NewModel("sale.order.option", orm.ModelOpts{ Description: "Sale Order Option", Order: "sequence, id", }) m.AddFields( orm.Many2one("order_id", "sale.order", orm.FieldOpts{ String: "Sale Order", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2one("product_id", "product.product", orm.FieldOpts{ String: "Product", Required: true, }), orm.Char("name", orm.FieldOpts{String: "Description", Required: true}), orm.Float("quantity", orm.FieldOpts{String: "Quantity", Default: 1}), orm.Many2one("uom_id", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}), orm.Float("price_unit", orm.FieldOpts{String: "Unit Price"}), orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}), orm.Boolean("is_present", orm.FieldOpts{ String: "Present on Order", Default: false, }), ) // Onchange: product_id → name + price_unit m.RegisterOnchange("product_id", func(env *orm.Environment, vals orm.Values) orm.Values { result := make(orm.Values) var productID int64 switch v := vals["product_id"].(type) { case int64: productID = v case float64: productID = int64(v) case map[string]interface{}: if id, ok := v["id"]; ok { switch n := id.(type) { case float64: productID = int64(n) case int64: productID = n } } } if productID <= 0 { return result } var name string var listPrice float64 err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(pt.name, ''), COALESCE(pt.list_price, 0) FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pp.id = $1`, productID, ).Scan(&name, &listPrice) if err != nil { return result } result["name"] = name result["price_unit"] = listPrice return result }) // button_add: Add this option as an order line. Delegates to sale.order action_add_option. m.RegisterMethod("button_add", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() optionID := rs.IDs()[0] var orderID int64 err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(order_id, 0) FROM sale_order_option WHERE id = $1`, optionID, ).Scan(&orderID) if err != nil || orderID == 0 { return nil, fmt.Errorf("sale_option: no order linked to option %d", optionID) } soRS := env.Model("sale.order").Browse(orderID) soModel := orm.Registry.Get("sale.order") if fn, ok := soModel.Methods["action_add_option"]; ok { return fn(soRS, float64(optionID)) } return nil, fmt.Errorf("sale_option: action_add_option not found on sale.order") }) }