Files
Odoo-Modules/fusion_centralize_billing/models/sale_order.py
gsinghpal 6f060896bf feat(billing): 2a NexaCloud→Odoo importer (read-only, idempotent, shadow-safe)
fusion.billing.import.wizard backfills NexaCloud into Odoo: read-only
psycopg2 reader (_read_nexacloud_rows, DSN from ir.config_parameter)
split from pure-Odoo writes (_import_rows/_do_import) so the logic is
unit-tested headless. Maps users→partners+links (reusing
_resolve_or_create_partner, stashing stripe_customer_id), plans→a
cpu_seconds charge catalog (included_quota=cpu_seconds_quota,
unit_batch=3600, $0.0075/core-hour, plan_id NULL), and deployments→one
DRAFT shadow sale.order per deployment with the flat price set
explicitly. Shadow-safe by construction: draft + no payment token +
charge plan_id NULL (rating cron is a no-op). Idempotent re-runs;
per-row savepoints isolate bad rows; dry-run rolls back. 11 tests,
50/50 green on odoo-trial.
2026-05-27 13:34:47 -04:00

43 lines
1.8 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_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