Files
goodie/addons/stock/models/stock_picking_batch.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

344 lines
12 KiB
Go

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"}),
)
}