Fix reconciliation: use SQL float8 cast for numeric fields

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) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-03 00:32:38 +02:00
parent 118d8ee4f5
commit 0143ddd655

View File

@@ -800,21 +800,35 @@ func initAccountMoveLine() {
return false, fmt.Errorf("reconcile requires at least 2 lines") return false, fmt.Errorf("reconcile requires at least 2 lines")
} }
// Read all lines // Read line data with explicit float casts (numeric → float8 for Go compatibility)
records, err := rs.Read([]string{"id", "debit", "credit", "amount_residual", "account_id", "partner_id", "move_id"}) type reconLine struct {
if err != nil { id int64
return nil, err 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) // Separate debit lines (receivable) from credit lines (payment)
var debitLines, creditLines []orm.Values var debitLines, creditLines []*reconLine
for _, rec := range records { for i := range allLines {
debit, _ := toFloat(rec["debit"]) if allLines[i].debit > allLines[i].credit {
credit, _ := toFloat(rec["credit"]) debitLines = append(debitLines, &allLines[i])
if debit > credit {
debitLines = append(debitLines, rec)
} else { } else {
creditLines = append(creditLines, rec) creditLines = append(creditLines, &allLines[i])
} }
} }
@@ -822,24 +836,20 @@ func initAccountMoveLine() {
partialRS := env.Model("account.partial.reconcile") partialRS := env.Model("account.partial.reconcile")
var partialIDs []int64 var partialIDs []int64
for _, debitLine := range debitLines { for _, dl := range debitLines {
debitResidual, _ := toFloat(debitLine["amount_residual"]) if dl.residual <= 0 {
if debitResidual <= 0 {
continue continue
} }
debitLineID, _ := toInt64Arg(debitLine["id"])
for _, creditLine := range creditLines { for _, cl := range creditLines {
creditResidual, _ := toFloat(creditLine["amount_residual"]) if cl.residual >= 0 {
if creditResidual >= 0 {
continue // credit residual is negative continue // credit residual is negative
} }
creditLineID, _ := toInt64Arg(creditLine["id"])
// Match amount = min of what's available // Match amount = min of what's available
matchAmount := debitResidual matchAmount := dl.residual
if -creditResidual < matchAmount { if -cl.residual < matchAmount {
matchAmount = -creditResidual matchAmount = -cl.residual
} }
if matchAmount <= 0 { if matchAmount <= 0 {
continue continue
@@ -847,8 +857,8 @@ func initAccountMoveLine() {
// Create partial reconcile // Create partial reconcile
partial, err := partialRS.Create(orm.Values{ partial, err := partialRS.Create(orm.Values{
"debit_move_id": debitLineID, "debit_move_id": dl.id,
"credit_move_id": creditLineID, "credit_move_id": cl.id,
"amount": matchAmount, "amount": matchAmount,
}) })
if err != nil { if err != nil {
@@ -859,30 +869,27 @@ func initAccountMoveLine() {
// Update residuals directly in the database // Update residuals directly in the database
env.Tx().Exec(env.Ctx(), env.Tx().Exec(env.Ctx(),
`UPDATE account_move_line SET amount_residual = amount_residual - $1 WHERE id = $2`, `UPDATE account_move_line SET amount_residual = amount_residual - $1 WHERE id = $2`,
matchAmount, debitLineID) matchAmount, dl.id)
env.Tx().Exec(env.Ctx(), env.Tx().Exec(env.Ctx(),
`UPDATE account_move_line SET amount_residual = amount_residual + $1 WHERE id = $2`, `UPDATE account_move_line SET amount_residual = amount_residual + $1 WHERE id = $2`,
matchAmount, creditLineID) matchAmount, cl.id)
debitResidual -= matchAmount dl.residual -= matchAmount
creditResidual += matchAmount cl.residual += matchAmount
creditLine["amount_residual"] = creditResidual
if debitResidual <= 0.005 { if dl.residual <= 0.005 {
break break
} }
} }
debitLine["amount_residual"] = debitResidual
} }
// Check if fully reconciled (all residuals ~ 0) // Check if fully reconciled (all residuals ~ 0)
allResolved := true allResolved := true
for _, rec := range records { for _, rl := range allLines {
recID, _ := toInt64Arg(rec["id"])
var residual float64 var residual float64
env.Tx().QueryRow(env.Ctx(), env.Tx().QueryRow(env.Ctx(),
`SELECT COALESCE(amount_residual, 0) FROM account_move_line WHERE id = $1`, `SELECT COALESCE(amount_residual::float8, 0) FROM account_move_line WHERE id = $1`,
recID).Scan(&residual) rl.id).Scan(&residual)
if residual > 0.005 || residual < -0.005 { if residual > 0.005 || residual < -0.005 {
allResolved = false allResolved = false
break break
@@ -896,14 +903,11 @@ func initAccountMoveLine() {
"name": fmt.Sprintf("FULL-%d", partialIDs[0]), "name": fmt.Sprintf("FULL-%d", partialIDs[0]),
}) })
if err == nil { if err == nil {
// Link all lines to the full reconcile for _, rl := range allLines {
for _, rec := range records {
lineID, _ := toInt64Arg(rec["id"])
env.Tx().Exec(env.Ctx(), env.Tx().Exec(env.Ctx(),
`UPDATE account_move_line SET full_reconcile_id = $1, reconciled = true WHERE id = $2`, `UPDATE account_move_line SET full_reconcile_id = $1 WHERE id = $2`,
fullRec.ID(), lineID) fullRec.ID(), rl.id)
} }
// Link partials to full reconcile
for _, pID := range partialIDs { for _, pID := range partialIDs {
env.Tx().Exec(env.Ctx(), env.Tx().Exec(env.Ctx(),
`UPDATE account_partial_reconcile SET full_reconcile_id = $1 WHERE id = $2`, `UPDATE account_partial_reconcile SET full_reconcile_id = $1 WHERE id = $2`,
@@ -914,9 +918,9 @@ func initAccountMoveLine() {
// Update payment_state on linked invoices // Update payment_state on linked invoices
moveIDs := make(map[int64]bool) moveIDs := make(map[int64]bool)
for _, rec := range records { for _, rl := range allLines {
if mid, ok := toInt64Arg(rec["move_id"]); ok { if rl.moveID > 0 {
moveIDs[mid] = true moveIDs[rl.moveID] = true
} }
} }
for moveID := range moveIDs { for moveID := range moveIDs {
@@ -1390,6 +1394,21 @@ func toFloat(v interface{}) (float64, bool) {
return float64(n), true return float64(n), true
case float32: case float32:
return float64(n), true 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 return 0, false
} }