package models

import (
	"fmt"
	"time"

	"odoo-go/pkg/orm"
)

// initSaleOrder registers sale.order — the sales quotation / order model.
// Mirrors: odoo/addons/sale/models/sale_order.py
func initSaleOrder() {
	m := orm.NewModel("sale.order", orm.ModelOpts{
		Description: "Sales Order",
		Order:       "date_order desc, id desc",
	})

	// -- Identity & State --
	m.AddFields(
		orm.Char("name", orm.FieldOpts{
			String: "Order Reference", Required: true, Index: true, Readonly: true, Default: "/",
		}),
		orm.Selection("state", []orm.SelectionItem{
			{Value: "draft", Label: "Quotation"},
			{Value: "sent", Label: "Quotation Sent"},
			{Value: "sale", Label: "Sales Order"},
			{Value: "cancel", Label: "Cancelled"},
		}, orm.FieldOpts{String: "Status", Default: "draft", Required: true, Readonly: true, Index: true}),
	)

	// -- Partners --
	m.AddFields(
		orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
			String: "Customer", Required: true, Index: true,
		}),
		orm.Many2one("partner_invoice_id", "res.partner", orm.FieldOpts{
			String: "Invoice Address",
		}),
		orm.Many2one("partner_shipping_id", "res.partner", orm.FieldOpts{
			String: "Delivery Address",
		}),
	)

	// -- Dates --
	m.AddFields(
		orm.Datetime("date_order", orm.FieldOpts{
			String: "Order Date", Required: true, Index: true, Default: "today",
		}),
		orm.Date("validity_date", orm.FieldOpts{String: "Expiration"}),
	)

	// -- Company & Currency --
	m.AddFields(
		orm.Many2one("company_id", "res.company", orm.FieldOpts{
			String: "Company", Required: true, Index: true,
		}),
		orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
			String: "Currency", Required: true,
		}),
	)

	// -- Order Lines --
	m.AddFields(
		orm.One2many("order_line", "sale.order.line", "order_id", orm.FieldOpts{
			String: "Order Lines",
		}),
	)

	// -- Amounts (Computed) --
	m.AddFields(
		orm.Monetary("amount_untaxed", orm.FieldOpts{
			String: "Untaxed Amount", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
		}),
		orm.Monetary("amount_tax", orm.FieldOpts{
			String: "Taxes", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
		}),
		orm.Monetary("amount_total", orm.FieldOpts{
			String: "Total", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
		}),
	)

	// -- Invoice Status --
	m.AddFields(
		orm.Selection("invoice_status", []orm.SelectionItem{
			{Value: "upselling", Label: "Upselling Opportunity"},
			{Value: "invoiced", Label: "Fully Invoiced"},
			{Value: "to invoice", Label: "To Invoice"},
			{Value: "no", Label: "Nothing to Invoice"},
		}, orm.FieldOpts{String: "Invoice Status", Compute: "_compute_invoice_status", Store: true}),
	)

	// -- Accounting & Terms --
	m.AddFields(
		orm.Many2one("fiscal_position_id", "account.fiscal.position", orm.FieldOpts{
			String: "Fiscal Position",
		}),
		orm.Many2one("payment_term_id", "account.payment.term", orm.FieldOpts{
			String: "Payment Terms",
		}),
		orm.Many2one("pricelist_id", "product.pricelist", orm.FieldOpts{
			String: "Pricelist",
		}),
		orm.Many2one("journal_id", "account.journal", orm.FieldOpts{
			String: "Invoicing Journal",
		}),
	)

	// -- Tags --
	m.AddFields(
		orm.Many2many("tag_ids", "crm.tag", orm.FieldOpts{String: "Tags"}),
	)

	// -- Counts (Computed placeholders) --
	m.AddFields(
		orm.Integer("invoice_count", orm.FieldOpts{
			String: "Invoice Count", Compute: "_compute_invoice_count", Store: false,
		}),
		orm.Integer("delivery_count", orm.FieldOpts{
			String: "Delivery Count", Compute: "_compute_delivery_count", Store: false,
		}),
	)

	// -- Misc --
	m.AddFields(
		orm.Text("note", orm.FieldOpts{String: "Terms and Conditions"}),
		orm.Boolean("require_signature", orm.FieldOpts{String: "Online Signature", Default: true}),
		orm.Boolean("require_payment", orm.FieldOpts{String: "Online Payment"}),
	)

	// -- Computed: _compute_amounts --
	// Computes untaxed, tax, and total amounts from order lines.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_amounts()
	computeSaleAmounts := func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		soID := rs.IDs()[0]

		var untaxed, tax, total float64
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT
				COALESCE(SUM(price_subtotal), 0),
				COALESCE(SUM(price_total - price_subtotal), 0),
				COALESCE(SUM(price_total), 0)
			 FROM sale_order_line WHERE order_id = $1
			 AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`,
			soID).Scan(&untaxed, &tax, &total)
		if err != nil {
			// Fallback: compute from raw line values if price_subtotal/price_total not yet stored
			err = env.Tx().QueryRow(env.Ctx(),
				`SELECT
					COALESCE(SUM(product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)), 0),
					COALESCE(SUM(
						product_uom_qty * price_unit * (1 - COALESCE(discount,0)/100)
						* COALESCE((SELECT t.amount / 100 FROM account_tax t
							JOIN account_tax_sale_order_line_rel rel ON rel.account_tax_id = t.id
							WHERE rel.sale_order_line_id = sol.id LIMIT 1), 0)
					), 0)
				 FROM sale_order_line sol WHERE sol.order_id = $1
				 AND (sol.display_type IS NULL OR sol.display_type = '' OR sol.display_type = 'product')`,
				soID).Scan(&untaxed, &tax)
			if err != nil {
				return nil, fmt.Errorf("sale: compute amounts for SO %d: %w", soID, err)
			}
			total = untaxed + tax
		}

		return orm.Values{
			"amount_untaxed": untaxed,
			"amount_tax":     tax,
			"amount_total":   total,
		}, nil
	}
	m.RegisterCompute("amount_untaxed", computeSaleAmounts)
	m.RegisterCompute("amount_tax", computeSaleAmounts)
	m.RegisterCompute("amount_total", computeSaleAmounts)

	// -- Computed: _compute_invoice_count --
	// Counts the number of invoices linked to this sale order.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_invoice_count()
	computeInvoiceCount := func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		soID := rs.IDs()[0]
		var count int
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT COUNT(*) FROM account_move WHERE invoice_origin = $1 AND move_type = 'out_invoice'`,
			fmt.Sprintf("SO%d", soID)).Scan(&count)
		if err != nil {
			count = 0
		}
		return orm.Values{"invoice_count": count}, nil
	}
	m.RegisterCompute("invoice_count", computeInvoiceCount)

	// -- Computed: _compute_delivery_count --
	// Counts the number of delivery pickings linked to this sale order.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._compute_delivery_count()
	computeDeliveryCount := func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		soID := rs.IDs()[0]
		var soName string
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(name, '') FROM sale_order WHERE id = $1`, soID).Scan(&soName)
		if err != nil {
			return orm.Values{"delivery_count": 0}, nil
		}
		var count int
		err = env.Tx().QueryRow(env.Ctx(),
			`SELECT COUNT(*) FROM stock_picking WHERE origin = $1`, soName).Scan(&count)
		if err != nil {
			count = 0
		}
		return orm.Values{"delivery_count": count}, nil
	}
	m.RegisterCompute("delivery_count", computeDeliveryCount)

	// -- DefaultGet: Provide dynamic defaults for new records --
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.default_get()
	// Supplies company_id, currency_id, date_order when creating a new quotation.
	m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values {
		vals := make(orm.Values)

		// Default company from the current user's session
		companyID := env.CompanyID()
		if companyID > 0 {
			vals["company_id"] = companyID
		}

		// Default currency from the company
		var currencyID int64
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT currency_id FROM res_company WHERE id = $1`, companyID).Scan(&currencyID)
		if err == nil && currencyID > 0 {
			vals["currency_id"] = currencyID
		}

		// Default date_order = now
		vals["date_order"] = time.Now().Format("2006-01-02 15:04:05")

		return vals
	}

	// -- Sequence Hook --
	m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
		name, _ := vals["name"].(string)
		if name == "" || name == "/" {
			seq, err := orm.NextByCode(env, "sale.order")
			if err != nil {
				// Fallback: generate a simple name like purchase.order does
				vals["name"] = fmt.Sprintf("SO/%d", time.Now().UnixNano()%100000)
			} else {
				vals["name"] = seq
			}
		}
		return nil
	}

	// -- Business Methods --

	// action_confirm: draft → sale
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_confirm()
	m.RegisterMethod("action_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, id := range rs.IDs() {
			var state string
			err := env.Tx().QueryRow(env.Ctx(),
				`SELECT state FROM sale_order WHERE id = $1`, id).Scan(&state)
			if err != nil {
				return nil, err
			}
			if state != "draft" && state != "sent" {
				return nil, fmt.Errorf("sale: can only confirm draft/sent orders (current: %s)", state)
			}
			if _, err := env.Tx().Exec(env.Ctx(),
				`UPDATE sale_order SET state = 'sale' WHERE id = $1`, id); err != nil {
				return nil, err
			}
		}
		return true, nil
	})

	// create_invoices: Generate invoice from confirmed sale order
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._create_invoices()
	m.RegisterMethod("create_invoices", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		var invoiceIDs []int64

		for _, soID := range rs.IDs() {
			// Read SO header
			var partnerID, companyID, currencyID int64
			var journalID int64
			var soName string
			err := env.Tx().QueryRow(env.Ctx(),
				`SELECT partner_id, company_id, currency_id, COALESCE(journal_id, 0), COALESCE(name, '')
				 FROM sale_order WHERE id = $1`, soID,
			).Scan(&partnerID, &companyID, &currencyID, &journalID, &soName)
			if err != nil {
				return nil, fmt.Errorf("sale: read SO %d: %w", soID, err)
			}

			// Find sales journal if not set on SO
			if journalID == 0 {
				env.Tx().QueryRow(env.Ctx(),
					`SELECT id FROM account_journal
					 WHERE type = 'sale' AND active = true AND company_id = $1
					 ORDER BY sequence, id LIMIT 1`, companyID,
				).Scan(&journalID)
			}
			if journalID == 0 {
				journalID = 1 // ultimate fallback
			}

			// Read SO lines
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT id, COALESCE(name,''), COALESCE(product_uom_qty,1),
				        COALESCE(price_unit,0), COALESCE(discount,0), COALESCE(product_id, 0)
				 FROM sale_order_line
				 WHERE order_id = $1
				   AND (display_type IS NULL OR display_type = '' OR display_type = 'product')
				 ORDER BY sequence, id`, soID)
			if err != nil {
				return nil, err
			}

			type soLine struct {
				id        int64
				name      string
				qty       float64
				price     float64
				discount  float64
				productID int64
			}
			var lines []soLine
			for rows.Next() {
				var l soLine
				if err := rows.Scan(&l.id, &l.name, &l.qty, &l.price, &l.discount, &l.productID); err != nil {
					rows.Close()
					return nil, err
				}
				lines = append(lines, l)
			}
			rows.Close()

			if len(lines) == 0 {
				continue
			}

			// Create invoice header (draft)
			invoiceRS := env.Model("account.move")
			inv, err := invoiceRS.Create(orm.Values{
				"move_type":      "out_invoice",
				"partner_id":     partnerID,
				"company_id":     companyID,
				"currency_id":    currencyID,
				"journal_id":     journalID,
				"invoice_origin": soName,
				"date":           time.Now().Format("2006-01-02"),
			})
			if err != nil {
				return nil, fmt.Errorf("sale: create invoice header for SO %d: %w", soID, err)
			}
			moveID := inv.ID()

			// Find the revenue account (8300 Erlöse 19% USt or fallback)
			var revenueAccountID int64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT id FROM account_account WHERE code = '8300' AND company_id = $1 LIMIT 1`,
				companyID,
			).Scan(&revenueAccountID)
			if revenueAccountID == 0 {
				// Fallback: any income account
				env.Tx().QueryRow(env.Ctx(),
					`SELECT id FROM account_account
					 WHERE account_type LIKE 'income%' AND company_id = $1
					 ORDER BY code LIMIT 1`, companyID,
				).Scan(&revenueAccountID)
			}
			if revenueAccountID == 0 {
				// Fallback: journal default account
				env.Tx().QueryRow(env.Ctx(),
					`SELECT default_account_id FROM account_journal WHERE id = $1`, journalID,
				).Scan(&revenueAccountID)
			}

			// Find the receivable account
			var receivableAccountID int64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT id FROM account_account
				 WHERE account_type = 'asset_receivable' AND company_id = $1
				 ORDER BY code LIMIT 1`, companyID,
			).Scan(&receivableAccountID)
			if receivableAccountID == 0 {
				return nil, fmt.Errorf("sale: no receivable account found for company %d", companyID)
			}

			lineRS := env.Model("account.move.line")
			var totalCredit float64 // accumulates all product + tax credits

			// Create product lines and tax lines for each SO line
			for _, line := range lines {
				baseAmount := line.qty * line.price * (1 - line.discount/100)

				// Determine revenue account: try product-specific, then default
				lineAccountID := revenueAccountID
				if line.productID > 0 {
					var prodAccID int64
					env.Tx().QueryRow(env.Ctx(),
						`SELECT COALESCE(pc.property_account_income_categ_id, 0)
						 FROM product_product pp
						 JOIN product_template pt ON pt.id = pp.product_tmpl_id
						 JOIN product_category pc ON pc.id = pt.categ_id
						 WHERE pp.id = $1`, line.productID,
					).Scan(&prodAccID)
					if prodAccID > 0 {
						lineAccountID = prodAccID
					}
				}

				// Product line (credit side for revenue on out_invoice)
				productLineVals := orm.Values{
					"move_id":      moveID,
					"name":         line.name,
					"quantity":     line.qty,
					"price_unit":   line.price,
					"discount":     line.discount,
					"account_id":   lineAccountID,
					"company_id":   companyID,
					"journal_id":   journalID,
					"currency_id":  currencyID,
					"partner_id":   partnerID,
					"display_type": "product",
					"debit":        0.0,
					"credit":       baseAmount,
					"balance":      -baseAmount,
				}
				if _, err := lineRS.Create(productLineVals); err != nil {
					return nil, fmt.Errorf("sale: create invoice product line: %w", err)
				}
				totalCredit += baseAmount

				// Look up taxes from SO line's tax_id M2M and compute tax lines
				taxRows, err := env.Tx().Query(env.Ctx(),
					`SELECT t.id, t.name, t.amount, t.amount_type, COALESCE(t.price_include, false)
					 FROM account_tax t
					 JOIN account_tax_sale_order_line_rel rel ON rel.account_tax_id = t.id
					 WHERE rel.sale_order_line_id = $1`, line.id)
				if err == nil {
					for taxRows.Next() {
						var taxID int64
						var taxName string
						var taxRate float64
						var amountType string
						var priceInclude bool
						if err := taxRows.Scan(&taxID, &taxName, &taxRate, &amountType, &priceInclude); err != nil {
							taxRows.Close()
							break
						}

						// Compute tax amount (mirrors account_tax_calc.go ComputeTax)
						var taxAmount float64
						switch amountType {
						case "percent":
							if priceInclude {
								taxAmount = baseAmount - (baseAmount / (1 + taxRate/100))
							} else {
								taxAmount = baseAmount * taxRate / 100
							}
						case "fixed":
							taxAmount = taxRate
						case "division":
							if priceInclude {
								taxAmount = baseAmount - (baseAmount / (1 + taxRate/100))
							} else {
								taxAmount = baseAmount * taxRate / 100
							}
						}

						if taxAmount == 0 {
							continue
						}

						// Find tax account from repartition lines
						var taxAccountID int64
						env.Tx().QueryRow(env.Ctx(),
							`SELECT COALESCE(account_id, 0) FROM account_tax_repartition_line
							 WHERE tax_id = $1 AND repartition_type = 'tax' AND document_type = 'invoice'
							 LIMIT 1`, taxID,
						).Scan(&taxAccountID)
						if taxAccountID == 0 {
							// Fallback: USt account 1776 (SKR03)
							env.Tx().QueryRow(env.Ctx(),
								`SELECT id FROM account_account WHERE code = '1776' LIMIT 1`,
							).Scan(&taxAccountID)
						}
						if taxAccountID == 0 {
							taxAccountID = lineAccountID // ultimate fallback
						}

						taxLineVals := orm.Values{
							"move_id":      moveID,
							"name":         taxName,
							"quantity":     1.0,
							"account_id":   taxAccountID,
							"company_id":   companyID,
							"journal_id":   journalID,
							"currency_id":  currencyID,
							"partner_id":   partnerID,
							"display_type": "tax",
							"tax_line_id":  taxID,
							"debit":        0.0,
							"credit":       taxAmount,
							"balance":      -taxAmount,
						}
						if _, err := lineRS.Create(taxLineVals); err != nil {
							taxRows.Close()
							return nil, fmt.Errorf("sale: create invoice tax line: %w", err)
						}
						totalCredit += taxAmount
					}
					taxRows.Close()
				}
			}

			// Create the receivable line (debit = total of all credits)
			receivableVals := orm.Values{
				"move_id":                  moveID,
				"name":                     "/",
				"quantity":                 1.0,
				"account_id":              receivableAccountID,
				"company_id":              companyID,
				"journal_id":              journalID,
				"currency_id":             currencyID,
				"partner_id":              partnerID,
				"display_type":            "payment_term",
				"debit":                   totalCredit,
				"credit":                  0.0,
				"balance":                 totalCredit,
				"amount_residual":         totalCredit,
				"amount_residual_currency": totalCredit,
			}
			if _, err := lineRS.Create(receivableVals); err != nil {
				return nil, fmt.Errorf("sale: create invoice receivable line: %w", err)
			}

			invoiceIDs = append(invoiceIDs, moveID)

			// Update qty_invoiced on SO lines
			for _, line := range lines {
				env.Tx().Exec(env.Ctx(),
					`UPDATE sale_order_line SET qty_invoiced = COALESCE(qty_invoiced, 0) + $1 WHERE id = $2`,
					line.qty, line.id)
			}

			// Update SO invoice_status
			env.Tx().Exec(env.Ctx(),
				`UPDATE sale_order SET invoice_status = 'invoiced' WHERE id = $1`, soID)
		}

		if len(invoiceIDs) == 0 {
			return false, nil
		}
		// Return action to open the created invoice(s)
		// Mirrors: odoo/addons/sale/models/sale_order.py action_view_invoice()
		if len(invoiceIDs) == 1 {
			return map[string]interface{}{
				"type":      "ir.actions.act_window",
				"res_model": "account.move",
				"res_id":    invoiceIDs[0],
				"view_mode": "form",
				"views":     [][]interface{}{{nil, "form"}},
				"target":    "current",
			}, nil
		}
		// Multiple invoices → list view
		ids := make([]interface{}, len(invoiceIDs))
		for i, id := range invoiceIDs {
			ids[i] = id
		}
		return map[string]interface{}{
			"type":      "ir.actions.act_window",
			"res_model": "account.move",
			"view_mode": "list,form",
			"views":     [][]interface{}{{nil, "list"}, {nil, "form"}},
			"domain":    []interface{}{[]interface{}{"id", "in", ids}},
			"target":    "current",
		}, nil
	})

	// action_cancel: Cancel a sale order and linked draft invoices.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_cancel()
	m.RegisterMethod("action_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, soID := range rs.IDs() {
			// Cancel linked draft invoices
			rows, _ := env.Tx().Query(env.Ctx(),
				`SELECT id FROM account_move WHERE invoice_origin = (SELECT name FROM sale_order WHERE id = $1) AND state = 'draft'`, soID)
			for rows.Next() {
				var invID int64
				rows.Scan(&invID)
				env.Tx().Exec(env.Ctx(), `UPDATE account_move SET state = 'cancel' WHERE id = $1`, invID)
			}
			rows.Close()
			env.Tx().Exec(env.Ctx(), `UPDATE sale_order SET state = 'cancel' WHERE id = $1`, soID)
		}
		return true, nil
	})

	// action_draft: Reset a cancelled sale order back to quotation.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_draft()
	m.RegisterMethod("action_draft", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		for _, soID := range rs.IDs() {
			env.Tx().Exec(env.Ctx(), `UPDATE sale_order SET state = 'draft' WHERE id = $1 AND state = 'cancel'`, soID)
		}
		return true, nil
	})

	// action_view_invoice: Open invoices linked to this sale order.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_view_invoice()
	m.RegisterMethod("action_view_invoice", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		soID := rs.IDs()[0]
		var soName string
		env.Tx().QueryRow(env.Ctx(), `SELECT name FROM sale_order WHERE id = $1`, soID).Scan(&soName)

		// Find invoices linked to this SO
		rows, _ := env.Tx().Query(env.Ctx(),
			`SELECT id FROM account_move WHERE invoice_origin = $1`, soName)
		var invIDs []interface{}
		for rows.Next() {
			var id int64
			rows.Scan(&id)
			invIDs = append(invIDs, id)
		}
		rows.Close()

		if len(invIDs) == 1 {
			return map[string]interface{}{
				"type": "ir.actions.act_window", "res_model": "account.move",
				"res_id": invIDs[0], "view_mode": "form",
				"views": [][]interface{}{{nil, "form"}}, "target": "current",
			}, nil
		}
		return map[string]interface{}{
			"type": "ir.actions.act_window", "res_model": "account.move",
			"view_mode": "list,form", "views": [][]interface{}{{nil, "list"}, {nil, "form"}},
			"domain": []interface{}{[]interface{}{"id", "in", invIDs}}, "target": "current",
		}, nil
	})

	// action_create_delivery: Generate a stock picking (delivery) from a confirmed sale order.
	// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._action_confirm() → _create_picking()
	m.RegisterMethod("action_create_delivery", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		var pickingIDs []int64

		for _, soID := range rs.IDs() {
			// Read SO header for partner and company
			var partnerID, companyID int64
			var soName string
			err := env.Tx().QueryRow(env.Ctx(),
				`SELECT COALESCE(partner_shipping_id, partner_id), company_id, name
				 FROM sale_order WHERE id = $1`, soID,
			).Scan(&partnerID, &companyID, &soName)
			if err != nil {
				return nil, fmt.Errorf("sale: read SO %d for delivery: %w", soID, err)
			}

			// Read SO lines with products
			rows, err := env.Tx().Query(env.Ctx(),
				`SELECT product_id, product_uom_qty, COALESCE(name, '') FROM sale_order_line
				 WHERE order_id = $1 AND product_id IS NOT NULL
				 AND (display_type IS NULL OR display_type = '' OR display_type = 'product')`, soID)
			if err != nil {
				return nil, fmt.Errorf("sale: read SO lines %d for delivery: %w", soID, err)
			}

			type soline struct {
				productID int64
				qty       float64
				name      string
			}
			var lines []soline
			for rows.Next() {
				var l soline
				if err := rows.Scan(&l.productID, &l.qty, &l.name); err != nil {
					rows.Close()
					return nil, err
				}
				lines = append(lines, l)
			}
			rows.Close()
			if len(lines) == 0 {
				continue
			}

			// Find outgoing picking type and locations
			var pickingTypeID, srcLocID, destLocID int64
			env.Tx().QueryRow(env.Ctx(),
				`SELECT pt.id, COALESCE(pt.default_location_src_id, 0), COALESCE(pt.default_location_dest_id, 0)
				 FROM stock_picking_type pt
				 WHERE pt.code = 'outgoing' AND pt.company_id = $1
				 LIMIT 1`, companyID,
			).Scan(&pickingTypeID, &srcLocID, &destLocID)

			// Fallback: find internal and customer locations
			if srcLocID == 0 {
				env.Tx().QueryRow(env.Ctx(),
					`SELECT id FROM stock_location WHERE usage = 'internal' AND COALESCE(company_id, $1) = $1 LIMIT 1`,
					companyID).Scan(&srcLocID)
			}
			if destLocID == 0 {
				env.Tx().QueryRow(env.Ctx(),
					`SELECT id FROM stock_location WHERE usage = 'customer' LIMIT 1`).Scan(&destLocID)
			}
			if pickingTypeID == 0 {
				env.Tx().QueryRow(env.Ctx(),
					`SELECT id FROM stock_picking_type WHERE code = 'outgoing' LIMIT 1`).Scan(&pickingTypeID)
			}

			// Create picking
			var pickingID int64
			err = env.Tx().QueryRow(env.Ctx(),
				`INSERT INTO stock_picking
				 (name, state, scheduled_date, company_id, partner_id, picking_type_id,
				  location_id, location_dest_id, origin)
				 VALUES ($1, 'confirmed', NOW(), $2, $3, $4, $5, $6, $7) RETURNING id`,
				fmt.Sprintf("WH/OUT/%05d", soID), companyID, partnerID, pickingTypeID,
				srcLocID, destLocID, soName,
			).Scan(&pickingID)
			if err != nil {
				return nil, fmt.Errorf("sale: create picking for SO %d: %w", soID, err)
			}

			// Create stock moves
			for _, l := range lines {
				_, err = env.Tx().Exec(env.Ctx(),
					`INSERT INTO stock_move
					 (name, product_id, product_uom_qty, state, picking_id, company_id,
					  location_id, location_dest_id, date, origin, product_uom)
					 VALUES ($1, $2, $3, 'confirmed', $4, $5, $6, $7, NOW(), $8, 1)`,
					l.name, l.productID, l.qty, pickingID, companyID,
					srcLocID, destLocID, soName)
				if err != nil {
					return nil, fmt.Errorf("sale: create stock move for SO %d: %w", soID, err)
				}
			}

			pickingIDs = append(pickingIDs, pickingID)
		}
		return pickingIDs, nil
	})

	// action_create_down_payment: Create a deposit invoice for a percentage of the SO total.
	// Mirrors: odoo/addons/sale/wizard/sale_make_invoice_advance.py
	m.RegisterMethod("action_create_down_payment", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
		env := rs.Env()
		soID := rs.IDs()[0]
		percentage := float64(10) // Default 10%
		if len(args) > 0 {
			if p, ok := args[0].(float64); ok {
				percentage = p
			}
		}

		var total float64
		var partnerID, companyID, currencyID, journalID int64
		var soName string
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(amount_total, 0), partner_id, company_id, currency_id,
			        COALESCE(journal_id, 0), COALESCE(name, '')
			 FROM sale_order WHERE id = $1`, soID,
		).Scan(&total, &partnerID, &companyID, &currencyID, &journalID, &soName)
		if err != nil {
			return nil, fmt.Errorf("sale: read SO %d for down payment: %w", soID, err)
		}

		downAmount := total * percentage / 100

		// Find sales journal if not set on SO
		if journalID == 0 {
			env.Tx().QueryRow(env.Ctx(),
				`SELECT id FROM account_journal
				 WHERE type = 'sale' AND active = true AND company_id = $1
				 ORDER BY sequence, id LIMIT 1`, companyID,
			).Scan(&journalID)
		}
		if journalID == 0 {
			journalID = 1
		}

		// Create deposit invoice
		invoiceRS := env.Model("account.move")
		inv, err := invoiceRS.Create(orm.Values{
			"move_type":      "out_invoice",
			"partner_id":     partnerID,
			"company_id":     companyID,
			"currency_id":    currencyID,
			"journal_id":     journalID,
			"invoice_origin": soName,
			"date":           time.Now().Format("2006-01-02"),
		})
		if err != nil {
			return nil, fmt.Errorf("sale: create down payment invoice for SO %d: %w", soID, err)
		}
		moveID := inv.ID()

		// Find revenue account
		var revenueAccountID int64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT id FROM account_account WHERE code = '8300' AND company_id = $1 LIMIT 1`,
			companyID).Scan(&revenueAccountID)
		if revenueAccountID == 0 {
			env.Tx().QueryRow(env.Ctx(),
				`SELECT id FROM account_account
				 WHERE account_type LIKE 'income%%' AND company_id = $1
				 ORDER BY code LIMIT 1`, companyID).Scan(&revenueAccountID)
		}

		// Find receivable account
		var receivableAccountID int64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT id FROM account_account
			 WHERE account_type = 'asset_receivable' AND company_id = $1
			 ORDER BY code LIMIT 1`, companyID).Scan(&receivableAccountID)
		if receivableAccountID == 0 {
			receivableAccountID = revenueAccountID
		}

		lineRS := env.Model("account.move.line")

		// Down payment product line (credit)
		_, err = lineRS.Create(orm.Values{
			"move_id":      moveID,
			"name":         fmt.Sprintf("Down payment of %.0f%%", percentage),
			"quantity":     1.0,
			"price_unit":   downAmount,
			"account_id":   revenueAccountID,
			"company_id":   companyID,
			"journal_id":   journalID,
			"currency_id":  currencyID,
			"partner_id":   partnerID,
			"display_type": "product",
			"debit":        0.0,
			"credit":       downAmount,
			"balance":      -downAmount,
		})
		if err != nil {
			return nil, fmt.Errorf("sale: create down payment line: %w", err)
		}

		// Receivable line (debit)
		_, err = lineRS.Create(orm.Values{
			"move_id":                  moveID,
			"name":                     "/",
			"quantity":                 1.0,
			"account_id":              receivableAccountID,
			"company_id":              companyID,
			"journal_id":              journalID,
			"currency_id":             currencyID,
			"partner_id":              partnerID,
			"display_type":            "payment_term",
			"debit":                   downAmount,
			"credit":                  0.0,
			"balance":                 downAmount,
			"amount_residual":         downAmount,
			"amount_residual_currency": downAmount,
		})
		if err != nil {
			return nil, fmt.Errorf("sale: create down payment receivable line: %w", err)
		}

		return map[string]interface{}{
			"type":      "ir.actions.act_window",
			"res_model": "account.move",
			"res_id":    moveID,
			"view_mode": "form",
			"target":    "current",
		}, nil
	})
}

// initSaleOrderLine registers sale.order.line — individual line items on a sales order.
// Mirrors: odoo/addons/sale/models/sale_order_line.py
func initSaleOrderLine() {
	m := orm.NewModel("sale.order.line", orm.ModelOpts{
		Description: "Sales Order Line",
		Order:       "order_id, sequence, id",
	})

	// -- Onchange: product_id → name + price_unit --
	// Mirrors: odoo/addons/sale/models/sale_order_line.py _compute_name(), _compute_price_unit()
	// When the user selects a product on a SO line, automatically fill in the
	// description from the product name and the unit price from the product list price.
	m.RegisterOnchange("product_id", func(env *orm.Environment, vals orm.Values) orm.Values {
		result := make(orm.Values)

		// Extract product_id — may arrive as int64, int32, float64, or map with "id" key
		var productID int64
		switch v := vals["product_id"].(type) {
		case int64:
			productID = v
		case int32:
			productID = int64(v)
		case float64:
			productID = int64(v)
		case map[string]interface{}:
			if id, ok := v["id"]; ok {
				switch n := id.(type) {
				case float64:
					productID = int64(n)
				case int64:
					productID = n
				}
			}
		}
		if productID <= 0 {
			return result
		}

		// Read product name and list price
		var name string
		var listPrice float64
		err := env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(pt.name, ''), COALESCE(pt.list_price, 0)
			 FROM product_product pp
			 JOIN product_template pt ON pt.id = pp.product_tmpl_id
			 WHERE pp.id = $1`, productID,
		).Scan(&name, &listPrice)
		if err != nil {
			return result
		}

		result["name"] = name
		result["price_unit"] = listPrice
		return result
	})

	// -- Parent --
	m.AddFields(
		orm.Many2one("order_id", "sale.order", orm.FieldOpts{
			String: "Order Reference", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
		}),
	)

	// -- Product --
	m.AddFields(
		orm.Many2one("product_id", "product.product", orm.FieldOpts{
			String: "Product",
		}),
		orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
		orm.Float("product_uom_qty", orm.FieldOpts{String: "Quantity", Required: true, Default: 1.0}),
		orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}),
	)

	// -- Pricing --
	m.AddFields(
		orm.Float("price_unit", orm.FieldOpts{String: "Unit Price", Required: true}),
		orm.Many2many("tax_id", "account.tax", orm.FieldOpts{String: "Taxes"}),
		orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}),
		orm.Monetary("price_subtotal", orm.FieldOpts{
			String: "Subtotal", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
		}),
		orm.Monetary("price_total", orm.FieldOpts{
			String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
		}),
	)

	// -- Display --
	m.AddFields(
		orm.Selection("display_type", []orm.SelectionItem{
			{Value: "line_section", Label: "Section"},
			{Value: "line_note", Label: "Note"},
		}, orm.FieldOpts{String: "Display Type"}),
		orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
	)

	// -- Computed: _compute_amount (line subtotal/total) --
	// Computes price_subtotal and price_total from qty, price, discount, and taxes.
	// Mirrors: odoo/addons/sale/models/sale_order_line.py SaleOrderLine._compute_amount()
	computeLineAmount := func(rs *orm.Recordset) (orm.Values, error) {
		env := rs.Env()
		lineID := rs.IDs()[0]
		var qty, price, discount float64
		env.Tx().QueryRow(env.Ctx(),
			`SELECT COALESCE(product_uom_qty, 0), COALESCE(price_unit, 0), COALESCE(discount, 0)
			 FROM sale_order_line WHERE id = $1`, lineID,
		).Scan(&qty, &price, &discount)
		subtotal := qty * price * (1 - discount/100)

		// Compute tax amount from linked taxes via M2M (inline, no cross-package call)
		var taxTotal float64
		taxRows, err := env.Tx().Query(env.Ctx(),
			`SELECT t.amount, t.amount_type, COALESCE(t.price_include, false)
			 FROM account_tax t
			 JOIN account_tax_sale_order_line_rel rel ON rel.account_tax_id = t.id
			 WHERE rel.sale_order_line_id = $1`, lineID)
		if err == nil {
			for taxRows.Next() {
				var taxRate float64
				var amountType string
				var priceInclude bool
				if err := taxRows.Scan(&taxRate, &amountType, &priceInclude); err != nil {
					break
				}
				switch amountType {
				case "percent":
					if priceInclude {
						taxTotal += subtotal - (subtotal / (1 + taxRate/100))
					} else {
						taxTotal += subtotal * taxRate / 100
					}
				case "fixed":
					taxTotal += taxRate
				case "division":
					if priceInclude {
						taxTotal += subtotal - (subtotal / (1 + taxRate/100))
					} else {
						taxTotal += subtotal * taxRate / 100
					}
				}
			}
			taxRows.Close()
		}

		return orm.Values{
			"price_subtotal": subtotal,
			"price_total":    subtotal + taxTotal,
		}, nil
	}
	m.RegisterCompute("price_subtotal", computeLineAmount)
	m.RegisterCompute("price_total", computeLineAmount)

	// -- Delivery & Invoicing Quantities --
	m.AddFields(
		orm.Float("qty_delivered", orm.FieldOpts{String: "Delivered Quantity"}),
		orm.Float("qty_invoiced", orm.FieldOpts{
			String: "Invoiced Quantity", Compute: "_compute_qty_invoiced", Store: true,
		}),
		orm.Float("qty_to_invoice", orm.FieldOpts{
			String: "To Invoice Quantity", Compute: "_compute_qty_to_invoice", Store: true,
		}),
		orm.Many2many("invoice_lines", "account.move.line", orm.FieldOpts{
			String: "Invoice Lines",
		}),
	)
}
