fix(billing): ledger live-run fixes — UUID cast, UTF-8, reconciling line

Surfaced by the nexamain dry-run against real data:
- reader: cast invoice_items.invoice_id::text (uuid = text[] mismatch).
- readers: set_client_encoding('UTF8') — invoice descriptions contain "×".
- ingest: add a balancing line when invoice.subtotal != sum(items). 9 paid
  base-plan invoices store the charge in subtotal with NO invoice_items, so
  itemized ingestion under-recorded revenue by ~$1,143 (37%); the reconciling
  line makes the Odoo invoice total match what Stripe billed.
74 tests green on odoo-trial.
This commit is contained in:
gsinghpal
2026-05-27 16:57:00 -04:00
parent 72d3130c88
commit 6b63df8c3d
3 changed files with 33 additions and 1 deletions

View File

@@ -116,6 +116,7 @@ class FusionBillingImportWizard(models.TransientModel):
raise UserError("Could not connect to the NexaCloud database: %s" % e)
try:
conn.set_session(readonly=True)
conn.set_client_encoding('UTF8')
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
data = {}
cur.execute(
@@ -161,6 +162,7 @@ class FusionBillingImportWizard(models.TransientModel):
raise UserError("Could not connect to the NexaCloud database: %s" % e)
try:
conn.set_session(readonly=True)
conn.set_client_encoding('UTF8')
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"SELECT subscription_id::text AS sub, "

View File

@@ -73,6 +73,7 @@ class FusionBillingInvoiceLedgerWizard(models.TransientModel):
raise UserError("Could not connect to the NexaCloud database: %s" % e)
try:
conn.set_session(readonly=True)
conn.set_client_encoding('UTF8') # invoice descriptions contain non-ASCII (e.g. "×")
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
where = "WHERE i.created_at >= %(since)s" if since else ""
cur.execute(
@@ -87,7 +88,7 @@ class FusionBillingInvoiceLedgerWizard(models.TransientModel):
if invoices:
cur.execute(
"SELECT ii.invoice_id, ii.description, ii.quantity, ii.unit_price, ii.amount "
"FROM invoice_items ii WHERE ii.invoice_id = ANY(%(ids)s)",
"FROM invoice_items ii WHERE ii.invoice_id::text = ANY(%(ids)s)",
{"ids": list(invoices.keys())})
for r in cur.fetchall():
inv = invoices.get(str(r["invoice_id"]))
@@ -150,6 +151,22 @@ class FusionBillingInvoiceLedgerWizard(models.TransientModel):
"account_id": self._fc_income_account(fam).id,
"tax_ids": [(6, 0, tax.ids)] if tax else [(5, 0, 0)],
}))
# Many NexaCloud base-plan invoices store the charge in `subtotal` with
# NO invoice_items. Add a balancing line for any gap so the Odoo invoice
# total matches what Stripe actually billed (captures un-itemized revenue
# and absorbs proration credits where items exceed subtotal).
items_total = round(sum(float(it.get("amount") or 0.0)
for it in inv.get("items", [])), 2)
gap = round(float(inv.get("subtotal") or 0.0) - items_total, 2)
if abs(gap) > 0.01:
summary["by_family"]["base"] = round(
summary["by_family"].get("base", 0.0) + gap, 2)
line_vals.append((0, 0, {
"name": "NexaCloud base/unitemized charge",
"quantity": 1.0, "price_unit": gap,
"account_id": self._fc_income_account("base").id,
"tax_ids": [(6, 0, tax.ids)] if tax else [(5, 0, 0)],
}))
move.write({"invoice_line_ids": line_vals})
summary["updated" if existing else "created"] += 1
if post: