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

@@ -748,6 +748,134 @@ func initSaleOrder() {
}
return pickingIDs, nil
})
// action_create_down_payment: Create a deposit invoice for a percentage of the SO total.
// Mirrors: odoo/addons/sale/wizard/sale_make_invoice_advance.py
m.RegisterMethod("action_create_down_payment", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
env := rs.Env()
soID := rs.IDs()[0]
percentage := float64(10) // Default 10%
if len(args) > 0 {
if p, ok := args[0].(float64); ok {
percentage = p
}
}
var total float64
var partnerID, companyID, currencyID, journalID int64
var soName string
err := env.Tx().QueryRow(env.Ctx(),
`SELECT COALESCE(amount_total, 0), partner_id, company_id, currency_id,
COALESCE(journal_id, 0), COALESCE(name, '')
FROM sale_order WHERE id = $1`, soID,
).Scan(&total, &partnerID, &companyID, &currencyID, &journalID, &soName)
if err != nil {
return nil, fmt.Errorf("sale: read SO %d for down payment: %w", soID, err)
}
downAmount := total * percentage / 100
// Find sales journal if not set on SO
if journalID == 0 {
env.Tx().QueryRow(env.Ctx(),
`SELECT id FROM account_journal
WHERE type = 'sale' AND active = true AND company_id = $1
ORDER BY sequence, id LIMIT 1`, companyID,
).Scan(&journalID)
}
if journalID == 0 {
journalID = 1
}
// Create deposit invoice
invoiceRS := env.Model("account.move")
inv, err := invoiceRS.Create(orm.Values{
"move_type": "out_invoice",
"partner_id": partnerID,
"company_id": companyID,
"currency_id": currencyID,
"journal_id": journalID,
"invoice_origin": soName,
"date": time.Now().Format("2006-01-02"),
})
if err != nil {
return nil, fmt.Errorf("sale: create down payment invoice for SO %d: %w", soID, err)
}
moveID := inv.ID()
// Find revenue account
var revenueAccountID int64
env.Tx().QueryRow(env.Ctx(),
`SELECT id FROM account_account WHERE code = '8300' AND company_id = $1 LIMIT 1`,
companyID).Scan(&revenueAccountID)
if revenueAccountID == 0 {
env.Tx().QueryRow(env.Ctx(),
`SELECT id FROM account_account
WHERE account_type LIKE 'income%%' AND company_id = $1
ORDER BY code LIMIT 1`, companyID).Scan(&revenueAccountID)
}
// Find receivable account
var receivableAccountID int64
env.Tx().QueryRow(env.Ctx(),
`SELECT id FROM account_account
WHERE account_type = 'asset_receivable' AND company_id = $1
ORDER BY code LIMIT 1`, companyID).Scan(&receivableAccountID)
if receivableAccountID == 0 {
receivableAccountID = revenueAccountID
}
lineRS := env.Model("account.move.line")
// Down payment product line (credit)
_, err = lineRS.Create(orm.Values{
"move_id": moveID,
"name": fmt.Sprintf("Down payment of %.0f%%", percentage),
"quantity": 1.0,
"price_unit": downAmount,
"account_id": revenueAccountID,
"company_id": companyID,
"journal_id": journalID,
"currency_id": currencyID,
"partner_id": partnerID,
"display_type": "product",
"debit": 0.0,
"credit": downAmount,
"balance": -downAmount,
})
if err != nil {
return nil, fmt.Errorf("sale: create down payment line: %w", err)
}
// Receivable line (debit)
_, err = lineRS.Create(orm.Values{
"move_id": moveID,
"name": "/",
"quantity": 1.0,
"account_id": receivableAccountID,
"company_id": companyID,
"journal_id": journalID,
"currency_id": currencyID,
"partner_id": partnerID,
"display_type": "payment_term",
"debit": downAmount,
"credit": 0.0,
"balance": downAmount,
"amount_residual": downAmount,
"amount_residual_currency": downAmount,
})
if err != nil {
return nil, fmt.Errorf("sale: create down payment receivable line: %w", err)
}
return map[string]interface{}{
"type": "ir.actions.act_window",
"res_model": "account.move",
"res_id": moveID,
"view_mode": "form",
"target": "current",
}, nil
})
}
// initSaleOrderLine registers sale.order.line — individual line items on a sales order.