Backend improvements: views, fields_get, session, RPC stubs

- Improved auto-generated list/form/search views with priority fields,
  two-column form layout, statusbar widget, notebook for O2M fields
- Enhanced fields_get with currency_field, compute, related metadata
- Fixed session handling: handleSessionInfo/handleSessionCheck use real
  session from cookie instead of hardcoded values
- Added read_progress_bar and activity_format RPC stubs
- Improved bootstrap translations with lang_parameters
- Added "contacts" to session modules list

Server starts successfully: 14 modules, 93 models, 378 XML templates,
503 JS modules transpiled — all from local frontend/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-03-31 23:16:26 +02:00
parent 8741282322
commit 9c444061fd
32 changed files with 3416 additions and 148 deletions

View File

@@ -1,6 +1,11 @@
package models
import "odoo-go/pkg/orm"
import (
"fmt"
"time"
"odoo-go/pkg/orm"
)
// initStock registers all stock models.
// Mirrors: odoo/addons/stock/models/stock_warehouse.py,
@@ -168,6 +173,150 @@ func initStockPicking() {
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 that are still in draft.
// 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)
}
// Also confirm all draft moves on this picking
_, err = env.Tx().Exec(env.Ctx(),
`UPDATE stock_move SET state = 'confirmed' WHERE picking_id = $1 AND state = 'draft'`, id)
if err != nil {
return nil, fmt.Errorf("stock: confirm moves for picking %d: %w", id, err)
}
}
return true, nil
})
// action_assign transitions a picking from confirmed → assigned (reserve stock).
// Mirrors: stock.picking.action_assign()
m.RegisterMethod("action_assign", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
env := rs.Env()
for _, id := range rs.IDs() {
_, err := env.Tx().Exec(env.Ctx(),
`UPDATE stock_picking SET state = 'assigned' WHERE id = $1`, id)
if err != nil {
return nil, fmt.Errorf("stock: assign picking %d: %w", id, err)
}
_, err = env.Tx().Exec(env.Ctx(),
`UPDATE stock_move SET state = 'assigned' WHERE picking_id = $1 AND state IN ('confirmed', 'partially_available')`, id)
if err != nil {
return nil, fmt.Errorf("stock: assign moves for picking %d: %w", id, err)
}
}
return true, nil
})
// button_validate transitions a picking from assigned → done (process the transfer).
// Updates all moves to done and adjusts stock quants (source decremented, dest incremented).
// Mirrors: stock.picking.button_validate()
m.RegisterMethod("button_validate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
env := rs.Env()
for _, id := range rs.IDs() {
// Mark picking as done
_, err := env.Tx().Exec(env.Ctx(),
`UPDATE stock_picking SET state = 'done', date_done = NOW() WHERE id = $1`, id)
if err != nil {
return nil, fmt.Errorf("stock: validate picking %d: %w", id, err)
}
// Mark all moves as done
_, err = env.Tx().Exec(env.Ctx(),
`UPDATE stock_move SET state = 'done', date = NOW() WHERE picking_id = $1`, id)
if err != nil {
return nil, fmt.Errorf("stock: validate moves for picking %d: %w", id, err)
}
// Update quants: for each done move, adjust source and destination locations
rows, err := env.Tx().Query(env.Ctx(),
`SELECT product_id, product_uom_qty, location_id, location_dest_id
FROM stock_move WHERE picking_id = $1 AND state = 'done'`, id)
if err != nil {
return nil, fmt.Errorf("stock: read moves for picking %d: %w", id, err)
}
type moveInfo struct {
productID int64
qty float64
srcLoc int64
dstLoc int64
}
var moves []moveInfo
for rows.Next() {
var mi moveInfo
if err := rows.Scan(&mi.productID, &mi.qty, &mi.srcLoc, &mi.dstLoc); err != nil {
rows.Close()
return nil, fmt.Errorf("stock: scan move for picking %d: %w", id, err)
}
moves = append(moves, mi)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("stock: iterate moves for picking %d: %w", id, err)
}
for _, mi := range moves {
// Decrease source location quant
if err := updateQuant(env, mi.productID, mi.srcLoc, -mi.qty); err != nil {
return nil, fmt.Errorf("stock: update source quant for picking %d: %w", id, err)
}
// Increase destination location quant
if err := updateQuant(env, mi.productID, mi.dstLoc, mi.qty); err != nil {
return nil, fmt.Errorf("stock: update dest quant for picking %d: %w", id, err)
}
}
}
return true, 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, company_id) VALUES ($1, $2, $3, 1)`,
productID, locationID, delta)
}
return err
}
// initStockMove registers stock.move — individual product movements.