fusion.billing.reconciliation gains the compute: _compute_reconciliation (flat + charge overage vs external, status match/delta at a tolerance) and _reconcile_rows (resolve shadow sub -> flat + charge, upsert one row per service/partner/period, per-row isolated). The wizard gains a read-only _read_reconciliation_rows (NexaCloud usage cpu_hours*3600 + invoice-item subtotals per YYYY-MM) and a "Run Reconciliation" button. 2a amended to stamp x_fc_nexacloud_plan_id on shadow subs so reconciliation can find the charge. Read-only on NexaCloud; writes only reconciliation rows (shadow guarantees intact). 8 new tests, full suite green on odoo-trial.
46 lines
2.0 KiB
Python
46 lines
2.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_inherit = "sale.order"
|
|
|
|
x_fc_nexacloud_subscription_id = fields.Char(
|
|
index=True, copy=False,
|
|
help="Source NexaCloud subscription id — the importer's idempotency key.")
|
|
x_fc_nexacloud_deployment_id = fields.Char(index=True, copy=False)
|
|
x_fc_nexacloud_plan_id = fields.Char(
|
|
index=True, copy=False,
|
|
help="Source NexaCloud plan id — links the shadow sub to its charge for 2d reconciliation.")
|
|
x_fc_billing_service_id = fields.Many2one(
|
|
"fusion.billing.service", index=True, copy=False, ondelete="set null")
|
|
x_fc_shadow = fields.Boolean(
|
|
default=False, copy=False,
|
|
help="Imported in shadow mode: Odoo computes but must not charge/post/email.")
|
|
|
|
def _fc_rate_usage(self, charge, period_start, period_end):
|
|
"""Aggregate this subscription's usage for `charge`'s metric in the period,
|
|
compute the overage amount, and upsert a matching overage order line.
|
|
Returns the amount.
|
|
|
|
A zero amount never *creates* a new line (no $0.00 overage clutter); if a
|
|
line already exists it is still updated so a dropped-to-zero overage clears.
|
|
"""
|
|
self.ensure_one()
|
|
Usage = self.env['fusion.billing.usage']
|
|
total = Usage._aggregate(self, charge.metric_id, period_start, period_end)
|
|
_overage, amount = charge._compute_billable(total)
|
|
if charge.product_id:
|
|
line = self.order_line.filtered(lambda l: l.product_id == charge.product_id)
|
|
if not line and amount == 0:
|
|
return amount
|
|
vals = {'product_uom_qty': 1, 'price_unit': amount}
|
|
if line:
|
|
line.write(vals)
|
|
else:
|
|
self.env['sale.order.line'].create(
|
|
{'order_id': self.id, 'product_id': charge.product_id.id, **vals})
|
|
return amount
|