feat(billing): post + reconcile only PAID invoices, keeping original dates
_post_and_reconcile_paid: for invoices NexaCloud marks paid, set the ledger entry's invoice_date AND accounting date to the original NexaCloud date, post, then reconcile the Stripe payment dated to the actual paid_at. Unpaid invoices stay draft. Per-invoice isolated. 76 tests green on odoo-trial.
This commit is contained in:
@@ -120,6 +120,24 @@ class TestLedgerIngest(TransactionCase):
|
|||||||
self.assertAlmostEqual(mv.amount_untaxed, 200.0, places=2) # captured via reconciling line
|
self.assertAlmostEqual(mv.amount_untaxed, 200.0, places=2) # captured via reconciling line
|
||||||
self.assertTrue(any('base/unitemized' in (l.name or '') for l in mv.invoice_line_ids))
|
self.assertTrue(any('base/unitemized' in (l.name or '') for l in mv.invoice_line_ids))
|
||||||
|
|
||||||
|
def test_post_and_reconcile_paid_only(self):
|
||||||
|
base = _inv_fixture()[0]
|
||||||
|
paid = dict(base, id='inv-paid', invoice_number='NEX-PAID',
|
||||||
|
status='paid', amount_paid=113.0, paid_at='2026-05-02',
|
||||||
|
invoice_date='2026-05-01')
|
||||||
|
unpaid = dict(base, id='inv-unpaid', invoice_number='NEX-UNPAID',
|
||||||
|
status='open', amount_paid=0.0, invoice_date='2026-04-01')
|
||||||
|
self.W._ingest_invoices([paid, unpaid], post=False)
|
||||||
|
summary = self.W._post_and_reconcile_paid([paid, unpaid])
|
||||||
|
pm = self.Move.search([('x_fc_nexacloud_invoice_id', '=', 'inv-paid')])
|
||||||
|
um = self.Move.search([('x_fc_nexacloud_invoice_id', '=', 'inv-unpaid')])
|
||||||
|
self.assertEqual(pm.state, 'posted')
|
||||||
|
self.assertIn(pm.payment_state, ('paid', 'in_payment'))
|
||||||
|
self.assertEqual(str(pm.invoice_date), '2026-05-01') # original invoice date kept
|
||||||
|
self.assertEqual(um.state, 'draft') # unpaid stays draft
|
||||||
|
self.assertEqual(summary['posted'], 1)
|
||||||
|
self.assertEqual(summary['skipped_unpaid'], 1)
|
||||||
|
|
||||||
def test_partner_named_by_company_not_person(self):
|
def test_partner_named_by_company_not_person(self):
|
||||||
data = _inv_fixture()
|
data = _inv_fixture()
|
||||||
data[0]['partner_company'] = 'Acme Holdings Inc' # full_name is "Acme"; company wins
|
data[0]['partner_company'] = 'Acme Holdings Inc' # full_name is "Acme"; company wins
|
||||||
|
|||||||
@@ -197,6 +197,41 @@ class FusionBillingInvoiceLedgerWizard(models.TransientModel):
|
|||||||
_logger.exception("Ledger post: move %s failed", mv.id)
|
_logger.exception("Ledger post: move %s failed", mv.id)
|
||||||
return posted
|
return posted
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _post_and_reconcile_paid(self, data):
|
||||||
|
"""Post + reconcile ONLY the invoices NexaCloud marks paid, dating the ledger entry
|
||||||
|
to the ORIGINAL invoice date and the payment to the actual paid_at. Leaves unpaid
|
||||||
|
invoices as draft. Per-invoice isolated."""
|
||||||
|
Move = self.env["account.move"]
|
||||||
|
summary = {"posted": 0, "reconciled": 0, "skipped_unpaid": 0,
|
||||||
|
"skipped_missing": 0, "failed": []}
|
||||||
|
for inv in data:
|
||||||
|
nc_id = str(inv.get("id") or "")
|
||||||
|
paid = float(inv.get("amount_paid") or 0.0)
|
||||||
|
if inv.get("status") != "paid" and paid <= 0:
|
||||||
|
summary["skipped_unpaid"] += 1
|
||||||
|
continue
|
||||||
|
mv = Move.search([("x_fc_nexacloud_invoice_id", "=", nc_id),
|
||||||
|
("move_type", "=", "out_invoice")], limit=1)
|
||||||
|
if not mv:
|
||||||
|
summary["skipped_missing"] += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with self.env.cr.savepoint():
|
||||||
|
if mv.state == "draft":
|
||||||
|
inv_date = inv.get("invoice_date")
|
||||||
|
# keep the original invoice + accounting date (not today)
|
||||||
|
mv.write({"invoice_date": inv_date, "date": inv_date})
|
||||||
|
mv.action_post()
|
||||||
|
summary["posted"] += 1
|
||||||
|
if mv.payment_state not in ("paid", "in_payment", "reversed"):
|
||||||
|
if self._fc_reconcile_payment(mv, inv):
|
||||||
|
summary["reconciled"] += 1
|
||||||
|
except Exception as e: # noqa: BLE001 - per-invoice isolation
|
||||||
|
_logger.exception("Post+pay: invoice %s failed", nc_id)
|
||||||
|
summary["failed"].append({"id": nc_id, "error": "%s: %s" % (type(e).__name__, e)})
|
||||||
|
return summary
|
||||||
|
|
||||||
def _cron_ingest_recent(self):
|
def _cron_ingest_recent(self):
|
||||||
since = fields.Datetime.to_string(fields.Datetime.now() - timedelta(days=2))
|
since = fields.Datetime.to_string(fields.Datetime.now() - timedelta(days=2))
|
||||||
return self._ingest_invoices(self._read_nexacloud_invoices(since=since), post=True)
|
return self._ingest_invoices(self._read_nexacloud_invoices(since=since), post=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user