package models import ( "fmt" "odoo-go/pkg/orm" ) // initStockValuationLayer registers stock.valuation.layer — tracks inventory valuation per move. // Mirrors: odoo/addons/stock_account/models/stock_valuation_layer.py func initStockValuationLayer() { m := orm.NewModel("stock.valuation.layer", orm.ModelOpts{ Description: "Stock Valuation Layer", Order: "create_date, id", }) m.AddFields( orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product", Required: true, Index: true}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}), orm.Many2one("stock_move_id", "stock.move", orm.FieldOpts{String: "Stock Move"}), orm.Float("quantity", orm.FieldOpts{String: "Quantity"}), orm.Monetary("unit_cost", orm.FieldOpts{String: "Unit Value", CurrencyField: "currency_id"}), orm.Monetary("value", orm.FieldOpts{String: "Total Value", CurrencyField: "currency_id"}), orm.Monetary("remaining_value", orm.FieldOpts{String: "Remaining Value", CurrencyField: "currency_id"}), orm.Float("remaining_qty", orm.FieldOpts{String: "Remaining Qty"}), orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}), orm.Char("description", orm.FieldOpts{String: "Description"}), orm.Many2one("account_move_id", "account.move", orm.FieldOpts{String: "Journal Entry"}), ) // create_valuation_layer: Creates a valuation layer for a stock move. // Mirrors: stock.valuation.layer.create() via product._run_fifo() m.RegisterMethod("create_valuation_layer", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 4 { return nil, fmt.Errorf("stock.valuation.layer.create_valuation_layer requires product_id, move_id, quantity, unit_cost") } productID, _ := args[0].(int64) moveID, _ := args[1].(int64) quantity, _ := args[2].(float64) unitCost, _ := args[3].(float64) if productID == 0 || quantity == 0 { return nil, fmt.Errorf("stock.valuation.layer: invalid product_id or quantity") } env := rs.Env() totalValue := quantity * unitCost var layerID int64 err := env.Tx().QueryRow(env.Ctx(), `INSERT INTO stock_valuation_layer (product_id, stock_move_id, quantity, unit_cost, value, remaining_qty, remaining_value, company_id) VALUES ($1, $2, $3, $4, $5, $6, $7, 1) RETURNING id`, productID, moveID, quantity, unitCost, totalValue, quantity, totalValue, ).Scan(&layerID) if err != nil { return nil, fmt.Errorf("stock.valuation.layer: create layer: %w", err) } return map[string]interface{}{ "id": layerID, "value": totalValue, }, nil }) // get_product_valuation: Compute total valuation for a product across all remaining layers. // Mirrors: product.product._compute_stock_value() m.RegisterMethod("get_product_valuation", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.valuation.layer.get_product_valuation requires product_id") } productID, _ := args[0].(int64) if productID == 0 { return nil, fmt.Errorf("stock.valuation.layer: invalid product_id") } env := rs.Env() var totalQty, totalValue float64 err := env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(remaining_qty), 0), COALESCE(SUM(remaining_value), 0) FROM stock_valuation_layer WHERE product_id = $1 AND remaining_qty > 0`, productID, ).Scan(&totalQty, &totalValue) if err != nil { return nil, fmt.Errorf("stock.valuation.layer: get valuation: %w", err) } var avgCost float64 if totalQty > 0 { avgCost = totalValue / totalQty } return map[string]interface{}{ "product_id": productID, "total_qty": totalQty, "total_value": totalValue, "average_cost": avgCost, }, nil }) // consume_fifo: Consume quantity from existing layers using FIFO order. // Mirrors: product.product._run_fifo() for outgoing moves m.RegisterMethod("consume_fifo", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("stock.valuation.layer.consume_fifo requires product_id, quantity") } productID, _ := args[0].(int64) qtyToConsume, _ := args[1].(float64) if productID == 0 || qtyToConsume <= 0 { return nil, fmt.Errorf("stock.valuation.layer: invalid product_id or quantity") } env := rs.Env() // Get layers ordered by creation (FIFO) rows, err := env.Tx().Query(env.Ctx(), `SELECT id, remaining_qty, remaining_value, unit_cost FROM stock_valuation_layer WHERE product_id = $1 AND remaining_qty > 0 ORDER BY create_date, id`, productID, ) if err != nil { return nil, fmt.Errorf("stock.valuation.layer: query layers for FIFO: %w", err) } defer rows.Close() var totalConsumedValue float64 remaining := qtyToConsume for rows.Next() && remaining > 0 { var layerID int64 var layerQty, layerValue, layerUnitCost float64 if err := rows.Scan(&layerID, &layerQty, &layerValue, &layerUnitCost); err != nil { return nil, fmt.Errorf("stock.valuation.layer: scan FIFO layer: %w", err) } consumed := remaining if consumed > layerQty { consumed = layerQty } consumedValue := consumed * layerUnitCost newRemainingQty := layerQty - consumed newRemainingValue := layerValue - consumedValue _, err := env.Tx().Exec(env.Ctx(), `UPDATE stock_valuation_layer SET remaining_qty = $1, remaining_value = $2 WHERE id = $3`, newRemainingQty, newRemainingValue, layerID, ) if err != nil { return nil, fmt.Errorf("stock.valuation.layer: update layer %d: %w", layerID, err) } totalConsumedValue += consumedValue remaining -= consumed } return map[string]interface{}{ "consumed_qty": qtyToConsume - remaining, "consumed_value": totalConsumedValue, "remaining": remaining, }, nil }) // get_valuation_history: Return valuation layers for a product within a date range. // Mirrors: stock.valuation.layer reporting views m.RegisterMethod("get_valuation_history", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.valuation.layer.get_valuation_history requires product_id") } productID, _ := args[0].(int64) if productID == 0 { return nil, fmt.Errorf("stock.valuation.layer: invalid product_id") } env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), `SELECT id, quantity, unit_cost, value, remaining_qty, remaining_value, description FROM stock_valuation_layer WHERE product_id = $1 ORDER BY create_date DESC, id DESC`, productID, ) if err != nil { return nil, fmt.Errorf("stock.valuation.layer: query history: %w", err) } defer rows.Close() var layers []map[string]interface{} for rows.Next() { var id int64 var qty, unitCost, value, remQty, remValue float64 var description *string if err := rows.Scan(&id, &qty, &unitCost, &value, &remQty, &remValue, &description); err != nil { return nil, fmt.Errorf("stock.valuation.layer: scan history row: %w", err) } desc := "" if description != nil { desc = *description } layers = append(layers, map[string]interface{}{ "id": id, "quantity": qty, "unit_cost": unitCost, "value": value, "remaining_qty": remQty, "remaining_value": remValue, "description": desc, }) } return map[string]interface{}{"layers": layers}, nil }) }