first push message
This commit is contained in:
@@ -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,
|
||||
})
|
||||
Reference in New Issue
Block a user