Deep dive: Account, Stock, Sale, Purchase — +800 LOC business logic

Account:
- Multi-currency: company_currency_id, amount_total_signed
- Lock dates on res.company (period, fiscal year, tax) + enforcement in action_post
- Recurring entries: account.move.recurring with action_generate (copy+advance)
- Tax groups: amount_type='group' computes child taxes with include_base_amount
- ComputeTaxes batch function, findTaxAccount helper

Stock:
- Lot/Serial tracking: enhanced stock.lot with expiration dates + qty compute
- Routes: stock.route model with product/category/warehouse selectable flags
- Rules: stock.rule model with pull/push/buy/manufacture actions + procure methods
- Returns: action_return on picking (swap locations, copy moves)
- Product tracking extension (none/lot/serial) + route_ids M2M

Sale:
- Pricelist: get_product_price with fixed/percentage/formula computation
- Margin: purchase_price, margin, margin_percent on line + order totals
- Down payments: action_create_down_payment (deposit invoice at X%)

Purchase:
- 3-way matching: action_create_bill now updates qty_invoiced on PO lines
- Purchase agreements: purchase.requisition + line with state machine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-03 19:05:39 +02:00
parent d9171191af
commit b8fa4719ad
14 changed files with 802 additions and 17 deletions

View File

@@ -0,0 +1,68 @@
package models
import "odoo-go/pkg/orm"
// initSaleMargin adds margin fields to sale.order.line and sale.order.
// Mirrors: odoo/addons/sale_margin/models/sale_order_line.py + sale_order.py
func initSaleMargin() {
// -- Extend sale.order.line with margin fields --
line := orm.ExtendModel("sale.order.line")
line.AddFields(
orm.Float("purchase_price", orm.FieldOpts{String: "Cost Price"}),
orm.Monetary("margin", orm.FieldOpts{
String: "Margin", Compute: "_compute_margin", Store: true, CurrencyField: "currency_id",
}),
orm.Float("margin_percent", orm.FieldOpts{
String: "Margin (%)", Compute: "_compute_margin", Store: true,
}),
)
computeLineMargin := func(rs *orm.Recordset) (orm.Values, error) {
env := rs.Env()
lineID := rs.IDs()[0]
var subtotal, cost float64
env.Tx().QueryRow(env.Ctx(),
`SELECT COALESCE(price_subtotal, 0), COALESCE(purchase_price, 0) * COALESCE(product_uom_qty, 0)
FROM sale_order_line WHERE id = $1`, lineID,
).Scan(&subtotal, &cost)
margin := subtotal - cost
pct := float64(0)
if subtotal > 0 {
pct = margin / subtotal * 100
}
return orm.Values{"margin": margin, "margin_percent": pct}, nil
}
line.RegisterCompute("margin", computeLineMargin)
line.RegisterCompute("margin_percent", computeLineMargin)
// -- Extend sale.order with total margin --
so := orm.ExtendModel("sale.order")
so.AddFields(
orm.Monetary("margin", orm.FieldOpts{
String: "Margin", Compute: "_compute_margin", Store: true, CurrencyField: "currency_id",
}),
orm.Float("margin_percent", orm.FieldOpts{
String: "Margin (%)", Compute: "_compute_margin", Store: true,
}),
)
computeSOMargin := func(rs *orm.Recordset) (orm.Values, error) {
env := rs.Env()
soID := rs.IDs()[0]
var margin, untaxed float64
env.Tx().QueryRow(env.Ctx(),
`SELECT COALESCE(SUM(margin), 0), COALESCE(SUM(price_subtotal), 0)
FROM sale_order_line WHERE order_id = $1
AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`,
soID,
).Scan(&margin, &untaxed)
pct := float64(0)
if untaxed > 0 {
pct = margin / untaxed * 100
}
return orm.Values{"margin": margin, "margin_percent": pct}, nil
}
so.RegisterCompute("margin", computeSOMargin)
so.RegisterCompute("margin_percent", computeSOMargin)
}