Files
goodie/addons/stock/models/stock_barcode.go
Marc bdb97f98ad Massive module expansion: Stock, CRM, HR — +2895 LOC
Stock (1193→2867 LOC):
- Valuation layers (FIFO consumption, product valuation history)
- Landed costs (split by equal/qty/cost/weight/volume, validation)
- Stock reports (by product, by location, move history, valuation)
- Forecasting (on_hand + incoming - outgoing per product)
- Batch transfers (confirm/assign/done with picking delegation)
- Barcode interface (scan product/lot/package/location, qty increment)

CRM (233→1113 LOC):
- Sales teams with dashboard KPIs (opportunity count/amount/unassigned)
- Team members with lead capacity + round-robin auto-assignment
- Lead extended: activities, UTM tracking, scoring, address fields
- Lead methods: merge, duplicate, schedule activity, set priority/stage
- Pipeline analysis (stages, win rate, conversion, team/salesperson perf)
- Partner onchange (auto-populate contact from partner)

HR (223→520 LOC):
- Leave management: hr.leave.type, hr.leave, hr.leave.allocation
  with full approval workflow (draft→confirm→validate/refuse)
- Attendance: check in/out with computed worked_hours
- Expenses: hr.expense + hr.expense.sheet with state machine
- Skills/Resume: skill types, employee skills, resume lines
- Employee extensions: skills, attendance, leave count links

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:21:52 +02:00

237 lines
7.6 KiB
Go

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
})
// 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
})
}