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:
Marc
2026-04-02 19:26:08 +02:00
parent 06e49c878a
commit b57176de2f
29 changed files with 3243 additions and 111 deletions

View File

@@ -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.

View File

@@ -7,6 +7,7 @@ func Init() {
initAccountMove()
initAccountMoveLine()
initAccountPayment()
initAccountPaymentRegister()
initAccountPaymentTerm()
initAccountReconcile()
initAccountBankStatement()