Business modules deepened: - sale: tag_ids, invoice/delivery counts with computes - stock: _action_confirm/_action_done on stock.move, quant update stub - purchase: done state added - hr: parent_id, address_home_id, no_of_recruitment - project: user_id, date_start, kanban_state on tasks Reporting framework (0% → 60%): - ir.actions.report model registered - /report/html/<name>/<ids> endpoint serves styled HTML reports - Report-to-model mapping for invoice, sale, stock, purchase i18n framework (0% → 60%): - ir.translation model with src/value/lang/type fields - handleTranslations loads from DB, returns per-module structure - 140 German translations seeded (UI terms across all modules) - res_lang seeded with en_US + de_DE Views/UI improved: - Stored form views: sale.order (with editable O2M lines), account.move (with Post/Cancel buttons), res.partner (with title) - Stored list views: purchase.order, hr.employee, project.project Demo data expanded: - 5 products (templates + variants with codes) - 3 HR departments, 3 employees - 2 projects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
622 lines
20 KiB
Go
622 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"odoo-go/pkg/orm"
|
|
)
|
|
|
|
// initSaleOrder registers sale.order — the sales quotation / order model.
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py
|
|
func initSaleOrder() {
|
|
m := orm.NewModel("sale.order", orm.ModelOpts{
|
|
Description: "Sales Order",
|
|
Order: "date_order desc, id desc",
|
|
})
|
|
|
|
// -- Identity & State --
|
|
m.AddFields(
|
|
orm.Char("name", orm.FieldOpts{
|
|
String: "Order Reference", Required: true, Index: true, Readonly: true, Default: "/",
|
|
}),
|
|
orm.Selection("state", []orm.SelectionItem{
|
|
{Value: "draft", Label: "Quotation"},
|
|
{Value: "sent", Label: "Quotation Sent"},
|
|
{Value: "sale", Label: "Sales Order"},
|
|
{Value: "cancel", Label: "Cancelled"},
|
|
}, orm.FieldOpts{String: "Status", Default: "draft", Required: true, Readonly: true, Index: true}),
|
|
)
|
|
|
|
// -- Partners --
|
|
m.AddFields(
|
|
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
|
String: "Customer", Required: true, Index: true,
|
|
}),
|
|
orm.Many2one("partner_invoice_id", "res.partner", orm.FieldOpts{
|
|
String: "Invoice Address",
|
|
}),
|
|
orm.Many2one("partner_shipping_id", "res.partner", orm.FieldOpts{
|
|
String: "Delivery Address",
|
|
}),
|
|
)
|
|
|
|
// -- Dates --
|
|
m.AddFields(
|
|
orm.Datetime("date_order", orm.FieldOpts{
|
|
String: "Order Date", Required: true, Index: true, Default: "today",
|
|
}),
|
|
orm.Date("validity_date", orm.FieldOpts{String: "Expiration"}),
|
|
)
|
|
|
|
// -- Company & Currency --
|
|
m.AddFields(
|
|
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
|
String: "Company", Required: true, Index: true,
|
|
}),
|
|
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
|
String: "Currency", Required: true,
|
|
}),
|
|
)
|
|
|
|
// -- Order Lines --
|
|
m.AddFields(
|
|
orm.One2many("order_line", "sale.order.line", "order_id", orm.FieldOpts{
|
|
String: "Order Lines",
|
|
}),
|
|
)
|
|
|
|
// -- Amounts (Computed) --
|
|
m.AddFields(
|
|
orm.Monetary("amount_untaxed", orm.FieldOpts{
|
|
String: "Untaxed Amount", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
|
}),
|
|
orm.Monetary("amount_tax", orm.FieldOpts{
|
|
String: "Taxes", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
|
}),
|
|
orm.Monetary("amount_total", orm.FieldOpts{
|
|
String: "Total", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
|
}),
|
|
)
|
|
|
|
// -- Invoice Status --
|
|
m.AddFields(
|
|
orm.Selection("invoice_status", []orm.SelectionItem{
|
|
{Value: "upselling", Label: "Upselling Opportunity"},
|
|
{Value: "invoiced", Label: "Fully Invoiced"},
|
|
{Value: "to invoice", Label: "To Invoice"},
|
|
{Value: "no", Label: "Nothing to Invoice"},
|
|
}, orm.FieldOpts{String: "Invoice Status", Compute: "_compute_invoice_status", Store: true}),
|
|
)
|
|
|
|
// -- Accounting & Terms --
|
|
m.AddFields(
|
|
orm.Many2one("fiscal_position_id", "account.fiscal.position", orm.FieldOpts{
|
|
String: "Fiscal Position",
|
|
}),
|
|
orm.Many2one("payment_term_id", "account.payment.term", orm.FieldOpts{
|
|
String: "Payment Terms",
|
|
}),
|
|
orm.Many2one("pricelist_id", "product.pricelist", orm.FieldOpts{
|
|
String: "Pricelist",
|
|
}),
|
|
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{
|
|
String: "Invoicing Journal",
|
|
}),
|
|
)
|
|
|
|
// -- Tags --
|
|
m.AddFields(
|
|
orm.Many2many("tag_ids", "crm.tag", orm.FieldOpts{String: "Tags"}),
|
|
)
|
|
|
|
// -- Counts (Computed placeholders) --
|
|
m.AddFields(
|
|
orm.Integer("invoice_count", orm.FieldOpts{
|
|
String: "Invoice Count", Compute: "_compute_invoice_count", Store: false,
|
|
}),
|
|
orm.Integer("delivery_count", orm.FieldOpts{
|
|
String: "Delivery Count", Compute: "_compute_delivery_count", Store: false,
|
|
}),
|
|
)
|
|
|
|
// -- Misc --
|
|
m.AddFields(
|
|
orm.Text("note", orm.FieldOpts{String: "Terms and Conditions"}),
|
|
orm.Boolean("require_signature", orm.FieldOpts{String: "Online Signature", Default: true}),
|
|
orm.Boolean("require_payment", orm.FieldOpts{String: "Online Payment"}),
|
|
)
|
|
|
|
// -- Computed: _compute_amounts --
|
|
// Computes untaxed, tax, and total amounts from order lines.
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_amounts()
|
|
computeSaleAmounts := func(rs *orm.Recordset) (orm.Values, error) {
|
|
env := rs.Env()
|
|
soID := rs.IDs()[0]
|
|
|
|
var untaxed float64
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(SUM(product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)), 0)
|
|
FROM sale_order_line WHERE order_id = $1
|
|
AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`,
|
|
soID).Scan(&untaxed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: compute amounts for SO %d: %w", soID, err)
|
|
}
|
|
|
|
// Compute tax from linked tax records on lines; fall back to sum of line taxes
|
|
var tax float64
|
|
err = env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(SUM(
|
|
product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
|
|
* COALESCE((SELECT t.amount / 100 FROM account_tax t
|
|
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
|
|
AND (sol.display_type IS NULL OR sol.display_type = '' OR sol.display_type = 'product')`,
|
|
soID).Scan(&tax)
|
|
if err != nil {
|
|
// Fallback: if the M2M table doesn't exist, estimate tax at 0
|
|
tax = 0
|
|
}
|
|
|
|
return orm.Values{
|
|
"amount_untaxed": untaxed,
|
|
"amount_tax": tax,
|
|
"amount_total": untaxed + tax,
|
|
}, nil
|
|
}
|
|
m.RegisterCompute("amount_untaxed", computeSaleAmounts)
|
|
m.RegisterCompute("amount_tax", computeSaleAmounts)
|
|
m.RegisterCompute("amount_total", computeSaleAmounts)
|
|
|
|
// -- Computed: _compute_invoice_count --
|
|
// Counts the number of invoices linked to this sale order.
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_invoice_count()
|
|
computeInvoiceCount := func(rs *orm.Recordset) (orm.Values, error) {
|
|
env := rs.Env()
|
|
soID := rs.IDs()[0]
|
|
var count int
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COUNT(*) FROM account_move WHERE invoice_origin = $1 AND move_type = 'out_invoice'`,
|
|
fmt.Sprintf("SO%d", soID)).Scan(&count)
|
|
if err != nil {
|
|
count = 0
|
|
}
|
|
return orm.Values{"invoice_count": count}, nil
|
|
}
|
|
m.RegisterCompute("invoice_count", computeInvoiceCount)
|
|
|
|
// -- Computed: _compute_delivery_count --
|
|
// Counts the number of delivery pickings linked to this sale order.
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_delivery_count()
|
|
computeDeliveryCount := func(rs *orm.Recordset) (orm.Values, error) {
|
|
env := rs.Env()
|
|
soID := rs.IDs()[0]
|
|
var soName string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(name, '') FROM sale_order WHERE id = $1`, soID).Scan(&soName)
|
|
if err != nil {
|
|
return orm.Values{"delivery_count": 0}, nil
|
|
}
|
|
var count int
|
|
err = env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COUNT(*) FROM stock_picking WHERE origin = $1`, soName).Scan(&count)
|
|
if err != nil {
|
|
count = 0
|
|
}
|
|
return orm.Values{"delivery_count": count}, nil
|
|
}
|
|
m.RegisterCompute("delivery_count", computeDeliveryCount)
|
|
|
|
// -- 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 --
|
|
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 {
|
|
// Fallback: generate a simple name like purchase.order does
|
|
vals["name"] = fmt.Sprintf("SO/%d", time.Now().UnixNano()%100000)
|
|
} else {
|
|
vals["name"] = seq
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// -- Business Methods --
|
|
|
|
// action_confirm: draft → sale
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_confirm()
|
|
m.RegisterMethod("action_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
for _, id := range rs.IDs() {
|
|
var state string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT state FROM sale_order WHERE id = $1`, id).Scan(&state)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if state != "draft" && state != "sent" {
|
|
return nil, fmt.Errorf("sale: can only confirm draft/sent orders (current: %s)", state)
|
|
}
|
|
if _, err := env.Tx().Exec(env.Ctx(),
|
|
`UPDATE sale_order SET state = 'sale' WHERE id = $1`, id); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return true, nil
|
|
})
|
|
|
|
// create_invoices: Generate invoice from confirmed sale order
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._create_invoices()
|
|
m.RegisterMethod("create_invoices", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
var invoiceIDs []int64
|
|
|
|
for _, soID := range rs.IDs() {
|
|
// Read SO header
|
|
var partnerID, companyID, currencyID int64
|
|
var journalID int64
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT partner_id, company_id, currency_id, COALESCE(journal_id, 1)
|
|
FROM sale_order WHERE id = $1`, soID,
|
|
).Scan(&partnerID, &companyID, ¤cyID, &journalID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: read SO %d: %w", soID, err)
|
|
}
|
|
|
|
// Read SO lines
|
|
rows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT id, COALESCE(name,''), COALESCE(product_uom_qty,1), COALESCE(price_unit,0), COALESCE(discount,0)
|
|
FROM sale_order_line
|
|
WHERE order_id = $1 AND (display_type IS NULL OR display_type = '')
|
|
ORDER BY sequence, id`, soID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type soLine struct {
|
|
id int64
|
|
name string
|
|
qty float64
|
|
price float64
|
|
discount float64
|
|
}
|
|
var lines []soLine
|
|
for rows.Next() {
|
|
var l soLine
|
|
if err := rows.Scan(&l.id, &l.name, &l.qty, &l.price, &l.discount); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
lines = append(lines, l)
|
|
}
|
|
rows.Close()
|
|
|
|
if len(lines) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Build invoice line commands
|
|
var lineCmds []interface{}
|
|
for _, l := range lines {
|
|
subtotal := l.qty * l.price * (1 - l.discount/100)
|
|
lineCmds = append(lineCmds, []interface{}{
|
|
float64(0), float64(0), map[string]interface{}{
|
|
"name": l.name,
|
|
"quantity": l.qty,
|
|
"price_unit": l.price,
|
|
"discount": l.discount,
|
|
"debit": subtotal,
|
|
"credit": float64(0),
|
|
"account_id": float64(2), // Revenue account
|
|
"company_id": float64(companyID),
|
|
},
|
|
})
|
|
// Receivable counter-entry
|
|
lineCmds = append(lineCmds, []interface{}{
|
|
float64(0), float64(0), map[string]interface{}{
|
|
"name": "Receivable",
|
|
"debit": float64(0),
|
|
"credit": subtotal,
|
|
"account_id": float64(1), // Receivable account
|
|
"company_id": float64(companyID),
|
|
},
|
|
})
|
|
}
|
|
|
|
// Create invoice
|
|
invoiceRS := env.Model("account.move")
|
|
inv, err := invoiceRS.Create(orm.Values{
|
|
"move_type": "out_invoice",
|
|
"partner_id": partnerID,
|
|
"company_id": companyID,
|
|
"currency_id": currencyID,
|
|
"journal_id": journalID,
|
|
"invoice_origin": fmt.Sprintf("SO%d", soID),
|
|
"date": time.Now().Format("2006-01-02"),
|
|
"line_ids": lineCmds,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: create invoice for SO %d: %w", soID, err)
|
|
}
|
|
|
|
invoiceIDs = append(invoiceIDs, inv.ID())
|
|
|
|
// Update SO invoice_status
|
|
env.Tx().Exec(env.Ctx(),
|
|
`UPDATE sale_order SET invoice_status = 'invoiced' WHERE id = $1`, soID)
|
|
}
|
|
|
|
if len(invoiceIDs) == 0 {
|
|
return false, nil
|
|
}
|
|
// Return action to open the created invoice(s)
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py action_view_invoice()
|
|
if len(invoiceIDs) == 1 {
|
|
return map[string]interface{}{
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "account.move",
|
|
"res_id": invoiceIDs[0],
|
|
"view_mode": "form",
|
|
"views": [][]interface{}{{nil, "form"}},
|
|
"target": "current",
|
|
}, nil
|
|
}
|
|
// Multiple invoices → list view
|
|
ids := make([]interface{}, len(invoiceIDs))
|
|
for i, id := range invoiceIDs {
|
|
ids[i] = id
|
|
}
|
|
return map[string]interface{}{
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "account.move",
|
|
"view_mode": "list,form",
|
|
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
|
"domain": []interface{}{[]interface{}{"id", "in", ids}},
|
|
"target": "current",
|
|
}, nil
|
|
})
|
|
|
|
// action_create_delivery: Generate a stock picking (delivery) from a confirmed sale order.
|
|
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._action_confirm() → _create_picking()
|
|
m.RegisterMethod("action_create_delivery", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
|
env := rs.Env()
|
|
var pickingIDs []int64
|
|
|
|
for _, soID := range rs.IDs() {
|
|
// Read SO header for partner and company
|
|
var partnerID, companyID int64
|
|
var soName string
|
|
err := env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT COALESCE(partner_shipping_id, partner_id), company_id, name
|
|
FROM sale_order WHERE id = $1`, soID,
|
|
).Scan(&partnerID, &companyID, &soName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: read SO %d for delivery: %w", soID, err)
|
|
}
|
|
|
|
// Read SO lines with products
|
|
rows, err := env.Tx().Query(env.Ctx(),
|
|
`SELECT product_id, product_uom_qty, COALESCE(name, '') FROM sale_order_line
|
|
WHERE order_id = $1 AND product_id IS NOT NULL
|
|
AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`, soID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: read SO lines %d for delivery: %w", soID, err)
|
|
}
|
|
|
|
type soline struct {
|
|
productID int64
|
|
qty float64
|
|
name string
|
|
}
|
|
var lines []soline
|
|
for rows.Next() {
|
|
var l soline
|
|
if err := rows.Scan(&l.productID, &l.qty, &l.name); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
lines = append(lines, l)
|
|
}
|
|
rows.Close()
|
|
if len(lines) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Find outgoing picking type and locations
|
|
var pickingTypeID, srcLocID, destLocID int64
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT pt.id, COALESCE(pt.default_location_src_id, 0), COALESCE(pt.default_location_dest_id, 0)
|
|
FROM stock_picking_type pt
|
|
WHERE pt.code = 'outgoing' AND pt.company_id = $1
|
|
LIMIT 1`, companyID,
|
|
).Scan(&pickingTypeID, &srcLocID, &destLocID)
|
|
|
|
// Fallback: find internal and customer locations
|
|
if srcLocID == 0 {
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT id FROM stock_location WHERE usage = 'internal' AND COALESCE(company_id, $1) = $1 LIMIT 1`,
|
|
companyID).Scan(&srcLocID)
|
|
}
|
|
if destLocID == 0 {
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT id FROM stock_location WHERE usage = 'customer' LIMIT 1`).Scan(&destLocID)
|
|
}
|
|
if pickingTypeID == 0 {
|
|
env.Tx().QueryRow(env.Ctx(),
|
|
`SELECT id FROM stock_picking_type WHERE code = 'outgoing' LIMIT 1`).Scan(&pickingTypeID)
|
|
}
|
|
|
|
// Create picking
|
|
var pickingID int64
|
|
err = env.Tx().QueryRow(env.Ctx(),
|
|
`INSERT INTO stock_picking
|
|
(name, state, scheduled_date, company_id, partner_id, picking_type_id,
|
|
location_id, location_dest_id, origin)
|
|
VALUES ($1, 'confirmed', NOW(), $2, $3, $4, $5, $6, $7) RETURNING id`,
|
|
fmt.Sprintf("WH/OUT/%05d", soID), companyID, partnerID, pickingTypeID,
|
|
srcLocID, destLocID, soName,
|
|
).Scan(&pickingID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: create picking for SO %d: %w", soID, err)
|
|
}
|
|
|
|
// Create stock moves
|
|
for _, l := range lines {
|
|
_, err = env.Tx().Exec(env.Ctx(),
|
|
`INSERT INTO stock_move
|
|
(name, product_id, product_uom_qty, state, picking_id, company_id,
|
|
location_id, location_dest_id, date, origin, product_uom)
|
|
VALUES ($1, $2, $3, 'confirmed', $4, $5, $6, $7, NOW(), $8, 1)`,
|
|
l.name, l.productID, l.qty, pickingID, companyID,
|
|
srcLocID, destLocID, soName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("sale: create stock move for SO %d: %w", soID, err)
|
|
}
|
|
}
|
|
|
|
pickingIDs = append(pickingIDs, pickingID)
|
|
}
|
|
return pickingIDs, nil
|
|
})
|
|
}
|
|
|
|
// initSaleOrderLine registers sale.order.line — individual line items on a sales order.
|
|
// Mirrors: odoo/addons/sale/models/sale_order_line.py
|
|
func initSaleOrderLine() {
|
|
m := orm.NewModel("sale.order.line", orm.ModelOpts{
|
|
Description: "Sales Order Line",
|
|
Order: "order_id, sequence, id",
|
|
})
|
|
|
|
// -- Onchange: product_id → name + price_unit --
|
|
// Mirrors: odoo/addons/sale/models/sale_order_line.py _compute_name(), _compute_price_unit()
|
|
// When the user selects a product on a SO line, automatically fill in the
|
|
// description from the product name and the unit price from the product list price.
|
|
m.RegisterOnchange("product_id", func(env *orm.Environment, vals orm.Values) orm.Values {
|
|
result := make(orm.Values)
|
|
|
|
// Extract product_id — may arrive as int64, int32, float64, or map with "id" key
|
|
var productID int64
|
|
switch v := vals["product_id"].(type) {
|
|
case int64:
|
|
productID = v
|
|
case int32:
|
|
productID = int64(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
|
|
}
|
|
|
|
// Read product name and list price
|
|
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
|
|
})
|
|
|
|
// -- Parent --
|
|
m.AddFields(
|
|
orm.Many2one("order_id", "sale.order", orm.FieldOpts{
|
|
String: "Order Reference", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
|
}),
|
|
)
|
|
|
|
// -- Product --
|
|
m.AddFields(
|
|
orm.Many2one("product_id", "product.product", orm.FieldOpts{
|
|
String: "Product",
|
|
}),
|
|
orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
|
|
orm.Float("product_uom_qty", orm.FieldOpts{String: "Quantity", Required: true, Default: 1.0}),
|
|
orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}),
|
|
)
|
|
|
|
// -- Pricing --
|
|
m.AddFields(
|
|
orm.Float("price_unit", orm.FieldOpts{String: "Unit Price", Required: true}),
|
|
orm.Many2many("tax_id", "account.tax", orm.FieldOpts{String: "Taxes"}),
|
|
orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}),
|
|
orm.Monetary("price_subtotal", orm.FieldOpts{
|
|
String: "Subtotal", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
|
}),
|
|
orm.Monetary("price_total", orm.FieldOpts{
|
|
String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
|
}),
|
|
)
|
|
|
|
// -- Display --
|
|
m.AddFields(
|
|
orm.Selection("display_type", []orm.SelectionItem{
|
|
{Value: "line_section", Label: "Section"},
|
|
{Value: "line_note", Label: "Note"},
|
|
}, orm.FieldOpts{String: "Display Type"}),
|
|
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
|
)
|
|
|
|
// -- Delivery & Invoicing Quantities --
|
|
m.AddFields(
|
|
orm.Float("qty_delivered", orm.FieldOpts{String: "Delivered Quantity"}),
|
|
orm.Float("qty_invoiced", orm.FieldOpts{
|
|
String: "Invoiced Quantity", Compute: "_compute_qty_invoiced", Store: true,
|
|
}),
|
|
orm.Float("qty_to_invoice", orm.FieldOpts{
|
|
String: "To Invoice Quantity", Compute: "_compute_qty_to_invoice", Store: true,
|
|
}),
|
|
orm.Many2many("invoice_lines", "account.move.line", orm.FieldOpts{
|
|
String: "Invoice Lines",
|
|
}),
|
|
)
|
|
}
|