package models import ( "fmt" "strings" "odoo-go/pkg/orm" ) // initAccountFollowup registers payment follow-up models. // Mirrors: odoo/addons/account_followup/models/account_followup.py // // Follow-up levels define escalation steps for overdue payments: // Level 1: Friendly reminder after X days // Level 2: Formal notice after X days // Level 3: Final warning / legal action after X days func initAccountFollowup() { // -- Follow-up Level -- fl := orm.NewModel("account.followup.line", orm.ModelOpts{ Description: "Follow-up Criteria", Order: "delay", }) fl.AddFields( orm.Char("name", orm.FieldOpts{String: "Follow-Up Action", Required: true, Translate: true}), orm.Integer("delay", orm.FieldOpts{ String: "Due Days", Required: true, Help: "Number of days after the due date of the invoice to trigger this action", }), orm.Text("description", orm.FieldOpts{ String: "Printed Message", Translate: true, Help: "Message printed on the follow-up report", }), orm.Text("email_body", orm.FieldOpts{ String: "Email Body", Translate: true, Help: "Email body for the follow-up email", }), orm.Char("email_subject", orm.FieldOpts{ String: "Email Subject", Translate: true, }), orm.Boolean("send_email", orm.FieldOpts{ String: "Send Email", Default: true, }), orm.Boolean("send_letter", orm.FieldOpts{ String: "Send Letter", Default: false, }), orm.Boolean("manual_action", orm.FieldOpts{ String: "Manual Action", Help: "Assign a manual action to be done", }), orm.Text("manual_action_note", orm.FieldOpts{ String: "Action To Do", Help: "Description of the manual action to be taken", }), orm.Many2one("manual_action_responsible_id", "res.users", orm.FieldOpts{ String: "Assign a Responsible", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Required: true, }), orm.Selection("sms_template", []orm.SelectionItem{ {Value: "none", Label: "None"}, {Value: "default", Label: "Default"}, }, orm.FieldOpts{String: "SMS Template", Default: "none"}), orm.Boolean("join_invoices", orm.FieldOpts{ String: "Attach Invoices", Default: false, Help: "Attach open invoice PDFs to the follow-up email", }), orm.Selection("auto_execute", []orm.SelectionItem{ {Value: "auto", Label: "Automatic"}, {Value: "manual", Label: "Manual"}, }, orm.FieldOpts{String: "Execution", Default: "manual"}), ) // -- Follow-up Report (partner-level) -- // Extends res.partner with follow-up state tracking. initFollowupPartnerExtension() // -- Follow-up processing method on partner -- initFollowupProcess() } // initFollowupPartnerExtension adds follow-up tracking fields to res.partner. func initFollowupPartnerExtension() { ext := orm.ExtendModel("res.partner") ext.AddFields( orm.Selection("followup_status", []orm.SelectionItem{ {Value: "no_action_needed", Label: "No Action Needed"}, {Value: "with_overdue_invoices", Label: "With Overdue Invoices"}, {Value: "in_need_of_action", Label: "In Need of Action"}, }, orm.FieldOpts{String: "Follow-up Status", Compute: "_compute_followup_status"}), orm.Many2one("followup_level_id", "account.followup.line", orm.FieldOpts{ String: "Follow-up Level", }), orm.Date("followup_next_action_date", orm.FieldOpts{ String: "Next Follow-up Date", }), orm.Text("followup_reminder", orm.FieldOpts{ String: "Customer Follow-up Note", }), orm.Many2one("followup_responsible_id", "res.users", orm.FieldOpts{ String: "Follow-up Responsible", }), orm.Integer("total_overdue_invoices", orm.FieldOpts{ String: "Total Overdue Invoices", Compute: "_compute_total_overdue", }), orm.Monetary("total_overdue_amount", orm.FieldOpts{ String: "Total Overdue Amount", Compute: "_compute_total_overdue", }), ) // _compute_followup_status ext.RegisterCompute("followup_status", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() partnerID := rs.IDs()[0] var overdueCount int env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*) FROM account_move m WHERE m.partner_id = $1 AND m.state = 'posted' AND m.move_type IN ('out_invoice', 'out_receipt') AND m.payment_state NOT IN ('paid', 'in_payment', 'reversed') AND m.invoice_date_due < CURRENT_DATE`, partnerID, ).Scan(&overdueCount) status := "no_action_needed" if overdueCount > 0 { // Check if there's a follow-up level set var levelID *int64 env.Tx().QueryRow(env.Ctx(), `SELECT followup_level_id FROM res_partner WHERE id = $1`, partnerID, ).Scan(&levelID) if levelID != nil && *levelID > 0 { status = "in_need_of_action" } else { status = "with_overdue_invoices" } } return orm.Values{"followup_status": status}, nil }) // _compute_total_overdue: computes both count and amount of overdue invoices computeOverdue := func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() partnerID := rs.IDs()[0] var count int var total float64 env.Tx().QueryRow(env.Ctx(), `SELECT COUNT(*), COALESCE(SUM(amount_residual::float8), 0) FROM account_move WHERE partner_id = $1 AND state = 'posted' AND move_type IN ('out_invoice', 'out_receipt') AND payment_state NOT IN ('paid', 'in_payment', 'reversed') AND invoice_date_due < CURRENT_DATE`, partnerID, ).Scan(&count, &total) return orm.Values{ "total_overdue_invoices": count, "total_overdue_amount": total, }, nil } ext.RegisterCompute("total_overdue_invoices", computeOverdue) ext.RegisterCompute("total_overdue_amount", computeOverdue) } // initFollowupProcess registers the follow-up processing methods. func initFollowupProcess() { ext := orm.ExtendModel("res.partner") // action_execute_followup: run follow-up for the partner. // Mirrors: odoo/addons/account_followup/models/res_partner.py execute_followup() ext.RegisterMethod("action_execute_followup", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, partnerID := range rs.IDs() { // Find overdue invoices for this partner rows, err := env.Tx().Query(env.Ctx(), `SELECT m.id, m.name, m.invoice_date_due, m.amount_residual::float8, CURRENT_DATE - m.invoice_date_due as overdue_days FROM account_move m WHERE m.partner_id = $1 AND m.state = 'posted' AND m.move_type IN ('out_invoice', 'out_receipt') AND m.payment_state NOT IN ('paid', 'in_payment', 'reversed') AND m.invoice_date_due < CURRENT_DATE ORDER BY m.invoice_date_due`, partnerID) if err != nil { return nil, fmt.Errorf("account: query overdue invoices for partner %d: %w", partnerID, err) } var maxOverdueDays int var overdueInvoiceIDs []int64 for rows.Next() { var invID int64 var invName string var dueDate interface{} var residual float64 var overdueDays int if err := rows.Scan(&invID, &invName, &dueDate, &residual, &overdueDays); err != nil { continue } overdueInvoiceIDs = append(overdueInvoiceIDs, invID) if overdueDays > maxOverdueDays { maxOverdueDays = overdueDays } } rows.Close() if len(overdueInvoiceIDs) == 0 { continue } // Find the appropriate follow-up level based on overdue days var companyID int64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(company_id, 1) FROM res_partner WHERE id = $1`, partnerID, ).Scan(&companyID) var levelID *int64 var levelName *string env.Tx().QueryRow(env.Ctx(), `SELECT id, name FROM account_followup_line WHERE company_id = $1 AND delay <= $2 ORDER BY delay DESC LIMIT 1`, companyID, maxOverdueDays, ).Scan(&levelID, &levelName) if levelID != nil { env.Tx().Exec(env.Ctx(), `UPDATE res_partner SET followup_level_id = $1, followup_next_action_date = CURRENT_DATE + INTERVAL '14 days' WHERE id = $2`, *levelID, partnerID) } } return true, nil }) // get_followup_html: generate HTML report of overdue invoices for a partner. // Mirrors: odoo/addons/account_followup/models/res_partner.py get_followup_html() ext.RegisterMethod("get_followup_html", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() partnerID := rs.IDs()[0] var partnerName string env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(name, 'Unknown') FROM res_partner WHERE id = $1`, partnerID, ).Scan(&partnerName) rows, err := env.Tx().Query(env.Ctx(), `SELECT m.name, m.invoice_date_due, m.amount_residual::float8, m.amount_total::float8, CURRENT_DATE - m.invoice_date_due as overdue_days FROM account_move m WHERE m.partner_id = $1 AND m.state = 'posted' AND m.move_type IN ('out_invoice', 'out_receipt') AND m.payment_state NOT IN ('paid', 'in_payment', 'reversed') ORDER BY m.invoice_date_due`, partnerID) if err != nil { return "", fmt.Errorf("account: query invoices for followup: %w", err) } defer rows.Close() var b strings.Builder b.WriteString(``) b.WriteString(fmt.Sprintf("
| Invoice | Due Date | Total | Amount Due | Overdue Days |
|---|---|---|---|---|
| %s | %v | %.2f | %.2f | %d |
| Total Due | %.2f | |||