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:
@@ -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, ¤cyID, &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.
|
||||
|
||||
Reference in New Issue
Block a user