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")
}
// 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
}