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")
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user