Deepen all business modules: methods, validations, workflows
Account: - action_post: partner validation, line count check, sequence number assignment (JOURNAL/YYYY/NNNN format) - action_register_payment: opens payment wizard from invoice - remove_move_reconcile: undo reconciliation, reset residuals - Register Payment button in invoice form (visible when posted+unpaid) Sale: - action_cancel: cancels linked draft invoices + SO state - action_draft: reset cancelled SO to draft - action_view_invoice: navigate to linked invoices - Cancel/Reset buttons in form view header Purchase: - button_draft: reset cancelled PO to draft - action_create_bill already existed Stock: - action_cancel on picking: cancels moves + picking state CRM: - action_set_won_rainbowman: sets Won stage + rainbow effect - convert_opportunity: lead→opportunity type switch HR: - hr.contract model (name, employee, wage, dates, state) Project: - action_blocked on task (kanban_state) - Task stage seed data (New, In Progress, Done, Cancelled) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -205,6 +205,25 @@ func initAccountMove() {
|
||||
if state != "draft" {
|
||||
return nil, fmt.Errorf("account: can only post draft entries (current: %s)", state)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if moveType != "entry" {
|
||||
var partnerID *int64
|
||||
env.Tx().QueryRow(env.Ctx(), `SELECT partner_id FROM account_move WHERE id = $1`, id).Scan(&partnerID)
|
||||
if partnerID == nil || *partnerID == 0 {
|
||||
return nil, fmt.Errorf("account: invoice requires a partner")
|
||||
}
|
||||
}
|
||||
|
||||
// Check at least one line exists
|
||||
var lineCount int
|
||||
env.Tx().QueryRow(env.Ctx(), `SELECT count(*) FROM account_move_line WHERE move_id = $1`, id).Scan(&lineCount)
|
||||
if lineCount == 0 {
|
||||
return nil, fmt.Errorf("account: cannot post an entry with no lines")
|
||||
}
|
||||
|
||||
// Check balanced
|
||||
var debitSum, creditSum float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
@@ -214,6 +233,34 @@ func initAccountMove() {
|
||||
if diff < -0.005 || diff > 0.005 {
|
||||
return nil, fmt.Errorf("account: cannot post unbalanced entry (debit=%.2f, credit=%.2f)", debitSum, creditSum)
|
||||
}
|
||||
|
||||
// Assign sequence number if name is still "/"
|
||||
var name string
|
||||
env.Tx().QueryRow(env.Ctx(), `SELECT name FROM account_move WHERE id = $1`, id).Scan(&name)
|
||||
if name == "/" || name == "" {
|
||||
// Generate sequence number
|
||||
var journalID int64
|
||||
var journalCode string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT j.id, j.code FROM account_journal j
|
||||
JOIN account_move m ON m.journal_id = j.id WHERE m.id = $1`, id,
|
||||
).Scan(&journalID, &journalCode)
|
||||
|
||||
// Get next sequence number
|
||||
var nextNum int64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(MAX(sequence_number), 0) + 1 FROM account_move WHERE journal_id = $1`,
|
||||
journalID).Scan(&nextNum)
|
||||
|
||||
// Format: journalCode/YYYY/NNNN
|
||||
year := time.Now().Format("2006")
|
||||
newName := fmt.Sprintf("%s/%s/%04d", journalCode, year, nextNum)
|
||||
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET name = $1, sequence_number = $2 WHERE id = $3`,
|
||||
newName, nextNum, id)
|
||||
}
|
||||
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET state = 'posted' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
@@ -255,6 +302,23 @@ func initAccountMove() {
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// action_register_payment: opens the payment register wizard.
|
||||
// Mirrors: odoo/addons/account/models/account_move.py action_register_payment()
|
||||
m.RegisterMethod("action_register_payment", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Register Payment",
|
||||
"res_model": "account.payment.register",
|
||||
"view_mode": "form",
|
||||
"views": [][]interface{}{{nil, "form"}},
|
||||
"target": "new",
|
||||
"context": map[string]interface{}{
|
||||
"active_model": "account.move",
|
||||
"active_ids": rs.IDs(),
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
|
||||
// -- Business Method: create_invoice_with_tax --
|
||||
// Creates a customer invoice with automatic tax line generation.
|
||||
// For each product line that carries a tax_id, a separate tax line
|
||||
@@ -929,6 +993,32 @@ func initAccountMoveLine() {
|
||||
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// remove_move_reconcile: undo reconciliation on selected lines.
|
||||
// Mirrors: odoo/addons/account/models/account_move_line.py remove_move_reconcile()
|
||||
m.RegisterMethod("remove_move_reconcile", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, lineID := range rs.IDs() {
|
||||
// Find partial reconciles involving this line
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`DELETE FROM account_partial_reconcile WHERE debit_move_id = $1 OR credit_move_id = $1`, lineID)
|
||||
// Reset residual to balance
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move_line SET amount_residual = balance, full_reconcile_id = NULL WHERE id = $1`, lineID)
|
||||
}
|
||||
// Clean up orphaned full reconciles
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`DELETE FROM account_full_reconcile WHERE id NOT IN (SELECT DISTINCT full_reconcile_id FROM account_partial_reconcile WHERE full_reconcile_id IS NOT NULL)`)
|
||||
// Update payment states
|
||||
for _, lineID := range rs.IDs() {
|
||||
var moveID int64
|
||||
env.Tx().QueryRow(env.Ctx(), `SELECT move_id FROM account_move_line WHERE id = $1`, lineID).Scan(&moveID)
|
||||
if moveID > 0 {
|
||||
updatePaymentState(env, moveID)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initAccountPayment registers account.payment.
|
||||
|
||||
Reference in New Issue
Block a user