first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
'name': "odoo_subscription",
|
||||
|
||||
'summary': "Short (1 phrase/line) summary of the module's purpose",
|
||||
|
||||
'description': """
|
||||
Long description of module's purpose
|
||||
""",
|
||||
|
||||
'author': "My Company",
|
||||
'website': "https://www.yourcompany.com",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Uncategorized',
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base', 'website', 'payment'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/subscription_plans.xml',
|
||||
'data/cron.xml',
|
||||
'data/email_templates.xml',
|
||||
'views/templates.xml',
|
||||
'views/trial_signup.xml',
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
'assets': {
|
||||
'web.assets_frontend': [
|
||||
'odoo_subscription/static/src/css/subscription.css',
|
||||
'odoo_subscription/static/src/js/subscription.js',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,265 @@
|
||||
from odoo import http, _
|
||||
from odoo.http import request, route
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SubscriptionController(http.Controller):
|
||||
|
||||
# ========== FLOW 1: User Registration & App Selection ==========
|
||||
|
||||
@route('/trial', type='http', auth='public', website=True)
|
||||
def trial_signup(self, **kwargs):
|
||||
"""Step 1: Show app selection and registration form"""
|
||||
apps = self._get_available_apps()
|
||||
|
||||
values = {
|
||||
'apps': apps,
|
||||
'countries': request.env['res.country'].sudo().search([]),
|
||||
}
|
||||
|
||||
return request.render('odoo_subscription.trial_signup_page', values)
|
||||
|
||||
@route('/trial/register', type='http', auth='public', website=True, methods=['POST'])
|
||||
def trial_register(self, **kwargs):
|
||||
"""Step 2: Process registration and create subscription"""
|
||||
# Get form data
|
||||
email = kwargs.get('email')
|
||||
name = kwargs.get('name')
|
||||
company_name = kwargs.get('company_name')
|
||||
phone = kwargs.get('phone')
|
||||
country_id = kwargs.get('country_id')
|
||||
selected_apps = kwargs.getlist('selected_apps')
|
||||
|
||||
# Validation
|
||||
if not all([email, name, company_name, selected_apps]):
|
||||
return request.render('odoo_subscription.trial_signup_page', {
|
||||
'error': 'All required fields must be filled',
|
||||
'apps': self._get_available_apps(),
|
||||
})
|
||||
|
||||
# Check if user exists
|
||||
user = request.env['res.users'].sudo().search([('login', '=', email)], limit=1)
|
||||
|
||||
# Create or get user
|
||||
if not user:
|
||||
user = request.env['res.users'].sudo().create({
|
||||
'name': name,
|
||||
'login': email,
|
||||
'email': email,
|
||||
})
|
||||
|
||||
# Create subscription
|
||||
subscription = request.env['user.subscription'].sudo().create({
|
||||
'user_id': user.id,
|
||||
'user_email': email,
|
||||
'user_name': name,
|
||||
'company_name': company_name,
|
||||
'phone': phone,
|
||||
'country_id': int(country_id) if country_id else False,
|
||||
'state': 'trial',
|
||||
})
|
||||
|
||||
# Save selected apps
|
||||
apps_data = self._get_available_apps()
|
||||
for app_name in selected_apps:
|
||||
app_data = next((a for a in apps_data if a['name'] == app_name), None)
|
||||
if app_data:
|
||||
request.env['selected.apps'].sudo().create({
|
||||
'subscription_id': subscription.id,
|
||||
'app_name': app_data['name'],
|
||||
'app_technical_name': app_data.get('technical_name', ''),
|
||||
'app_icon': app_data.get('icon', ''),
|
||||
'app_category': app_data.get('category', 'other'),
|
||||
})
|
||||
|
||||
# Send verification email
|
||||
subscription._send_verification_email()
|
||||
|
||||
# Redirect to verification page
|
||||
return request.redirect(f'/trial/verify?subscription={subscription.id}')
|
||||
|
||||
@route('/trial/verify', type='http', auth='public', website=True)
|
||||
def trial_verify(self, **kwargs):
|
||||
"""Step 3: Email verification page"""
|
||||
subscription_id = kwargs.get('subscription')
|
||||
|
||||
values = {
|
||||
'subscription_id': subscription_id,
|
||||
}
|
||||
|
||||
return request.render('odoo_subscription.email_verification_page', values)
|
||||
|
||||
@route('/trial/verify-email', type='http', auth='public', website=True)
|
||||
def verify_email(self, **kwargs):
|
||||
"""Step 4: Verify email token"""
|
||||
subscription_id = kwargs.get('subscription')
|
||||
token = kwargs.get('token')
|
||||
|
||||
subscription = request.env['user.subscription'].sudo().browse(int(subscription_id))
|
||||
|
||||
if subscription and subscription.state == 'trial':
|
||||
# In production, verify token
|
||||
# For now, just proceed
|
||||
return request.redirect(f'/trial/provision?subscription={subscription.id}')
|
||||
|
||||
return request.redirect('/trial')
|
||||
|
||||
@route('/trial/provision', type='http', auth='public', website=True)
|
||||
def trial_provision(self, **kwargs):
|
||||
"""Step 5: Provision database and show progress"""
|
||||
subscription_id = kwargs.get('subscription')
|
||||
subscription = request.env['user.subscription'].sudo().browse(int(subscription_id))
|
||||
|
||||
if not subscription or subscription.state != 'trial':
|
||||
return request.redirect('/trial')
|
||||
|
||||
# Get selected apps
|
||||
selected_apps = [app.app_technical_name for app in subscription.selected_app_ids]
|
||||
|
||||
# Provision database (this runs asynchronously in production)
|
||||
result = subscription.provision_database(selected_apps)
|
||||
|
||||
values = {
|
||||
'subscription': subscription,
|
||||
'provisioning_result': result,
|
||||
}
|
||||
|
||||
if result.get('success'):
|
||||
return request.render('odoo_subscription.provisioning_success', values)
|
||||
else:
|
||||
return request.render('odoo_subscription.provisioning_failed', values)
|
||||
|
||||
# ========== FLOW 2: Database & Instance Routing ==========
|
||||
|
||||
@route('/database/info', type='json', auth='user', website=True)
|
||||
def get_database_info(self, **kwargs):
|
||||
"""Get current database information"""
|
||||
user = request.env.user
|
||||
subscription = request.env['user.subscription'].search(
|
||||
[('user_id', '=', user.id)], limit=1
|
||||
)
|
||||
|
||||
if subscription and subscription.active_database_id:
|
||||
db = subscription.active_database_id
|
||||
return {
|
||||
'database_name': db.database_name,
|
||||
'subdomain': db.subdomain,
|
||||
'full_domain': db.full_domain,
|
||||
'state': db.db_state,
|
||||
'installed_apps': db.installed_apps,
|
||||
}
|
||||
|
||||
return {}
|
||||
|
||||
# ========== FLOW 3: Subscription & Payment ==========
|
||||
|
||||
@route('/subscription/upgrade', type='http', auth='user', website=True)
|
||||
def subscription_upgrade(self, **kwargs):
|
||||
"""Show subscription plans for upgrade"""
|
||||
user = request.env.user
|
||||
subscription = request.env['user.subscription'].search(
|
||||
[('user_id', '=', user.id)], limit=1
|
||||
)
|
||||
|
||||
plans = request.env['subscription.plan'].get_paid_plans()
|
||||
|
||||
values = {
|
||||
'subscription': subscription,
|
||||
'plans': plans,
|
||||
'currency': request.env.company.currency_id,
|
||||
}
|
||||
|
||||
return request.render('odoo_subscription.subscription_plans', values)
|
||||
|
||||
@route('/subscription/checkout', type='http', auth='user', website=True, methods=['POST'])
|
||||
def subscription_checkout(self, **kwargs):
|
||||
"""Process subscription checkout"""
|
||||
plan_id = kwargs.get('plan_id')
|
||||
payment_method = kwargs.get('payment_method') # aba, stripe, paypal
|
||||
|
||||
user = request.env.user
|
||||
subscription = request.env['user.subscription'].search(
|
||||
[('user_id', '=', user.id)], limit=1
|
||||
)
|
||||
|
||||
if not subscription or not plan_id:
|
||||
return request.redirect('/subscription/upgrade')
|
||||
|
||||
# Create payment transaction
|
||||
# In production, integrate with payment gateway
|
||||
payment_reference = f"SUB-{datetime.now().strftime('%Y%m%d%H%M%S')}-{user.id}"
|
||||
|
||||
# For demo, activate directly
|
||||
subscription.activate_subscription(int(plan_id), payment_reference)
|
||||
|
||||
return request.redirect('/subscription/success')
|
||||
|
||||
@route('/subscription/success', type='http', auth='user', website=True)
|
||||
def subscription_success(self, **kwargs):
|
||||
"""Subscription success page"""
|
||||
return request.render('odoo_subscription.subscription_success')
|
||||
|
||||
@route('/subscription/webhook/<provider>', type='json', auth='public', methods=['POST'])
|
||||
def payment_webhook(self, provider, **kwargs):
|
||||
"""Payment gateway webhook handler"""
|
||||
data = request.jsonrequest
|
||||
|
||||
# Handle different payment providers
|
||||
if provider == 'aba_payway':
|
||||
return self._handle_aba_webhook(data)
|
||||
elif provider == 'stripe':
|
||||
return self._handle_stripe_webhook(data)
|
||||
elif provider == 'paypal':
|
||||
return self._handle_paypal_webhook(data)
|
||||
|
||||
return {'status': 'error', 'message': 'Unknown provider'}
|
||||
|
||||
def _handle_aba_webhook(self, data):
|
||||
"""Handle ABA PayWay webhook"""
|
||||
# Implement ABA PayWay webhook logic
|
||||
return {'status': 'success'}
|
||||
|
||||
def _handle_stripe_webhook(self, data):
|
||||
"""Handle Stripe webhook"""
|
||||
# Implement Stripe webhook logic
|
||||
return {'status': 'success'}
|
||||
|
||||
def _handle_paypal_webhook(self, data):
|
||||
"""Handle PayPal webhook"""
|
||||
# Implement PayPal webhook logic
|
||||
return {'status': 'success'}
|
||||
|
||||
# ========== Helper Methods ==========
|
||||
|
||||
def _get_available_apps(self):
|
||||
"""Get list of available Odoo apps"""
|
||||
return [
|
||||
# Sales
|
||||
{'name': 'CRM', 'technical_name': 'crm', 'icon': 'fa-handshake-o', 'category': 'sales'},
|
||||
{'name': 'Sales', 'technical_name': 'sale_management', 'icon': 'fa-shopping-cart', 'category': 'sales'},
|
||||
{'name': 'Point of Sale', 'technical_name': 'point_of_sale', 'icon': 'fa-th', 'category': 'sales'},
|
||||
{'name': 'Subscriptions', 'technical_name': 'subscription', 'icon': 'fa-refresh', 'category': 'sales'},
|
||||
|
||||
# Finance
|
||||
{'name': 'Invoicing', 'technical_name': 'account', 'icon': 'fa-file-text', 'category': 'finance'},
|
||||
{'name': 'Accounting', 'technical_name': 'account_accountant', 'icon': 'fa-calculator',
|
||||
'category': 'finance'},
|
||||
{'name': 'Expenses', 'technical_name': 'hr_expense', 'icon': 'fa-money', 'category': 'finance'},
|
||||
|
||||
# Services
|
||||
{'name': 'Project', 'technical_name': 'project', 'icon': 'fa-tasks', 'category': 'services'},
|
||||
{'name': 'Timesheets', 'technical_name': 'hr_timesheet', 'icon': 'fa-clock-o', 'category': 'services'},
|
||||
{'name': 'Field Service', 'technical_name': 'hr_field_service', 'icon': 'fa-wrench',
|
||||
'category': 'services'},
|
||||
|
||||
# Marketing
|
||||
{'name': 'Website', 'technical_name': 'website', 'icon': 'fa-globe', 'category': 'marketing'},
|
||||
{'name': 'eCommerce', 'technical_name': 'website_sale', 'icon': 'fa-shopping-bag', 'category': 'marketing'},
|
||||
{'name': 'Blog', 'technical_name': 'website_blog', 'icon': 'fa-pencil', 'category': 'marketing'},
|
||||
{'name': 'Forum', 'technical_name': 'website_forum', 'icon': 'fa-comments', 'category': 'marketing'},
|
||||
]
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="ir_cron_check_expired_trials" model="ir.cron">
|
||||
<field name="name">Check Expired Trials</field>
|
||||
<field name="model_id" ref="model_user_subscription"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.check_expired_trials()</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,194 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Email Verification Template -->
|
||||
<record id="email_verification_template" model="mail.template">
|
||||
<field name="name">Trial Signup - Email Verification</field>
|
||||
<field name="model_id" ref="model_user_subscription"/>
|
||||
<field name="subject">Verify your email to start your free trial</field>
|
||||
<field name="email_to">{{ object.user_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<![CDATA[
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; font-size: 12px; width: 100%; border-collapse:separate;border-spacing: 0px;">
|
||||
<tr>
|
||||
<td align="center" valign="top" style="padding: 32px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width: 600px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<tr>
|
||||
<td style="padding: 32px; text-align: center;">
|
||||
<h1 style="color: #714B67; margin-bottom: 16px;">Welcome to Odoo Trial!</h1>
|
||||
<p style="font-size: 16px; color: #666; margin-bottom: 24px;">
|
||||
Thank you for signing up for a 15-day free trial.<br/>
|
||||
Please verify your email address to continue.
|
||||
</p>
|
||||
<a t-attf-href="/trial/verify-email?subscription={{ object.id }}&token={{ object.access_token }}"
|
||||
style="display: inline-block; padding: 12px 32px; background-color: #714B67; color: white; text-decoration: none; border-radius: 4px; font-weight: bold; margin: 16px 0;">
|
||||
Verify Email Address
|
||||
</a>
|
||||
<p style="font-size: 14px; color: #999; margin-top: 24px;">
|
||||
Or copy and paste this link in your browser:<br/>
|
||||
<a t-attf-href="/trial/verify-email?subscription={{ object.id }}&token={{ object.access_token }}"
|
||||
style="color: #714B67; word-break: break-all;">
|
||||
/trial/verify-email?subscription={{ object.id }}
|
||||
</a>
|
||||
</p>
|
||||
<p style="font-size: 12px; color: #999; margin-top: 32px; border-top: 1px solid #eee; padding-top: 16px;">
|
||||
This link will expire in 24 hours.<br/>
|
||||
If you didn't request this trial, you can safely ignore this email.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Email Credentials Template -->
|
||||
<record id="email_credentials_template" model="mail.template">
|
||||
<field name="name">Trial Ready - Admin Credentials</field>
|
||||
<field name="model_id" ref="model_user_subscription"/>
|
||||
<field name="subject">Your Odoo instance is ready! - Login credentials inside</field>
|
||||
<field name="email_to">{{ object.user_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<![CDATA[
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; font-size: 12px; width: 100%; border-collapse:separate;border-spacing: 0px;">
|
||||
<tr>
|
||||
<td align="center" valign="top" style="padding: 32px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width: 600px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<tr>
|
||||
<td style="padding: 32px;">
|
||||
<h1 style="color: #28a745; margin-bottom: 16px; text-align: center;">
|
||||
🎉 Your Instance is Ready!
|
||||
</h1>
|
||||
<p style="font-size: 16px; color: #666; margin-bottom: 24px;">
|
||||
Great news! Your Odoo instance has been successfully created and is ready to use.
|
||||
</p>
|
||||
|
||||
<table border="1" cellpadding="10" cellspacing="0" style="width: 100%; background-color: #f8f9fa; border-radius: 4px; margin: 24px 0;">
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #e9ecef;">Instance URL:</td>
|
||||
<td>
|
||||
<a t-attf-href="https://{{ object.active_database_id.full_domain }}"
|
||||
style="color: #714B67; font-weight: bold;">
|
||||
https://{{ object.active_database_id.full_domain }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #e9ecef;">Admin Login:</td>
|
||||
<td style="font-family: monospace; background-color: white;">{{ object.active_database_id.admin_login }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight: bold; background-color: #e9ecef;">Password:</td>
|
||||
<td style="font-family: monospace; background-color: white;">{{ object.active_database_id.admin_password }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 16px; margin: 24px 0;">
|
||||
<strong style="color: #856404;">⚠️ Security Notice:</strong>
|
||||
<p style="margin: 8px 0 0 0; color: #856404;">
|
||||
Please change your password immediately after your first login.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 32px 0;">
|
||||
<a t-attf-href="https://{{ object.active_database_id.full_domain }}"
|
||||
style="display: inline-block; padding: 12px 32px; background-color: #28a745; color: white; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Launch Your Instance
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 4px; padding: 16px; margin: 24px 0;">
|
||||
<h3 style="color: #155724; margin-top: 0;">Trial Details:</h3>
|
||||
<ul style="margin: 0; padding-left: 20px; color: #155724;">
|
||||
<li><strong>Duration:</strong> 15 days</li>
|
||||
<li><strong>Start Date:</strong> <t t-esc="object.trial_start_date.strftime('%Y-%m-%d') if object.trial_start_date else 'N/A'"/></li>
|
||||
<li><strong>End Date:</strong> <t t-esc="object.trial_end_date.strftime('%Y-%m-%d') if object.trial_end_date else 'N/A'"/></li>
|
||||
<li><strong>Selected Apps:</strong> <t t-esc="', '.join([app.app_name for app in object.selected_app_ids])"/></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #999; margin-top: 32px; border-top: 1px solid #eee; padding-top: 16px; text-align: center;">
|
||||
Need help? Contact our support team at support@yourcompany.com
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Payment Receipt Template -->
|
||||
<record id="email_payment_receipt" model="mail.template">
|
||||
<field name="name">Subscription Payment Receipt</field>
|
||||
<field name="model_id" ref="model_user_subscription"/>
|
||||
<field name="subject">Payment Receipt - {{ object.subscription_plan_id.name }}</field>
|
||||
<field name="email_to">{{ object.user_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<![CDATA[
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; font-size: 12px; width: 100%; border-collapse:separate;border-spacing: 0px;">
|
||||
<tr>
|
||||
<td align="center" valign="top" style="padding: 32px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width: 600px; background-color: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<tr>
|
||||
<td style="padding: 32px;">
|
||||
<h1 style="color: #28a745; margin-bottom: 16px; text-align: center;">
|
||||
✓ Payment Successful
|
||||
</h1>
|
||||
<p style="text-align: center; font-size: 16px; color: #666; margin-bottom: 32px;">
|
||||
Thank you for your subscription!
|
||||
</p>
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="width: 100%; margin: 24px 0;">
|
||||
<tr>
|
||||
<td style="padding: 12px; background-color: #f8f9fa; border-radius: 4px;">
|
||||
<strong>Receipt Number:</strong> {{ object.payment_reference }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px;">
|
||||
<strong>Plan:</strong> {{ object.subscription_plan_id.name }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; background-color: #f8f9fa; border-radius: 4px;">
|
||||
<strong>Amount Paid:</strong> ${{ object.subscription_plan_id.price }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px;">
|
||||
<strong>Subscription Period:</strong>
|
||||
<t t-if="object.subscription_start_date" t-esc="object.subscription_start_date.strftime('%Y-%m-%d')"/>
|
||||
to
|
||||
<t t-if="object.subscription_end_date" t-esc="object.subscription_end_date.strftime('%Y-%m-%d')"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="text-align: center; margin: 32px 0;">
|
||||
<a t-attf-href="https://{{ object.active_database_id.full_domain }}"
|
||||
style="display: inline-block; padding: 12px 32px; background-color: #714B67; color: white; text-decoration: none; border-radius: 4px; font-weight: bold;">
|
||||
Access Your Instance
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 14px; color: #999; margin-top: 32px; border-top: 1px solid #eee; padding-top: 16px; text-align: center;">
|
||||
This is an automated receipt. Please keep it for your records.<br/>
|
||||
Questions? Contact billing@yourcompany.com
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Trial Plan -->
|
||||
<record id="subscription_plan_trial" model="subscription.plan">
|
||||
<field name="name">Free Trial</field>
|
||||
<field name="plan_code">trial</field>
|
||||
<field name="duration_months">0</field>
|
||||
<field name="price">0.00</field>
|
||||
<field name="is_trial">True</field>
|
||||
<field name="trial_days">15</field>
|
||||
<field name="max_apps">10</field>
|
||||
<field name="max_users">1</field>
|
||||
<field name="max_storage_mb">100</field>
|
||||
<field name="description">15-day free trial with full access to all features</field>
|
||||
<field name="features">10 Apps, 1 User, 100MB Storage, Email Support</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 3 Months Plan -->
|
||||
<record id="subscription_plan_3months" model="subscription.plan">
|
||||
<field name="name">3 Months Plan</field>
|
||||
<field name="plan_code">3_months</field>
|
||||
<field name="duration_months">3</field>
|
||||
<field name="price">89.99</field>
|
||||
<field name="is_trial">False</field>
|
||||
<field name="max_apps">10</field>
|
||||
<field name="max_users">3</field>
|
||||
<field name="max_storage_mb">500</field>
|
||||
<field name="description">Perfect for short-term projects and testing</field>
|
||||
<field name="features">10 Apps, 3 Users, 500MB Storage, Email Support, Automatic Backups</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 6 Months Plan -->
|
||||
<record id="subscription_plan_6months" model="subscription.plan">
|
||||
<field name="name">6 Months Plan</field>
|
||||
<field name="plan_code">6_months</field>
|
||||
<field name="duration_months">6</field>
|
||||
<field name="price">159.99</field>
|
||||
<field name="is_trial">False</field>
|
||||
<field name="max_apps">10</field>
|
||||
<field name="max_users">5</field>
|
||||
<field name="max_storage_mb">1024</field>
|
||||
<field name="description">Great for growing businesses</field>
|
||||
<field name="features">10 Apps, 5 Users, 1GB Storage, Priority Support, Automatic Backups</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
<!-- 1 Year Plan -->
|
||||
<record id="subscription_plan_1year" model="subscription.plan">
|
||||
<field name="name">1 Year Plan</field>
|
||||
<field name="plan_code">1_year</field>
|
||||
<field name="duration_months">12</field>
|
||||
<field name="price">299.99</field>
|
||||
<field name="is_trial">False</field>
|
||||
<field name="max_apps">10</field>
|
||||
<field name="max_users">10</field>
|
||||
<field name="max_storage_mb">2048</field>
|
||||
<field name="description">Best value - Save 17% compared to monthly billing</field>
|
||||
<field name="features">10 Apps, 10 Users, 2GB Storage, Priority Support, Automatic Backups, 99.9% Uptime SLA</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,30 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
<record id="object0" model="odoo_subscription.odoo_subscription">
|
||||
<field name="name">Object 0</field>
|
||||
<field name="value">0</field>
|
||||
</record>
|
||||
|
||||
<record id="object1" model="odoo_subscription.odoo_subscription">
|
||||
<field name="name">Object 1</field>
|
||||
<field name="value">10</field>
|
||||
</record>
|
||||
|
||||
<record id="object2" model="odoo_subscription.odoo_subscription">
|
||||
<field name="name">Object 2</field>
|
||||
<field name="value">20</field>
|
||||
</record>
|
||||
|
||||
<record id="object3" model="odoo_subscription.odoo_subscription">
|
||||
<field name="name">Object 3</field>
|
||||
<field name="value">30</field>
|
||||
</record>
|
||||
|
||||
<record id="object4" model="odoo_subscription.odoo_subscription">
|
||||
<field name="name">Object 4</field>
|
||||
<field name="value">40</field>
|
||||
</record>
|
||||
-->
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,5 @@
|
||||
from . import subscription_plan
|
||||
from . import user_subscription
|
||||
from . import selected_apps
|
||||
from . import cron
|
||||
from . import database_instance
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
from odoo import models, fields, api
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SubscriptionCron(models.Model):
|
||||
_inherit = 'user.subscription'
|
||||
|
||||
@api.model
|
||||
def check_expired_trials(self):
|
||||
"""Cron job to check and expire trials"""
|
||||
subscriptions = self.search([('state', '=', 'trial')])
|
||||
expired_count = 0
|
||||
|
||||
for sub in subscriptions:
|
||||
if sub.check_trial_expired():
|
||||
expired_count += 1
|
||||
# Send notification email
|
||||
sub.message_post(
|
||||
body='Your trial period has expired. Please subscribe to continue.',
|
||||
message_type='notification'
|
||||
)
|
||||
|
||||
return f"Checked {len(subscriptions)} subscriptions, {expired_count} expired"
|
||||
@@ -0,0 +1,220 @@
|
||||
import psycopg2
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
import secrets
|
||||
import string
|
||||
import subprocess
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseInstance(models.Model):
|
||||
_name = 'database.instance'
|
||||
_description = 'Database Instance'
|
||||
_rec_name = 'database_name'
|
||||
|
||||
subscription_id = fields.Many2one('user.subscription', string='Subscription',
|
||||
required=True, ondelete='cascade')
|
||||
database_name = fields.Char(string='Database Name', required=True, index=True)
|
||||
subdomain = fields.Char(string='Subdomain', required=True)
|
||||
full_domain = fields.Char(string='Full Domain', compute='_compute_full_domain')
|
||||
|
||||
# Database status
|
||||
db_state = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
('creating', 'Creating'),
|
||||
('installing', 'Installing Apps'),
|
||||
('ready', 'Ready'),
|
||||
('failed', 'Failed'),
|
||||
], string='Status', default='pending', tracking=True)
|
||||
|
||||
# PostgreSQL connection
|
||||
pg_host = fields.Char(string='PostgreSQL Host', default='localhost')
|
||||
pg_port = fields.Integer(string='PostgreSQL Port', default=5432)
|
||||
pg_user = fields.Char(string='Database User')
|
||||
pg_password = fields.Char(string='Database Password')
|
||||
|
||||
# Instance configuration
|
||||
workers = fields.Integer(string='Workers', default=2)
|
||||
timeout = fields.Integer(string='Timeout (seconds)', default=300)
|
||||
memory_limit_mb = fields.Integer(string='Memory Limit (MB)', default=512)
|
||||
max_connections = fields.Integer(string='Max Connections', default=20)
|
||||
|
||||
# Timestamps
|
||||
created_at = fields.Datetime(string='Created At', default=fields.Datetime.now)
|
||||
ready_at = fields.Datetime(string='Ready At')
|
||||
last_accessed = fields.Datetime(string='Last Accessed')
|
||||
|
||||
# Admin credentials
|
||||
admin_login = fields.Char(string='Admin Login')
|
||||
admin_password = fields.Char(string='Admin Password')
|
||||
admin_email = fields.Char(string='Admin Email')
|
||||
|
||||
installed_apps = fields.Text(string='Installed Apps')
|
||||
error_message = fields.Text(string='Error Message')
|
||||
|
||||
@api.depends('subdomain')
|
||||
def _compute_full_domain(self):
|
||||
base_domain = self.env['ir.config_parameter'].sudo().get_param('web.base.domain', 'localhost')
|
||||
for record in self:
|
||||
if record.subdomain:
|
||||
record.full_domain = f"{record.subdomain}.{base_domain}"
|
||||
else:
|
||||
record.full_domain = False
|
||||
|
||||
def generate_unique_subdomain(self, company_name):
|
||||
"""Generate unique subdomain from company name"""
|
||||
import re
|
||||
# Convert to lowercase and remove special characters
|
||||
base = re.sub(r'[^a-z0-9]+', '-', company_name.lower()).strip('-')
|
||||
# Add random string
|
||||
random_str = ''.join(secrets.choice(string.digits) for _ in range(5))
|
||||
subdomain = f"{base}-{random_str}"
|
||||
|
||||
# Ensure uniqueness
|
||||
counter = 1
|
||||
original_subdomain = subdomain
|
||||
while self.search_count([('subdomain', '=', subdomain)]) > 0:
|
||||
subdomain = f"{original_subdomain}-{counter}"
|
||||
counter += 1
|
||||
|
||||
return subdomain
|
||||
|
||||
def generate_password(self, length=12):
|
||||
"""Generate secure random password"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(secrets.choice(chars) for _ in range(length))
|
||||
|
||||
def create_database(self):
|
||||
"""Create PostgreSQL database for tenant"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
self.db_state = 'creating'
|
||||
self._cr.commit()
|
||||
|
||||
# Generate credentials
|
||||
self.pg_user = f"user_{self.database_name.replace('-', '_')}"
|
||||
self.pg_password = self.generate_password()
|
||||
self.admin_login = 'admin'
|
||||
self.admin_password = self.generate_password()
|
||||
|
||||
# Connect to PostgreSQL
|
||||
conn = psycopg2.connect(
|
||||
host=self.pg_host,
|
||||
port=self.pg_port,
|
||||
database='postgres',
|
||||
user='odoo', # Master PostgreSQL user
|
||||
password='odoo_password' # Should be in config
|
||||
)
|
||||
conn.autocommit = True
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create database user
|
||||
try:
|
||||
cursor.execute(f"DROP ROLE IF EXISTS {self.pg_user}")
|
||||
cursor.execute(
|
||||
f"CREATE ROLE {self.pg_user} WITH LOGIN PASSWORD %s",
|
||||
(self.pg_password,)
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Error creating user: {e}")
|
||||
|
||||
# Create database
|
||||
cursor.execute(f"DROP DATABASE IF EXISTS {self.database_name}")
|
||||
cursor.execute(
|
||||
f"CREATE DATABASE {self.database_name} "
|
||||
f"OWNER {self.pg_user} "
|
||||
f"ENCODING 'UTF8' "
|
||||
f"TEMPLATE template0"
|
||||
)
|
||||
|
||||
# Set permissions
|
||||
cursor.execute(
|
||||
f"GRANT ALL PRIVILEGES ON DATABASE {self.database_name} TO {self.pg_user}"
|
||||
)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
_logger.info(f"Database {self.database_name} created successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.db_state = 'failed'
|
||||
self.error_message = str(e)
|
||||
_logger.error(f"Failed to create database: {e}")
|
||||
return False
|
||||
|
||||
def install_apps(self, app_list):
|
||||
"""Install selected Odoo apps in the database"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
self.db_state = 'installing'
|
||||
self.installed_apps = ', '.join(app_list)
|
||||
self._cr.commit()
|
||||
|
||||
# This would typically use Odoo's database provisioning API
|
||||
# For now, we'll simulate it
|
||||
import odoo
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
# Initialize new database
|
||||
registry = odoo.modules.registry.Registry.new(
|
||||
self.database_name,
|
||||
force_demo=False,
|
||||
signal=False,
|
||||
update_module=True
|
||||
)
|
||||
|
||||
with api.Environment.manage(), registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# Install selected modules
|
||||
for module_name in app_list:
|
||||
try:
|
||||
module = env['ir.module.module'].search([('name', '=', module_name)])
|
||||
if module:
|
||||
module.button_immediate_install()
|
||||
_logger.info(f"Installed module: {module_name}")
|
||||
except Exception as e:
|
||||
_logger.warning(f"Failed to install {module_name}: {e}")
|
||||
|
||||
# Create admin user
|
||||
env['res.users'].create({
|
||||
'name': 'Administrator',
|
||||
'login': self.admin_login,
|
||||
'password': self.admin_password,
|
||||
'email': self.admin_email,
|
||||
'groups_id': [(6, 0, [env.ref('base.group_system').id])],
|
||||
})
|
||||
|
||||
self.db_state = 'ready'
|
||||
self.ready_at = fields.Datetime.now()
|
||||
_logger.info(f"Database {self.database_name} is ready")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.db_state = 'failed'
|
||||
self.error_message = str(e)
|
||||
_logger.error(f"Failed to install apps: {e}")
|
||||
return False
|
||||
|
||||
def configure_instance(self):
|
||||
"""Configure system instance parameters"""
|
||||
self.ensure_one()
|
||||
|
||||
# Update odoo.conf or use environment variables
|
||||
config_params = {
|
||||
'workers': self.workers,
|
||||
'timeout': self.timeout,
|
||||
'limit_memory_hard': self.memory_limit_mb * 1024 * 1024,
|
||||
'max_cron_threads': 1,
|
||||
}
|
||||
|
||||
# These would be applied at the Odoo server level
|
||||
_logger.info(f"Configuring instance {self.database_name}: {config_params}")
|
||||
return True
|
||||
@@ -0,0 +1,31 @@
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class SelectedApps(models.Model):
|
||||
_name = 'selected.apps'
|
||||
_description = 'Selected Applications'
|
||||
|
||||
subscription_id = fields.Many2one('user.subscription', string='Subscription',
|
||||
required=True, ondelete='cascade')
|
||||
app_name = fields.Char(string='App Name', required=True)
|
||||
app_technical_name = fields.Char(string='Technical Name')
|
||||
app_icon = fields.Char(string='App Icon')
|
||||
app_category = fields.Selection([
|
||||
('sales', 'Sales'),
|
||||
('finance', 'Finance'),
|
||||
('services', 'Services'),
|
||||
('marketing', 'Marketing'),
|
||||
('other', 'Other'),
|
||||
], string='Category', default='other')
|
||||
is_installed = fields.Boolean(string='Installed', default=False)
|
||||
|
||||
@api.constrains('subscription_id')
|
||||
def _check_max_apps(self):
|
||||
for record in self:
|
||||
subscription = record.subscription_id
|
||||
if subscription.max_apps_allowed:
|
||||
app_count = self.search_count([('subscription_id', '=', subscription.id)])
|
||||
if app_count > subscription.max_apps_allowed:
|
||||
raise UserError(
|
||||
f'You can select at most {subscription.max_apps_allowed} apps'
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
from odoo import models, fields, api
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class SubscriptionPlan(models.Model):
|
||||
_name = 'subscription.plan'
|
||||
_description = 'Subscription Plan'
|
||||
_order = 'price, duration_months'
|
||||
|
||||
name = fields.Char(string='Plan Name', required=True)
|
||||
duration_months = fields.Integer(string='Duration (Months)', required=True)
|
||||
price = fields.Float(string='Price', required=True)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency',
|
||||
default=lambda self: self.env.company.currency_id)
|
||||
description = fields.Text(string='Description')
|
||||
features = fields.Text(string='Features')
|
||||
is_trial = fields.Boolean(string='Is Trial Plan', default=False)
|
||||
trial_days = fields.Integer(string='Trial Days', default=15)
|
||||
max_apps = fields.Integer(string='Maximum Apps', default=10)
|
||||
max_users = fields.Integer(string='Maximum Users', default=1)
|
||||
max_storage_mb = fields.Integer(string='Max Storage (MB)', default=100)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
|
||||
plan_code = fields.Selection([
|
||||
('3_months', '3 Months'),
|
||||
('6_months', '6 Months'),
|
||||
('1_year', '1 Year'),
|
||||
('trial', 'Free Trial'),
|
||||
], string='Plan Code', required=True)
|
||||
|
||||
@api.model
|
||||
def get_trial_plan(self):
|
||||
return self.search([('is_trial', '=', True), ('active', '=', True)], limit=1)
|
||||
|
||||
@api.model
|
||||
def get_paid_plans(self):
|
||||
return self.search([('is_trial', '=', False), ('active', '=', True)],
|
||||
order='duration_months ASC')
|
||||
@@ -0,0 +1,218 @@
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserSubscription(models.Model):
|
||||
_name = 'user.subscription'
|
||||
_description = 'User Subscription'
|
||||
_inherit = ['mail.thread']
|
||||
_rec_name = 'user_email'
|
||||
|
||||
user_id = fields.Many2one('res.users', string='User', ondelete='cascade')
|
||||
user_email = fields.Char(string='Email', required=True)
|
||||
user_name = fields.Char(string='Full Name', required=True)
|
||||
company_name = fields.Char(string='Company Name', required=True)
|
||||
phone = fields.Char(string='Phone')
|
||||
country_id = fields.Many2one('res.country', string='Country')
|
||||
|
||||
# Subscription details
|
||||
subscription_plan_id = fields.Many2one('subscription.plan', string='Subscription Plan')
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('trial', 'Trial'),
|
||||
('active', 'Active'),
|
||||
('expired', 'Expired'),
|
||||
('cancelled', 'Cancelled'),
|
||||
], string='Status', default='draft', tracking=True)
|
||||
|
||||
# Trial period
|
||||
trial_start_date = fields.Datetime(string='Trial Start Date')
|
||||
trial_end_date = fields.Datetime(string='Trial End Date')
|
||||
|
||||
# Subscription period
|
||||
subscription_start_date = fields.Datetime(string='Subscription Start Date')
|
||||
subscription_end_date = fields.Datetime(string='Subscription End Date')
|
||||
|
||||
# Flags
|
||||
is_trial = fields.Boolean(string='Is Trial', compute='_compute_is_trial')
|
||||
is_premium = fields.Boolean(string='Is Premium', default=False)
|
||||
days_remaining = fields.Integer(string='Days Remaining', compute='_compute_days_remaining')
|
||||
|
||||
# Payment
|
||||
payment_reference = fields.Char(string='Payment Reference')
|
||||
payment_state = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
('paid', 'Paid'),
|
||||
('failed', 'Failed'),
|
||||
], string='Payment Status', default='pending')
|
||||
|
||||
# Database instance
|
||||
database_instance_id = fields.One2many('database.instance', 'subscription_id',
|
||||
string='Database Instances')
|
||||
active_database_id = fields.Many2one('database.instance', string='Active Database')
|
||||
|
||||
# Selected apps
|
||||
selected_app_ids = fields.One2many('selected.apps', 'subscription_id', string='Selected Apps')
|
||||
max_apps_allowed = fields.Integer(related='subscription_plan_id.max_apps',
|
||||
string='Max Apps Allowed')
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""Create subscription with automatic trial setup"""
|
||||
subscription = super().create(vals)
|
||||
|
||||
if subscription.state == 'trial' and not subscription.trial_start_date:
|
||||
trial_plan = self.env['subscription.plan'].get_trial_plan()
|
||||
trial_days = trial_plan.trial_days if trial_plan else 15
|
||||
|
||||
subscription.write({
|
||||
'subscription_plan_id': trial_plan.id if trial_plan else False,
|
||||
'trial_start_date': fields.Datetime.now(),
|
||||
'trial_end_date': fields.Datetime.now() + timedelta(days=trial_days),
|
||||
})
|
||||
|
||||
# Send verification email
|
||||
subscription._send_verification_email()
|
||||
|
||||
return subscription
|
||||
|
||||
def _compute_is_trial(self):
|
||||
for record in self:
|
||||
record.is_trial = record.state == 'trial'
|
||||
|
||||
def _compute_days_remaining(self):
|
||||
now = fields.Datetime.now()
|
||||
for record in self:
|
||||
if record.state == 'trial' and record.trial_end_date:
|
||||
delta = record.trial_end_date - now
|
||||
record.days_remaining = max(0, delta.days)
|
||||
elif record.state == 'active' and record.subscription_end_date:
|
||||
delta = record.subscription_end_date - now
|
||||
record.days_remaining = max(0, delta.days)
|
||||
else:
|
||||
record.days_remaining = 0
|
||||
|
||||
def _send_verification_email(self):
|
||||
"""Send email verification to user"""
|
||||
self.ensure_one()
|
||||
template = self.env.ref('odoo_subscription.email_verification_template',
|
||||
raise_if_not_found=False)
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
|
||||
def _send_credentials_email(self):
|
||||
"""Send admin credentials to user"""
|
||||
self.ensure_one()
|
||||
template = self.env.ref('odoo_subscription.email_credentials_template',
|
||||
raise_if_not_found=False)
|
||||
if template:
|
||||
template.send_mail(self.id, force_send=True)
|
||||
|
||||
def check_trial_expired(self):
|
||||
"""Check if trial has expired"""
|
||||
self.ensure_one()
|
||||
if self.state == 'trial' and self.trial_end_date:
|
||||
if fields.Datetime.now() > self.trial_end_date:
|
||||
self.state = 'expired'
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_subscription_valid(self):
|
||||
"""Check if subscription is valid"""
|
||||
self.ensure_one()
|
||||
|
||||
if self.state == 'active':
|
||||
if self.subscription_end_date and fields.Datetime.now() <= self.subscription_end_date:
|
||||
return True
|
||||
elif self.state == 'trial':
|
||||
if not self.check_trial_expired():
|
||||
return True
|
||||
return False
|
||||
|
||||
def provision_database(self, app_list):
|
||||
"""Provision database and install apps"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
# Create database instance record
|
||||
db_instance_obj = self.env['database.instance']
|
||||
|
||||
# Generate unique subdomain
|
||||
subdomain = db_instance_obj.generate_unique_subdomain(self.company_name)
|
||||
database_name = subdomain.replace('-', '_')
|
||||
|
||||
# Create database instance
|
||||
db_instance = db_instance_obj.create({
|
||||
'subscription_id': self.id,
|
||||
'database_name': database_name,
|
||||
'subdomain': subdomain,
|
||||
'admin_email': self.user_email,
|
||||
'db_state': 'pending',
|
||||
})
|
||||
|
||||
self.active_database_id = db_instance.id
|
||||
self.state = 'trial'
|
||||
self._cr.commit()
|
||||
|
||||
# Create database (this may take time)
|
||||
if db_instance.create_database():
|
||||
# Install selected apps
|
||||
if db_instance.install_apps(app_list):
|
||||
# Configure instance
|
||||
db_instance.configure_instance()
|
||||
|
||||
# Send credentials
|
||||
self._send_credentials_email()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'database_url': db_instance.full_domain,
|
||||
'admin_login': db_instance.admin_login,
|
||||
'admin_password': db_instance.admin_password,
|
||||
}
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Database provisioning failed',
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Database provisioning error: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
}
|
||||
|
||||
def activate_subscription(self, plan_id, payment_ref=None):
|
||||
"""Activate paid subscription"""
|
||||
self.ensure_one()
|
||||
plan = self.env['subscription.plan'].browse(plan_id)
|
||||
|
||||
self.write({
|
||||
'state': 'active',
|
||||
'subscription_plan_id': plan.id,
|
||||
'is_premium': True,
|
||||
'subscription_start_date': fields.Datetime.now(),
|
||||
'payment_reference': payment_ref,
|
||||
'payment_state': 'paid',
|
||||
})
|
||||
|
||||
# Calculate end date based on plan duration
|
||||
if plan.plan_code == '3_months':
|
||||
self.subscription_end_date = fields.Datetime.now() + timedelta(days=90)
|
||||
elif plan.plan_code == '6_months':
|
||||
self.subscription_end_date = fields.Datetime.now() + timedelta(days=180)
|
||||
elif plan.plan_code == '1_year':
|
||||
self.subscription_end_date = fields.Datetime.now() + timedelta(days=365)
|
||||
|
||||
# Update database instance limits
|
||||
if self.active_database_id:
|
||||
self.active_database_id.write({
|
||||
'workers': 4,
|
||||
'memory_limit_mb': 1024,
|
||||
'max_connections': 50,
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_subscription_plan_user,subscription.plan.user,model_subscription_plan,base.group_user,1,0,0,0
|
||||
access_subscription_plan_manager,subscription.plan.manager,model_subscription_plan,base.group_system,1,1,1,1
|
||||
access_user_subscription_user,user.subscription.user,model_user_subscription,base.group_user,1,1,1,0
|
||||
access_user_subscription_manager,user.subscription.manager,model_user_subscription,base.group_system,1,1,1,1
|
||||
access_selected_apps_user,selected.apps.user,model_selected_apps,base.group_user,1,1,1,0
|
||||
access_selected_apps_manager,selected.apps.manager,model_selected_apps,base.group_system,1,1,1,1
|
||||
access_database_instance_user,database.instance.user,model_database_instance,base.group_user,1,0,0,0
|
||||
access_database_instance_manager,database.instance.manager,model_database_instance,base.group_system,1,1,1,1
|
||||
|
@@ -0,0 +1,127 @@
|
||||
/* Trial Signup Page Styles */
|
||||
.trial-signup-page .app-selection-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #e0e0e0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.trial-signup-page .app-selection-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
border-color: #714b67;
|
||||
}
|
||||
|
||||
.trial-signup-page .app-selection-card.selected {
|
||||
border-color: #714b67;
|
||||
background-color: #f8f5f7;
|
||||
}
|
||||
|
||||
.trial-signup-page .app-checkbox {
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.trial-signup-page .form-switch .form-check-input {
|
||||
width: 3em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
.trial-signup-page .sticky-top {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Subscription Plans Page */
|
||||
.subscription-plans-page .plan-card {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.subscription-plans-page .plan-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.subscription-plans-page .plan-featured {
|
||||
border-color: #714b67;
|
||||
box-shadow: 0 4px 12px rgba(113, 75, 103, 0.2);
|
||||
}
|
||||
|
||||
.subscription-plans-page .btn-check:checked + .btn {
|
||||
background-color: #714b67;
|
||||
border-color: #714b67;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* General Styles */
|
||||
.btn-primary {
|
||||
background-color: #714b67;
|
||||
border-color: #714b67;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #5a3d52;
|
||||
border-color: #5a3d52;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.85em;
|
||||
padding: 0.5em 0.75em;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
/* Responsive Adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.trial-signup-page .app-selection-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.subscription-plans-page .plan-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Selected Apps Tags */
|
||||
.selected-app-tag {
|
||||
background-color: #714b67;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.selected-app-tag .remove-tag {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.selected-app-tag .remove-tag:hover {
|
||||
color: #ffc107;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
// App Selection Functionality
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const appCheckboxes = document.querySelectorAll('.app-checkbox');
|
||||
const selectedCountEl = document.getElementById('selected-count');
|
||||
const selectedTagsEl = document.getElementById('selected-apps-tags');
|
||||
const submitBtn = document.getElementById('submit-trial-btn');
|
||||
const maxApps = 10;
|
||||
|
||||
if (appCheckboxes.length > 0) {
|
||||
// Update counter and tags on page load
|
||||
updateSelectedApps();
|
||||
|
||||
appCheckboxes.forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const selectedApps = document.querySelectorAll('.app-checkbox:checked');
|
||||
|
||||
// Check limit
|
||||
if (selectedApps.length > maxApps) {
|
||||
this.checked = false;
|
||||
alert(`You can select a maximum of ${maxApps} apps.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
updateSelectedApps();
|
||||
|
||||
// Update card visual state
|
||||
const card = this.closest('.app-selection-card');
|
||||
if (this.checked) {
|
||||
card.classList.add('selected');
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
|
||||
// Disable unchecked boxes if at limit
|
||||
appCheckboxes.forEach(cb => {
|
||||
if (!cb.checked && selectedApps.length >= maxApps) {
|
||||
cb.disabled = true;
|
||||
cb.closest('.app-selection-card').style.opacity = '0.5';
|
||||
} else {
|
||||
cb.disabled = false;
|
||||
cb.closest('.app-selection-card').style.opacity = '1';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedApps() {
|
||||
const selectedApps = document.querySelectorAll('.app-checkbox:checked');
|
||||
const count = selectedApps.length;
|
||||
|
||||
if (selectedCountEl) {
|
||||
selectedCountEl.textContent = count;
|
||||
}
|
||||
|
||||
if (selectedTagsEl) {
|
||||
selectedTagsEl.innerHTML = '';
|
||||
selectedApps.forEach(checkbox => {
|
||||
const appName = checkbox.value;
|
||||
const tag = document.createElement('span');
|
||||
tag.className = 'selected-app-tag';
|
||||
tag.innerHTML = `
|
||||
${appName}
|
||||
<span class="remove-tag" data-app="${checkbox.id}">×</span>
|
||||
`;
|
||||
|
||||
// Add remove functionality
|
||||
tag.querySelector('.remove-tag').addEventListener('click', function() {
|
||||
checkbox.checked = false;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
|
||||
selectedTagsEl.appendChild(tag);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation
|
||||
const trialForm = document.getElementById('trial-registration-form');
|
||||
if (trialForm) {
|
||||
trialForm.addEventListener('submit', function(e) {
|
||||
const selectedApps = document.querySelectorAll('.app-checkbox:checked');
|
||||
|
||||
if (selectedApps.length === 0) {
|
||||
e.preventDefault();
|
||||
alert('Please select at least one app to continue.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fa fa-spinner fa-spin me-2"></i>Creating your instance...';
|
||||
}
|
||||
|
||||
// Create loading overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mb-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h4>Creating your Odoo instance...</h4>
|
||||
<p class="text-muted">This may take 30-60 seconds</p>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
// Resend verification email
|
||||
const resendBtn = document.getElementById('resend-verification');
|
||||
if (resendBtn) {
|
||||
resendBtn.addEventListener('click', async function() {
|
||||
try {
|
||||
const response = await fetch('/trial/resend-verification', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert('Verification email has been resent. Please check your inbox.');
|
||||
} else {
|
||||
alert('Failed to resend verification email. Please try again.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Payment method selection visual feedback
|
||||
const paymentRadios = document.querySelectorAll('input[name="payment_method"]');
|
||||
paymentRadios.forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
// Visual feedback is handled by Bootstrap btn-check
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Database status checker (for provisioning page)
|
||||
function checkDatabaseStatus(subscriptionId) {
|
||||
fetch(`/database/status?subscription=${subscriptionId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.state === 'ready') {
|
||||
window.location.reload();
|
||||
} else if (data.state === 'failed') {
|
||||
window.location.href = '/trial/provisioning-failed';
|
||||
} else {
|
||||
// Continue checking
|
||||
setTimeout(() => checkDatabaseStatus(subscriptionId), 3000);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking database status:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Export for use in other modules
|
||||
export const subscriptionUtils = {
|
||||
checkDatabaseStatus,
|
||||
maxApps: 10,
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Flow 3: Subscription Plans Page -->
|
||||
<template id="subscription_plans" name="Subscription Plans">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="subscription-plans-page">
|
||||
<div class="container py-5">
|
||||
<!-- Header -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12 text-center">
|
||||
<h1 class="display-4 mb-3">Choose Your Plan</h1>
|
||||
<p class="lead">Upgrade to continue using your instance after the trial ends</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Plan Info -->
|
||||
<div t-if="subscription" class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>Current Plan:</strong>
|
||||
<span t-if="subscription.is_trial" class="badge bg-warning">
|
||||
Free Trial
|
||||
</span>
|
||||
<span t-else="" class="badge bg-success">
|
||||
Premium
|
||||
</span>
|
||||
<t t-if="subscription.days_remaining">
|
||||
- <span t-esc="subscription.days_remaining"/> days remaining
|
||||
</t>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Instance:</strong>
|
||||
<span t-if="subscription.active_database_id"
|
||||
t-esc="subscription.active_database_id.subdomain"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plans Grid -->
|
||||
<div class="row justify-content-center">
|
||||
<t t-foreach="plans" t-as="plan" t-key="plan.id">
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card h-100 plan-card" t-attf-class="#{'border-primary plan-featured' if plan.plan_code == '1_year' else ''}">
|
||||
<!-- Featured Badge -->
|
||||
<t t-if="plan.plan_code == '1_year'">
|
||||
<div class="position-absolute top-0 start-50 translate-middle">
|
||||
<span class="badge bg-primary px-3 py-2">Best Value</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div class="card-header text-center py-4">
|
||||
<h3 class="mb-0" t-esc="plan.name"/>
|
||||
<div class="mt-3">
|
||||
<span class="display-4 fw-bold text-primary">
|
||||
<t t-esc="currency.symbol"/>
|
||||
<t t-esc="plan.price"/>
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
/<t t-if="plan.plan_code == '3_months'">3 months</t>
|
||||
<t t-elif="plan.plan_code == '6_months'">6 months</t>
|
||||
<t t-else="">year</t>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 text-muted">
|
||||
<small>Save <t t-esc="int((1 - plan.price / (plan.duration_months * 10)) * 100)"/>% vs monthly</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-check text-success me-2"/>
|
||||
<strong t-esc="plan.max_apps"/> Apps Included
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-check text-success me-2"/>
|
||||
<strong t-esc="plan.max_users"/> User(s)
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-check text-success me-2"/>
|
||||
<t t-esc="plan.max_storage_mb"/> MB Storage
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-check text-success me-2"/>
|
||||
Email Support
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-check text-success me-2"/>
|
||||
Automatic Backups
|
||||
</li>
|
||||
<t t-if="plan.plan_code == '1_year'">
|
||||
<li class="mb-3">
|
||||
<i class="fa fa-star text-warning me-2"/>
|
||||
Priority Support
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-center py-4 bg-transparent">
|
||||
<form t-attf-action="/subscription/checkout" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="plan_id" t-att-value="plan.id"/>
|
||||
|
||||
<!-- Payment Method Selection -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Payment Method:</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<input type="radio" class="btn-check" name="payment_method"
|
||||
id="aba_{{plan.id}}" value="aba" checked="checked"/>
|
||||
<label class="btn btn-outline-primary" t-att-for="aba_{{plan.id}}">
|
||||
<i class="fa fa-university me-1"/>ABA
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="payment_method"
|
||||
id="stripe_{{plan.id}}" value="stripe"/>
|
||||
<label class="btn btn-outline-primary" t-att-for="stripe_{{plan.id}}">
|
||||
<i class="fa fa-cc-stripe me-1"/>Stripe
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="payment_method"
|
||||
id="paypal_{{plan.id}}" value="paypal"/>
|
||||
<label class="btn btn-outline-primary" t-att-for="paypal_{{plan.id}}">
|
||||
<i class="fa fa-paypal me-1"/>PayPal
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100">
|
||||
<i class="fa fa-credit-card me-2"/>
|
||||
Subscribe Now
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<!-- Money Back Guarantee -->
|
||||
<div class="row mt-5">
|
||||
<div class="col-12 text-center">
|
||||
<div class="alert alert-success d-inline-block">
|
||||
<i class="fa fa-shield me-2"/>
|
||||
<strong>30-Day Money Back Guarantee</strong> - Not satisfied? Get a full refund within 30 days.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Subscription Success Page -->
|
||||
<template id="subscription_success" name="Subscription Success">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="subscription-success-page">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<div class="card border-success">
|
||||
<div class="card-body py-5">
|
||||
<i class="fa fa-check-circle fa-5x text-success mb-4"/>
|
||||
<h1 class="display-4 mb-3">Payment Successful!</h1>
|
||||
<p class="lead mb-4">
|
||||
Thank you for subscribing. Your account has been upgraded to Premium.<br/>
|
||||
You now have access to all premium features.
|
||||
</p>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5>
|
||||
<i class="fa fa-calendar me-2"/>Subscription Details
|
||||
</h5>
|
||||
<p class="mb-1">
|
||||
<strong>Plan:</strong>
|
||||
<span t-if="subscription.subscription_plan_id"
|
||||
t-esc="subscription.subscription_plan_id.name"/>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>Valid Until:</strong>
|
||||
<span t-if="subscription.subscription_end_date"
|
||||
t-esc="subscription.subscription_end_date.strftime('%Y-%m-%d')"/>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>Payment Reference:</strong>
|
||||
<span t-if="subscription.payment_reference"
|
||||
t-esc="subscription.payment_reference"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5>
|
||||
<i class="fa fa-gift me-2"/>What's Included
|
||||
</h5>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li><i class="fa fa-check text-success me-2"/>Full Feature Access</li>
|
||||
<li><i class="fa fa-check text-success me-2"/>Priority Support</li>
|
||||
<li><i class="fa fa-check text-success me-2"/>Automatic Backups</li>
|
||||
<li><i class="fa fa-check text-success me-2"/>99.9% Uptime SLA</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5">
|
||||
<a t-if="subscription.active_database_id"
|
||||
t-attf-href="https://{{subscription.active_database_id.full_domain}}"
|
||||
class="btn btn-success btn-lg me-3" target="_blank">
|
||||
<i class="fa fa-external-link me-2"/>Go to Your Instance
|
||||
</a>
|
||||
<a href="/web" class="btn btn-outline-primary btn-lg">
|
||||
<i class="fa fa-dashboard me-2"/>Dashboard
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<small class="text-muted">
|
||||
A receipt has been sent to <strong t-if="subscription" t-esc="subscription.user_email"/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Trial Expired Page -->
|
||||
<template id="trial_expired" name="Trial Expired">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="trial-expired-page">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<div class="card border-warning">
|
||||
<div class="card-body py-5">
|
||||
<i class="fa fa-exclamation-circle fa-5x text-warning mb-4"/>
|
||||
<h2 class="mb-3">Trial Period Expired</h2>
|
||||
<p class="lead mb-4">
|
||||
Your 15-day free trial has ended.<br/>
|
||||
Upgrade now to continue using your instance and keep all your data.
|
||||
</p>
|
||||
<a href="/subscription/upgrade" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-arrow-up me-2"/>Upgrade to Premium
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,390 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Flow 1: Trial Signup Page -->
|
||||
<template id="trial_signup_page" name="Free Trial - App Selection">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="trial-signup-page oe_structure">
|
||||
<div class="container py-5">
|
||||
<!-- Header -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12 text-center">
|
||||
<h1 class="display-4 mb-3">Start Your Free 15-Day Trial</h1>
|
||||
<p class="lead">No credit card required • Select up to 10 apps • Get started in 60 seconds</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="trial-registration-form" t-attf-action="/trial/register" method="post">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column: App Selection -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="mb-0"><i class="fa fa-th me-2"/>Step 1: Select Your Apps</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-4">Choose the apps you want to try (Maximum 10 apps)</p>
|
||||
|
||||
<!-- Sales Category -->
|
||||
<div class="mb-4">
|
||||
<h4 class="h5 mb-3 text-primary">Sales & CRM</h4>
|
||||
<div class="row">
|
||||
<t t-foreach="apps" t-as="app" t-key="app['name']">
|
||||
<t t-if="app['category'] == 'sales'">
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="app-selection-card card h-100"
|
||||
t-att-data-app="app['technical_name']">
|
||||
<div class="card-body text-center p-3">
|
||||
<i t-attf-class="fa #{app['icon']} fa-2x mb-2 text-primary"/>
|
||||
<h6 class="card-title mb-1" t-esc="app['name']"/>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent text-center">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input app-checkbox"
|
||||
type="checkbox"
|
||||
t-att-id="'app_' + app['technical_name']"
|
||||
t-att-name="'selected_apps'"
|
||||
t-att-value="app['name']"
|
||||
t-att-data-technical="app['technical_name']"/>
|
||||
<label class="form-check-label"
|
||||
t-att-for="'app_' + app['technical_name']">
|
||||
Select
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Finance Category -->
|
||||
<div class="mb-4">
|
||||
<h4 class="h5 mb-3 text-success">Finance & Accounting</h4>
|
||||
<div class="row">
|
||||
<t t-foreach="apps" t-as="app" t-key="app['name']">
|
||||
<t t-if="app['category'] == 'finance'">
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="app-selection-card card h-100"
|
||||
t-att-data-app="app['technical_name']">
|
||||
<div class="card-body text-center p-3">
|
||||
<i t-attf-class="fa #{app['icon']} fa-2x mb-2 text-success"/>
|
||||
<h6 class="card-title mb-1" t-esc="app['name']"/>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent text-center">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input app-checkbox"
|
||||
type="checkbox"
|
||||
t-att-id="'app_' + app['technical_name']"
|
||||
t-att-name="'selected_apps'"
|
||||
t-att-value="app['name']"
|
||||
t-att-data-technical="app['technical_name']"/>
|
||||
<label class="form-check-label"
|
||||
t-att-for="'app_' + app['technical_name']">
|
||||
Select
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services Category -->
|
||||
<div class="mb-4">
|
||||
<h4 class="h5 mb-3 text-info">Services & Projects</h4>
|
||||
<div class="row">
|
||||
<t t-foreach="apps" t-as="app" t-key="app['name']">
|
||||
<t t-if="app['category'] == 'services'">
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="app-selection-card card h-100"
|
||||
t-att-data-app="app['technical_name']">
|
||||
<div class="card-body text-center p-3">
|
||||
<i t-attf-class="fa #{app['icon']} fa-2x mb-2 text-info"/>
|
||||
<h6 class="card-title mb-1" t-esc="app['name']"/>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent text-center">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input app-checkbox"
|
||||
type="checkbox"
|
||||
t-att-id="'app_' + app['technical_name']"
|
||||
t-att-name="'selected_apps'"
|
||||
t-att-value="app['name']"
|
||||
t-att-data-technical="app['technical_name']"/>
|
||||
<label class="form-check-label"
|
||||
t-att-for="'app_' + app['technical_name']">
|
||||
Select
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Marketing Category -->
|
||||
<div class="mb-4">
|
||||
<h4 class="h5 mb-3 text-warning">Marketing & Website</h4>
|
||||
<div class="row">
|
||||
<t t-foreach="apps" t-as="app" t-key="app['name']">
|
||||
<t t-if="app['category'] == 'marketing'">
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="app-selection-card card h-100"
|
||||
t-att-data-app="app['technical_name']">
|
||||
<div class="card-body text-center p-3">
|
||||
<i t-attf-class="fa #{app['icon']} fa-2x mb-2 text-warning"/>
|
||||
<h6 class="card-title mb-1" t-esc="app['name']"/>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent text-center">
|
||||
<div class="form-check form-switch d-inline-block">
|
||||
<input class="form-check-input app-checkbox"
|
||||
type="checkbox"
|
||||
t-att-id="'app_' + app['technical_name']"
|
||||
t-att-name="'selected_apps'"
|
||||
t-att-value="app['name']"
|
||||
t-att-data-technical="app['technical_name']"/>
|
||||
<label class="form-check-label"
|
||||
t-att-for="'app_' + app['technical_name']">
|
||||
Select
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Apps Counter -->
|
||||
<div class="alert alert-info mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fa fa-info-circle me-2"/>
|
||||
<span id="selected-count">0</span> / 10 apps selected
|
||||
</div>
|
||||
<div id="selected-apps-tags" class="d-flex gap-2 flex-wrap">
|
||||
<!-- Tags will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Registration Form -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4 sticky-top" style="top: 20px;">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h3 class="mb-0"><i class="fa fa-user me-2"/>Step 2: Your Information</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email Address <span class="text-danger">*</span></label>
|
||||
<input type="email" class="form-control" id="email" name="email" required="required"
|
||||
placeholder="your.email@company.com"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Full Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name" required="required"
|
||||
placeholder="John Doe"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="company_name" class="form-label">Company Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="company_name" name="company_name" required="required"
|
||||
placeholder="Your Company Ltd."/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="phone" class="form-label">Phone Number</label>
|
||||
<input type="tel" class="form-control" id="phone" name="phone"
|
||||
placeholder="+855 12 345 678"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="country_id" class="form-label">Country</label>
|
||||
<select class="form-select" id="country_id" name="country_id">
|
||||
<option value="">Select Country</option>
|
||||
<t t-foreach="countries" t-as="country" t-key="country.id">
|
||||
<option t-att-value="country.id" t-esc="country.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-3">
|
||||
<small>
|
||||
<i class="fa fa-shield-alt me-1"/>
|
||||
By registering, you agree to our Terms of Service and Privacy Policy
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success btn-lg w-100 mt-3" id="submit-trial-btn">
|
||||
<i class="fa fa-rocket me-2"/>Start Free Trial
|
||||
</button>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">
|
||||
<i class="fa fa-clock me-1"/> Setup takes 30-60 seconds
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Email Verification Page -->
|
||||
<template id="email_verification_page" name="Email Verification">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="email-verification-page">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<div class="card">
|
||||
<div class="card-body py-5">
|
||||
<i class="fa fa-envelope-open-o fa-5x text-primary mb-4"/>
|
||||
<h2 class="mb-3">Verify Your Email</h2>
|
||||
<p class="lead mb-4">
|
||||
We've sent a verification link to your email address.<br/>
|
||||
Please check your inbox and click the link to continue.
|
||||
</p>
|
||||
<div class="alert alert-info">
|
||||
<p class="mb-2"><strong>Didn't receive the email?</strong></p>
|
||||
<button type="button" class="btn btn-link" id="resend-verification">
|
||||
Click here to resend
|
||||
</button>
|
||||
</div>
|
||||
<a href="/trial" class="btn btn-outline-secondary mt-3">
|
||||
<i class="fa fa-arrow-left me-2"/>Back to Trial Signup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Provisioning Success Page -->
|
||||
<template id="provisioning_success" name="Provisioning Success">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="provisioning-success-page">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center py-5">
|
||||
<i class="fa fa-check-circle fa-5x text-success mb-4"/>
|
||||
<h1 class="display-4 mb-3">Your Trial is Ready!</h1>
|
||||
<p class="lead mb-4">
|
||||
Your database has been successfully created and configured.<br/>
|
||||
You can now start using your selected applications.
|
||||
</p>
|
||||
|
||||
<div class="row mt-5">
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fa fa-link me-2"/>Your Instance URL
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<a t-attf-href="https://{{provisioning_result.database_url}}"
|
||||
class="btn btn-primary btn-lg" target="_blank">
|
||||
<t t-esc="provisioning_result.database_url"/>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="fa fa-key me-2"/>Admin Credentials
|
||||
</h5>
|
||||
<p class="card-text">
|
||||
<strong>Login:</strong> <t t-esc="provisioning_result.admin_login"/><br/>
|
||||
<strong>Password:</strong> <t t-esc="provisioning_result.admin_password"/>
|
||||
</p>
|
||||
<small class="text-muted">
|
||||
<i class="fa fa-info-circle"/>
|
||||
These credentials have been sent to your email
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-4">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fa fa-gift me-2"/>15-Day Free Trial Activated
|
||||
</h5>
|
||||
<p>
|
||||
You have full access to all features for the next 15 days.<br/>
|
||||
No credit card required during the trial period.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a t-attf-href="https://{{provisioning_result.database_url}}"
|
||||
class="btn btn-success btn-lg me-3" target="_blank">
|
||||
<i class="fa fa-external-link me-2"/>Launch Your Instance
|
||||
</a>
|
||||
<a href="/subscription/upgrade" class="btn btn-outline-primary btn-lg">
|
||||
<i class="fa fa-arrow-up me-2"/>Upgrade Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Provisioning Failed Page -->
|
||||
<template id="provisioning_failed" name="Provisioning Failed">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="provisioning-failed-page">
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body py-5">
|
||||
<i class="fa fa-exclamation-triangle fa-5x text-danger mb-4"/>
|
||||
<h2 class="mb-3">Provisioning Failed</h2>
|
||||
<p class="lead mb-4">
|
||||
We encountered an issue while creating your database.<br/>
|
||||
Please try again or contact support.
|
||||
</p>
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong>
|
||||
<t t-esc="provisioning_result.get('error', 'Unknown error')"/>
|
||||
</div>
|
||||
<a href="/trial" class="btn btn-primary btn-lg">
|
||||
<i class="fa fa-refresh me-2"/>Try Again
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user