Files

172 lines
6.7 KiB
Python
Raw Permalink Normal View History

2026-07-01 14:41:49 +07:00
# -*- coding: utf-8 -*-
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class SaasSubscription(models.Model):
"""Flow 3: Subscription & Payment Flow
User Clicks "Upgrade Now"
-> Select Plan (3 Months / 6 Months / 1 Year)
-> Payment Gateway (ABA PayWay / Stripe / PayPal)
-> Success -> Update Account (is_trial=False, is_premium=True,
expiry_date=...) -> Send Receipt & Activate
Full Features
-> Failed -> stay on trial / show retry
"""
_name = 'saas.subscription'
_description = 'SaaS Subscription / Billing Account'
_inherit = ['mail.thread']
trial_request_id = fields.Many2one('saas.trial.request', required=True, ondelete='cascade')
plan_id = fields.Many2one('saas.plan', string='Selected Plan')
is_trial = fields.Boolean(default=True, tracking=True)
is_premium = fields.Boolean(default=False, tracking=True)
expiry_date = fields.Datetime(tracking=True)
state = fields.Selection([
('trial', 'Trial'),
('plan_selected', 'Plan Selected'),
('payment_pending', 'Payment Pending'),
('payment_failed', 'Payment Failed'),
('active', 'Active Subscription'),
], default='trial', tracking=True)
payment_provider_code = fields.Selection([
('aba_payway', 'ABA PayWay'),
('stripe', 'Stripe'),
('paypal', 'PayPal'),
], string='Payment Gateway')
payment_transaction_id = fields.Many2one('payment.transaction', readonly=True, copy=False)
last_payment_status = fields.Char(readonly=True)
# ------------------------------------------------------------------
# Step: User Clicks "Upgrade Now" -> Select Plan
# ------------------------------------------------------------------
def action_select_plan(self, plan_id):
self.ensure_one()
self.write({
'plan_id': plan_id,
'state': 'plan_selected',
})
return True
# ------------------------------------------------------------------
# Step: Payment Gateway (creates a payment.transaction via Odoo's
# native `payment` module, which already supports ABA PayWay
# community connectors, Stripe and PayPal out of the box in v19)
# ------------------------------------------------------------------
def action_create_payment_transaction(self, provider_code):
self.ensure_one()
if not self.plan_id:
raise UserError(_("Please select a plan before proceeding to payment."))
provider = self.env['payment.provider'].sudo().search(
[('code', '=', provider_code), ('state', 'in', ('enabled', 'test'))], limit=1
)
if not provider:
raise UserError(_("Payment provider '%s' is not configured/enabled.") % provider_code)
partner = self._get_or_create_billing_partner()
tx = self.env['payment.transaction'].sudo().create({
'provider_id': provider.id,
'reference': f"SUB-{self.id}-{self.plan_id.id}",
'amount': self.plan_id.price,
'currency_id': self.plan_id.currency_id.id,
'partner_id': partner.id,
'landing_route': '/saas/subscription/return',
})
self.write({
'payment_provider_code': provider_code,
'payment_transaction_id': tx.id,
'state': 'payment_pending',
})
return tx
def _get_or_create_billing_partner(self):
self.ensure_one()
Partner = self.env['res.partner'].sudo()
partner = Partner.search([('email', '=', self.trial_request_id.email)], limit=1)
if not partner:
partner = Partner.create({
'name': self.trial_request_id.company_name,
'email': self.trial_request_id.email,
'phone': self.trial_request_id.phone,
})
return partner
# ------------------------------------------------------------------
# Webhook/callback target: payment.transaction state changes call
# this via the standard Odoo `payment` module post-processing hooks.
# ------------------------------------------------------------------
def _handle_payment_feedback(self, tx):
self.ensure_one()
self.last_payment_status = tx.state
if tx.state == 'done':
self._on_payment_success()
elif tx.state in ('cancel', 'error'):
self._on_payment_failed()
# ---- Success branch ----
def _on_payment_success(self):
self.ensure_one()
months = self.plan_id.duration_months
new_expiry = fields.Datetime.now() + relativedelta(months=months)
self.write({
'is_trial': False,
'is_premium': True,
'expiry_date': new_expiry,
'state': 'active',
})
self.trial_request_id.write({'state': 'ready'})
self._send_receipt_and_activate_features()
def _send_receipt_and_activate_features(self):
self.ensure_one()
template = self.env.ref('saas_trial_portal.mail_template_payment_receipt')
template.send_mail(self.id, force_send=True)
# Activate full feature set on the tenant DB: lift any
# trial-mode restrictions (e.g. user limits, watermarks).
database = self.trial_request_id.database_id
if database and database.db_name:
self._lift_trial_restrictions(database.db_name)
def _lift_trial_restrictions(self, db_name):
import odoo
registry = odoo.modules.registry.Registry.new(db_name)
with registry.cursor() as cr:
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
icp = env['ir.config_parameter']
icp.set_param('saas_trial_portal.is_premium', 'True')
cr.commit()
# ---- Failed branch ----
def _on_payment_failed(self):
self.ensure_one()
self.state = 'payment_failed'
# ------------------------------------------------------------------
# Scheduled action: expire trials / suspend overdue paid subscriptions
# ------------------------------------------------------------------
@api.model
def _cron_expire_trials(self):
now = fields.Datetime.now()
expired = self.search([
('expiry_date', '<=', now),
('state', 'in', ('trial', 'active')),
])
for sub in expired:
if sub.is_trial:
sub.write({'state': 'payment_failed'})
sub.trial_request_id.message_post(
body=_("Your 15-day trial has expired. Upgrade now to keep your data.")
)
else:
sub.write({'is_premium': False, 'state': 'payment_failed'})