172 lines
6.7 KiB
Python
172 lines
6.7 KiB
Python
# -*- 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'})
|