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
+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,
})