fix(billing): charge rate precision — Float not Monetary, no premature cent-rounding
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.
This commit is contained in:
@@ -36,7 +36,13 @@ class FusionBillingCharge(models.Model):
|
|||||||
included_quota = fields.Float(
|
included_quota = fields.Float(
|
||||||
default=0.0, help="Units included before overage applies, per period.",
|
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(
|
unit_batch = fields.Float(
|
||||||
default=1.0, help="Batch size for overage pricing, e.g. 1000 = priced per 1k.",
|
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.
|
- 'standard': price the overage in (rounded-up) `unit_batch` blocks.
|
||||||
- 'package': price whole packages over the RAW quantity (quota ignored for
|
- 'package': price whole packages over the RAW quantity (quota ignored for
|
||||||
package counting); a partial package rounds up.
|
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()
|
self.ensure_one()
|
||||||
overage = max(0.0, (total_quantity or 0.0) - (self.included_quota or 0.0))
|
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':
|
if self.charge_model == 'package':
|
||||||
# whole packages over the RAW quantity (quota ignored for package counting)
|
# 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
|
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
|
# standard: price the overage in (rounded-up) batches
|
||||||
blocks = math.ceil(overage / batch) if overage > 0 else 0
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user