# -*- 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'})