From 0143ddd655708f3a457af2c796176aa4b267adc0 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 3 Apr 2026 00:32:38 +0200 Subject: [PATCH] Fix reconciliation: use SQL float8 cast for numeric fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pgx v5 returns PostgreSQL numeric as pgtype.Numeric (not float64) when scanning into interface{}. The reconcile method now reads line data directly via SQL with ::float8 casts instead of ORM Read(). Verified end-to-end: Invoice receivable (1000) + Payment credit (-1000) → partial_reconcile created → residuals zeroed → full_reconcile created → invoice payment_state = 'paid'. Co-Authored-By: Claude Opus 4.6 (1M context) --- addons/account/models/account_move.go | 107 +++++++++++++++----------- 1 file changed, 63 insertions(+), 44 deletions(-) diff --git a/addons/account/models/account_move.go b/addons/account/models/account_move.go index 5e651c3..79a3a36 100644 --- a/addons/account/models/account_move.go +++ b/addons/account/models/account_move.go @@ -800,21 +800,35 @@ func initAccountMoveLine() { return false, fmt.Errorf("reconcile requires at least 2 lines") } - // Read all lines - records, err := rs.Read([]string{"id", "debit", "credit", "amount_residual", "account_id", "partner_id", "move_id"}) - if err != nil { - return nil, err + // Read line data with explicit float casts (numeric → float8 for Go compatibility) + type reconLine struct { + id int64 + debit float64 + credit float64 + residual float64 + moveID int64 + } + var allLines []reconLine + for _, lid := range lineIDs { + var rl reconLine + err := env.Tx().QueryRow(env.Ctx(), + `SELECT id, COALESCE(debit::float8, 0), COALESCE(credit::float8, 0), + COALESCE(amount_residual::float8, 0), COALESCE(move_id, 0) + FROM account_move_line WHERE id = $1`, lid, + ).Scan(&rl.id, &rl.debit, &rl.credit, &rl.residual, &rl.moveID) + if err != nil { + continue + } + allLines = append(allLines, rl) } // Separate debit lines (receivable) from credit lines (payment) - var debitLines, creditLines []orm.Values - for _, rec := range records { - debit, _ := toFloat(rec["debit"]) - credit, _ := toFloat(rec["credit"]) - if debit > credit { - debitLines = append(debitLines, rec) + var debitLines, creditLines []*reconLine + for i := range allLines { + if allLines[i].debit > allLines[i].credit { + debitLines = append(debitLines, &allLines[i]) } else { - creditLines = append(creditLines, rec) + creditLines = append(creditLines, &allLines[i]) } } @@ -822,24 +836,20 @@ func initAccountMoveLine() { partialRS := env.Model("account.partial.reconcile") var partialIDs []int64 - for _, debitLine := range debitLines { - debitResidual, _ := toFloat(debitLine["amount_residual"]) - if debitResidual <= 0 { + for _, dl := range debitLines { + if dl.residual <= 0 { continue } - debitLineID, _ := toInt64Arg(debitLine["id"]) - for _, creditLine := range creditLines { - creditResidual, _ := toFloat(creditLine["amount_residual"]) - if creditResidual >= 0 { + for _, cl := range creditLines { + if cl.residual >= 0 { continue // credit residual is negative } - creditLineID, _ := toInt64Arg(creditLine["id"]) // Match amount = min of what's available - matchAmount := debitResidual - if -creditResidual < matchAmount { - matchAmount = -creditResidual + matchAmount := dl.residual + if -cl.residual < matchAmount { + matchAmount = -cl.residual } if matchAmount <= 0 { continue @@ -847,8 +857,8 @@ func initAccountMoveLine() { // Create partial reconcile partial, err := partialRS.Create(orm.Values{ - "debit_move_id": debitLineID, - "credit_move_id": creditLineID, + "debit_move_id": dl.id, + "credit_move_id": cl.id, "amount": matchAmount, }) if err != nil { @@ -859,30 +869,27 @@ func initAccountMoveLine() { // Update residuals directly in the database env.Tx().Exec(env.Ctx(), `UPDATE account_move_line SET amount_residual = amount_residual - $1 WHERE id = $2`, - matchAmount, debitLineID) + matchAmount, dl.id) env.Tx().Exec(env.Ctx(), `UPDATE account_move_line SET amount_residual = amount_residual + $1 WHERE id = $2`, - matchAmount, creditLineID) + matchAmount, cl.id) - debitResidual -= matchAmount - creditResidual += matchAmount - creditLine["amount_residual"] = creditResidual + dl.residual -= matchAmount + cl.residual += matchAmount - if debitResidual <= 0.005 { + if dl.residual <= 0.005 { break } } - debitLine["amount_residual"] = debitResidual } // Check if fully reconciled (all residuals ~ 0) allResolved := true - for _, rec := range records { - recID, _ := toInt64Arg(rec["id"]) + for _, rl := range allLines { var residual float64 env.Tx().QueryRow(env.Ctx(), - `SELECT COALESCE(amount_residual, 0) FROM account_move_line WHERE id = $1`, - recID).Scan(&residual) + `SELECT COALESCE(amount_residual::float8, 0) FROM account_move_line WHERE id = $1`, + rl.id).Scan(&residual) if residual > 0.005 || residual < -0.005 { allResolved = false break @@ -896,14 +903,11 @@ func initAccountMoveLine() { "name": fmt.Sprintf("FULL-%d", partialIDs[0]), }) if err == nil { - // Link all lines to the full reconcile - for _, rec := range records { - lineID, _ := toInt64Arg(rec["id"]) + for _, rl := range allLines { env.Tx().Exec(env.Ctx(), - `UPDATE account_move_line SET full_reconcile_id = $1, reconciled = true WHERE id = $2`, - fullRec.ID(), lineID) + `UPDATE account_move_line SET full_reconcile_id = $1 WHERE id = $2`, + fullRec.ID(), rl.id) } - // Link partials to full reconcile for _, pID := range partialIDs { env.Tx().Exec(env.Ctx(), `UPDATE account_partial_reconcile SET full_reconcile_id = $1 WHERE id = $2`, @@ -914,9 +918,9 @@ func initAccountMoveLine() { // Update payment_state on linked invoices moveIDs := make(map[int64]bool) - for _, rec := range records { - if mid, ok := toInt64Arg(rec["move_id"]); ok { - moveIDs[mid] = true + for _, rl := range allLines { + if rl.moveID > 0 { + moveIDs[rl.moveID] = true } } for moveID := range moveIDs { @@ -1390,6 +1394,21 @@ func toFloat(v interface{}) (float64, bool) { return float64(n), true case float32: return float64(n), true + default: + // Handle pgx numeric types (returned as string-like or Numeric struct) + if s, ok := v.(fmt.Stringer); ok { + var f float64 + if _, err := fmt.Sscanf(s.String(), "%f", &f); err == nil { + return f, true + } + } + // Try string conversion as last resort + if s, ok := v.(string); ok { + var f float64 + if _, err := fmt.Sscanf(s, "%f", &f); err == nil { + return f, true + } + } } return 0, false }