package models import ( "log" "odoo-go/pkg/orm" ) // initAccountRecurring registers account.move.recurring — recurring entry templates. // Mirrors: odoo/addons/account/models/account_move.py (recurring entries feature) // // Allows defining templates that automatically generate journal entries // on a schedule (daily, weekly, monthly, quarterly, yearly). func initAccountRecurring() { m := orm.NewModel("account.move.recurring", orm.ModelOpts{ Description: "Recurring Entry", Order: "name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Required: true}), orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Required: true}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), orm.Selection("period", []orm.SelectionItem{ {Value: "daily", Label: "Daily"}, {Value: "weekly", Label: "Weekly"}, {Value: "monthly", Label: "Monthly"}, {Value: "quarterly", Label: "Quarterly"}, {Value: "yearly", Label: "Yearly"}, }, orm.FieldOpts{String: "Period", Required: true, Default: "monthly"}), orm.Date("date_next", orm.FieldOpts{String: "Next Date", Required: true}), orm.Date("date_end", orm.FieldOpts{String: "End Date"}), orm.Many2one("template_move_id", "account.move", orm.FieldOpts{String: "Template Entry"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Selection("state", []orm.SelectionItem{ {Value: "draft", Label: "Draft"}, {Value: "running", Label: "Running"}, {Value: "done", Label: "Done"}, }, orm.FieldOpts{String: "Status", Default: "draft"}), ) // action_start: draft -> running m.RegisterMethod("action_start", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE account_move_recurring SET state = 'running' WHERE id = $1 AND state = 'draft'`, id) } return true, nil }) // action_done: running -> done m.RegisterMethod("action_done", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE account_move_recurring SET state = 'done' WHERE id = $1`, id) } return true, nil }) // action_generate: create journal entries from the template and advance next date. // Mirrors: odoo/addons/account/models/account_move.py _cron_recurring_entries() m.RegisterMethod("action_generate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, recID := range rs.IDs() { var templateID *int64 var dateNext, period string var dateEnd *string err := env.Tx().QueryRow(env.Ctx(), `SELECT template_move_id, date_next::text, period, date_end::text FROM account_move_recurring WHERE id = $1 AND state = 'running'`, recID, ).Scan(&templateID, &dateNext, &period, &dateEnd) if err != nil { continue // not running or not found } if templateID == nil || *templateID == 0 { continue } // Check if past end date if dateEnd != nil && dateNext > *dateEnd { env.Tx().Exec(env.Ctx(), `UPDATE account_move_recurring SET state = 'done' WHERE id = $1`, recID) continue } // Copy the template move with the next date templateRS := env.Model("account.move").Browse(*templateID) newMove, err := templateRS.Copy(orm.Values{"date": dateNext}) if err != nil { continue } _ = newMove // Advance next date based on period var interval string switch period { case "daily": interval = "1 day" case "weekly": interval = "7 days" case "monthly": interval = "1 month" case "quarterly": interval = "3 months" case "yearly": interval = "1 year" default: interval = "1 month" } env.Tx().Exec(env.Ctx(), `UPDATE account_move_recurring SET date_next = date_next + $1::interval WHERE id = $2`, interval, recID) } return true, nil }) // _cron_auto_post: cron job that auto-posts draft moves and generates recurring entries. // Mirrors: odoo/addons/account/models/account_move.py _cron_auto_post_draft_entry() // // 1) Find draft account.move entries with auto_post=true and date <= today, post them. // 2) Find recurring entries (state='running') with date_next <= today, generate them. m.RegisterMethod("_cron_auto_post", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // --- Part 1: Auto-post draft moves --- rows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM account_move WHERE auto_post = true AND state = 'draft' AND date <= CURRENT_DATE`) if err != nil { return nil, err } var moveIDs []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { continue } moveIDs = append(moveIDs, id) } rows.Close() if len(moveIDs) > 0 { moveModelDef := orm.Registry.Get("account.move") if moveModelDef != nil { for _, mid := range moveIDs { moveRS := env.Model("account.move").Browse(mid) if postFn, ok := moveModelDef.Methods["action_post"]; ok { if _, err := postFn(moveRS); err != nil { log.Printf("account: auto-post move %d failed: %v", mid, err) } } } } } // --- Part 2: Generate recurring entries due today --- recRows, err := env.Tx().Query(env.Ctx(), `SELECT id FROM account_move_recurring WHERE state = 'running' AND date_next <= CURRENT_DATE AND active = true`) if err != nil { return nil, err } var recIDs []int64 for recRows.Next() { var id int64 if err := recRows.Scan(&id); err != nil { continue } recIDs = append(recIDs, id) } recRows.Close() if len(recIDs) > 0 { recModelDef := orm.Registry.Get("account.move.recurring") if recModelDef != nil { for _, rid := range recIDs { recRS := env.Model("account.move.recurring").Browse(rid) if genFn, ok := recModelDef.Methods["action_generate"]; ok { if _, err := genFn(recRS); err != nil { log.Printf("account: recurring generate %d failed: %v", rid, err) } } } } } return true, nil }) }