- Introduced `saas.plan` model to define subscription plans with limits and pricing. - Created `saas.restaurant` model to manage restaurant tenants, including database provisioning and subscription management. - Implemented views for managing SaaS plans and restaurant tenants, including tree and form views. - Added security access rights for the new models. - Developed a backup management view for database backups. - Updated menu structure to include new SaaS management options. - Added Docker and deployment configurations for PostgreSQL, Redis, and Odoo services. - Included scaling guide and backup scripts for production environments. - Enhanced theme with new images and layout adjustments.
174 lines
7.5 KiB
Python
174 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
import json
|
|
import logging
|
|
from odoo import http, fields
|
|
from odoo.http import request
|
|
from odoo.service import db
|
|
from odoo.tools import config
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class SaasApiController(http.Controller):
|
|
|
|
def _authenticate(self):
|
|
# Authenticate using a SaaS Token defined in odoo.conf (e.g. saas_api_token = abcxyz123)
|
|
expected_token = config.get('saas_api_token', 'METATRON_DINE360_SECRET_SaaS_2026')
|
|
auth_header = request.httprequest.headers.get('Authorization')
|
|
if not auth_header or not auth_header.startswith('Bearer '):
|
|
return False
|
|
token = auth_header.split(' ')[1]
|
|
return token == expected_token
|
|
|
|
def _json_response(self, data, status=200):
|
|
return request.make_response(
|
|
json.dumps(data),
|
|
headers=[('Content-Type', 'application/json')],
|
|
status=status
|
|
)
|
|
|
|
@http.route('/api/v1/saas/create_restaurant', type='json', auth='none', methods=['POST'], csrf=False)
|
|
def api_create_restaurant(self):
|
|
if not self._authenticate():
|
|
return self._json_response({'error': 'Unauthorized'}, status=401)
|
|
|
|
try:
|
|
data = request.get_json_data()
|
|
name = data.get('name')
|
|
owner_name = data.get('owner_name')
|
|
email = data.get('email')
|
|
plan_name = data.get('plan_name', 'Starter')
|
|
billing_cycle = data.get('billing_cycle', 'monthly')
|
|
expiry_date_str = data.get('expiry_date') # Format YYYY-MM-DD
|
|
|
|
if not all([name, owner_name, email, expiry_date_str]):
|
|
return self._json_response({'error': 'Missing required fields'}, status=400)
|
|
|
|
# Find subscription plan
|
|
plan = request.env['saas.plan'].sudo().search([('name', '=', plan_name)], limit=1)
|
|
if not plan:
|
|
return self._json_response({'error': f"Plan '{plan_name}' not found"}, status=404)
|
|
|
|
expiry_date = fields.Date.from_string(expiry_date_str)
|
|
|
|
# Create Restaurant record inside the master DB env
|
|
restaurant = request.env['saas.restaurant'].sudo().create({
|
|
'name': name,
|
|
'owner_name': owner_name,
|
|
'email': email,
|
|
'phone': data.get('phone', ''),
|
|
'street': data.get('street', ''),
|
|
'city': data.get('city', ''),
|
|
'plan_id': plan.id,
|
|
'billing_cycle': billing_cycle,
|
|
'expiry_date': expiry_date,
|
|
'currency_id': request.env['res.currency'].sudo().search([('name', '=', data.get('currency', 'USD'))], limit=1).id,
|
|
'timezone': data.get('timezone', 'America/New_York')
|
|
})
|
|
|
|
# Programmatically trigger database creation in Odoo
|
|
restaurant.action_create_database()
|
|
|
|
return self._json_response({
|
|
'success': True,
|
|
'restaurant_id': restaurant.id,
|
|
'database_name': restaurant.database_name,
|
|
'subdomain': restaurant.subdomain,
|
|
'status': restaurant.status
|
|
})
|
|
except Exception as e:
|
|
_logger.error(f"SaaS API Create Restaurant Error: {str(e)}")
|
|
return self._json_response({'error': str(e)}, status=500)
|
|
|
|
@http.route('/api/v1/saas/suspend_restaurant', type='json', auth='none', methods=['POST'], csrf=False)
|
|
def api_suspend_restaurant(self):
|
|
if not self._authenticate():
|
|
return self._json_response({'error': 'Unauthorized'}, status=401)
|
|
|
|
data = request.get_json_data()
|
|
db_name = data.get('database_name')
|
|
|
|
restaurant = request.env['saas.restaurant'].sudo().search([('database_name', '=', db_name)], limit=1)
|
|
if not restaurant:
|
|
return self._json_response({'error': 'Restaurant database not found'}, status=404)
|
|
|
|
try:
|
|
restaurant.action_suspend()
|
|
return self._json_response({'success': True, 'status': restaurant.status})
|
|
except Exception as e:
|
|
return self._json_response({'error': str(e)}, status=500)
|
|
|
|
@http.route('/api/v1/saas/renew_subscription', type='json', auth='none', methods=['POST'], csrf=False)
|
|
def api_renew_subscription(self):
|
|
if not self._authenticate():
|
|
return self._json_response({'error': 'Unauthorized'}, status=401)
|
|
|
|
data = request.get_json_data()
|
|
db_name = data.get('database_name')
|
|
new_expiry_str = data.get('expiry_date')
|
|
|
|
restaurant = request.env['saas.restaurant'].sudo().search([('database_name', '=', db_name)], limit=1)
|
|
if not restaurant:
|
|
return self._json_response({'error': 'Restaurant database not found'}, status=404)
|
|
|
|
try:
|
|
new_expiry = fields.Date.from_string(new_expiry_str)
|
|
restaurant.write({'expiry_date': new_expiry})
|
|
if restaurant.status in ['suspended', 'expired']:
|
|
restaurant.action_activate()
|
|
return self._json_response({
|
|
'success': True,
|
|
'status': restaurant.status,
|
|
'expiry_date': fields.Date.to_string(restaurant.expiry_date)
|
|
})
|
|
except Exception as e:
|
|
return self._json_response({'error': str(e)}, status=500)
|
|
|
|
@http.route('/api/v1/saas/delete_restaurant', type='json', auth='none', methods=['POST'], csrf=False)
|
|
def api_delete_restaurant(self):
|
|
if not self._authenticate():
|
|
return self._json_response({'error': 'Unauthorized'}, status=401)
|
|
|
|
data = request.get_json_data()
|
|
db_name = data.get('database_name')
|
|
|
|
restaurant = request.env['saas.restaurant'].sudo().search([('database_name', '=', db_name)], limit=1)
|
|
if not restaurant:
|
|
return self._json_response({'error': 'Restaurant database not found'}, status=404)
|
|
|
|
try:
|
|
master_pwd = config.get('admin_passwd', 'admin')
|
|
# 1. Drop database in PostgreSQL via Odoo service API
|
|
db.exp_drop(master_pwd, db_name)
|
|
# 2. Delete database registry entry in master
|
|
restaurant.unlink()
|
|
return self._json_response({'success': True})
|
|
except Exception as e:
|
|
return self._json_response({'error': str(e)}, status=500)
|
|
|
|
@http.route('/api/v1/saas/backup_restaurant', type='json', auth='none', methods=['POST'], csrf=False)
|
|
def api_backup_restaurant(self):
|
|
if not self._authenticate():
|
|
return self._json_response({'error': 'Unauthorized'}, status=401)
|
|
|
|
data = request.get_json_data()
|
|
db_name = data.get('database_name')
|
|
storage_type = data.get('storage_type', 'local')
|
|
|
|
restaurant = request.env['saas.restaurant'].sudo().search([('database_name', '=', db_name)], limit=1)
|
|
if not restaurant:
|
|
return self._json_response({'error': 'Restaurant database not found'}, status=404)
|
|
|
|
try:
|
|
backup = request.env['saas.backup'].sudo().create({
|
|
'restaurant_id': restaurant.id,
|
|
'storage_type': storage_type
|
|
})
|
|
backup.action_backup_database()
|
|
return self._json_response({
|
|
'success': True,
|
|
'backup_file': backup.backup_file,
|
|
's3_url': backup.s3_url
|
|
})
|
|
except Exception as e:
|
|
return self._json_response({'error': str(e)}, status=500)
|