Centralize billing for all NexaSystems services (NexaCloud, NexaDesk, NexaMaps, custom apps, memberships) on the Odoo 19 Enterprise instance, replacing Lago. The module adds only the metering + integration layer; native sale_subscription / account_accountant / payment_stripe do all the financial work (invoicing, HST, dunning, portal, credit notes, Stripe). Includes: - Design spec (docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md): 6 locked decisions, architecture, data model, usage engine, Lago-shaped API, webhook control loop, NexaCloud pilot, phased dual-run migration. - Module scaffold: 7 fusion.billing.* models (service, account.link, metric, charge, usage, webhook, reconciliation), bearer-auth API controller shell, security ACLs, README. Compiles on Odoo 19.0; engine/API bodies are stubs pending the implementation plan. - CLAUDE.md rule #15: no sale.subscription model in Odoo 19 — a subscription is a sale.order(is_subscription) + sale.subscription.plan (verified live). Task 0 verified: a single Stripe account is shared across NexaCloud and all Lago providers, so no Stripe account/card migration is required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
34 lines
1.1 KiB
Python
34 lines
1.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1
|
|
from odoo import fields, models
|
|
|
|
|
|
class FusionBillingAccountLink(models.Model):
|
|
"""Identity resolution: maps an app's external account id to one res.partner.
|
|
|
|
Folds the NexaCloud user / NexaDesk tenant / NexaMaps client for the same
|
|
real-world client onto a single partner (the unified customer). See spec §5.1.
|
|
"""
|
|
|
|
_name = "fusion.billing.account.link"
|
|
_description = "Fusion Billing — External Account → Partner Link"
|
|
_order = "service_id, external_id"
|
|
|
|
service_id = fields.Many2one(
|
|
"fusion.billing.service", required=True, ondelete="cascade", index=True,
|
|
)
|
|
external_id = fields.Char(
|
|
required=True, index=True,
|
|
help="The app's own account id (NexaCloud user, NexaDesk tenant, Maps client).",
|
|
)
|
|
external_email = fields.Char()
|
|
partner_id = fields.Many2one(
|
|
"res.partner", required=True, ondelete="restrict", index=True,
|
|
)
|
|
|
|
_service_external_uniq = models.Constraint(
|
|
"unique(service_id, external_id)",
|
|
"An external account can only link to one partner per service.",
|
|
)
|