package models import ( "fmt" "time" "odoo-go/pkg/orm" ) // initPurchaseReport registers purchase.report — a transient model for purchase analysis. // Mirrors: odoo/addons/purchase/report/purchase_report.py // // class PurchaseReport(models.Model): // _name = 'purchase.report' // _description = 'Purchase Report' // _auto = False func initPurchaseReport() { m := orm.NewModel("purchase.report", orm.ModelOpts{ Description: "Purchase Analysis Report", Type: orm.ModelTransient, }) m.AddFields( orm.Date("date_from", orm.FieldOpts{String: "Start Date"}), orm.Date("date_to", orm.FieldOpts{String: "End Date"}), orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Vendor"}), orm.Many2one("product_id", "product.product", orm.FieldOpts{String: "Product"}), orm.Many2one("product_tmpl_id", "product.template", orm.FieldOpts{String: "Product Template"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}), orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Representative"}), orm.Many2one("categ_id", "product.category", orm.FieldOpts{String: "Product Category"}), orm.Selection("state", []orm.SelectionItem{ {Value: "draft", Label: "RFQ"}, {Value: "purchase", Label: "Purchase Order"}, {Value: "done", Label: "Done"}, {Value: "cancel", Label: "Cancelled"}, }, orm.FieldOpts{String: "Status"}), ) // get_purchase_data: Aggregated purchase data for dashboards and reports. // Mirrors: odoo/addons/purchase/report/purchase_report.py (SQL view query) // Returns: { months, top_vendors, top_products, summary } m.RegisterMethod("get_purchase_data", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() // ── Spending by month (last 12 months) ── monthRows, err := env.Tx().Query(env.Ctx(), ` SELECT date_trunc('month', po.date_order) AS month, COUNT(DISTINCT po.id) AS order_count, COALESCE(SUM(po.amount_total::float8), 0) AS spending, COALESCE(AVG(po.amount_total::float8), 0) AS avg_order FROM purchase_order po WHERE po.state IN ('purchase', 'done') GROUP BY month ORDER BY month DESC LIMIT 12`) if err != nil { return nil, fmt.Errorf("purchase_report: monthly spending query: %w", err) } defer monthRows.Close() var months []map[string]interface{} for monthRows.Next() { var month time.Time var cnt int64 var spending, avg float64 if err := monthRows.Scan(&month, &cnt, &spending, &avg); err != nil { continue } months = append(months, map[string]interface{}{ "month": month.Format("2006-01"), "orders": cnt, "spending": spending, "avg_order": avg, }) } // ── Top 10 vendors by spending ── vendorRows, err := env.Tx().Query(env.Ctx(), ` SELECT p.name, COUNT(DISTINCT po.id) AS orders, COALESCE(SUM(po.amount_total::float8), 0) AS spending, COALESCE(AVG(po.amount_total::float8), 0) AS avg_order FROM purchase_order po JOIN res_partner p ON p.id = po.partner_id WHERE po.state IN ('purchase', 'done') GROUP BY p.id, p.name ORDER BY spending DESC LIMIT 10`) if err != nil { return nil, fmt.Errorf("purchase_report: top vendors query: %w", err) } defer vendorRows.Close() var vendors []map[string]interface{} for vendorRows.Next() { var name string var cnt int64 var spending, avg float64 if err := vendorRows.Scan(&name, &cnt, &spending, &avg); err != nil { continue } vendors = append(vendors, map[string]interface{}{ "vendor": name, "orders": cnt, "spending": spending, "avg_order": avg, }) } // ── Top 10 products by purchase volume ── prodRows, err := env.Tx().Query(env.Ctx(), ` SELECT pt.name, SUM(pol.product_qty) AS qty, COALESCE(SUM(pol.price_subtotal::float8), 0) AS spending, COUNT(DISTINCT pol.order_id) AS order_count FROM purchase_order_line pol JOIN purchase_order po ON po.id = pol.order_id AND po.state IN ('purchase', 'done') JOIN product_product pp ON pp.id = pol.product_id JOIN product_template pt ON pt.id = pp.product_tmpl_id WHERE pol.product_id IS NOT NULL GROUP BY pt.name ORDER BY spending DESC LIMIT 10`) if err != nil { return nil, fmt.Errorf("purchase_report: top products query: %w", err) } defer prodRows.Close() var products []map[string]interface{} for prodRows.Next() { var name string var qty, spending float64 var orderCnt int64 if err := prodRows.Scan(&name, &qty, &spending, &orderCnt); err != nil { continue } products = append(products, map[string]interface{}{ "product": name, "qty": qty, "spending": spending, "orders": orderCnt, }) } // ── Summary totals ── var totalOrders int64 var totalSpending, avgOrderValue float64 env.Tx().QueryRow(env.Ctx(), ` SELECT COUNT(*), COALESCE(SUM(amount_total::float8), 0), COALESCE(AVG(amount_total::float8), 0) FROM purchase_order WHERE state IN ('purchase', 'done') `).Scan(&totalOrders, &totalSpending, &avgOrderValue) return map[string]interface{}{ "months": months, "top_vendors": vendors, "top_products": products, "summary": map[string]interface{}{ "total_orders": totalOrders, "total_spending": totalSpending, "avg_order_value": avgOrderValue, }, }, nil }) // get_purchases_by_category: Breakdown by product category. // Mirrors: odoo/addons/purchase/report/purchase_report.py (grouped by categ_id) m.RegisterMethod("get_purchases_by_category", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), ` SELECT COALESCE(pc.name, 'Uncategorized') AS category, COUNT(DISTINCT pol.order_id) AS orders, SUM(pol.product_qty) AS qty, COALESCE(SUM(pol.price_subtotal::float8), 0) AS spending FROM purchase_order_line pol JOIN purchase_order po ON po.id = pol.order_id AND po.state IN ('purchase', 'done') LEFT JOIN product_product pp ON pp.id = pol.product_id LEFT JOIN product_template pt ON pt.id = pp.product_tmpl_id LEFT JOIN product_category pc ON pc.id = pt.categ_id WHERE pol.product_id IS NOT NULL GROUP BY pc.name ORDER BY spending DESC`) if err != nil { return nil, fmt.Errorf("purchase_report: category query: %w", err) } defer rows.Close() var results []map[string]interface{} for rows.Next() { var name string var orders int64 var qty, spending float64 if err := rows.Scan(&name, &orders, &qty, &spending); err != nil { continue } results = append(results, map[string]interface{}{ "category": name, "orders": orders, "qty": qty, "spending": spending, }) } return results, nil }) // get_bill_status_analysis: Invoice/bill status analysis. // Mirrors: odoo/addons/purchase/report/purchase_report.py (invoice_status grouping) m.RegisterMethod("get_bill_status_analysis", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), ` SELECT COALESCE(invoice_status, 'no') AS status, COUNT(*) AS count, COALESCE(SUM(amount_total::float8), 0) AS spending FROM purchase_order WHERE state IN ('purchase', 'done') GROUP BY invoice_status ORDER BY spending DESC`) if err != nil { return nil, fmt.Errorf("purchase_report: bill status query: %w", err) } defer rows.Close() var results []map[string]interface{} for rows.Next() { var status string var count int64 var spending float64 if err := rows.Scan(&status, &count, &spending); err != nil { continue } results = append(results, map[string]interface{}{ "status": status, "count": count, "spending": spending, }) } return results, nil }) // get_receipt_analysis: Receipt/delivery status analysis. // Mirrors: odoo/addons/purchase/report/purchase_report.py (receipt_status grouping) m.RegisterMethod("get_receipt_analysis", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() rows, err := env.Tx().Query(env.Ctx(), ` SELECT CASE WHEN COALESCE(SUM(pol.qty_received), 0) = 0 THEN 'pending' WHEN SUM(pol.qty_received) < SUM(pol.product_qty) THEN 'partial' ELSE 'full' END AS status, COUNT(DISTINCT po.id) AS count, COALESCE(SUM(po.amount_total::float8), 0) AS spending FROM purchase_order po LEFT JOIN purchase_order_line pol ON pol.order_id = po.id WHERE po.state IN ('purchase', 'done') GROUP BY po.id`) if err != nil { return nil, fmt.Errorf("purchase_report: receipt analysis query: %w", err) } defer rows.Close() // Aggregate the per-PO results statusMap := map[string]map[string]interface{}{ "pending": {"status": "pending", "count": int64(0), "spending": float64(0)}, "partial": {"status": "partial", "count": int64(0), "spending": float64(0)}, "full": {"status": "full", "count": int64(0), "spending": float64(0)}, } for rows.Next() { var status string var count int64 var spending float64 if err := rows.Scan(&status, &count, &spending); err != nil { continue } if entry, ok := statusMap[status]; ok { entry["count"] = entry["count"].(int64) + count entry["spending"] = entry["spending"].(float64) + spending } } var results []map[string]interface{} for _, v := range statusMap { if v["count"].(int64) > 0 { results = append(results, v) } } return results, nil }) }