From 4664bb8f3fe54231da555af6246ad917d08a22bb Mon Sep 17 00:00:00 2001 From: litnimax Date: Mon, 15 Dec 2025 14:49:31 +0200 Subject: [PATCH 01/38] Step 1 --- CLAUDE.md | 318 ++++++++++++++++++ LICENSE_README.md | 203 +++++++++++ connect/__init__.py | 1 + connect/__manifest__.py | 7 +- connect/hooks.py | 42 +++ connect/models/__init__.py | 2 + connect/models/license.py | 254 ++++++++++++++ connect/models/module.py | 18 + connect/models/settings.py | 6 + connect/security/admin.xml | 11 + .../license_banner/license_banner.js | 72 ++++ .../license_banner/license_banner.scss | 63 ++++ .../license_banner/license_banner.xml | 13 + connect/views/settings.xml | 8 + generate_license_token.py | 183 ++++++++++ 15 files changed, 1199 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md create mode 100644 LICENSE_README.md create mode 100644 connect/hooks.py create mode 100644 connect/models/license.py create mode 100644 connect/models/module.py create mode 100644 connect/static/src/components/license_banner/license_banner.js create mode 100644 connect/static/src/components/license_banner/license_banner.scss create mode 100644 connect/static/src/components/license_banner/license_banner.xml create mode 100755 generate_license_token.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..454995dc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,318 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +This is the **Oduist Connect** repository - a collection of Odoo addon modules that integrate Twilio telephony (voice calls, SMS, WhatsApp) and ElevenLabs AI conversational agents with Odoo. The repository contains 7 modules organized in a modular architecture for different features and integrations. + +## Repository Structure + +### Core Modules + +- **connect** - Main Twilio-Odoo integration (voice calls, WhatsApp, SMS) +- **connect_byoc** - Bring Your Own Carrier functionality (€499) +- **connect_crm** - CRM integration for lead tracking from calls +- **connect_elevenlabs** - AI conversational agents using ElevenLabs +- **connect_elevenlabs_sale** - Sales management extension for AI agents (€999) +- **connect_helpdesk** - Helpdesk ticket integration +- **connect_website** - Website snippets for click-to-call + +### Branch Strategy + +- **Current working branch**: `19.0` (Odoo 19.0) +- **Main branch for PRs**: `18.0` (Odoo 18.0) +- Version branches correspond to Odoo major versions + +## Development Commands + +### Odoo Module Development + +This is an Odoo addon repository. Modules are installed and tested within an Odoo instance. + +**Install a module:** +```bash +# From Odoo instance, install via Apps menu or: +odoo-bin -i connect -d your_database +``` + +**Upgrade a module after changes:** +```bash +odoo-bin -u connect -d your_database +``` + +**Restart Odoo with auto-reload:** +```bash +odoo-bin --dev=all -d your_database +``` + +### ElevenLabs Service (Standalone Python Service) + +The `connect_elevenlabs/service/` directory contains a standalone FastAPI service that bridges Twilio calls with ElevenLabs AI. + +**Setup:** +```bash +cd connect_elevenlabs/service +cp .env.example .env +# Edit .env with your credentials +uv sync +``` + +**Run the service:** +```bash +cd connect_elevenlabs/service +uv run main.py +``` + +**Required environment variables:** +- `ELEVENLABS_API_KEY` - ElevenLabs API key +- `ODOO_URL` - Full Odoo instance URL (e.g., https://your-instance.odoo.com) +- `ODOO_DB` - Database name +- `ODOO_USER` - Odoo user (typically 'connect') +- `ODOO_PASSWORD` - Odoo user password + +## Architecture Overview + +### Webhook-Driven Architecture + +All real-time telephony events flow through Twilio webhooks: + +``` +Twilio → Odoo Webhook Controller → Model Business Logic → Database → UI Update +``` + +**Key webhook controller:** `connect/controllers/twilio_webhooks.py` +- 11 webhook routes handling domain routing, call status, messages, recordings +- Request signature verification for security +- Uses special webhook user (`connect.user_connect_webhook`) + +### Core Models (connect/models/) + +**Call Management:** +- `connect.call` - Master call record (user-facing) +- `connect.channel` - Individual call legs (A-leg, B-leg technical tracking) +- `connect.recording` - Call recordings with OpenAI transcription + +**Communication:** +- `connect.message` - WhatsApp and SMS messages (bidirectional) +- `connect.whatsapp_sender` - WhatsApp Business number management + +**PBX Infrastructure:** +- `connect.user` - PBX users with SIP/WebRTC credentials +- `connect.exten` - Extensions (like traditional PBX: 101, 102, etc.) +- `connect.domain` - Twilio SIP domains +- `connect.number` - Phone numbers +- `connect.callflow` - IVR/call routing flows +- `connect.twiml` - TwiML applications + +**Configuration:** +- `connect.settings` - Single-record model storing all configuration (API keys, regions, features) + +### Two-Tier Call Tracking + +- **connect.call** - High-level call record that users see +- **connect.channel** - Low-level tracking of individual call legs (for transfers, conferences) + +### Extension System (PBX-like) + +Extensions (`connect.exten`) can point to: +- Users (ring their phone) +- Call flows (IVR menus) +- TwiML applications (custom logic) + +Uses Reference field pattern for polymorphic destinations. + +### WhatsApp Integration + +**Incoming messages flow:** +``` +WhatsApp User → Twilio → /twilio/webhook/message → connect.message.receive() +``` + +**Message Configuration** (`connect.message_configuration`): +- Routes incoming messages to specific Odoo models (Partner, Lead, Ticket) +- Pattern matching on sender/recipient numbers +- Automatic record creation + +### ElevenLabs Service Architecture + +``` +┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ Twilio │ WebRTC │ FastAPI │ HTTP │ Odoo │ +│ Phone │◄───────►│ Service │◄───────►│ Instance │ +│ Call │ │ (main.py) │ │ │ +└─────────────┘ └──────────────────┘ └──────────────┘ + │ + │ WebSocket + ▼ + ┌──────────────┐ + │ ElevenLabs │ + │ Conv. AI │ + └──────────────┘ +``` + +**Components:** +- `main.py` - FastAPI WebSocket handler, ElevenLabs integration, Odoo RPC connection +- `twilio_audio_interface.py` - Audio format conversion between Twilio and ElevenLabs + +**AI Agents** (`connect.elevenlabs_agent` model): +- Configurable voice, prompt, temperature, LLM model +- Supports GPT-4, Claude, Gemini, Grok +- 30+ language support +- Custom tools/functions for Odoo queries and transfers + +### Frontend Architecture + +**Technology:** Odoo OWL framework (Odoo Web Library) + +**Main phone component:** `connect/static/src/components/phone/phone/phone.js` +- Floating phone widget with drag-and-drop positioning +- Twilio JS SDK for WebRTC +- BroadcastChannel API for cross-tab call synchronization +- Tabs: Contacts, Favorites, Call History +- Features: Mute, hold, transfer, DTMF keypad + +**Services:** +- `active_calls` - System-wide call state management +- `actions` - Phone actions (dial, hangup) +- `mail` - Integration with Odoo mail/chatter + +## Code Patterns and Conventions + +### Settings Model Pattern + +Single-record model for configuration (`connect/models/settings.py`): +- Uses `get_param()` and `set_param()` methods +- Protected fields for auth tokens (limited to security groups) +- Stores Twilio credentials, regions, API keys + +### Webhook Security + +All webhooks validate Twilio signatures: +```python +validator = RequestValidator(auth_token) +signature = request.headers.get('X-Twilio-Signature') +is_valid = validator.validate(url, data, signature) +``` + +### Reference Field Pattern + +Used for polymorphic relationships (e.g., messages/calls can link to any model): +```python +record = fields.Reference( + selection='_selection_target_model', + string='Related Record' +) +``` + +### Phone Number Normalization + +Always use E.164 format via `phonenumbers` library: +```python +import phonenumbers +parsed = phonenumbers.parse(number, region) +formatted = phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) +``` + +### TwiML Generation + +Three methods used throughout: +1. Static TwiML XML (stored in `connect.twiml`) +2. TwiPy (Python code that generates TwiML) +3. Model methods (e.g., `connect.domain.route_call()`) + +### Migration System + +- Migrations in `connect/migrations/` folder +- Version-based folders (0.6, 0.7, 0.8, 0.9, 1.0.1, 1.0.5, 1.0.6) +- Includes pre-migrate and post-migrate scripts + +## Security + +### Security Groups + +- `connect.group_connect_admin` - Full administrative access +- `connect.group_connect_user` - Regular user access +- Special webhook user with minimal permissions for API calls + +### Record Rules + +- User-level record rules in `security/user_record_rules.xml` +- Admin-level record rules in `security/admin_record_rules.xml` + +## External Dependencies + +### Python Packages (Odoo modules) + +**connect:** +- `twilio` - Twilio Python SDK +- `openai` - For call transcription +- `phonenumbers` - Phone number parsing/formatting +- `httpx` - Async HTTP client + +**connect_elevenlabs:** +- `elevenlabs==1.59.0` - ElevenLabs Python SDK + +### Python Packages (ElevenLabs service) + +See `connect_elevenlabs/service/pyproject.toml`: +- `fastapi[standard]>=0.115.11` +- `uvicorn>=0.34.0` +- `websockets>=15.0.1` +- `elevenlabs==1.59.0` +- `twilio>=9.5.1` +- `aio-odoorpc` - Async Odoo RPC +- `python-dotenv>=1.0.1` + +### JavaScript + +- Twilio Client JS SDK (WebRTC) +- Odoo OWL framework + +## Important Files + +### Configuration +- `connect/models/settings.py` - All Twilio/Connect settings +- `connect_elevenlabs/service/.env` - Service environment variables (not committed) +- `connect_elevenlabs/service/.env.example` - Template for service config + +### Webhooks +- `connect/controllers/twilio_webhooks.py` - All Twilio webhook handlers + +### Main Models +- `connect/models/call.py` - Call management (~627 lines) +- `connect/models/message.py` - WhatsApp/SMS messages (~517 lines) +- `connect/models/user.py` - PBX users (~461 lines) +- `connect_elevenlabs/models/agent.py` - AI agent configuration + +### Frontend +- `connect/static/src/components/phone/phone/phone.js` - Main phone widget +- `connect/static/src/services/active_calls/` - Call state management + +### Service +- `connect_elevenlabs/service/main.py` - FastAPI WebSocket service +- `connect_elevenlabs/service/twilio_audio_interface.py` - Audio streaming + +## Manifest Files + +Each module has a `__manifest__.py` defining: +- Module metadata (name, version, author, price) +- Dependencies +- External Python dependencies +- Data files (security, views, data) +- Assets (JavaScript, CSS) + +## Documentation References + +- [Connect Knowledge Base](https://oduist.com/knowledge/article/32) +- [Installation Video](https://www.youtube.com/watch?v=wPvkV3A-7Sw) + +## Key Architectural Insights + +1. **Stateless Webhooks** - All webhooks are stateless; state stored in database models +2. **Async Service** - ElevenLabs service uses async Python for real-time AI conversations +3. **Modular Design** - Each integration module extends core independently +4. **Multi-channel** - Same infrastructure handles voice calls, WhatsApp, and SMS +5. **Edge Computing** - Twilio edge support for low-latency global deployment (8 regions) +6. **Real-time UI** - WebRTC in browser, BroadcastChannel for multi-tab synchronization +7. **Security First** - Signature verification, special webhook users, group-based access control diff --git a/LICENSE_README.md b/LICENSE_README.md new file mode 100644 index 00000000..e1b13837 --- /dev/null +++ b/LICENSE_README.md @@ -0,0 +1,203 @@ +# Connect OPL-1 Licensing System + +This document describes the OPL-1 licensing system implemented for Connect modules. + +## Overview + +Connect modules now use the **Odoo Proprietary License v1.0 (OPL-1)** with a 30-day trial period and JWT-based license tokens. + +## How It Works + +### Trial Period +- Upon first installation, a 30-day trial period begins automatically +- The trial start date is tracked in the `connect.module` model +- No license token required during trial +- A banner appears in the navbar showing days remaining + +### License Token +- After trial expiration, a valid license token is required +- Tokens are JWT (JSON Web Tokens) signed with RS256 algorithm +- Tokens are module-specific and instance-specific +- Tokens can be obtained from [oduist.com](https://oduist.com/my) + +### Banner Behavior +- **Trial Active (>7 days)**: Blue info banner showing days left +- **Trial Active (≤7 days)**: Yellow warning banner with pulse animation +- **Trial Expired**: Red danger banner with pulse animation +- **Licensed**: No banner shown +- Click banner to open Settings page + +## Token Structure + +JWT payload contains: +```json +{ + "issuer": "oduist.com", + "instance_uid": "database_uuid", + "type": "production", + "expire": 1735689600, + "modules": ["connect", "connect_crm"], + "partner_id": "1", + "partner_name": "Partner Name" +} +``` + +## Generating Tokens + +### Using the Script + +Run the token generation script: +```bash +python3 generate_license_token.py +``` + +Follow the interactive prompts to: +1. Enter instance UID (get from Settings or database.uuid) +2. Select license type (production/trial/development) +3. Choose modules to license +4. Enter partner information +5. Set expiration date + +### Manual Token Generation + +```python +import jwt +from datetime import datetime, timedelta + +# Use the PRIVATE_KEY from generate_license_token.py + +payload = { + "issuer": "oduist.com", + "instance_uid": "your-database-uuid", + "type": "production", + "expire": int((datetime.now() + timedelta(days=365)).timestamp()), + "modules": ["connect"], + "partner_id": "1", + "partner_name": "Customer Name" +} + +token = jwt.encode(payload, PRIVATE_KEY, algorithm='RS256') +print(token) +``` + +## Installing a License + +1. Go to **Connect → Settings → API Keys** +2. Scroll to **License (OPL-1)** section +3. Paste the JWT token +4. Save settings +5. Refresh the page + +## Using the License Decorator + +Protect module functionality with the `@_license` decorator: + +```python +from odoo.addons.connect.models.license import _license + +class CRMLead(models.Model): + _inherit = 'crm.lead' + + @_license(module='connect_crm') + def action_call(self): + # This method requires connect_crm license + pass +``` + +The decorator: +- Checks if the trial is active or a valid license exists +- Raises `UserError` if trial expired and no valid license +- Shows helpful error message directing user to obtain license + +## Module Installation Tracking + +Each Connect module should: +1. Add entry to `connect.module` in `post_init_hook` +2. Remove entry in `uninstall_hook` + +Example (in module's `hooks.py`): +```python +def post_init_hook(env): + env['connect.module'].sudo().create({ + 'name': 'connect_crm', + 'description': 'Connect CRM Integration' + }) + +def uninstall_hook(env): + module = env['connect.module'].sudo().search([('name', '=', 'connect_crm')]) + if module: + module.unlink() +``` + +## Files Changed + +### New Files +- `connect/models/module.py` - Installation tracking +- `connect/models/license.py` - License validation and decorator +- `connect/hooks.py` - Installation hooks +- `connect/static/src/components/license_banner/` - JS banner component +- `generate_license_token.py` - Token generation script + +### Modified Files +- `connect/__manifest__.py` - License, hooks, dependencies, assets +- `connect/__init__.py` - Import hooks +- `connect/models/__init__.py` - Import new models +- `connect/models/settings.py` - Add license_token field +- `connect/views/settings.xml` - Add license_token UI +- `connect/security/admin.xml` - Add connect.module security + +## Security + +### Public Key +The public key for token verification is embedded in `connect/models/license.py` + +### Private Key +**IMPORTANT**: Keep the private key secure! +- Used only for signing tokens +- Store in secure location (password manager, encrypted file) +- Do not commit to public repositories +- Included in `generate_license_token.py` for convenience (should be moved to secure location in production) + +## API + +### Python +```python +# Check license status +self.env['connect.license'].get_license_status('connect') + +# Returns: +# { +# 'status': 'trial_active' | 'trial_expired' | 'licensed', +# 'days_left': 15, # for trial +# 'message': 'Trial: 15 days remaining' +# } +``` + +### JavaScript +The banner component automatically checks license status on load via RPC: +```javascript +await this.orm.call("connect.license", "get_license_status", ["connect"]) +``` + +## Troubleshooting + +### Token Invalid +- Check instance_uid matches database UUID +- Verify token hasn't expired +- Ensure token includes required module + +### Banner Not Showing +- Clear browser cache +- Check browser console for JS errors +- Verify assets were properly loaded + +### Trial Not Starting +- Check `connect.module` record exists +- Verify `create_date` is set correctly +- Check hooks executed during installation + +## Support + +For license-related issues: +- Email: support@oduist.com +- Website: https://oduist.com/support diff --git a/connect/__init__.py b/connect/__init__.py index e4f4917a..8057e32e 100644 --- a/connect/__init__.py +++ b/connect/__init__.py @@ -1,3 +1,4 @@ from . import controllers from . import models from . import wizard +from . import hooks diff --git a/connect/__manifest__.py b/connect/__manifest__.py index 7b05d8b6..990b7c0b 100644 --- a/connect/__manifest__.py +++ b/connect/__manifest__.py @@ -9,13 +9,13 @@ 'price': 0, 'currency': 'EUR', 'support': 'support@oduist.com', - 'license': 'Other proprietary', + 'license': 'OPL-1', 'category': 'Phone', 'summary': 'Twilio and Odoo integration application', 'description': '', 'depends': ['mail', 'contacts', 'sms'], 'external_dependencies': { - 'python': ['twilio', 'openai'], + 'python': ['twilio', 'openai', 'PyJWT'], }, 'data': [ 'data/res_users.xml', @@ -68,6 +68,7 @@ 'web.assets_backend': [ '/connect/static/src/icomoon/style.css', '/connect/static/src/components/phone/*/*', + '/connect/static/src/components/license_banner/*', '/connect/static/src/js/main.js', '/connect/static/src/js/utils.js', '/connect/static/src/widgets/phone_field/*', @@ -76,4 +77,6 @@ '/connect/static/src/services/mail/*', ], }, + 'post_init_hook': 'post_init_hook', + 'uninstall_hook': 'uninstall_hook', } diff --git a/connect/hooks.py b/connect/hooks.py new file mode 100644 index 00000000..f89bb156 --- /dev/null +++ b/connect/hooks.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +import logging + +_logger = logging.getLogger(__name__) + + +def post_init_hook(env): + """ + Hook called after module installation. + Creates a record in connect.module to track installation date for trial period. + """ + try: + # Check if record already exists + existing = env['connect.module'].sudo().search([('name', '=', 'connect')], limit=1) + + if not existing: + env['connect.module'].sudo().create({ + 'name': 'connect', + 'description': 'Oduist Connect - Twilio Integration for Odoo' + }) + _logger.info('Connect module installation tracked for trial period') + else: + _logger.info('Connect module already tracked (reinstall)') + + except Exception as e: + _logger.error('Error in post_init_hook: %s', str(e)) + + +def uninstall_hook(env): + """ + Hook called before module uninstallation. + Removes the tracking record from connect.module. + """ + try: + module = env['connect.module'].sudo().search([('name', '=', 'connect')], limit=1) + + if module: + module.unlink() + _logger.info('Connect module uninstall: tracking record removed') + + except Exception as e: + _logger.error('Error in uninstall_hook: %s', str(e)) diff --git a/connect/models/__init__.py b/connect/models/__init__.py index 7af4e4dd..ec35f7b8 100644 --- a/connect/models/__init__.py +++ b/connect/models/__init__.py @@ -5,6 +5,8 @@ from . import domain from . import exten from . import favorite +from . import license +from . import module from . import mail from . import message from . import message_configuration diff --git a/connect/models/license.py b/connect/models/license.py new file mode 100644 index 00000000..8630d74d --- /dev/null +++ b/connect/models/license.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +import logging +import jwt +from datetime import datetime, timedelta +from functools import wraps + +from odoo import models, api, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +# Public key for JWT RS256 verification +PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApghblBkWHc4sGxTcKRvb +sSs70Z9TMcGNZVJhZbPB6/WKREBrfNOJjz06pfae8iMk8LSnYlGLcKw6lVutpBce +FIMifmYQJRmgFbNqZ2M+Ew8me8o6gF2k2gwhNT6yo8avq2iFd9cC/Lbb+p7M3Lkn +vVz/RXHJ4QRvS+xbzfSsyE9E1CpYus8MccapuE3Jq4g6dWWU5Pk7bC5jJN3cpbcf +c2r29Wjh3dGw45tx54IQ7kz56z4uHPQZ6UKaZZDLhFxYq59rE3rrYi1m4YZIs36E +PXsj0dij391KNtMmUZ/qSNqYII6eprKiHjZNVpANb2ee32xJTq2fIPSvw4qILOn/ +nwIDAQAB +-----END PUBLIC KEY-----""" + +TRIAL_DAYS = 30 + + +class ConnectLicense(models.AbstractModel): + """ + License validation and management for Connect OPL-1 modules. + Handles JWT token validation and 30-day trial period. + """ + _name = 'connect.license' + _description = 'Connect License Manager' + + @api.model + def _get_license_token(self): + """Get license token from settings.""" + return self.env['connect.settings'].sudo().get_param('license_token', default='') + + @api.model + def _get_database_uuid(self): + """Get database UUID from system parameters.""" + return self.env['ir.config_parameter'].sudo().get_param('database.uuid', default='') + + @api.model + def validate_token(self, token): + """ + Validate JWT token with RS256 algorithm. + + Returns: + dict: Decoded payload if valid + None: If token is invalid + """ + if not token: + return None + + try: + payload = jwt.decode( + token, + PUBLIC_KEY, + algorithms=['RS256'], + options={'verify_signature': True} + ) + + # Verify issuer + if payload.get('issuer') != 'oduist.com': + _logger.warning('Invalid token issuer: %s', payload.get('issuer')) + return None + + # Verify instance UUID + db_uuid = self._get_database_uuid() + if payload.get('instance_uid') != db_uuid: + _logger.warning('Token instance_uid mismatch. Expected: %s, Got: %s', + db_uuid, payload.get('instance_uid')) + return None + + # Verify expiration + expire_timestamp = payload.get('expire') + if expire_timestamp and datetime.now().timestamp() > expire_timestamp: + _logger.warning('Token expired at %s', + datetime.fromtimestamp(expire_timestamp)) + return None + + return payload + + except jwt.ExpiredSignatureError: + _logger.warning('Token signature expired') + return None + except jwt.InvalidTokenError as e: + _logger.warning('Invalid token: %s', str(e)) + return None + except Exception as e: + _logger.error('Error validating token: %s', str(e)) + return None + + @api.model + def is_trial_valid(self, module_name='connect'): + """ + Check if trial period is still valid (30 days from installation). + + Args: + module_name: Name of the module to check + + Returns: + tuple: (is_valid: bool, days_left: int) + """ + module = self.env['connect.module'].sudo().search([('name', '=', module_name)], limit=1) + + if not module: + # Module not found, consider trial invalid + return False, 0 + + install_date = module.create_date + now = datetime.now() + days_passed = (now - install_date).days + days_left = TRIAL_DAYS - days_passed + + is_valid = days_left > 0 + + return is_valid, max(0, days_left) + + @api.model + def get_license_status(self, module_name='connect'): + """ + Get comprehensive license status for a module. + + Returns: + dict: { + 'status': 'trial_active' | 'trial_expired' | 'licensed' | 'invalid_token', + 'days_left': int (for trial), + 'expire_date': datetime (for licensed), + 'modules': list (for licensed), + 'partner_name': str (for licensed), + 'message': str (user-friendly message) + } + """ + token = self._get_license_token() + + # Try to validate token first + if token: + payload = self.validate_token(token) + if payload: + modules = payload.get('modules', []) + if module_name in modules: + expire_timestamp = payload.get('expire') + expire_date = datetime.fromtimestamp(expire_timestamp) if expire_timestamp else None + + return { + 'status': 'licensed', + 'expire_date': expire_date, + 'modules': modules, + 'partner_name': payload.get('partner_name', ''), + 'partner_id': payload.get('partner_id', ''), + 'type': payload.get('type', 'production'), + 'message': _('Licensed to %s') % payload.get('partner_name', 'Unknown') + } + else: + # Token exists but invalid + # Fall back to trial check + pass + + # No valid token, check trial + is_valid, days_left = self.is_trial_valid(module_name) + + if is_valid: + return { + 'status': 'trial_active', + 'days_left': days_left, + 'message': _('Trial: %d days remaining') % days_left + } + else: + return { + 'status': 'trial_expired', + 'days_left': 0, + 'message': _('Trial expired. Please register your copy.') + } + + @api.model + def check_license(self, module_name): + """ + Check if user has access to a specific module. + Raises UserError if no valid license found. + + Args: + module_name: Name of the module to check + + Raises: + UserError: If license is invalid or expired + """ + status = self.get_license_status(module_name) + + # For now, we don't block functionality, just inform + # In future, you can uncomment the following to block: + # if status['status'] not in ['trial_active', 'licensed']: + # raise UserError(_( + # 'License required for module "%s".\n\n' + # 'Your trial period has expired. Please obtain a valid license token.\n\n' + # 'Contact: oduist.com' + # ) % module_name) + + return status + + +def _license(module='connect'): + """ + Decorator to check license for specific module before executing method. + + Usage: + from odoo.addons.connect.models.license import _license + + class MyModel(models.Model): + _name = 'my.model' + + @_license(module='connect_crm') + def action_call(self): + # This method requires connect_crm license + pass + + Args: + module: Name of the module to check license for + + Raises: + UserError: If license is not valid for the specified module + """ + def decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + # Check license + license_manager = self.env['connect.license'] + status = license_manager.get_license_status(module) + + # Block if trial expired and no valid license + if status['status'] == 'trial_expired': + raise UserError(_( + 'License required for module "%s".\n\n' + 'Your trial period has expired. Please obtain a valid license token ' + 'from oduist.com and enter it in Connect Settings.\n\n' + 'Status: %s' + ) % (module, status['message'])) + + # If licensed, check if module is in the licensed modules list + if status['status'] == 'licensed': + if module not in status.get('modules', []): + raise UserError(_( + 'License required for module "%s".\n\n' + 'Your license does not include this module. ' + 'Please upgrade your license at oduist.com.\n\n' + 'Currently licensed modules: %s' + ) % (module, ', '.join(status.get('modules', [])))) + + # Execute the original function + return func(self, *args, **kwargs) + + return wrapper + return decorator diff --git a/connect/models/module.py b/connect/models/module.py new file mode 100644 index 00000000..98d7b02c --- /dev/null +++ b/connect/models/module.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class ConnectModule(models.Model): + """ + Service model to track installed Connect modules and their installation dates. + Used for trial period calculation (30 days from create_date). + """ + _name = 'connect.module' + _description = 'Connect Module Installation Tracker' + + name = fields.Char(string='Module Name', required=True, index=True) + description = fields.Char(string='Description') + + _sql_constraints = [ + ('name_unique', 'UNIQUE(name)', 'Module name must be unique!') + ] diff --git a/connect/models/settings.py b/connect/models/settings.py index 63c48939..2349fd58 100644 --- a/connect/models/settings.py +++ b/connect/models/settings.py @@ -120,6 +120,12 @@ class Settings(models.Model): twilio_balance = fields.Char(readonly=True) openai_api_key = fields.Char(groups="base.group_erp_manager") display_openai_api_key = fields.Char() + # License token for OPL-1 validation + license_token = fields.Text( + string="License Token", + groups="base.group_erp_manager", + help="JWT license token from oduist.com. Leave empty to use 30-day trial." + ) number_search_operation = fields.Selection( [("=", "Equal"), ("like", "Like")], default="=", required=True ) diff --git a/connect/security/admin.xml b/connect/security/admin.xml index 78013517..ef49f29d 100644 --- a/connect/security/admin.xml +++ b/connect/security/admin.xml @@ -11,6 +11,17 @@ + + + connect_module_admin + + + + + + + + connect_transfer_wizard_admin diff --git a/connect/static/src/components/license_banner/license_banner.js b/connect/static/src/components/license_banner/license_banner.js new file mode 100644 index 00000000..9b355cab --- /dev/null +++ b/connect/static/src/components/license_banner/license_banner.js @@ -0,0 +1,72 @@ +/** @odoo-module **/ + +import { Component, onWillStart, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +export class LicenseBanner extends Component { + static template = "connect.LicenseBanner"; + + setup() { + this.orm = useService("orm"); + this.state = useState({ + visible: false, + status: null, + message: "", + type: "info", // info, warning, danger + }); + + onWillStart(async () => { + await this.loadLicenseStatus(); + }); + } + + async loadLicenseStatus() { + try { + const result = await this.orm.call( + "connect.license", + "get_license_status", + ["connect"] + ); + + if (!result) { + return; + } + + // Show banner for trial and expired states + if (result.status === "trial_active") { + this.state.visible = true; + this.state.status = result.status; + this.state.message = `Connect Trial: ${result.days_left} days remaining`; + this.state.type = result.days_left <= 7 ? "warning" : "info"; + } else if (result.status === "trial_expired") { + this.state.visible = true; + this.state.status = result.status; + this.state.message = "Connect Trial Expired - Please Register"; + this.state.type = "danger"; + } + // Don't show banner for licensed state + } catch (error) { + console.error("Failed to load license status:", error); + } + } + + get bannerClass() { + const baseClass = "connect-license-banner"; + return `${baseClass} ${baseClass}-${this.state.type}`; + } + + openSettings() { + this.env.services.action.doAction({ + type: "ir.actions.server", + id: "connect.connect_settings_action", + }); + } +} + +// Register as a systray item to appear in navbar +export const systrayItem = { + Component: LicenseBanner, +}; + +registry.category("systray").add("connect.LicenseBanner", systrayItem, { sequence: 1 }); diff --git a/connect/static/src/components/license_banner/license_banner.scss b/connect/static/src/components/license_banner/license_banner.scss new file mode 100644 index 00000000..1e4786f7 --- /dev/null +++ b/connect/static/src/components/license_banner/license_banner.scss @@ -0,0 +1,63 @@ +.connect-license-banner { + display: inline-flex; + align-items: center; + padding: 4px 12px; + margin: 0 8px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + + &:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + i { + font-size: 14px; + } + + // Info state (trial with more than 7 days) + &-info { + background-color: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; + } + + // Warning state (trial with 7 or fewer days) + &-warning { + background-color: #fff3cd; + color: #856404; + border: 1px solid #ffeaa7; + animation: pulse-warning 2s ease-in-out infinite; + } + + // Danger state (trial expired) + &-danger { + background-color: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + animation: pulse-danger 1.5s ease-in-out infinite; + } +} + +@keyframes pulse-warning { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.4); + } + 50% { + box-shadow: 0 0 0 4px rgba(255, 193, 7, 0); + } +} + +@keyframes pulse-danger { + 0%, 100% { + box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); + } + 50% { + box-shadow: 0 0 0 4px rgba(220, 53, 69, 0); + } +} diff --git a/connect/static/src/components/license_banner/license_banner.xml b/connect/static/src/components/license_banner/license_banner.xml new file mode 100644 index 00000000..d0c0eee2 --- /dev/null +++ b/connect/static/src/components/license_banner/license_banner.xml @@ -0,0 +1,13 @@ + + + + +
+ + + + +
+
+ +
diff --git a/connect/views/settings.xml b/connect/views/settings.xml index e0183817..9a79d3ea 100644 --- a/connect/views/settings.xml +++ b/connect/views/settings.xml @@ -59,6 +59,14 @@ + + +
+ Enter your license token from oduist.com. Leave empty to use 30-day trial. +
+ +
+
diff --git a/generate_license_token.py b/generate_license_token.py new file mode 100755 index 00000000..5790a55f --- /dev/null +++ b/generate_license_token.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +License Token Generator for Connect OPL-1 + +This script generates JWT license tokens for Connect module instances. +The tokens are signed with RS256 algorithm using the private key. + +Usage: + python3 generate_license_token.py + +Author: Oduist +""" + +import jwt +import json +from datetime import datetime, timedelta + +# Private key for signing tokens (keep this secure!) +PRIVATE_KEY = """-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmCFuUGRYdziwb +FNwpG9uxKzvRn1MxwY1lUmFls8Hr9YpEQGt804mPPTql9p7yIyTwtKdiUYtwrDqV +W62kFx4UgyJ+ZhAlGaAVs2pnYz4TDyZ7yjqAXaTaDCE1PrKjxq+raIV31wL8ttv6 +nszcuSe9XP9FccnhBG9L7FvN9KzIT0TUKli6zwxxxqm4TcmriDp1ZZTk+TtsLmMk +3dyltx9zavb1aOHd0bDjm3HnghDuTPnrPi4c9BnpQpplkMuEXFirn2sTeutiLWbh +hkizfoQ9eyPR2KPf3Uo20yZRn+pI2pggjp6msqIeNk1WkA1vZ57fbElOrZ8g9K/D +iogs6f+fAgMBAAECggEAEWUyGxZ0b75TIVSSgH1vtg8+iYbffjNg2hXDKJb9y/gC +JYZ4/IS5QS2mwGP8tofMw1SvbiihqwtlvpJIDgzHVQTW5Kv9fbWX784daXmcs6h/ +CAc3ETiTy1iWqMUfOEjad4364IRsHAgYjMMohD5N6z7GwVgw+4dYayRMtguwMp2Y +DrHb+Z9M6SunUjY6Pxd0dEqJAEBgdwkjRhP09fAewAf5AAnELiTRBjwCHsAPdAoV +OGqJsQJ8DKFyCRoC86vPGIUJHFP27U/UPEG30gOvqQGmAHx0rktBxdbRzXL6/6Oy +MBtM7RxzoLdqwXkU+I8wdZljBKv6P1EpP2Y+VeCG8QKBgQDlU75uViNyShre6wmB +HJC7W96DnZB7WdxuJBFYY4MF5lT2I1UIG6MyZgfQL5P5dOgzjwSJu+/EQOb6hGDI +vpqxz+zrgj9wB5q6PlsigU92k9L/ASEX+TP1cUAurcEBN9uLnpSSUv71FLu4p7ib +sZ/X9ppPgRu1u3fPX5o3HAMhJwKBgQC5WAJtrMQdX/XX7jrIsIISIUIR6IV4Ew5G +tTEq+AgOD+xxik2oE1j9qYUnDYN0JP0t9uxAEPm05mEEqTk0lRxPCyqgbN2nrGTo +Q0Cr143NcocvXImR41ukfhE/PTAiWQoEQCdFiqS4/H14pmWQeegAZVcHjvJN/QeJ +gv1puw5IyQKBgQDQ1tya2nLZR8cEroIvQ/ZByT3wGfNTgdgNrWbmWWkeXE2PAUoU +YibSZLxEyK83A1HacimtzKpizMAL77W72mhB+ZpGNozS1vn/FX4lBCF7WM9TTpH2 +pQi+Qe4zFCSpmVaj5TxjrJVmVwVE+ehSUQXBxF9ue6LicuB+xw9HlIj9DQKBgH7r +3OXcDISNJR5UXl72OGxP6B25XEToz7rt85iYN3PhxanO6vTxIty6TJt8rotHlTT3 +xbrtpQITTVbSx4DRp4wdenhXdMaQ0J0ZCN1khA+voRF2ziJgTm5rgkYLEb5DuQ9G +G16M3dZr2URYtm5kfNJgk2NyqU1su8+YKw9PcC25AoGBAM9cKUb9FnpKRZaK5e6G +Y3ZSKYQOkXGaGX+lkPivUodrWJOBW+Sjk2moQVR4Le3csdbrYxokG4w8rUCjXl2r +Nnef0FGy9k6m7kWXJGIhznROoCw14SWy9fUb+zFuYNA2C5zJnMIzZXNh79BzaJ5G +LMxcrbPKXV/ZXwyQQXoYE5Rp +-----END PRIVATE KEY-----""" + + +def print_banner(): + """Print script banner.""" + print("=" * 60) + print("Connect OPL-1 License Token Generator") + print("=" * 60) + print() + + +def get_input(prompt, default=None): + """Get user input with optional default value.""" + if default: + prompt = f"{prompt} [{default}]" + value = input(f"{prompt}: ").strip() + return value if value else default + + +def get_modules(): + """Get list of modules from user.""" + print("\nEnter modules (comma-separated):") + print("Available: connect, connect_crm, connect_byoc, connect_elevenlabs, connect_helpdesk") + modules_str = input("Modules: ").strip() + return [m.strip() for m in modules_str.split(',') if m.strip()] + + +def get_expiration_date(): + """Get expiration date from user.""" + print("\nExpiration date:") + print("1. 1 year from now") + print("2. 2 years from now") + print("3. Custom date (YYYY-MM-DD)") + print("4. Never (no expiration)") + + choice = input("Choose option [1]: ").strip() or "1" + + if choice == "1": + return int((datetime.now() + timedelta(days=365)).timestamp()) + elif choice == "2": + return int((datetime.now() + timedelta(days=730)).timestamp()) + elif choice == "3": + date_str = input("Enter date (YYYY-MM-DD): ").strip() + try: + date_obj = datetime.strptime(date_str, "%Y-%m-%d") + return int(date_obj.timestamp()) + except ValueError: + print("Invalid date format. Using 1 year from now.") + return int((datetime.now() + timedelta(days=365)).timestamp()) + elif choice == "4": + # Set expiration to year 2100 + return int(datetime(2100, 1, 1).timestamp()) + else: + print("Invalid choice. Using 1 year from now.") + return int((datetime.now() + timedelta(days=365)).timestamp()) + + +def generate_token(): + """Generate license token interactively.""" + print_banner() + + # Collect license information + instance_uid = get_input("Instance UID (database UUID)", "") + if not instance_uid: + print("Error: Instance UID is required!") + return None + + license_type = get_input("License type (production/trial/development)", "production") + modules = get_modules() + + if not modules: + print("Error: At least one module is required!") + return None + + partner_id = get_input("Partner ID", "1") + partner_name = get_input("Partner Name", "Customer") + expire_timestamp = get_expiration_date() + + # Create payload + payload = { + "issuer": "oduist.com", + "instance_uid": instance_uid, + "type": license_type, + "expire": expire_timestamp, + "modules": modules, + "partner_id": partner_id, + "partner_name": partner_name + } + + # Display payload + print("\n" + "=" * 60) + print("License Token Payload:") + print("=" * 60) + print(json.dumps(payload, indent=2)) + print() + + # Confirm + confirm = input("Generate token with this payload? (y/n) [y]: ").strip().lower() + if confirm and confirm != 'y': + print("Token generation cancelled.") + return None + + # Generate token + try: + token = jwt.encode(payload, PRIVATE_KEY, algorithm='RS256') + return token + except Exception as e: + print(f"Error generating token: {e}") + return None + + +def main(): + """Main function.""" + token = generate_token() + + if token: + print("\n" + "=" * 60) + print("SUCCESS! Your license token:") + print("=" * 60) + print(token) + print("=" * 60) + print("\nCopy this token and paste it into Connect Settings -> API Keys -> License Token") + print() + + # Save to file + save = input("Save token to file? (y/n) [y]: ").strip().lower() + if not save or save == 'y': + filename = input("Filename [license_token.txt]: ").strip() or "license_token.txt" + try: + with open(filename, 'w') as f: + f.write(token) + print(f"Token saved to {filename}") + except Exception as e: + print(f"Error saving file: {e}") + + +if __name__ == "__main__": + main() From 563ba25359c3789c188a30176b179c38248730af Mon Sep 17 00:00:00 2001 From: litnimax Date: Mon, 15 Dec 2025 14:02:47 +0000 Subject: [PATCH 02/38] Step 2 --- .goosehints | 113 ++++++++++++++++++++++++++ connect/.goose/memory/development.txt | 27 ++++++ connect/.goose/memory/project.txt | 26 ++++++ connect/__init__.py | 39 ++++++++- connect/__manifest__.py | 2 +- connect/hooks.py | 42 ---------- connect/models/__init__.py | 1 - connect/models/license.py | 2 +- connect/models/module.py | 18 ---- connect/security/admin.xml | 11 --- connect/views/settings.xml | 2 + 11 files changed, 208 insertions(+), 75 deletions(-) create mode 100644 .goosehints create mode 100644 connect/.goose/memory/development.txt create mode 100644 connect/.goose/memory/project.txt delete mode 100644 connect/hooks.py delete mode 100644 connect/models/module.py diff --git a/.goosehints b/.goosehints new file mode 100644 index 00000000..f0136ffb --- /dev/null +++ b/.goosehints @@ -0,0 +1,113 @@ +# .goosehints - Repository-wide guidance for goose AI assistant + +## Repository Overview + +This is the **Oduist Connect** repository - a collection of Odoo addon modules for Twilio telephony and ElevenLabs AI integration with Odoo. + +### Module Architecture + +**Core Modules:** +- **connect** - Main Twilio-Odoo integration (voice calls, WhatsApp, SMS) +- **connect_byoc** - Bring Your Own Carrier functionality (€499) +- **connect_crm** - CRM integration for lead tracking from calls +- **connect_elevenlabs** - AI conversational agents using ElevenLabs +- **connect_elevenlabs_sale** - Sales management extension for AI agents (€999) +- **connect_helpdesk** - Helpdesk ticket integration +- **connect_website** - Website snippets for click-to-call + +### Key Technical Patterns + +#### Module Structure +Each module follows standard Odoo addon structure: +- `models/` - Python model definitions +- `views/` - XML view definitions +- `security/` - Access rights and groups +- `data/` - Data files and default records +- `controllers/` - HTTP routes and web controllers +- `static/` - Static assets (JS, CSS, images) + +#### Core Data Models (from `connect` module) +**PBX Infrastructure:** +- `connect.user` - PBX users with SIP/WebRTC credentials +- `connect.exten` - Extensions (101, 102, etc.) +- `connect.domain` - Twilio SIP domains +- `connect.number` - Phone numbers +- `connect.callflow` - IVR/call routing flows +- `connect.twiml` - TwiML applications + +**Call Tracking:** +- `connect.call` - High-level call records (user-facing) +- `connect.channel` - Low-level call leg tracking + +**Configuration:** +- `connect.settings` - Single-record model for all configuration + +### Development Guidelines + +#### License System (OPL-1) +- Uses JWT tokens with RS256 validation +- 30-day trial period from module installation date +- License validation in `models/license.py` (ConnectLicense abstract model) +- Uses `ir.module.module.create_date` for trial tracking (NOT custom connect.module model) + +#### Cross-Version Compatibility +- Supports Odoo 15.0+ +- `post_init_hook` works with both Odoo 15 (cr, registry) and Odoo 16+ (env) signatures +- Use `*args` and detect argument count for compatibility + +#### Common Patterns +- Use `@api.model` for model-level methods +- Use `.sudo()` for admin operations +- Single-record settings pattern (get_param/set_param) +- Reference fields for polymorphic relationships + +### File Naming Conventions +- Model files: `snake_case.py` (e.g., `call.py`, `user.py`) +- View files: `snake_case.xml` (e.g., `call.xml`, `user.xml`) +- Classes: `PascalCase` (e.g., `ConnectCall`, `ConnectUser`) +- Models: `snake_case` with underscore (e.g., `connect.call`, `connect.user`) + +### Useful Commands + +```bash +# Find files by name +rg --files | rg + +# Search for patterns +rg --files-with-matches 'pattern' +grep -r "pattern" --exclude-dir=.git + +# Find model definitions +find . -name "*.py" -exec grep -l "_name.*=.*connect\." {} \; + +# Check XML model references +find . -name "*.xml" -exec grep -l "connect\." {} \; +``` + +### Security & Access Control +- Use `env['res.users'].has_group()` for group-based permissions +- Follow Odoo's record rules and access rights patterns +- Use `.sudo()` for administrative operations only when necessary + +### Testing & Debugging +- Use `--dev=all` for auto-reload during development +- Check logs for proper error handling +- Verify license validation flows during testing +- Test with both trial and licensed scenarios + +### Integration Points +- **Twilio**: Webhooks, REST API, real-time communications +- **ElevenLabs**: AI voice conversations via FastAPI service +- **Odoo Core**: CRM, Helpdesk, Sales, Website modules + +## Memory Storage +- Project-specific: `.goose/memory/` (in each module) +- Global: `~/.config/goose/memory/` +- Store all technical information in English +- Use structured tags for better retrieval + +## Current Working Context +- Working branch: 18.0 (main development) +- Target branch: 19.0 (next version) +- Minimum Odoo version: 15.0 +- License: OPL-1 (Open Power License 1.0) diff --git a/connect/.goose/memory/development.txt b/connect/.goose/memory/development.txt new file mode 100644 index 00000000..be6dcf75 --- /dev/null +++ b/connect/.goose/memory/development.txt @@ -0,0 +1,27 @@ +# refactoring connect license module ir.module.module post_init_hook odoo15 odoo16 +Connect Module Refactoring (OPL-1) - removed connect.module model and switched to ir.module.module: + +✅ COMPLETED: +- Removed custom 'connect.module' model (file models/module.py) +- Updated license.py to use ir.module.module.create_date instead of connect.module +- Updated post_init_hook for Odoo 15.0+ compatibility (cross-version support) +- Removed uninstall_hook (no longer needed) +- Removed connect.module access rights from security/admin.xml +- Removed module import from models/__init__.py +- File hooks.py deleted, code moved to __init__.py +- Uncommented post_init_hook in __manifest__.py + +🔧 KEY CHANGES: +- Method is_trial_valid() now uses: env['ir.module.module'].sudo().search([('name', '=', module_name)]) +- post_init_hook(*) supports Odoo 15 (cr, registry) and Odoo 16+ (env) +- Installation date updated via: module.sudo().write({'create_date': fields.Datetime.now()}) + +📁 AFFECTED FILES: +- models/module.py (deleted) +- models/license.py (updated) +- __init__.py (added post_init_hook) +- __manifest__.py (post_init_hook activated) +- security/admin.xml (removed rights) +- models/__init__.py (removed import) +- hooks.py (deleted, code moved) + diff --git a/connect/.goose/memory/project.txt b/connect/.goose/memory/project.txt new file mode 100644 index 00000000..01b9666a --- /dev/null +++ b/connect/.goose/memory/project.txt @@ -0,0 +1,26 @@ +# connect project configuration license dependencies architecture +Connect OPL-1 Module - current project information: + +📋 CONFIGURATION: +- Path: /srv/oduist/connect_addons/18.0/connect/ +- Version: 1.0.12 +- Minimum Odoo version: 15.0 +- License: OPL-1 +- Author: Oduist + +🔧 DEPENDENCIES: +- 'mail', 'contacts', 'sms' +- Python: 'twilio', 'openai', 'PyJWT' + +🔐 LICENSING SYSTEM: +- JWT tokens with RS256 validation +- 30-day trial period from create_date +- Uses ir.module.module.create_date for tracking +- Public key stored in license.py + +🏗️ ARCHITECTURE: +- Removed connect.module model (previously used for tracking) +- License system in models/license.py (ConnectLicense abstract model) +- Settings in views/settings.xml (license_token, registration_number fields) +- post_init_hook in __init__.py for create_date setup + diff --git a/connect/__init__.py b/connect/__init__.py index 8057e32e..8748f072 100644 --- a/connect/__init__.py +++ b/connect/__init__.py @@ -1,4 +1,41 @@ +# -*- coding: utf-8 -*- +import logging +from odoo import fields, api, SUPERUSER_ID + +_logger = logging.getLogger(__name__) + + +def post_init_hook(*args): + """ + Hook called after module installation. + Updates the create_date field in ir.module.module for trial period calculation. + + Compatible with both Odoo 15 (cr, registry) and Odoo 16+ (env) signatures. + """ + try: + # Handle different Odoo versions + if len(args) == 1: + # Odoo 16+ - single env argument + env = args[0] + else: + # Odoo 15 - cr and registry arguments + cr, registry = args + env = api.Environment(cr, SUPERUSER_ID, {}) + + # Find the connect module record + module = env['ir.module.module'].sudo().search([('name', '=', 'connect')], limit=1) + + if module: + # Update create_date to current time to ensure proper trial period calculation + module.sudo().write({'create_date': fields.Datetime.now()}) + _logger.info('Connect module installation date updated for trial period') + else: + _logger.warning('Connect module not found in ir.module.module') + + except Exception as e: + _logger.error('Error in post_init_hook: %s', str(e)) + + from . import controllers from . import models from . import wizard -from . import hooks diff --git a/connect/__manifest__.py b/connect/__manifest__.py index 990b7c0b..f38b9e1f 100644 --- a/connect/__manifest__.py +++ b/connect/__manifest__.py @@ -17,6 +17,7 @@ 'external_dependencies': { 'python': ['twilio', 'openai', 'PyJWT'], }, + 'sequences': True, 'data': [ 'data/res_users.xml', 'data/data.xml', @@ -78,5 +79,4 @@ ], }, 'post_init_hook': 'post_init_hook', - 'uninstall_hook': 'uninstall_hook', } diff --git a/connect/hooks.py b/connect/hooks.py deleted file mode 100644 index f89bb156..00000000 --- a/connect/hooks.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -import logging - -_logger = logging.getLogger(__name__) - - -def post_init_hook(env): - """ - Hook called after module installation. - Creates a record in connect.module to track installation date for trial period. - """ - try: - # Check if record already exists - existing = env['connect.module'].sudo().search([('name', '=', 'connect')], limit=1) - - if not existing: - env['connect.module'].sudo().create({ - 'name': 'connect', - 'description': 'Oduist Connect - Twilio Integration for Odoo' - }) - _logger.info('Connect module installation tracked for trial period') - else: - _logger.info('Connect module already tracked (reinstall)') - - except Exception as e: - _logger.error('Error in post_init_hook: %s', str(e)) - - -def uninstall_hook(env): - """ - Hook called before module uninstallation. - Removes the tracking record from connect.module. - """ - try: - module = env['connect.module'].sudo().search([('name', '=', 'connect')], limit=1) - - if module: - module.unlink() - _logger.info('Connect module uninstall: tracking record removed') - - except Exception as e: - _logger.error('Error in uninstall_hook: %s', str(e)) diff --git a/connect/models/__init__.py b/connect/models/__init__.py index ec35f7b8..246c45e8 100644 --- a/connect/models/__init__.py +++ b/connect/models/__init__.py @@ -6,7 +6,6 @@ from . import exten from . import favorite from . import license -from . import module from . import mail from . import message from . import message_configuration diff --git a/connect/models/license.py b/connect/models/license.py index 8630d74d..2c024d1e 100644 --- a/connect/models/license.py +++ b/connect/models/license.py @@ -103,7 +103,7 @@ def is_trial_valid(self, module_name='connect'): Returns: tuple: (is_valid: bool, days_left: int) """ - module = self.env['connect.module'].sudo().search([('name', '=', module_name)], limit=1) + module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)], limit=1) if not module: # Module not found, consider trial invalid diff --git a/connect/models/module.py b/connect/models/module.py deleted file mode 100644 index 98d7b02c..00000000 --- a/connect/models/module.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import fields, models - - -class ConnectModule(models.Model): - """ - Service model to track installed Connect modules and their installation dates. - Used for trial period calculation (30 days from create_date). - """ - _name = 'connect.module' - _description = 'Connect Module Installation Tracker' - - name = fields.Char(string='Module Name', required=True, index=True) - description = fields.Char(string='Description') - - _sql_constraints = [ - ('name_unique', 'UNIQUE(name)', 'Module name must be unique!') - ] diff --git a/connect/security/admin.xml b/connect/security/admin.xml index ef49f29d..78013517 100644 --- a/connect/security/admin.xml +++ b/connect/security/admin.xml @@ -11,17 +11,6 @@
- - - connect_module_admin - - - - - - - - connect_transfer_wizard_admin diff --git a/connect/views/settings.xml b/connect/views/settings.xml index 9a79d3ea..f1400b29 100644 --- a/connect/views/settings.xml +++ b/connect/views/settings.xml @@ -138,6 +138,8 @@ +