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:
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
@@ -43,7 +44,7 @@ func initSaleOrder() {
|
||||
// -- Dates --
|
||||
m.AddFields(
|
||||
orm.Datetime("date_order", orm.FieldOpts{
|
||||
String: "Order Date", Required: true, Index: true,
|
||||
String: "Order Date", Required: true, Index: true, Default: "today",
|
||||
}),
|
||||
orm.Date("validity_date", orm.FieldOpts{String: "Expiration"}),
|
||||
)
|
||||
@@ -111,6 +112,50 @@ func initSaleOrder() {
|
||||
orm.Boolean("require_payment", orm.FieldOpts{String: "Online Payment"}),
|
||||
)
|
||||
|
||||
// -- Computed: _compute_amounts --
|
||||
// Computes untaxed, tax, and total amounts from order lines.
|
||||
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_amounts()
|
||||
computeSaleAmounts := func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
soID := rs.IDs()[0]
|
||||
|
||||
var untaxed float64
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)), 0)
|
||||
FROM sale_order_line WHERE order_id = $1
|
||||
AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`,
|
||||
soID).Scan(&untaxed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: compute amounts for SO %d: %w", soID, err)
|
||||
}
|
||||
|
||||
// Compute tax from linked tax records on lines; fall back to sum of line taxes
|
||||
var tax float64
|
||||
err = env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(
|
||||
product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
|
||||
* COALESCE((SELECT t.amount / 100 FROM account_tax t
|
||||
JOIN sale_order_line_account_tax_rel rel ON rel.account_tax_id = t.id
|
||||
WHERE rel.sale_order_line_id = sol.id LIMIT 1), 0)
|
||||
), 0)
|
||||
FROM sale_order_line sol WHERE sol.order_id = $1
|
||||
AND (sol.display_type IS NULL OR sol.display_type = '' OR sol.display_type = 'product')`,
|
||||
soID).Scan(&tax)
|
||||
if err != nil {
|
||||
// Fallback: if the M2M table doesn't exist, estimate tax at 0
|
||||
tax = 0
|
||||
}
|
||||
|
||||
return orm.Values{
|
||||
"amount_untaxed": untaxed,
|
||||
"amount_tax": tax,
|
||||
"amount_total": untaxed + tax,
|
||||
}, nil
|
||||
}
|
||||
m.RegisterCompute("amount_untaxed", computeSaleAmounts)
|
||||
m.RegisterCompute("amount_tax", computeSaleAmounts)
|
||||
m.RegisterCompute("amount_total", computeSaleAmounts)
|
||||
|
||||
// -- Sequence Hook --
|
||||
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
|
||||
name, _ := vals["name"].(string)
|
||||
@@ -234,7 +279,7 @@ func initSaleOrder() {
|
||||
"currency_id": currencyID,
|
||||
"journal_id": journalID,
|
||||
"invoice_origin": fmt.Sprintf("SO%d", soID),
|
||||
"date": "2026-03-30", // TODO: use current date
|
||||
"date": time.Now().Format("2006-01-02"),
|
||||
"line_ids": lineCmds,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -250,6 +295,109 @@ func initSaleOrder() {
|
||||
|
||||
return invoiceIDs, nil
|
||||
})
|
||||
|
||||
// action_create_delivery: Generate a stock picking (delivery) from a confirmed sale order.
|
||||
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._action_confirm() → _create_picking()
|
||||
m.RegisterMethod("action_create_delivery", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
var pickingIDs []int64
|
||||
|
||||
for _, soID := range rs.IDs() {
|
||||
// Read SO header for partner and company
|
||||
var partnerID, companyID int64
|
||||
var soName string
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(partner_shipping_id, partner_id), company_id, name
|
||||
FROM sale_order WHERE id = $1`, soID,
|
||||
).Scan(&partnerID, &companyID, &soName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: read SO %d for delivery: %w", soID, err)
|
||||
}
|
||||
|
||||
// Read SO lines with products
|
||||
rows, err := env.Tx().Query(env.Ctx(),
|
||||
`SELECT product_id, product_uom_qty, COALESCE(name, '') FROM sale_order_line
|
||||
WHERE order_id = $1 AND product_id IS NOT NULL
|
||||
AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`, soID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: read SO lines %d for delivery: %w", soID, err)
|
||||
}
|
||||
|
||||
type soline struct {
|
||||
productID int64
|
||||
qty float64
|
||||
name string
|
||||
}
|
||||
var lines []soline
|
||||
for rows.Next() {
|
||||
var l soline
|
||||
if err := rows.Scan(&l.productID, &l.qty, &l.name); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
lines = append(lines, l)
|
||||
}
|
||||
rows.Close()
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find outgoing picking type and locations
|
||||
var pickingTypeID, srcLocID, destLocID int64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT pt.id, COALESCE(pt.default_location_src_id, 0), COALESCE(pt.default_location_dest_id, 0)
|
||||
FROM stock_picking_type pt
|
||||
WHERE pt.code = 'outgoing' AND pt.company_id = $1
|
||||
LIMIT 1`, companyID,
|
||||
).Scan(&pickingTypeID, &srcLocID, &destLocID)
|
||||
|
||||
// Fallback: find internal and customer locations
|
||||
if srcLocID == 0 {
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT id FROM stock_location WHERE usage = 'internal' AND COALESCE(company_id, $1) = $1 LIMIT 1`,
|
||||
companyID).Scan(&srcLocID)
|
||||
}
|
||||
if destLocID == 0 {
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT id FROM stock_location WHERE usage = 'customer' LIMIT 1`).Scan(&destLocID)
|
||||
}
|
||||
if pickingTypeID == 0 {
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT id FROM stock_picking_type WHERE code = 'outgoing' LIMIT 1`).Scan(&pickingTypeID)
|
||||
}
|
||||
|
||||
// Create picking
|
||||
var pickingID int64
|
||||
err = env.Tx().QueryRow(env.Ctx(),
|
||||
`INSERT INTO stock_picking
|
||||
(name, state, scheduled_date, company_id, partner_id, picking_type_id,
|
||||
location_id, location_dest_id, origin)
|
||||
VALUES ($1, 'confirmed', NOW(), $2, $3, $4, $5, $6, $7) RETURNING id`,
|
||||
fmt.Sprintf("WH/OUT/%05d", soID), companyID, partnerID, pickingTypeID,
|
||||
srcLocID, destLocID, soName,
|
||||
).Scan(&pickingID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: create picking for SO %d: %w", soID, err)
|
||||
}
|
||||
|
||||
// Create stock moves
|
||||
for _, l := range lines {
|
||||
_, err = env.Tx().Exec(env.Ctx(),
|
||||
`INSERT INTO stock_move
|
||||
(name, product_id, product_uom_qty, state, picking_id, company_id,
|
||||
location_id, location_dest_id, date, origin, product_uom)
|
||||
VALUES ($1, $2, $3, 'confirmed', $4, $5, $6, $7, NOW(), $8, 1)`,
|
||||
l.name, l.productID, l.qty, pickingID, companyID,
|
||||
srcLocID, destLocID, soName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: create stock move for SO %d: %w", soID, err)
|
||||
}
|
||||
}
|
||||
|
||||
pickingIDs = append(pickingIDs, pickingID)
|
||||
}
|
||||
return pickingIDs, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initSaleOrderLine registers sale.order.line — individual line items on a sales order.
|
||||
|
||||
Reference in New Issue
Block a user