package models

import (
	"fmt"
	"time"

	"odoo-go/pkg/orm"
)

// initStock registers all stock models.
// Mirrors: odoo/addons/stock/models/stock_warehouse.py,
//          stock_location.py, stock_picking.py, stock_move.py,
//          stock_move_line.py, stock_quant.py, stock_lot.py

func initStock() {
	initStockWarehouse()
	initStockLocation()
	initStockPickingType()
	initStockPicking()
	initStockMove()
	initStockMoveLine()
	initStockQuant()
	initStockLot()
	initStockRoute()
	initStockRule()
	initStockOrderpoint()
	initStockScrap()
	initStockInventory()
	initProductStockExtension()
	initStockValuationLayer()
	initStockLandedCost()
	initStockReport()
	initStockForecast()
	initStockPickingBatch()
	initStockPickingBatchExtension()
	initStockBarcode()
}

// initStockWarehouse registers stock.warehouse.
// Mirrors: odoo/addons/stock/models/stock_warehouse.py
func initStockWarehouse() {
	m := orm.NewModel("stock.warehouse", orm.ModelOpts{
		Description: "Warehouse",
		Order:       "sequence, id",
		RecName:     "name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Warehouse", Required: true, Index: true}),
		orm.Char("code", orm.FieldOpts{String: "Short Name", Required: true, Size: 5}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
			String: "Address",
		}),
		orm.Many2one("lot_stock_id", "stock.location", orm.FieldOpts{
			String: "Location Stock", Required: true, OnDelete: orm.OnDeleteRestrict,
		}),
		orm.Many2one("wh_input_stock_loc_id", "stock.location", orm.FieldOpts{
			String: "Input Location",
		}),
		orm.Many2one("wh_output_stock_loc_id", "stock.location", orm.FieldOpts{
			String: "Output Location",
		}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
	)
}

// initStockLocation registers stock.location.
// Mirrors: odoo/addons/stock/models/stock_location.py
func initStockLocation() {
	m := orm.NewModel("stock.location", orm.ModelOpts{
		Description: "Location",
		Order:       "complete_name, id",
		RecName:     "complete_name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Location Name", Required: true, Translate: true}),
		orm.Char("complete_name", orm.FieldOpts{
			String: "Full Location Name", Compute: "_compute_complete_name", Store: true,
		}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
		orm.Selection("usage", []orm.SelectionItem{
			{Value: "supplier", Label: "Vendor Location"},
			{Value: "view", Label: "View"},
			{Value: "internal", Label: "Internal Location"},
			{Value: "customer", Label: "Customer Location"},
			{Value: "inventory", Label: "Inventory Loss"},
			{Value: "production", Label: "Production"},
			{Value: "transit", Label: "Transit Location"},
		}, orm.FieldOpts{String: "Location Type", Required: true, Default: "internal", Index: true}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "Parent Location", Index: true, OnDelete: orm.OnDeleteCascade,
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Index: true,
		}),
		orm.Many2one("removal_strategy_id", "product.removal", orm.FieldOpts{
			String: "Removal Strategy",
		}),
		orm.Boolean("scrap_location", orm.FieldOpts{String: "Is a Scrap Location?"}),
		orm.Boolean("return_location", orm.FieldOpts{String: "Is a Return Location?"}),
		orm.Char("barcode", orm.FieldOpts{String: "Barcode", Index: true}),
	)
}

// initStockPickingType registers stock.picking.type.
// Mirrors: odoo/addons/stock/models/stock_picking.py StockPickingType
func initStockPickingType() {
	m := orm.NewModel("stock.picking.type", orm.ModelOpts{
		Description: "Picking Type",
		Order:       "sequence, id",
		RecName:     "name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Operation Type", Required: true, Translate: true}),
		orm.Selection("code", []orm.SelectionItem{
			{Value: "incoming", Label: "Receipt"},
			{Value: "outgoing", Label: "Delivery"},
			{Value: "internal", Label: "Internal Transfer"},
		}, orm.FieldOpts{String: "Type of Operation", Required: true}),
		orm.Char("sequence_code", orm.FieldOpts{String: "Code", Required: true}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
		orm.Many2one("warehouse_id", "stock.warehouse", orm.FieldOpts{
			String: "Warehouse", OnDelete: orm.OnDeleteCascade,
		}),
		orm.Many2one("default_location_src_id", "stock.location", orm.FieldOpts{
			String: "Default Source Location",
		}),
		orm.Many2one("default_location_dest_id", "stock.location", orm.FieldOpts{
			String: "Default Destination Location",
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
		orm.Boolean("show_operations", orm.FieldOpts{String: "Show Detailed Operations"}),
		orm.Boolean("show_reserved", orm.FieldOpts{String: "Show Reserved"}),
	)
}

// initStockPicking registers stock.picking — the transfer order.
// Mirrors: odoo/addons/stock/models/stock_picking.py StockPicking
func initStockPicking() {
	m := orm.NewModel("stock.picking", orm.ModelOpts{
		Description: "Transfer",
		Order:       "priority desc, scheduled_date asc, id desc",
		RecName:     "name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{
			String: "Reference", Default: "/", Required: true, Index: true, Readonly: true,
		}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "draft", Label: "Draft"},
			{Value: "waiting", Label: "Waiting Another Operation"},
			{Value: "confirmed", Label: "Waiting"},
			{Value: "assigned", Label: "Ready"},
			{Value: "done", Label: "Done"},
			{Value: "cancel", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Status", Default: "draft", Compute: "_compute_state", Store: true, Index: true}),
		orm.Selection("priority", []orm.SelectionItem{
			{Value: "0", Label: "Normal"},
			{Value: "1", Label: "Urgent"},
		}, orm.FieldOpts{String: "Priority", Default: "0", Index: true}),
		orm.Many2one("picking_type_id", "stock.picking.type", orm.FieldOpts{
			String: "Operation Type", Required: true, Index: true,
		}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "Source Location", Required: true, Index: true,
		}),
		orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
			String: "Destination Location", Required: true, Index: true,
		}),
		orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
			String: "Contact", Index: true,
		}),
		orm.Datetime("scheduled_date", orm.FieldOpts{String: "Scheduled Date", Required: true, Index: true}),
		orm.Datetime("date_deadline", orm.FieldOpts{String: "Deadline"}),
		orm.Datetime("date_done", orm.FieldOpts{String: "Date of Transfer", Readonly: true}),
		orm.One2many("move_ids", "stock.move", "picking_id", orm.FieldOpts{
			String: "Stock Moves",
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Text("note", orm.FieldOpts{String: "Notes"}),
		orm.Char("origin", orm.FieldOpts{String: "Source Document", Index: true}),
	)

	// --- BeforeCreate hook: auto-generate picking reference ---
	// Mirrors: stock.picking._create_sequence() / ir.sequence
	m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
		name, _ := vals["name"].(string)
		if name == "" || name == "/" {
			vals["name"] = fmt.Sprintf("WH/IN/%05d", time.Now().UnixNano()%100000)
		}
		return nil
	}

	// --- Business methods: stock move workflow ---

	// action_confirm transitions a picking from draft → confirmed.
	// Confirms all associated stock moves via _action_confirm (which also reserves).
	// Mirrors: stock.picking.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 stock_picking WHERE id = $1`, id).Scan(&state)
			if err != nil {
				return nil, fmt.Errorf("stock: cannot read picking %d: %w", id, err)
			}
			if state != "draft" {
				return nil, fmt.Errorf("stock: can only confirm draft pickings (picking %d is %q)", id, state)
			}
			_, err = env.Tx().Exec(env.Ctx(),
				`UPDATE stock_picking SET state = 'confirmed' WHERE id = $1`, id)
			if err != nil {
				return nil, fmt.Errorf("stock: confirm picking %d: %w", id, err)
			}

			// Confirm all draft moves via _action_confirm (which also tries to reserve)
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT id FROM stock_move WHERE picking_id = $1 AND state = 'draft'`, id)
			if err != nil {
				return nil, fmt.Errorf("stock: read draft moves for picking %d: %w", id, err)
			}
			var moveIDs []int64
			for rows.Next() {
				var mid int64
				if err := rows.Scan(&mid); err != nil {
					rows.Close()
					return nil, fmt.Errorf("stock: scan move for picking %d: %w", id, err)
				}
				moveIDs = append(moveIDs, mid)
			}
			rows.Close()
			if err := rows.Err(); err != nil {
				return nil, fmt.Errorf("stock: iterate moves for picking %d: %w", id, err)
			}

			if len(moveIDs) > 0 {
				moveRS := env.Model("stock.move").Browse(moveIDs...)
				moveModel := orm.Registry.Get("stock.move")
				if moveModel != nil {
					if confirmMethod, ok := moveModel.Methods["_action_confirm"]; ok {
						if _, err := confirmMethod(moveRS); err != nil {
							return nil, fmt.Errorf("stock: confirm moves for picking %d: %w", id, err)
						}
					}
				}
			}

			// Update picking state based on move states after reservation
			var allAssigned bool
			err = env.Tx().QueryRow(env.Ctx(),
				`SELECT NOT EXISTS(
					SELECT 1 FROM stock_move
					WHERE picking_id = $1 AND state NOT IN ('assigned', 'done', 'cancel')
				)`, id).Scan(&allAssigned)
			if err != nil {
				return nil, fmt.Errorf("stock: check move states for picking %d: %w", id, err)
			}
			if allAssigned {
				_, err = env.Tx().Exec(env.Ctx(),
					`UPDATE stock_picking SET state = 'assigned' WHERE id = $1`, id)
				if err != nil {
					return nil, fmt.Errorf("stock: update picking %d to assigned: %w", id, err)
				}
			}
		}
		return true, nil
	})

	// action_assign reserves stock for all confirmed/partially_available moves on the picking.
	// Mirrors: stock.picking.action_assign()
	m.RegisterMethod("action_assign", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, pickingID := range rs.IDs() {
			// Get moves that need reservation
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT id FROM stock_move WHERE picking_id = $1 AND state IN ('confirmed', 'partially_available')`, pickingID)
			if err != nil {
				return nil, fmt.Errorf("stock: read moves for assign picking %d: %w", pickingID, err)
			}
			var moveIDs []int64
			for rows.Next() {
				var id int64
				if err := rows.Scan(&id); err != nil {
					rows.Close()
					return nil, fmt.Errorf("stock: scan move for picking %d: %w", pickingID, err)
				}
				moveIDs = append(moveIDs, id)
			}
			rows.Close()
			if err := rows.Err(); err != nil {
				return nil, fmt.Errorf("stock: iterate moves for picking %d: %w", pickingID, err)
			}

			if len(moveIDs) > 0 {
				moveRS := env.Model("stock.move").Browse(moveIDs...)
				moveModel := orm.Registry.Get("stock.move")
				if moveModel != nil {
					if assignMethod, ok := moveModel.Methods["_action_assign"]; ok {
						if _, err := assignMethod(moveRS); err != nil {
							return nil, fmt.Errorf("stock: assign moves for picking %d: %w", pickingID, err)
						}
					}
				}
			}

			// Update picking state based on move states
			var allAssigned bool
			err = env.Tx().QueryRow(env.Ctx(),
				`SELECT NOT EXISTS(
					SELECT 1 FROM stock_move
					WHERE picking_id = $1 AND state NOT IN ('assigned', 'done', 'cancel')
				)`, pickingID).Scan(&allAssigned)
			if err != nil {
				return nil, fmt.Errorf("stock: check move states for picking %d: %w", pickingID, err)
			}
			if allAssigned {
				_, err = env.Tx().Exec(env.Ctx(),
					`UPDATE stock_picking SET state = 'assigned' WHERE id = $1`, pickingID)
			}
			if err != nil {
				return nil, fmt.Errorf("stock: update picking %d state: %w", pickingID, err)
			}
		}
		return true, nil
	})

	// action_cancel: Cancel a picking and all its moves.
	// Mirrors: odoo/addons/stock/models/stock_picking.py StockPicking.action_cancel()
	m.RegisterMethod("action_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, pickingID := range rs.IDs() {
			env.Tx().Exec(env.Ctx(), `UPDATE stock_move SET state = 'cancel' WHERE picking_id = $1`, pickingID)
			env.Tx().Exec(env.Ctx(), `UPDATE stock_picking SET state = 'cancel' WHERE id = $1`, pickingID)
		}
		return true, nil
	})

	// button_validate transitions a picking → done via _action_done on its moves.
	// Properly updates quants and clears reservations.
	// Mirrors: stock.picking.button_validate()
	m.RegisterMethod("button_validate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, pickingID := range rs.IDs() {
			// Get all non-cancelled moves for this picking
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT id FROM stock_move WHERE picking_id = $1 AND state != 'cancel'`, pickingID)
			if err != nil {
				return nil, fmt.Errorf("stock: read moves for picking %d: %w", pickingID, err)
			}
			var moveIDs []int64
			for rows.Next() {
				var id int64
				if err := rows.Scan(&id); err != nil {
					rows.Close()
					return nil, fmt.Errorf("stock: scan move for picking %d: %w", pickingID, err)
				}
				moveIDs = append(moveIDs, id)
			}
			rows.Close()
			if err := rows.Err(); err != nil {
				return nil, fmt.Errorf("stock: iterate moves for picking %d: %w", pickingID, err)
			}

			if len(moveIDs) == 0 {
				continue
			}

			// Call _action_done on all moves
			moveRS := env.Model("stock.move").Browse(moveIDs...)
			moveModel := orm.Registry.Get("stock.move")
			if moveModel != nil {
				if doneMethod, ok := moveModel.Methods["_action_done"]; ok {
					if _, err := doneMethod(moveRS); err != nil {
						return nil, fmt.Errorf("stock: action_done for picking %d: %w", pickingID, err)
					}
				}
			}

			// Update picking state
			_, err = env.Tx().Exec(env.Ctx(),
				`UPDATE stock_picking SET state = 'done', date_done = NOW() WHERE id = $1`, pickingID)
			if err != nil {
				return nil, fmt.Errorf("stock: validate picking %d: %w", pickingID, err)
			}
		}
		return true, nil
	})

	// action_return creates a reverse transfer (return picking) with swapped locations.
	// Copies all done moves from the original picking to the return picking.
	// Mirrors: odoo/addons/stock/wizard/stock_picking_return.py
	m.RegisterMethod("action_return", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		pickingID := rs.IDs()[0]

		// Read original picking
		var partnerID, pickTypeID, locID, locDestID int64
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(partner_id,0), picking_type_id, location_id, location_dest_id
			 FROM stock_picking WHERE id = $1`, pickingID,
		).Scan(&partnerID, &pickTypeID, &locID, &locDestID)
		if err != nil {
			return nil, fmt.Errorf("stock: read picking %d for return: %w", pickingID, err)
		}

		// Create return picking (swap source and destination)
		returnRS := env.Model("stock.picking")
		returnVals := orm.Values{
			"name":            fmt.Sprintf("Return of %d", pickingID),
			"picking_type_id": pickTypeID,
			"location_id":     locDestID, // Swap!
			"location_dest_id": locID,    // Swap!
			"company_id":      int64(1),
			"state":           "draft",
			"scheduled_date":  time.Now().Format("2006-01-02"),
		}
		if partnerID > 0 {
			returnVals["partner_id"] = partnerID
		}

		returnPicking, err := returnRS.Create(returnVals)
		if err != nil {
			return nil, fmt.Errorf("stock: create return picking for %d: %w", pickingID, err)
		}

		// Copy moves with swapped locations
		moveRows, err := env.Tx().Query(env.Ctx(),
			`SELECT product_id, product_uom_qty, product_uom FROM stock_move
			 WHERE picking_id = $1 AND state = 'done'`, pickingID)
		if err != nil {
			return nil, fmt.Errorf("stock: read moves for return of picking %d: %w", pickingID, err)
		}
		defer moveRows.Close()

		moveRS := env.Model("stock.move")
		for moveRows.Next() {
			var prodID int64
			var qty float64
			var uomID int64
			if err := moveRows.Scan(&prodID, &qty, &uomID); err != nil {
				return nil, fmt.Errorf("stock: scan move for return of picking %d: %w", pickingID, err)
			}
			_, err := moveRS.Create(orm.Values{
				"name":             fmt.Sprintf("Return: product %d", prodID),
				"product_id":       prodID,
				"product_uom_qty":  qty,
				"product_uom":      uomID,
				"location_id":      locDestID,
				"location_dest_id": locID,
				"picking_id":       returnPicking.ID(),
				"company_id":       int64(1),
				"state":            "draft",
				"date":             time.Now(),
			})
			if err != nil {
				return nil, fmt.Errorf("stock: create return move for picking %d: %w", pickingID, err)
			}
		}

		return map[string]interface{}{
			"type":      "ir.actions.act_window",
			"res_model": "stock.picking",
			"res_id":    returnPicking.ID(),
			"view_mode": "form",
			"views":     [][]interface{}{{nil, "form"}},
			"target":    "current",
		}, nil
	})
}

// updateQuant adjusts the on-hand quantity for a product at a location.
// If no quant row exists yet it inserts one; otherwise it updates in place.
func updateQuant(env *orm.Environment, productID, locationID int64, delta float64) error {
	var exists bool
	err := env.Tx().QueryRow(env.Ctx(),
		`SELECT EXISTS(SELECT 1 FROM stock_quant WHERE product_id = $1 AND location_id = $2)`,
		productID, locationID).Scan(&exists)
	if err != nil {
		return err
	}
	if exists {
		_, err = env.Tx().Exec(env.Ctx(),
			`UPDATE stock_quant SET quantity = quantity + $1 WHERE product_id = $2 AND location_id = $3`,
			delta, productID, locationID)
	} else {
		_, err = env.Tx().Exec(env.Ctx(),
			`INSERT INTO stock_quant (product_id, location_id, quantity, reserved_quantity, company_id) VALUES ($1, $2, $3, 0, 1)`,
			productID, locationID, delta)
	}
	return err
}

// getAvailableQty returns unreserved on-hand quantity for a product at a location.
// Mirrors: stock.quant._get_available_quantity()
func getAvailableQty(env *orm.Environment, productID, locationID int64) float64 {
	var available float64
	env.Tx().QueryRow(env.Ctx(),
		`SELECT COALESCE(SUM(quantity - reserved_quantity), 0)
		 FROM stock_quant
		 WHERE product_id = $1 AND location_id = $2`,
		productID, locationID).Scan(&available)
	return available
}

// assignMove reserves available stock for a single move.
// Creates a stock.move.line (reservation) and updates quant reserved_quantity.
// Mirrors: stock.move._action_assign() per-move logic
func assignMove(env *orm.Environment, moveID int64) error {
	// Read move details
	var productID, locationID int64
	var qty float64
	var state string
	err := env.Tx().QueryRow(env.Ctx(),
		`SELECT product_id, product_uom_qty, location_id, state FROM stock_move WHERE id = $1`,
		moveID).Scan(&productID, &qty, &locationID, &state)
	if err != nil {
		return fmt.Errorf("stock: read move %d for assign: %w", moveID, err)
	}

	if state == "done" || state == "cancel" || qty <= 0 {
		return nil
	}

	// Check available quantity in source location
	available := getAvailableQty(env, productID, locationID)

	// Reserve what we can
	reserved := qty
	if available < reserved {
		reserved = available
	}
	if reserved <= 0 {
		return nil // Nothing to reserve
	}

	// Create move line (reservation)
	_, err = env.Tx().Exec(env.Ctx(),
		`INSERT INTO stock_move_line (move_id, product_id, product_uom_id, location_id, location_dest_id, quantity, company_id, date)
		 SELECT $1, product_id, product_uom, location_id, location_dest_id, $2, company_id, COALESCE(date, NOW())
		 FROM stock_move WHERE id = $1`,
		moveID, reserved)
	if err != nil {
		return fmt.Errorf("stock: create move line for move %d: %w", moveID, err)
	}

	// Update quant reserved_quantity
	_, err = env.Tx().Exec(env.Ctx(),
		`UPDATE stock_quant SET reserved_quantity = reserved_quantity + $1
		 WHERE product_id = $2 AND location_id = $3`,
		reserved, productID, locationID)
	if err != nil {
		return fmt.Errorf("stock: update reserved qty for move %d: %w", moveID, err)
	}

	// Update move state
	if reserved >= qty-0.005 {
		_, err = env.Tx().Exec(env.Ctx(),
			`UPDATE stock_move SET state = 'assigned' WHERE id = $1`, moveID)
	} else {
		_, err = env.Tx().Exec(env.Ctx(),
			`UPDATE stock_move SET state = 'partially_available' WHERE id = $1`, moveID)
	}
	if err != nil {
		return fmt.Errorf("stock: update state for move %d: %w", moveID, err)
	}

	return nil
}

// initStockMove registers stock.move — individual product movements.
// Mirrors: odoo/addons/stock/models/stock_move.py
func initStockMove() {
	m := orm.NewModel("stock.move", orm.ModelOpts{
		Description: "Stock Move",
		Order:       "sequence, id",
		RecName:     "name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
		orm.Char("reference", orm.FieldOpts{String: "Reference", Index: true}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "draft", Label: "New"},
			{Value: "confirmed", Label: "Waiting Availability"},
			{Value: "partially_available", Label: "Partially Available"},
			{Value: "assigned", Label: "Available"},
			{Value: "done", Label: "Done"},
			{Value: "cancel", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Status", Default: "draft", Index: true}),
		orm.Selection("priority", []orm.SelectionItem{
			{Value: "0", Label: "Normal"},
			{Value: "1", Label: "Urgent"},
		}, orm.FieldOpts{String: "Priority", Default: "0"}),
		orm.Many2one("product_id", "product.product", orm.FieldOpts{
			String: "Product", Required: true, Index: true,
		}),
		orm.Float("product_uom_qty", orm.FieldOpts{String: "Demand", Required: true, Default: 1.0}),
		orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{
			String: "UoM", Required: true,
		}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "Source Location", Required: true, Index: true,
		}),
		orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
			String: "Destination Location", Required: true, Index: true,
		}),
		orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{
			String: "Transfer", Index: true,
		}),
		orm.Datetime("date", orm.FieldOpts{String: "Date Scheduled", Required: true, Index: true}),
		orm.Datetime("date_deadline", orm.FieldOpts{String: "Deadline"}),
		orm.Monetary("price_unit", orm.FieldOpts{String: "Unit Price", CurrencyField: "currency_id"}),
		orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
		orm.Monetary("value", orm.FieldOpts{String: "Value", Compute: "_compute_value", Store: true, CurrencyField: "currency_id"}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
		orm.Char("origin", orm.FieldOpts{String: "Source Document"}),
	)

	// _compute_value: value = price_unit * product_uom_qty
	// Mirrors: odoo/addons/stock/models/stock_move.py StockMove._compute_value()
	m.RegisterCompute("value", func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		moveID := rs.IDs()[0]
		var priceUnit, qty float64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(price_unit, 0), COALESCE(product_uom_qty, 0) FROM stock_move WHERE id = $1`,
			moveID).Scan(&priceUnit, &qty)
		return orm.Values{"value": priceUnit * qty}, nil
	})

	// _action_confirm: Confirm stock moves (draft → confirmed), then try to reserve.
	// Mirrors: odoo/addons/stock/models/stock_move.py StockMove._action_confirm()
	m.RegisterMethod("_action_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, moveID := range rs.IDs() {
			var state string
			err := env.Tx().QueryRow(env.Ctx(),
				`SELECT state FROM stock_move WHERE id = $1`, moveID).Scan(&state)
			if err != nil {
				return nil, fmt.Errorf("stock: read move %d for confirm: %w", moveID, err)
			}
			if state != "draft" {
				continue
			}

			// Set to confirmed
			_, err = env.Tx().Exec(env.Ctx(),
				`UPDATE stock_move SET state = 'confirmed' WHERE id = $1`, moveID)
			if err != nil {
				return nil, fmt.Errorf("stock: confirm move %d: %w", moveID, err)
			}

			// Try to reserve (assign) immediately
			if err := assignMove(env, moveID); err != nil {
				return nil, fmt.Errorf("stock: assign move %d after confirm: %w", moveID, err)
			}
		}
		return true, nil
	})

	// _action_assign: Reserve stock for confirmed moves.
	// Mirrors: odoo/addons/stock/models/stock_move.py StockMove._action_assign()
	m.RegisterMethod("_action_assign", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, moveID := range rs.IDs() {
			if err := assignMove(env, moveID); err != nil {
				return nil, fmt.Errorf("stock: assign move %d: %w", moveID, err)
			}
		}
		return true, nil
	})

	// _action_done: Finalize stock moves (assigned → done), updating quants and clearing reservations.
	// Mirrors: odoo/addons/stock/models/stock_move.py StockMove._action_done()
	m.RegisterMethod("_action_done", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, id := range rs.IDs() {
			var productID, srcLoc, dstLoc int64
			var qty float64
			err := env.Tx().QueryRow(env.Ctx(),
				`SELECT product_id, product_uom_qty, location_id, location_dest_id
				 FROM stock_move WHERE id = $1`, id).Scan(&productID, &qty, &srcLoc, &dstLoc)
			if err != nil {
				return nil, fmt.Errorf("stock: read move %d for done: %w", id, err)
			}

			// Decrease source quant
			if err := updateQuant(env, productID, srcLoc, -qty); err != nil {
				return nil, fmt.Errorf("stock: update source quant for move %d: %w", id, err)
			}
			// Increase destination quant
			if err := updateQuant(env, productID, dstLoc, qty); err != nil {
				return nil, fmt.Errorf("stock: update dest quant for move %d: %w", id, err)
			}

			// Clear reservation on source quant
			_, err = env.Tx().Exec(env.Ctx(),
				`UPDATE stock_quant SET reserved_quantity = GREATEST(reserved_quantity - $1, 0)
				 WHERE product_id = $2 AND location_id = $3`,
				qty, productID, srcLoc)
			if err != nil {
				return nil, fmt.Errorf("stock: clear reservation for move %d: %w", id, err)
			}

			// Mark move as done
			_, err = env.Tx().Exec(env.Ctx(),
				`UPDATE stock_move SET state = 'done', date = NOW() WHERE id = $1`, id)
			if err != nil {
				return nil, fmt.Errorf("stock: done move %d: %w", id, err)
			}
		}
		return true, nil
	})
}

// initStockMoveLine registers stock.move.line — detailed operations per lot/package.
// Mirrors: odoo/addons/stock/models/stock_move_line.py
func initStockMoveLine() {
	m := orm.NewModel("stock.move.line", orm.ModelOpts{
		Description: "Product Moves (Stock Move Line)",
		Order:       "result_package_id desc, id",
	})

	m.AddFields(
		orm.Many2one("move_id", "stock.move", orm.FieldOpts{
			String: "Stock Move", Index: true, OnDelete: orm.OnDeleteCascade,
		}),
		orm.Many2one("product_id", "product.product", orm.FieldOpts{
			String: "Product", Required: true, Index: true,
		}),
		orm.Float("quantity", orm.FieldOpts{String: "Quantity", Required: true, Default: 0.0}),
		orm.Many2one("product_uom_id", "uom.uom", orm.FieldOpts{
			String: "Unit of Measure", Required: true,
		}),
		orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{
			String: "Lot/Serial Number", Index: true,
		}),
		orm.Many2one("package_id", "stock.quant.package", orm.FieldOpts{
			String: "Source Package",
		}),
		orm.Many2one("result_package_id", "stock.quant.package", orm.FieldOpts{
			String: "Destination Package",
		}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "From", Required: true, Index: true,
		}),
		orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
			String: "To", Required: true, Index: true,
		}),
		orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{
			String: "Transfer", Index: true,
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Datetime("date", orm.FieldOpts{String: "Date", Required: true}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "draft", Label: "New"},
			{Value: "confirmed", Label: "Waiting"},
			{Value: "assigned", Label: "Reserved"},
			{Value: "done", Label: "Done"},
			{Value: "cancel", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Status", Default: "draft"}),
	)
}

// initStockQuant registers stock.quant — on-hand inventory quantities.
// Mirrors: odoo/addons/stock/models/stock_quant.py
func initStockQuant() {
	m := orm.NewModel("stock.quant", orm.ModelOpts{
		Description: "Quants",
		Order:       "removal_date, in_date, id",
	})

	m.AddFields(
		orm.Many2one("product_id", "product.product", orm.FieldOpts{
			String: "Product", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
		}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "Location", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
		}),
		orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{
			String: "Lot/Serial Number", Index: true,
		}),
		orm.Float("quantity", orm.FieldOpts{String: "Quantity", Required: true, Default: 0.0}),
		orm.Float("reserved_quantity", orm.FieldOpts{String: "Reserved Quantity", Required: true, Default: 0.0}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Monetary("value", orm.FieldOpts{String: "Value", CurrencyField: "currency_id"}),
		orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
		orm.Datetime("in_date", orm.FieldOpts{String: "Incoming Date", Index: true}),
		orm.Many2one("package_id", "stock.quant.package", orm.FieldOpts{
			String: "Package",
		}),
		orm.Many2one("owner_id", "res.partner", orm.FieldOpts{
			String: "Owner",
		}),
		orm.Datetime("removal_date", orm.FieldOpts{String: "Removal Date"}),
	)

	// _update_available_quantity: Adjust available quantity for a product at a location.
	// Mirrors: odoo/addons/stock/models/stock_quant.py StockQuant._update_available_quantity()
	m.RegisterMethod("_update_available_quantity", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		if len(args) < 3 {
			return nil, fmt.Errorf("stock.quant._update_available_quantity requires product_id, location_id, quantity")
		}
		productID, _ := args[0].(int64)
		locationID, _ := args[1].(int64)
		quantity, _ := args[2].(float64)
		if productID == 0 || locationID == 0 {
			return nil, fmt.Errorf("stock.quant._update_available_quantity: invalid product_id or location_id")
		}
		env := rs.Env()
		if err := updateQuant(env, productID, locationID, quantity); err != nil {
			return nil, fmt.Errorf("stock.quant._update_available_quantity: %w", err)
		}
		return true, nil
	})

	// stock.quant.package — physical packages / containers
	orm.NewModel("stock.quant.package", orm.ModelOpts{
		Description: "Packages",
		Order:       "name",
	}).AddFields(
		orm.Char("name", orm.FieldOpts{String: "Package Reference", Required: true, Index: true}),
		orm.Many2one("package_type_id", "stock.package.type", orm.FieldOpts{
			String: "Package Type",
		}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{
			String: "Location", Index: true,
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Index: true,
		}),
		orm.Many2one("owner_id", "res.partner", orm.FieldOpts{
			String: "Owner",
		}),
	)

	// stock.package.type — packaging types (box, pallet, etc.)
	orm.NewModel("stock.package.type", orm.ModelOpts{
		Description: "Package Type",
		Order:       "sequence, id",
	}).AddFields(
		orm.Char("name", orm.FieldOpts{String: "Package Type", Required: true}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
		orm.Float("height", orm.FieldOpts{String: "Height"}),
		orm.Float("width", orm.FieldOpts{String: "Width"}),
		orm.Float("packaging_length", orm.FieldOpts{String: "Length"}),
		orm.Float("max_weight", orm.FieldOpts{String: "Max Weight"}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
	)

	// product.removal — removal strategies (FIFO, LIFO, etc.)
	orm.NewModel("product.removal", orm.ModelOpts{
		Description: "Removal Strategy",
		Order:       "name",
	}).AddFields(
		orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
		orm.Char("method", orm.FieldOpts{String: "Method", Required: true}),
	)
}

// initStockLot registers stock.lot — lot/serial number tracking.
// Mirrors: odoo/addons/stock/models/stock_lot.py
func initStockLot() {
	m := orm.NewModel("stock.lot", orm.ModelOpts{
		Description: "Lot/Serial",
		Order:       "name, id",
		RecName:     "name",
	})

	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Lot/Serial Number", Required: true, Index: true}),
		orm.Many2one("product_id", "product.product", orm.FieldOpts{
			String: "Product", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
		}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Text("note", orm.FieldOpts{String: "Description"}),
		orm.Date("expiration_date", orm.FieldOpts{String: "Expiration Date"}),
		orm.Date("use_date", orm.FieldOpts{String: "Best Before Date"}),
		orm.Date("removal_date", orm.FieldOpts{String: "Removal Date"}),
		orm.Float("product_qty", orm.FieldOpts{String: "Quantity", Compute: "_compute_qty"}),
	)

	// Compute lot quantity from quants.
	// Mirrors: odoo/addons/stock/models/stock_lot.py StockLot._product_qty()
	m.RegisterCompute("product_qty", func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		lotID := rs.IDs()[0]
		var qty float64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(SUM(quantity), 0) FROM stock_quant WHERE lot_id = $1`, lotID,
		).Scan(&qty)
		return orm.Values{"product_qty": qty}, nil
	})
}

// initStockOrderpoint registers stock.warehouse.orderpoint — reorder rules.
// Mirrors: odoo/addons/stock/models/stock_orderpoint.py
func initStockOrderpoint() {
	m := orm.NewModel("stock.warehouse.orderpoint", orm.ModelOpts{
		Description: "Reorder Rule",
		Order:       "name",
	})
	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
		orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product", Required: true}),
		orm.Many2one("warehouse_id", "stock.warehouse", orm.FieldOpts{String: "Warehouse", Required: true}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{String: "Location", Required: true}),
		orm.Float("product_min_qty", orm.FieldOpts{String: "Minimum Quantity"}),
		orm.Float("product_max_qty", orm.FieldOpts{String: "Maximum Quantity"}),
		orm.Float("qty_multiple", orm.FieldOpts{String: "Quantity Multiple", Default: 1}),
		orm.Float("qty_on_hand", orm.FieldOpts{String: "On Hand", Compute: "_compute_qty", Store: false}),
		orm.Float("qty_forecast", orm.FieldOpts{String: "Forecast", Compute: "_compute_qty", Store: false}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
	)

	// Compute on-hand qty from quants
	m.RegisterCompute("qty_on_hand", func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		opID := rs.IDs()[0]
		var productID, locationID int64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT product_id, location_id FROM stock_warehouse_orderpoint WHERE id = $1`, opID,
		).Scan(&productID, &locationID)

		var qty float64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(SUM(quantity - reserved_quantity), 0) FROM stock_quant WHERE product_id = $1 AND location_id = $2`,
			productID, locationID,
		).Scan(&qty)
		return orm.Values{"qty_on_hand": qty, "qty_forecast": qty}, nil
	})

	// action_replenish: check all orderpoints and create procurement if below min.
	// Mirrors: odoo/addons/stock/models/stock_orderpoint.py action_replenish()
	m.RegisterMethod("action_replenish", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, opID := range rs.IDs() {
			var productID, locationID int64
			var minQty, maxQty, qtyMultiple float64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT product_id, location_id, product_min_qty, product_max_qty, qty_multiple
				 FROM stock_warehouse_orderpoint WHERE id = $1`, opID,
			).Scan(&productID, &locationID, &minQty, &maxQty, &qtyMultiple)

			// Check current stock
			var onHand float64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT COALESCE(SUM(quantity - reserved_quantity), 0) FROM stock_quant WHERE product_id = $1 AND location_id = $2`,
				productID, locationID,
			).Scan(&onHand)

			if onHand < minQty {
				// Need to replenish: qty = max - on_hand, rounded up to qty_multiple
				needed := maxQty - onHand
				if qtyMultiple > 0 {
					remainder := int(needed) % int(qtyMultiple)
					if remainder > 0 {
						needed += qtyMultiple - float64(remainder)
					}
				}

				// Create internal transfer or purchase (simplified: just log)
				fmt.Printf("stock: orderpoint %d needs %.0f units of product %d\n", opID, needed, productID)
			}
		}
		return true, nil
	})
}

// initStockScrap registers stock.scrap — scrap/disposal of products.
// Mirrors: odoo/addons/stock/models/stock_scrap.py
func initStockScrap() {
	m := orm.NewModel("stock.scrap", orm.ModelOpts{
		Description: "Scrap",
		Order:       "name desc",
	})
	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Reference", Readonly: true, Default: "New"}),
		orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product", Required: true}),
		orm.Float("scrap_qty", orm.FieldOpts{String: "Quantity", Required: true, Default: 1}),
		orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{String: "Lot/Serial"}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{String: "Source Location", Required: true}),
		orm.Many2one("scrap_location_id", "stock.location", orm.FieldOpts{String: "Scrap Location", Required: true}),
		orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{String: "Picking"}),
		orm.Many2one("move_id", "stock.move", orm.FieldOpts{String: "Scrap Move"}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
		orm.Date("date_done", orm.FieldOpts{String: "Date"}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "draft", Label: "Draft"},
			{Value: "done", Label: "Done"},
		}, orm.FieldOpts{String: "Status", Default: "draft"}),
	)

	// action_validate: Validate scrap, move product from source to scrap location.
	// Mirrors: odoo/addons/stock/models/stock_scrap.py StockScrap.action_validate()
	m.RegisterMethod("action_validate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, scrapID := range rs.IDs() {
			var productID, locID, scrapLocID int64
			var qty float64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT product_id, scrap_qty, location_id, scrap_location_id FROM stock_scrap WHERE id = $1`,
				scrapID).Scan(&productID, &qty, &locID, &scrapLocID)

			// Update quants (move from location to scrap location)
			if err := updateQuant(env, productID, locID, -qty); err != nil {
				return nil, fmt.Errorf("stock.scrap: update source quant for scrap %d: %w", scrapID, err)
			}
			if err := updateQuant(env, productID, scrapLocID, qty); err != nil {
				return nil, fmt.Errorf("stock.scrap: update scrap quant for scrap %d: %w", scrapID, err)
			}

			_, err := env.Tx().Exec(env.Ctx(),
				`UPDATE stock_scrap SET state = 'done', date_done = NOW() WHERE id = $1`, scrapID)
			if err != nil {
				return nil, fmt.Errorf("stock.scrap: validate scrap %d: %w", scrapID, err)
			}
		}
		return true, nil
	})
}

// initStockInventory registers stock.quant.adjust — inventory adjustment wizard.
// Mirrors: odoo/addons/stock/wizard/stock_change_product_qty.py (transient model)
func initStockInventory() {
	m := orm.NewModel("stock.quant.adjust", orm.ModelOpts{
		Description: "Inventory Adjustment",
		Type:        orm.ModelTransient,
	})
	m.AddFields(
		orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product", Required: true}),
		orm.Many2one("location_id", "stock.location", orm.FieldOpts{String: "Location", Required: true}),
		orm.Float("new_quantity", orm.FieldOpts{String: "New Quantity", Required: true}),
		orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{String: "Lot/Serial"}),
	)

	// action_apply: Set quant quantity to the specified new quantity.
	// Mirrors: stock.change.product.qty.change_product_qty()
	m.RegisterMethod("action_apply", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		data, err := rs.Read([]string{"product_id", "location_id", "new_quantity"})
		if err != nil || len(data) == 0 {
			return nil, err
		}
		d := data[0]

		productID := toInt64(d["product_id"])
		locationID := toInt64(d["location_id"])
		newQty, _ := d["new_quantity"].(float64)

		// Get current quantity
		var currentQty float64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(SUM(quantity), 0) FROM stock_quant WHERE product_id = $1 AND location_id = $2`,
			productID, locationID).Scan(&currentQty)

		diff := newQty - currentQty
		if diff != 0 {
			if err := updateQuant(env, productID, locationID, diff); err != nil {
				return nil, fmt.Errorf("stock.quant.adjust: update quant: %w", err)
			}
		}

		return map[string]interface{}{"type": "ir.actions.act_window_close"}, nil
	})
}

// initStockRule registers stock.rule — procurement/push/pull rules.
// Mirrors: odoo/addons/stock/models/stock_rule.py
func initStockRule() {
	m := orm.NewModel("stock.rule", orm.ModelOpts{
		Description: "Stock Rule",
		Order:       "sequence, id",
	})
	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
		orm.Selection("action", []orm.SelectionItem{
			{Value: "pull", Label: "Pull From"},
			{Value: "push", Label: "Push To"},
			{Value: "pull_push", Label: "Pull & Push"},
			{Value: "buy", Label: "Buy"},
			{Value: "manufacture", Label: "Manufacture"},
		}, orm.FieldOpts{String: "Action", Required: true}),
		orm.Many2one("location_src_id", "stock.location", orm.FieldOpts{String: "Source Location"}),
		orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{String: "Destination Location"}),
		orm.Many2one("route_id", "stock.route", orm.FieldOpts{String: "Route"}),
		orm.Many2one("picking_type_id", "stock.picking.type", orm.FieldOpts{String: "Operation Type"}),
		orm.Many2one("warehouse_id", "stock.warehouse", orm.FieldOpts{String: "Warehouse"}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 20}),
		orm.Integer("delay", orm.FieldOpts{String: "Delay (days)"}),
		orm.Selection("procure_method", []orm.SelectionItem{
			{Value: "make_to_stock", Label: "Take From Stock"},
			{Value: "make_to_order", Label: "Trigger Another Rule"},
			{Value: "mts_else_mto", Label: "Take from Stock, if unavailable, Trigger Another Rule"},
		}, orm.FieldOpts{String: "Supply Method", Default: "make_to_stock"}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
	)
	_ = m
}

// initStockRoute registers stock.route — inventory routes linking rules.
// Mirrors: odoo/addons/stock/models/stock_route.py
func initStockRoute() {
	m := orm.NewModel("stock.route", orm.ModelOpts{
		Description: "Inventory Route",
		Order:       "sequence, id",
	})
	m.AddFields(
		orm.Char("name", orm.FieldOpts{String: "Route", Required: true, Translate: true}),
		orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 0}),
		orm.One2many("rule_ids", "stock.rule", "route_id", orm.FieldOpts{String: "Rules"}),
		orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
		orm.Boolean("product_selectable", orm.FieldOpts{String: "Applicable on Product", Default: true}),
		orm.Boolean("product_categ_selectable", orm.FieldOpts{String: "Applicable on Product Category"}),
		orm.Boolean("warehouse_selectable", orm.FieldOpts{String: "Applicable on Warehouse"}),
		orm.Many2many("warehouse_ids", "stock.warehouse", orm.FieldOpts{String: "Warehouses"}),
	)
	_ = m
}

// initProductStockExtension extends product.template with stock-specific fields.
// Mirrors: odoo/addons/stock/models/product.py (tracking, route_ids)
func initProductStockExtension() {
	pt := orm.ExtendModel("product.template")
	pt.AddFields(
		orm.Selection("tracking", []orm.SelectionItem{
			{Value: "none", Label: "No Tracking"},
			{Value: "lot", Label: "By Lots"},
			{Value: "serial", Label: "By Unique Serial Number"},
		}, orm.FieldOpts{String: "Tracking", Default: "none"}),
		orm.Many2many("route_ids", "stock.route", orm.FieldOpts{String: "Routes"}),
	)
}

// toInt64 converts various numeric types to int64 for use in business methods.
func toInt64(v interface{}) int64 {
	switch n := v.(type) {
	case int64:
		return n
	case float64:
		return int64(n)
	case int:
		return int64(n)
	case int32:
		return int64(n)
	}
	return 0
}
