Bring odoo-go to ~70%: read_group, record rules, admin, sessions
Phase 1: read_group/web_read_group with SQL GROUP BY, aggregates (sum/avg/min/max/count/array_agg/sum_currency), date granularity, M2O groupby resolution to [id, display_name]. Phase 2: Record rules with domain_force parsing (Python literal parser), global AND + group OR merging. Domain operators: child_of, parent_of, any, not any compiled to SQL hierarchy/EXISTS queries. Phase 3: Button dispatch via /web/dataset/call_button, method return values interpreted as actions. Payment register wizard (account.payment.register) for sale→invoice→pay flow. Phase 4: ir.filters, ir.default, product fields expanded, SO line product_id onchange, ir_model+ir_model_fields DB seeding. Phase 5: CSV export (/web/export/csv), attachment upload/download via ir.attachment, fields_get with aggregator hints. Admin/System: Session persistence (PostgreSQL-backed), ir.config_parameter with get_param/set_param, ir.cron, ir.logging, res.lang, res.config.settings with company-related fields, Settings form view. Technical menu with Views/Actions/Parameters/Security/Logging sub-menus. User change_password, preferences. Password never exposed in UI/API. Bugfixes: false→nil for varchar/int fields, int32 in toInt64, call_button route with trailing slash, create_invoices returns action, search view always included, get_formview_action, name_create, ir.http stub. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -727,6 +727,200 @@ func initAccountPayment() {
|
||||
orm.Char("payment_reference", orm.FieldOpts{String: "Payment Reference"}),
|
||||
orm.Char("payment_method_code", orm.FieldOpts{String: "Payment Method Code"}),
|
||||
)
|
||||
|
||||
// action_post: confirm and post the payment.
|
||||
// Mirrors: odoo/addons/account/models/account_payment.py action_post()
|
||||
m.RegisterMethod("action_post", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_payment SET state = 'paid' WHERE id = $1 AND state = 'draft'`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// action_cancel: cancel the payment.
|
||||
m.RegisterMethod("action_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_payment SET state = 'canceled' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initAccountPaymentRegister registers the payment register wizard.
|
||||
// Mirrors: odoo/addons/account/wizard/account_payment_register.py
|
||||
// This is a TransientModel wizard opened via "Register Payment" button on invoices.
|
||||
func initAccountPaymentRegister() {
|
||||
m := orm.NewModel("account.payment.register", orm.ModelOpts{
|
||||
Description: "Register Payment",
|
||||
Type: orm.ModelTransient,
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Date("payment_date", orm.FieldOpts{String: "Payment Date", Required: true}),
|
||||
orm.Monetary("amount", orm.FieldOpts{String: "Amount", CurrencyField: "currency_id"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency", Required: true}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Required: true}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer/Vendor"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Selection("payment_type", []orm.SelectionItem{
|
||||
{Value: "outbound", Label: "Send"},
|
||||
{Value: "inbound", Label: "Receive"},
|
||||
}, orm.FieldOpts{String: "Payment Type", Default: "inbound"}),
|
||||
orm.Selection("partner_type", []orm.SelectionItem{
|
||||
{Value: "customer", Label: "Customer"},
|
||||
{Value: "supplier", Label: "Vendor"},
|
||||
}, orm.FieldOpts{String: "Partner Type", Default: "customer"}),
|
||||
orm.Char("communication", orm.FieldOpts{String: "Memo"}),
|
||||
// Context-only: which invoice(s) are being paid
|
||||
orm.Many2many("line_ids", "account.move.line", orm.FieldOpts{
|
||||
String: "Journal items",
|
||||
Relation: "payment_register_move_line_rel",
|
||||
Column1: "wizard_id",
|
||||
Column2: "line_id",
|
||||
}),
|
||||
)
|
||||
|
||||
// action_create_payments: create account.payment from the wizard and mark invoice as paid.
|
||||
// Mirrors: odoo/addons/account/wizard/account_payment_register.py action_create_payments()
|
||||
m.RegisterMethod("action_create_payments", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
wizardData, err := rs.Read([]string{
|
||||
"payment_date", "amount", "currency_id", "journal_id",
|
||||
"partner_id", "company_id", "payment_type", "partner_type", "communication",
|
||||
})
|
||||
if err != nil || len(wizardData) == 0 {
|
||||
return nil, fmt.Errorf("account: cannot read payment register wizard")
|
||||
}
|
||||
wiz := wizardData[0]
|
||||
|
||||
paymentRS := env.Model("account.payment")
|
||||
paymentVals := orm.Values{
|
||||
"payment_type": wiz["payment_type"],
|
||||
"partner_type": wiz["partner_type"],
|
||||
"amount": wiz["amount"],
|
||||
"date": wiz["payment_date"],
|
||||
"currency_id": wiz["currency_id"],
|
||||
"journal_id": wiz["journal_id"],
|
||||
"partner_id": wiz["partner_id"],
|
||||
"company_id": wiz["company_id"],
|
||||
"payment_reference": wiz["communication"],
|
||||
"state": "draft",
|
||||
}
|
||||
|
||||
payment, err := paymentRS.Create(paymentVals)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("account: create payment: %w", err)
|
||||
}
|
||||
|
||||
// Auto-post the payment
|
||||
paymentModel := orm.Registry.Get("account.payment")
|
||||
if paymentModel != nil {
|
||||
if postMethod, ok := paymentModel.Methods["action_post"]; ok {
|
||||
if _, err := postMethod(payment); err != nil {
|
||||
return nil, fmt.Errorf("account: post payment: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark related invoices as paid (simplified: update payment_state on active invoices in context)
|
||||
// In Python Odoo this happens through reconciliation; we simplify for 70% target.
|
||||
if ctx := env.Context(); ctx != nil {
|
||||
if activeIDs, ok := ctx["active_ids"].([]interface{}); ok {
|
||||
for _, rawID := range activeIDs {
|
||||
if moveID, ok := toInt64Arg(rawID); ok && moveID > 0 {
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET payment_state = 'paid' WHERE id = $1 AND state = 'posted'`, moveID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return action to close wizard (standard Odoo pattern)
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window_close",
|
||||
}, nil
|
||||
})
|
||||
|
||||
// DefaultGet: pre-fill wizard from active invoice context.
|
||||
// Mirrors: odoo/addons/account/wizard/account_payment_register.py default_get()
|
||||
m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
|
||||
vals := orm.Values{
|
||||
"payment_date": time.Now().Format("2006-01-02"),
|
||||
}
|
||||
|
||||
ctx := env.Context()
|
||||
if ctx == nil {
|
||||
return vals
|
||||
}
|
||||
|
||||
// Get active invoice IDs from context
|
||||
var moveIDs []int64
|
||||
if ids, ok := ctx["active_ids"].([]interface{}); ok {
|
||||
for _, rawID := range ids {
|
||||
if id, ok := toInt64Arg(rawID); ok && id > 0 {
|
||||
moveIDs = append(moveIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(moveIDs) == 0 {
|
||||
return vals
|
||||
}
|
||||
|
||||
// Read first invoice to pre-fill defaults
|
||||
moveRS := env.Model("account.move").Browse(moveIDs[0])
|
||||
moveData, err := moveRS.Read([]string{
|
||||
"partner_id", "company_id", "currency_id", "amount_residual", "move_type",
|
||||
})
|
||||
if err != nil || len(moveData) == 0 {
|
||||
return vals
|
||||
}
|
||||
mv := moveData[0]
|
||||
|
||||
if pid, ok := toInt64Arg(mv["partner_id"]); ok && pid > 0 {
|
||||
vals["partner_id"] = pid
|
||||
}
|
||||
if cid, ok := toInt64Arg(mv["company_id"]); ok && cid > 0 {
|
||||
vals["company_id"] = cid
|
||||
}
|
||||
if curID, ok := toInt64Arg(mv["currency_id"]); ok && curID > 0 {
|
||||
vals["currency_id"] = curID
|
||||
}
|
||||
if amt, ok := mv["amount_residual"].(float64); ok {
|
||||
vals["amount"] = amt
|
||||
}
|
||||
|
||||
// Determine payment type from move type
|
||||
moveType, _ := mv["move_type"].(string)
|
||||
switch moveType {
|
||||
case "out_invoice", "out_receipt":
|
||||
vals["payment_type"] = "inbound"
|
||||
vals["partner_type"] = "customer"
|
||||
case "in_invoice", "in_receipt":
|
||||
vals["payment_type"] = "outbound"
|
||||
vals["partner_type"] = "supplier"
|
||||
}
|
||||
|
||||
// Default bank journal
|
||||
var journalID int64
|
||||
companyID := env.CompanyID()
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT id FROM account_journal
|
||||
WHERE type = 'bank' AND active = true AND company_id = $1
|
||||
ORDER BY sequence, id LIMIT 1`, companyID).Scan(&journalID)
|
||||
if journalID > 0 {
|
||||
vals["journal_id"] = journalID
|
||||
}
|
||||
|
||||
return vals
|
||||
}
|
||||
}
|
||||
|
||||
// initAccountPaymentTerm registers payment terms.
|
||||
|
||||
Reference in New Issue
Block a user