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:
@@ -126,6 +126,13 @@ func initAccountMove() {
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Multi-Currency --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Company Currency", Related: "company_id.currency_id",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Amounts (Computed) --
|
||||
m.AddFields(
|
||||
orm.Monetary("amount_untaxed", orm.FieldOpts{String: "Untaxed Amount", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
@@ -133,6 +140,7 @@ func initAccountMove() {
|
||||
orm.Monetary("amount_total", orm.FieldOpts{String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_residual", orm.FieldOpts{String: "Amount Due", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_total_in_currency_signed", orm.FieldOpts{String: "Total in Currency Signed", Compute: "_compute_amount", Store: true}),
|
||||
orm.Monetary("amount_total_signed", orm.FieldOpts{String: "Total Signed", Compute: "_compute_amount", Store: true, CurrencyField: "company_currency_id"}),
|
||||
)
|
||||
|
||||
// -- Invoice specific --
|
||||
@@ -176,11 +184,24 @@ func initAccountMove() {
|
||||
|
||||
total := untaxed + tax
|
||||
|
||||
// amount_total_signed: total in company currency (sign depends on move type)
|
||||
// For customer invoices/receipts the sign is positive, for credit notes negative.
|
||||
var moveType string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(move_type, 'entry') FROM account_move WHERE id = $1`, moveID,
|
||||
).Scan(&moveType)
|
||||
|
||||
sign := 1.0
|
||||
if moveType == "out_refund" || moveType == "in_refund" {
|
||||
sign = -1.0
|
||||
}
|
||||
|
||||
return orm.Values{
|
||||
"amount_untaxed": untaxed,
|
||||
"amount_tax": tax,
|
||||
"amount_total": total,
|
||||
"amount_residual": total, // Simplified: residual = total until payments
|
||||
"amount_untaxed": untaxed,
|
||||
"amount_tax": tax,
|
||||
"amount_total": total,
|
||||
"amount_residual": total, // Simplified: residual = total until payments
|
||||
"amount_total_signed": total * sign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -188,6 +209,7 @@ func initAccountMove() {
|
||||
m.RegisterCompute("amount_tax", computeAmount)
|
||||
m.RegisterCompute("amount_total", computeAmount)
|
||||
m.RegisterCompute("amount_residual", computeAmount)
|
||||
m.RegisterCompute("amount_total_signed", computeAmount)
|
||||
|
||||
// -- Business Methods: State Transitions --
|
||||
// Mirrors: odoo/addons/account/models/account_move.py action_post(), button_cancel()
|
||||
@@ -206,6 +228,24 @@ func initAccountMove() {
|
||||
return nil, fmt.Errorf("account: can only post draft entries (current: %s)", state)
|
||||
}
|
||||
|
||||
// Check lock dates
|
||||
// Mirrors: odoo/addons/account/models/account_move.py _check_fiscalyear_lock_date()
|
||||
var moveDate, periodLock, fiscalLock, taxLock *string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT m.date::text, c.period_lock_date::text, c.fiscalyear_lock_date::text, c.tax_lock_date::text
|
||||
FROM account_move m JOIN res_company c ON c.id = m.company_id WHERE m.id = $1`, id,
|
||||
).Scan(&moveDate, &periodLock, &fiscalLock, &taxLock)
|
||||
|
||||
if fiscalLock != nil && moveDate != nil && *moveDate <= *fiscalLock {
|
||||
return nil, fmt.Errorf("account: cannot post entry dated %s, fiscal year is locked until %s", *moveDate, *fiscalLock)
|
||||
}
|
||||
if periodLock != nil && moveDate != nil && *moveDate <= *periodLock {
|
||||
return nil, fmt.Errorf("account: cannot post entry dated %s, period is locked until %s", *moveDate, *periodLock)
|
||||
}
|
||||
if taxLock != nil && moveDate != nil && *moveDate <= *taxLock {
|
||||
return nil, fmt.Errorf("account: cannot post entry dated %s, tax return is locked until %s", *moveDate, *taxLock)
|
||||
}
|
||||
|
||||
// Check partner is set for invoice types
|
||||
var moveType string
|
||||
env.Tx().QueryRow(env.Ctx(), `SELECT move_type FROM account_move WHERE id = $1`, id).Scan(&moveType)
|
||||
|
||||
Reference in New Issue
Block a user