From 3e0b53111075c8497b77a6ca8e742ebd1ed48bc7 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 13:34:37 -0400 Subject: [PATCH] =?UTF-8?q?fix(billing):=20charge=20rate=20precision=20?= =?UTF-8?q?=E2=80=94=20Float=20not=20Monetary,=20no=20premature=20cent-rou?= =?UTF-8?q?nding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit price_per_unit was a Monetary field, so a realistic sub-cent rate like $0.0075/core-hour was rounded to $0.01 on write, corrupting the rate. Make it Float(16,6). Also stop _compute_billable from rounding the overage amount to 2 decimals mid-calc — that lost the half-cent on sub-cent rates and would drift against the source app, which keeps usage amounts at 4 decimals and only rounds at the invoice total. Now rounds to 6 dp (float-noise only); cent-rounding defers to the invoice line. Exposed while building the NexaCloud importer. --- fusion_centralize_billing/models/charge.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/fusion_centralize_billing/models/charge.py b/fusion_centralize_billing/models/charge.py index bc40f960..c1b0a2f5 100644 --- a/fusion_centralize_billing/models/charge.py +++ b/fusion_centralize_billing/models/charge.py @@ -36,7 +36,13 @@ class FusionBillingCharge(models.Model): included_quota = fields.Float( default=0.0, help="Units included before overage applies, per period.", ) - price_per_unit = fields.Monetary(help="Overage price per unit_batch.") + price_per_unit = fields.Float( + digits=(16, 6), + help="Overage price per unit_batch. A Float (not Monetary) so sub-cent rates " + "like $0.0075/core-hour are stored exactly — Monetary rounds to the " + "currency's 2 decimals and would corrupt the rate. Final cent-rounding " + "happens at the invoice line/total, not in the per-charge math.", + ) unit_batch = fields.Float( default=1.0, help="Batch size for overage pricing, e.g. 1000 = priced per 1k.", ) @@ -67,6 +73,12 @@ class FusionBillingCharge(models.Model): - 'standard': price the overage in (rounded-up) `unit_batch` blocks. - 'package': price whole packages over the RAW quantity (quota ignored for package counting); a partial package rounds up. + + The amount keeps the rate's precision (rounded to 6 dp only to clear float + noise) — it must NOT be rounded to cents here. Sub-cent rates (e.g. + $0.0075/core-hour) and fractional totals are preserved so they match the + source app's own sub-cent usage amounts; final cent-rounding happens once at + the invoice line / invoice total, exactly as the source app does. """ self.ensure_one() overage = max(0.0, (total_quantity or 0.0) - (self.included_quota or 0.0)) @@ -74,7 +86,7 @@ class FusionBillingCharge(models.Model): if self.charge_model == 'package': # whole packages over the RAW quantity (quota ignored for package counting) blocks = math.ceil((total_quantity or 0.0) / batch) if total_quantity else 0 - return overage, round(blocks * (self.price_per_unit or 0.0), 2) + return overage, round(blocks * (self.price_per_unit or 0.0), 6) # standard: price the overage in (rounded-up) batches blocks = math.ceil(overage / batch) if overage > 0 else 0 - return overage, round(blocks * (self.price_per_unit or 0.0), 2) + return overage, round(blocks * (self.price_per_unit or 0.0), 6)