first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+42
View File
@@ -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
@@ -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'},
]
+12
View File
@@ -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>
+194
View File
@@ -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>
+30
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
from . import subscription_plan
from . import user_subscription
from . import selected_apps
from . import cron
from . import database_instance
+23
View File
@@ -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
+31
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subscription_plan_user subscription.plan.user model_subscription_plan base.group_user 1 0 0 0
3 access_subscription_plan_manager subscription.plan.manager model_subscription_plan base.group_system 1 1 1 1
4 access_user_subscription_user user.subscription.user model_user_subscription base.group_user 1 1 1 0
5 access_user_subscription_manager user.subscription.manager model_user_subscription base.group_system 1 1 1 1
6 access_selected_apps_user selected.apps.user model_selected_apps base.group_user 1 1 1 0
7 access_selected_apps_manager selected.apps.manager model_selected_apps base.group_system 1 1 1 1
8 access_database_instance_user database.instance.user model_database_instance base.group_user 1 0 0 0
9 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}">&times;</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,
};
+267
View File
@@ -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>
+390
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>