package models import ( "fmt" "odoo-go/pkg/orm" ) // initStockBarcode registers stock.barcode.picking — transient model for barcode scanning interface. // Mirrors: odoo/addons/stock_barcode/models/stock_picking.py barcode processing func initStockBarcode() { m := orm.NewModel("stock.barcode.picking", orm.ModelOpts{ Description: "Barcode Picking Interface", Type: orm.ModelTransient, }) m.AddFields( orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{String: "Transfer"}), orm.Char("barcode", orm.FieldOpts{String: "Barcode"}), ) // process_barcode: Look up a product or lot/serial by barcode. // Mirrors: stock_barcode barcode scanning logic m.RegisterMethod("process_barcode", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() if len(args) < 1 { return nil, fmt.Errorf("barcode required") } barcode, _ := args[0].(string) // Try to find product by barcode var productID int64 env.Tx().QueryRow(env.Ctx(), `SELECT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pt.barcode = $1 OR pp.barcode = $1 LIMIT 1`, barcode, ).Scan(&productID) if productID > 0 { return map[string]interface{}{"product_id": productID, "found": true}, nil } // Try lot/serial var lotID, lotProductID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id, product_id FROM stock_lot WHERE name = $1 LIMIT 1`, barcode, ).Scan(&lotID, &lotProductID) if lotID > 0 { return map[string]interface{}{"lot_id": lotID, "product_id": lotProductID, "found": true}, nil } // Try package var packageID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM stock_quant_package WHERE name = $1 LIMIT 1`, barcode, ).Scan(&packageID) if packageID > 0 { return map[string]interface{}{"package_id": packageID, "found": true}, nil } // Try location barcode var locationID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM stock_location WHERE barcode = $1 LIMIT 1`, barcode, ).Scan(&locationID) if locationID > 0 { return map[string]interface{}{"location_id": locationID, "found": true}, nil } return map[string]interface{}{"found": false, "barcode": barcode}, nil }) // action_process_barcode: Enhanced barcode scan loop — handles UPC/EAN by searching // product.product.barcode field directly. Supports UPC-A (12 digits), EAN-13 (13 digits), // EAN-8 (8 digits), and arbitrary barcodes. In the context of a picking, increments // qty_done on the matching move line. // Mirrors: stock_barcode.picking barcode scan loop with UPC/EAN support m.RegisterMethod("action_process_barcode", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("stock.barcode.picking.action_process_barcode requires picking_id, barcode") } pickingID, _ := args[0].(int64) barcode, _ := args[1].(string) if pickingID == 0 || barcode == "" { return nil, fmt.Errorf("stock.barcode.picking: invalid picking_id or barcode") } env := rs.Env() // Step 1: Try to find product by barcode on product.product.barcode (UPC/EAN stored here) var productID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM product_product WHERE barcode = $1 LIMIT 1`, barcode, ).Scan(&productID) // Step 2: If not found on product_product, try product_template.barcode if productID == 0 { env.Tx().QueryRow(env.Ctx(), `SELECT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pt.barcode = $1 LIMIT 1`, barcode, ).Scan(&productID) } // Step 3: For UPC-A (12 digits), try converting to EAN-13 by prepending '0' if productID == 0 && len(barcode) == 12 && isNumeric(barcode) { ean13 := "0" + barcode env.Tx().QueryRow(env.Ctx(), `SELECT id FROM product_product WHERE barcode = $1 LIMIT 1`, ean13, ).Scan(&productID) // Also try the reverse: if stored as UPC but scanned as EAN if productID == 0 { env.Tx().QueryRow(env.Ctx(), `SELECT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pt.barcode = $1 LIMIT 1`, ean13, ).Scan(&productID) } } // Step 4: For EAN-13 (13 digits starting with 0), try stripping leading 0 to get UPC-A if productID == 0 && len(barcode) == 13 && barcode[0] == '0' && isNumeric(barcode) { upc := barcode[1:] env.Tx().QueryRow(env.Ctx(), `SELECT id FROM product_product WHERE barcode = $1 LIMIT 1`, upc, ).Scan(&productID) if productID == 0 { env.Tx().QueryRow(env.Ctx(), `SELECT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pt.barcode = $1 LIMIT 1`, upc, ).Scan(&productID) } } // Step 5: Try lot/serial number if productID == 0 { var lotProductID int64 env.Tx().QueryRow(env.Ctx(), `SELECT product_id FROM stock_lot WHERE name = $1 LIMIT 1`, barcode, ).Scan(&lotProductID) productID = lotProductID } if productID == 0 { return map[string]interface{}{ "found": false, "barcode": barcode, "message": fmt.Sprintf("No product found for barcode %q (tried UPC/EAN lookup)", barcode), }, nil } // Step 6: Find matching move line in the picking var moveLineID int64 err := env.Tx().QueryRow(env.Ctx(), `SELECT sml.id FROM stock_move_line sml JOIN stock_move sm ON sm.id = sml.move_id WHERE sm.picking_id = $1 AND sml.product_id = $2 AND sm.state NOT IN ('done', 'cancel') ORDER BY sml.id LIMIT 1`, pickingID, productID, ).Scan(&moveLineID) if err != nil || moveLineID == 0 { // Check if product expected in any move var moveID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM stock_move WHERE picking_id = $1 AND product_id = $2 AND state NOT IN ('done', 'cancel') LIMIT 1`, pickingID, productID, ).Scan(&moveID) if moveID == 0 { return map[string]interface{}{ "found": false, "product_id": productID, "message": fmt.Sprintf("Product %d not expected in this transfer", productID), }, nil } return map[string]interface{}{ "found": true, "product_id": productID, "move_id": moveID, "action": "new_line", "message": "Product found in move, new line needed", }, nil } // Increment quantity on the move line _, err = env.Tx().Exec(env.Ctx(), `UPDATE stock_move_line SET quantity = quantity + 1 WHERE id = $1`, moveLineID) if err != nil { return nil, fmt.Errorf("stock.barcode.picking: increment qty on move line %d: %w", moveLineID, err) } return map[string]interface{}{ "found": true, "product_id": productID, "move_line_id": moveLineID, "action": "incremented", "message": "Quantity incremented", }, nil }) // process_barcode_picking: Process a barcode in the context of a picking. // Finds the product and increments qty_done on the matching move line. // Mirrors: stock_barcode.picking barcode processing m.RegisterMethod("process_barcode_picking", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 2 { return nil, fmt.Errorf("stock.barcode.picking.process_barcode_picking requires picking_id, barcode") } pickingID, _ := args[0].(int64) barcode, _ := args[1].(string) if pickingID == 0 || barcode == "" { return nil, fmt.Errorf("stock.barcode.picking: invalid picking_id or barcode") } env := rs.Env() // Find product by barcode var productID int64 env.Tx().QueryRow(env.Ctx(), `SELECT pp.id FROM product_product pp JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pt.barcode = $1 OR pp.barcode = $1 LIMIT 1`, barcode, ).Scan(&productID) if productID == 0 { // Try lot/serial var lotProductID int64 env.Tx().QueryRow(env.Ctx(), `SELECT product_id FROM stock_lot WHERE name = $1 LIMIT 1`, barcode, ).Scan(&lotProductID) productID = lotProductID } if productID == 0 { return map[string]interface{}{ "found": false, "barcode": barcode, "message": fmt.Sprintf("No product found for barcode %q", barcode), }, nil } // Find matching move line var moveLineID int64 err := env.Tx().QueryRow(env.Ctx(), `SELECT sml.id FROM stock_move_line sml JOIN stock_move sm ON sm.id = sml.move_id WHERE sm.picking_id = $1 AND sml.product_id = $2 AND sm.state != 'done' ORDER BY sml.id LIMIT 1`, pickingID, productID, ).Scan(&moveLineID) if err != nil || moveLineID == 0 { // No existing move line — check if there is a move for this product var moveID int64 env.Tx().QueryRow(env.Ctx(), `SELECT id FROM stock_move WHERE picking_id = $1 AND product_id = $2 AND state != 'done' LIMIT 1`, pickingID, productID, ).Scan(&moveID) if moveID == 0 { return map[string]interface{}{ "found": false, "product_id": productID, "message": fmt.Sprintf("Product %d not expected in this transfer", productID), }, nil } return map[string]interface{}{ "found": true, "product_id": productID, "move_id": moveID, "action": "new_line", "message": "Product found in move, new line needed", }, nil } // Increment quantity on the move line _, err = env.Tx().Exec(env.Ctx(), `UPDATE stock_move_line SET quantity = quantity + 1 WHERE id = $1`, moveLineID) if err != nil { return nil, fmt.Errorf("stock.barcode.picking: increment qty on move line %d: %w", moveLineID, err) } return map[string]interface{}{ "found": true, "product_id": productID, "move_line_id": moveLineID, "action": "incremented", "message": "Quantity incremented", }, nil }) // get_picking_data: Return the full picking data for the barcode interface. // Mirrors: stock_barcode.picking get_barcode_data() m.RegisterMethod("get_picking_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) < 1 { return nil, fmt.Errorf("stock.barcode.picking.get_picking_data requires picking_id") } pickingID, _ := args[0].(int64) if pickingID == 0 { return nil, fmt.Errorf("stock.barcode.picking: invalid picking_id") } env := rs.Env() // Get picking header var pickingName, state string var srcLocID, dstLocID int64 err := env.Tx().QueryRow(env.Ctx(), `SELECT name, state, location_id, location_dest_id FROM stock_picking WHERE id = $1`, pickingID, ).Scan(&pickingName, &state, &srcLocID, &dstLocID) if err != nil { return nil, fmt.Errorf("stock.barcode.picking: read picking %d: %w", pickingID, err) } // Get source/dest location names var srcLocName, dstLocName string env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(complete_name, name) FROM stock_location WHERE id = $1`, srcLocID, ).Scan(&srcLocName) env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(complete_name, name) FROM stock_location WHERE id = $1`, dstLocID, ).Scan(&dstLocName) // Get move lines rows, err := env.Tx().Query(env.Ctx(), `SELECT sm.id, sm.product_id, pt.name as product_name, sm.product_uom_qty as demand, COALESCE(SUM(sml.quantity), 0) as done_qty FROM stock_move sm JOIN product_product pp ON pp.id = sm.product_id JOIN product_template pt ON pt.id = pp.product_tmpl_id LEFT JOIN stock_move_line sml ON sml.move_id = sm.id WHERE sm.picking_id = $1 AND sm.state != 'cancel' GROUP BY sm.id, sm.product_id, pt.name, sm.product_uom_qty ORDER BY pt.name`, pickingID, ) if err != nil { return nil, fmt.Errorf("stock.barcode.picking: query moves for %d: %w", pickingID, err) } defer rows.Close() var moveLines []map[string]interface{} for rows.Next() { var moveID, prodID int64 var prodName string var demand, doneQty float64 if err := rows.Scan(&moveID, &prodID, &prodName, &demand, &doneQty); err != nil { return nil, fmt.Errorf("stock.barcode.picking: scan move: %w", err) } moveLines = append(moveLines, map[string]interface{}{ "move_id": moveID, "product_id": prodID, "product": prodName, "demand": demand, "done": doneQty, "remaining": demand - doneQty, }) } return map[string]interface{}{ "picking_id": pickingID, "name": pickingName, "state": state, "source_location": srcLocName, "dest_location": dstLocName, "lines": moveLines, }, nil }) } // isNumeric checks if a string contains only digit characters. func isNumeric(s string) bool { for _, c := range s { if c < '0' || c > '9' { return false } } return len(s) > 0 }