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:
@@ -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"})
|
||||
// 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 {
|
||||
return nil, err
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user