package models import ( "fmt" "odoo-go/pkg/orm" ) // initStockPickingBatch registers stock.picking.batch — batch processing of transfers. // Mirrors: odoo/addons/stock_picking_batch/models/stock_picking_batch.py func initStockPickingBatch() { m := orm.NewModel("stock.picking.batch", orm.ModelOpts{ Description: "Batch Transfer", Order: "name desc", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Default: "New"}), orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Responsible"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), orm.One2many("picking_ids", "stock.picking", "batch_id", orm.FieldOpts{String: "Transfers"}), orm.Selection("state", []orm.SelectionItem{ {Value: "draft", Label: "Draft"}, {Value: "in_progress", Label: "In Progress"}, {Value: "done", Label: "Done"}, {Value: "cancel", Label: "Cancelled"}, }, orm.FieldOpts{String: "Status", Default: "draft"}), orm.Selection("picking_type_code", []orm.SelectionItem{ {Value: "incoming", Label: "Receipt"}, {Value: "outgoing", Label: "Delivery"}, {Value: "internal", Label: "Internal Transfer"}, }, orm.FieldOpts{String: "Operation Type"}), orm.Integer("picking_count", orm.FieldOpts{String: "Transfers Count", Compute: "_compute_picking_count"}), orm.Integer("move_line_count", orm.FieldOpts{String: "Move Lines Count", Compute: "_compute_move_line_count"}), ) // _compute_picking_count: Count the number of pickings in this batch. // Mirrors: stock.picking.batch._compute_picking_count() m.RegisterCompute("picking_count", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() batchID := rs.IDs()[0] var count int64 env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM stock_picking WHERE batch_id = $1`, batchID, ).Scan(&count) return orm.Values{"picking_count": count}, nil }) // _compute_move_line_count: Count the total move lines across all pickings in batch. // Mirrors: stock.picking.batch._compute_move_line_count() m.RegisterCompute("move_line_count", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() batchID := rs.IDs()[0] var count int64 env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM stock_move_line sml JOIN stock_move sm ON sm.id = sml.move_id JOIN stock_picking sp ON sp.id = sm.picking_id WHERE sp.batch_id = $1`, batchID, ).Scan(&count) return orm.Values{"move_line_count": count}, nil }) // action_confirm: Move batch from draft to in_progress. // Mirrors: stock.picking.batch.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_batch WHERE id = $1`, id, ).Scan(&state) if err != nil { return nil, fmt.Errorf("stock.picking.batch: read state for %d: %w", id, err) } if state != "draft" { return nil, fmt.Errorf("stock.picking.batch: can only confirm draft batches (batch %d is %q)", id, state) } // Confirm all draft pickings in batch pickRows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM stock_picking WHERE batch_id = $1 AND state = 'draft'`, id) if err != nil { return nil, fmt.Errorf("stock.picking.batch: query draft pickings for %d: %w", id, err) } var pickIDs []int64 for pickRows.Next() { var pid int64 pickRows.Scan(&pid) pickIDs = append(pickIDs, pid) } pickRows.Close() if len(pickIDs) > 0 { pickModel := orm.Registry.Get("stock.picking") if pickModel != nil { if confirmMethod, ok := pickModel.Methods["action_confirm"]; ok { pickRS := env.Model("stock.picking").Browse(pickIDs...) if _, err := confirmMethod(pickRS); err != nil { return nil, fmt.Errorf("stock.picking.batch: confirm pickings for batch %d: %w", id, err) } } } } _, err = env.Tx().Exec(env.Ctx(), `UPDATE stock_picking_batch SET state = 'in_progress' WHERE id = $1`, id) if err != nil { return nil, fmt.Errorf("stock.picking.batch: confirm batch %d: %w", id, err) } } return true, nil }) // action_assign: Reserve stock for all pickings in the batch. // Mirrors: stock.picking.batch.action_assign() m.RegisterMethod("action_assign", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, batchID := range rs.IDs() { pickRows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM stock_picking WHERE batch_id = $1 AND state IN ('confirmed', 'assigned')`, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: query pickings for assign %d: %w", batchID, err) } var pickIDs []int64 for pickRows.Next() { var pid int64 pickRows.Scan(&pid) pickIDs = append(pickIDs, pid) } pickRows.Close() if len(pickIDs) > 0 { pickModel := orm.Registry.Get("stock.picking") if pickModel != nil { if assignMethod, ok := pickModel.Methods["action_assign"]; ok { pickRS := env.Model("stock.picking").Browse(pickIDs...) if _, err := assignMethod(pickRS); err != nil { return nil, fmt.Errorf("stock.picking.batch: assign pickings for batch %d: %w", batchID, err) } } } } } return true, nil }) // action_done: Validate all pickings in the batch, then set batch to done. // Mirrors: stock.picking.batch.action_done() m.RegisterMethod("action_done", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, batchID := range rs.IDs() { var state string err := env.Tx().QueryRow(env.Ctx(), `SELECT state FROM stock_picking_batch WHERE id = $1`, batchID, ).Scan(&state) if err != nil { return nil, fmt.Errorf("stock.picking.batch: read state for %d: %w", batchID, err) } if state != "in_progress" { return nil, fmt.Errorf("stock.picking.batch: can only validate in-progress batches (batch %d is %q)", batchID, state) } // Validate all non-done pickings pickRows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM stock_picking WHERE batch_id = $1 AND state != 'done'`, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: query pickings for done %d: %w", batchID, err) } var pickIDs []int64 for pickRows.Next() { var pid int64 pickRows.Scan(&pid) pickIDs = append(pickIDs, pid) } pickRows.Close() if len(pickIDs) > 0 { pickModel := orm.Registry.Get("stock.picking") if pickModel != nil { if validateMethod, ok := pickModel.Methods["button_validate"]; ok { pickRS := env.Model("stock.picking").Browse(pickIDs...) if _, err := validateMethod(pickRS); err != nil { return nil, fmt.Errorf("stock.picking.batch: validate pickings for batch %d: %w", batchID, err) } } } } _, err = env.Tx().Exec(env.Ctx(), `UPDATE stock_picking_batch SET state = 'done' WHERE id = $1`, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: set done for %d: %w", batchID, err) } } return true, nil }) // action_cancel: Cancel the batch and all non-done pickings. // Mirrors: stock.picking.batch.action_cancel() m.RegisterMethod("action_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, batchID := range rs.IDs() { // Cancel non-done pickings pickRows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM stock_picking WHERE batch_id = $1 AND state NOT IN ('done', 'cancel')`, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: query pickings for cancel %d: %w", batchID, err) } var pickIDs []int64 for pickRows.Next() { var pid int64 pickRows.Scan(&pid) pickIDs = append(pickIDs, pid) } pickRows.Close() if len(pickIDs) > 0 { pickModel := orm.Registry.Get("stock.picking") if pickModel != nil { if cancelMethod, ok := pickModel.Methods["action_cancel"]; ok { pickRS := env.Model("stock.picking").Browse(pickIDs...) if _, err := cancelMethod(pickRS); err != nil { return nil, fmt.Errorf("stock.picking.batch: cancel pickings for batch %d: %w", batchID, err) } } } } _, err = env.Tx().Exec(env.Ctx(), `UPDATE stock_picking_batch SET state = 'cancel' WHERE id = $1`, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: cancel batch %d: %w", batchID, err) } } return true, nil }) // action_print: Generate a summary of the batch for printing. // Mirrors: stock.picking.batch print report m.RegisterMethod("action_print", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() batchID := rs.IDs()[0] rows, err := env.Tx().Query(env.Ctx(), `SELECT sp.name as picking_name, sp.state as picking_state, sm.product_id, pt.name as product_name, sm.product_uom_qty, sl_src.name as source_location, sl_dst.name as dest_location FROM stock_picking sp JOIN stock_move sm ON sm.picking_id = sp.id JOIN product_product pp ON pp.id = sm.product_id JOIN product_template pt ON pt.id = pp.product_tmpl_id JOIN stock_location sl_src ON sl_src.id = sm.location_id JOIN stock_location sl_dst ON sl_dst.id = sm.location_dest_id WHERE sp.batch_id = $1 ORDER BY sp.name, pt.name`, batchID, ) if err != nil { return nil, fmt.Errorf("stock.picking.batch: query print data for %d: %w", batchID, err) } defer rows.Close() var lines []map[string]interface{} for rows.Next() { var pickingName, pickingState, prodName, srcLoc, dstLoc string var prodID int64 var qty float64 if err := rows.Scan(&pickingName, &pickingState, &prodID, &prodName, &qty, &srcLoc, &dstLoc); err != nil { return nil, fmt.Errorf("stock.picking.batch: scan print row: %w", err) } lines = append(lines, map[string]interface{}{ "picking": pickingName, "state": pickingState, "product_id": prodID, "product": prodName, "quantity": qty, "source": srcLoc, "destination": dstLoc, }) } return map[string]interface{}{ "batch_id": batchID, "lines": lines, }, nil }) // add_pickings: Add pickings to an existing batch. // Mirrors: stock.picking.batch add picking wizard m.RegisterMethod("add_pickings", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.picking.batch.add_pickings requires picking_ids") } pickingIDs, ok := args[0].([]int64) if !ok || len(pickingIDs) == 0 { return nil, fmt.Errorf("stock.picking.batch: invalid picking_ids") } env := rs.Env() batchID := rs.IDs()[0] for _, pid := range pickingIDs { _, err := env.Tx().Exec(env.Ctx(), `UPDATE stock_picking SET batch_id = $1 WHERE id = $2`, batchID, pid) if err != nil { return nil, fmt.Errorf("stock.picking.batch: add picking %d to batch %d: %w", pid, batchID, err) } } return true, nil }) // remove_pickings: Remove pickings from the batch. // Mirrors: stock.picking.batch remove picking m.RegisterMethod("remove_pickings", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.picking.batch.remove_pickings requires picking_ids") } pickingIDs, ok := args[0].([]int64) if !ok || len(pickingIDs) == 0 { return nil, fmt.Errorf("stock.picking.batch: invalid picking_ids") } env := rs.Env() batchID := rs.IDs()[0] for _, pid := range pickingIDs { _, err := env.Tx().Exec(env.Ctx(), `UPDATE stock_picking SET batch_id = NULL WHERE id = $1 AND batch_id = $2`, pid, batchID) if err != nil { return nil, fmt.Errorf("stock.picking.batch: remove picking %d from batch %d: %w", pid, batchID, err) } } return true, nil }) } // initStockPickingBatchExtension extends stock.picking with batch_id field. // Mirrors: stock_picking_batch module's extension of stock.picking func initStockPickingBatchExtension() { p := orm.ExtendModel("stock.picking") p.AddFields( orm.Many2one("batch_id", "stock.picking.batch", orm.FieldOpts{String: "Batch Transfer"}), ) }