|
@@ -0,0 +1,2227 @@
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
|
+
|
|
|
|
|
+import requests
|
|
|
|
|
+import json
|
|
|
|
|
+import logging
|
|
|
|
|
+import re
|
|
|
|
|
+from datetime import datetime, timedelta
|
|
|
|
|
+from odoo import fields, models, api, _
|
|
|
|
|
+from odoo.exceptions import UserError, ValidationError
|
|
|
|
|
+
|
|
|
|
|
+_logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
|
|
+# Cache for Google Drive API responses (5 minutes)
|
|
|
|
|
+_GOOGLE_DRIVE_CACHE = {}
|
|
|
|
|
+_CACHE_TIMEOUT = 300 # 5 minutes
|
|
|
|
|
+
|
|
|
|
|
+def _clear_google_drive_cache():
|
|
|
|
|
+ """Clear expired cache entries"""
|
|
|
|
|
+ global _GOOGLE_DRIVE_CACHE
|
|
|
|
|
+ current_time = datetime.now()
|
|
|
|
|
+ expired_keys = [
|
|
|
|
|
+ key for key, entry in _GOOGLE_DRIVE_CACHE.items()
|
|
|
|
|
+ if current_time >= entry['expires']
|
|
|
|
|
+ ]
|
|
|
|
|
+ for key in expired_keys:
|
|
|
|
|
+ del _GOOGLE_DRIVE_CACHE[key]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class CrmLead(models.Model):
|
|
|
|
|
+ _inherit = 'crm.lead'
|
|
|
|
|
+
|
|
|
|
|
+ google_drive_documents_count = fields.Integer(
|
|
|
|
|
+ string='Google Drive Documents',
|
|
|
|
|
+ compute='_compute_google_drive_documents_count',
|
|
|
|
|
+ help='Number of documents in Google Drive for this opportunity'
|
|
|
|
|
+ )
|
|
|
|
|
+ google_drive_folder_id = fields.Char(
|
|
|
|
|
+ string='Google Drive Folder ID',
|
|
|
|
|
+ help='ID del folder específico en Google Drive para esta oportunidad',
|
|
|
|
|
+ readonly=True
|
|
|
|
|
+ )
|
|
|
|
|
+ google_drive_folder_name = fields.Char(
|
|
|
|
|
+ string='Google Drive Folder Name',
|
|
|
|
|
+ help='Nombre del folder en Google Drive para esta oportunidad',
|
|
|
|
|
+ readonly=True
|
|
|
|
|
+ )
|
|
|
|
|
+ google_drive_url = fields.Char(
|
|
|
|
|
+ string='URL Drive',
|
|
|
|
|
+ help='URL de la carpeta de Google Drive de la oportunidad'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ @api.depends('google_drive_folder_id')
|
|
|
|
|
+ def _compute_google_drive_documents_count(self):
|
|
|
|
|
+ """Compute the number of documents in Google Drive with caching"""
|
|
|
|
|
+ for record in self:
|
|
|
|
|
+ if record.google_drive_folder_id:
|
|
|
|
|
+ # Check cache first
|
|
|
|
|
+ cache_key = f'doc_count_{record.google_drive_folder_id}'
|
|
|
|
|
+ if cache_key in _GOOGLE_DRIVE_CACHE:
|
|
|
|
|
+ cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
|
|
|
|
|
+ if datetime.now() < cache_entry['expires']:
|
|
|
|
|
+ record.google_drive_documents_count = cache_entry['count']
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # TODO: Implement Google Drive API call to count documents
|
|
|
|
|
+ # For now, return 0 but cache the result
|
|
|
|
|
+ count = 0
|
|
|
|
|
+ _GOOGLE_DRIVE_CACHE[cache_key] = {
|
|
|
|
|
+ 'count': count,
|
|
|
|
|
+ 'expires': datetime.now() + timedelta(seconds=300) # 5 minutes
|
|
|
|
|
+ }
|
|
|
|
|
+ record.google_drive_documents_count = count
|
|
|
|
|
+ else:
|
|
|
|
|
+ record.google_drive_documents_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ def _get_google_drive_access_token(self):
|
|
|
|
|
+ """Get Google Drive access token from system parameters with caching"""
|
|
|
|
|
+ # Check cache first
|
|
|
|
|
+ cache_key = 'access_token'
|
|
|
|
|
+ if cache_key in _GOOGLE_DRIVE_CACHE:
|
|
|
|
|
+ cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
|
|
|
|
|
+ if datetime.now() < cache_entry['expires']:
|
|
|
|
|
+ return cache_entry['token']
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
|
|
|
|
|
+ if not access_token:
|
|
|
|
|
+ raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
|
|
|
|
|
+
|
|
|
|
|
+ # Test if the token is still valid
|
|
|
|
|
+ if not self._test_access_token(access_token):
|
|
|
|
|
+ # Try to refresh the token
|
|
|
|
|
+ if self._refresh_access_token():
|
|
|
|
|
+ access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise UserError(_('Google Drive access token has expired and could not be refreshed. Please reconnect your Google account.'))
|
|
|
|
|
+
|
|
|
|
|
+ # Cache the token for 5 minutes
|
|
|
|
|
+ _GOOGLE_DRIVE_CACHE[cache_key] = {
|
|
|
|
|
+ 'token': access_token,
|
|
|
|
|
+ 'expires': datetime.now() + timedelta(seconds=_CACHE_TIMEOUT)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return access_token
|
|
|
|
|
+
|
|
|
|
|
+ def _test_access_token(self, access_token):
|
|
|
|
|
+ """Test if the access token is still valid with better error handling"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ 'https://www.googleapis.com/drive/v3/about',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ params={'fields': 'user'},
|
|
|
|
|
+ timeout=10
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ return True
|
|
|
|
|
+ elif response.status_code == 401:
|
|
|
|
|
+ _logger.warning("Google Drive access token is invalid (401)")
|
|
|
|
|
+ return False
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"Google Drive API test failed with status {response.status_code}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ except requests.exceptions.Timeout:
|
|
|
|
|
+ _logger.error("Google Drive API test timeout")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except requests.exceptions.ConnectionError:
|
|
|
|
|
+ _logger.error("Google Drive API connection error")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Google Drive API test error: {str(e)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _refresh_access_token(self):
|
|
|
|
|
+ """Refresh the access token using the refresh token"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ refresh_token = self.env['ir.config_parameter'].sudo().get_param('google_api.refresh_token')
|
|
|
|
|
+ client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
|
|
|
|
|
+ client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
|
|
|
|
|
+
|
|
|
|
|
+ if not all([refresh_token, client_id, client_secret]):
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Exchange refresh token for new access token
|
|
|
|
|
+ token_url = 'https://oauth2.googleapis.com/token'
|
|
|
|
|
+ data = {
|
|
|
|
|
+ 'client_id': client_id,
|
|
|
|
|
+ 'client_secret': client_secret,
|
|
|
|
|
+ 'refresh_token': refresh_token,
|
|
|
|
|
+ 'grant_type': 'refresh_token'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.post(token_url, data=data, timeout=30)
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ token_data = response.json()
|
|
|
|
|
+ new_access_token = token_data.get('access_token')
|
|
|
|
|
+
|
|
|
|
|
+ if new_access_token:
|
|
|
|
|
+ # Store the new access token
|
|
|
|
|
+ self.env['ir.config_parameter'].sudo().set_param('google_api.access_token', new_access_token)
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Failed to refresh access token: {str(e)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _get_company_root_folder_id(self):
|
|
|
|
|
+ """Get the company's root Google Drive folder ID"""
|
|
|
|
|
+ if not self.company_id or not self.company_id.google_drive_crm_enabled:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ if not self.company_id.google_drive_crm_folder_id:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ return self.company_id.google_drive_crm_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ def _extract_folder_id_from_url(self, url):
|
|
|
|
|
+ """Extract folder ID from Google Drive URL"""
|
|
|
|
|
+ if not url:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # Handle different Google Drive URL formats
|
|
|
|
|
+ import re
|
|
|
|
|
+
|
|
|
|
|
+ # Format: https://drive.google.com/drive/folders/FOLDER_ID
|
|
|
|
|
+ folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
|
|
|
|
|
+ match = re.search(folder_pattern, url)
|
|
|
|
|
+
|
|
|
|
|
+ if match:
|
|
|
|
|
+ return match.group(1)
|
|
|
|
|
+
|
|
|
|
|
+ # Format: https://drive.google.com/open?id=FOLDER_ID
|
|
|
|
|
+ open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
|
|
|
|
|
+ match = re.search(open_pattern, url)
|
|
|
|
|
+
|
|
|
|
|
+ if match:
|
|
|
|
|
+ return match.group(1)
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _get_google_drive_field_value(self):
|
|
|
|
|
+ """Get value from the configured Google Drive field in company settings"""
|
|
|
|
|
+ if not self.company_id.google_drive_crm_field_id:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ field_name = self.company_id.google_drive_crm_field_id.name
|
|
|
|
|
+ if not field_name:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # Get the field value from the record
|
|
|
|
|
+ field_value = getattr(self, field_name, None)
|
|
|
|
|
+ return field_value
|
|
|
|
|
+
|
|
|
|
|
+ def _try_extract_folder_id_from_field(self):
|
|
|
|
|
+ """Try to extract folder ID from the configured field value"""
|
|
|
|
|
+ field_value = self._get_google_drive_field_value()
|
|
|
|
|
+ if not field_value:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # Try to extract folder ID from the field value (could be URL or direct ID)
|
|
|
|
|
+ folder_id = self._extract_folder_id_from_url(field_value)
|
|
|
|
|
+ if folder_id:
|
|
|
|
|
+ return folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # If it's not a URL, check if it looks like a folder ID
|
|
|
|
|
+ if isinstance(field_value, str) and len(field_value) >= 10 and len(field_value) <= 50:
|
|
|
|
|
+ # Basic validation for Google Drive folder ID format
|
|
|
|
|
+ import re
|
|
|
|
|
+ if re.match(r'^[a-zA-Z0-9_-]+$', field_value):
|
|
|
|
|
+ return field_value
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_folder_creation_prerequisites(self):
|
|
|
|
|
+ """Validate prerequisites before creating Google Drive folder"""
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Validate contact exists
|
|
|
|
|
+ if not self.partner_id:
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ def _try_get_existing_folder_id(self):
|
|
|
|
|
+ """Try to get existing folder ID from various sources"""
|
|
|
|
|
+ # First, check if we already have a folder ID
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ return self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Second, try to extract from the configured field
|
|
|
|
|
+ field_folder_id = self._try_extract_folder_id_from_field()
|
|
|
|
|
+ if field_folder_id:
|
|
|
|
|
+ _logger.info(f"Found folder ID from configured field: {field_folder_id}")
|
|
|
|
|
+ return field_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Third, try to extract from google_drive_url field
|
|
|
|
|
+ if self.google_drive_url:
|
|
|
|
|
+ url_folder_id = self._extract_folder_id_from_url(self.google_drive_url)
|
|
|
|
|
+ if url_folder_id:
|
|
|
|
|
+ _logger.info(f"Found folder ID from URL field: {url_folder_id}")
|
|
|
|
|
+ return url_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _check_folder_company_mismatch(self):
|
|
|
|
|
+ """Check if the current folder belongs to the correct company"""
|
|
|
|
|
+ if not self.google_drive_folder_id or not self.company_id:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get access token
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Get company root folder ID
|
|
|
|
|
+ company_root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not company_root_folder_id:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder info
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ if not parent_ids:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up to find if the folder is under the correct company root
|
|
|
|
|
+ current_parent_id = parent_ids[0]
|
|
|
|
|
+ max_levels = 5 # Prevent infinite loop
|
|
|
|
|
+
|
|
|
|
|
+ for _ in range(max_levels):
|
|
|
|
|
+ # Check if current parent is the company root folder
|
|
|
|
|
+ if current_parent_id == company_root_folder_id:
|
|
|
|
|
+ # Folder is under the correct company root
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ parent_response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_parent_id}?fields=parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if parent_response.status_code != 200:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ parent_data = parent_response.json()
|
|
|
|
|
+ parent_parent_ids = parent_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ if not parent_parent_ids:
|
|
|
|
|
+ # Reached the top level (My Drive), folder is not under the correct company root
|
|
|
|
|
+ _logger.info(f"Folder belongs to wrong company. Current root: {current_parent_id}, Expected: {company_root_folder_id}")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ current_parent_id = parent_parent_ids[0]
|
|
|
|
|
+
|
|
|
|
|
+ # If we reach here, folder is not under the correct company root
|
|
|
|
|
+ _logger.info(f"Folder belongs to wrong company. Could not find company root: {company_root_folder_id}")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error checking folder company mismatch: {str(e)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _get_folder_name_components(self):
|
|
|
|
|
+ """Get the components for folder naming with priority for partner_id"""
|
|
|
|
|
+ primary_name = None
|
|
|
|
|
+
|
|
|
|
|
+ if self.partner_id:
|
|
|
|
|
+ _logger.info(f"Contact found: {self.partner_id.name} (ID: {self.partner_id.id})")
|
|
|
|
|
+
|
|
|
|
|
+ # Prioridad 1: partner_id.company_id.name
|
|
|
|
|
+ if self.partner_id.company_id and self.partner_id.company_id.name:
|
|
|
|
|
+ primary_name = self.partner_id.company_id.name
|
|
|
|
|
+ _logger.info(f"Using company_id.name: {primary_name}")
|
|
|
|
|
+ # Prioridad 2: partner_id.company_name
|
|
|
|
|
+ elif self.partner_id.company_name:
|
|
|
|
|
+ primary_name = self.partner_id.company_name
|
|
|
|
|
+ _logger.info(f"Using company_name: {primary_name}")
|
|
|
|
|
+ # Prioridad 3: partner_id.name
|
|
|
|
|
+ else:
|
|
|
|
|
+ primary_name = self.partner_id.name
|
|
|
|
|
+ _logger.info(f"Using partner name: {primary_name}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning("No contact assigned to opportunity")
|
|
|
|
|
+ primary_name = "Sin Contacto"
|
|
|
|
|
+
|
|
|
|
|
+ if not primary_name:
|
|
|
|
|
+ raise UserError(_('No company or contact name available. Please assign a contact with company information before creating Google Drive folders.'))
|
|
|
|
|
+
|
|
|
|
|
+ # Validate and sanitize the primary name
|
|
|
|
|
+ sanitized_primary_name = self._sanitize_folder_name(primary_name)
|
|
|
|
|
+ sanitized_opportunity_name = self._sanitize_folder_name(self.name)
|
|
|
|
|
+ year = str(self.create_date.year) if self.create_date else str(datetime.now().year)
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Folder components - Primary: '{sanitized_primary_name}', Opportunity: '{sanitized_opportunity_name}', Year: {year}")
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'primary_name': sanitized_primary_name,
|
|
|
|
|
+ 'opportunity_name': sanitized_opportunity_name,
|
|
|
|
|
+ 'year': year
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def _check_partner_name_changes(self, vals):
|
|
|
|
|
+ """Check if partner or partner's company name has changed"""
|
|
|
|
|
+ if 'partner_id' not in vals:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ old_partner = self.partner_id
|
|
|
|
|
+ new_partner_id = vals['partner_id']
|
|
|
|
|
+
|
|
|
|
|
+ if not new_partner_id:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ new_partner = self.env['res.partner'].browse(new_partner_id)
|
|
|
|
|
+
|
|
|
|
|
+ # Check if partner changed
|
|
|
|
|
+ if old_partner.id != new_partner.id:
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ # Check if partner's company name changed
|
|
|
|
|
+ old_company_name = old_partner.parent_id.name if old_partner.parent_id else old_partner.name
|
|
|
|
|
+ new_company_name = new_partner.parent_id.name if new_partner.parent_id else new_partner.name
|
|
|
|
|
+
|
|
|
|
|
+ return old_company_name != new_company_name
|
|
|
|
|
+
|
|
|
|
|
+ def _sanitize_folder_name(self, name):
|
|
|
|
|
+ """Sanitize folder name to be Google Drive compatible with optimization"""
|
|
|
|
|
+ if not name:
|
|
|
|
|
+ return 'Sin nombre'
|
|
|
|
|
+
|
|
|
|
|
+ # Use regex for better performance
|
|
|
|
|
+ sanitized_name = re.sub(r'[<>:"|?*/\\]', '_', name)
|
|
|
|
|
+
|
|
|
|
|
+ # Remove leading/trailing spaces and dots
|
|
|
|
|
+ sanitized_name = sanitized_name.strip(' .')
|
|
|
|
|
+
|
|
|
|
|
+ # Ensure it's not empty after sanitization
|
|
|
|
|
+ if not sanitized_name:
|
|
|
|
|
+ return 'Sin nombre'
|
|
|
|
|
+
|
|
|
|
|
+ # Limit length to 255 characters (Google Drive limit)
|
|
|
|
|
+ if len(sanitized_name) > 255:
|
|
|
|
|
+ sanitized_name = sanitized_name[:252] + '...'
|
|
|
|
|
+
|
|
|
|
|
+ return sanitized_name
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_folder_id_with_google_drive(self, folder_id):
|
|
|
|
|
+ """Validate if the folder ID exists and is accessible in Google Drive"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Test access to the specific folder
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=10
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
|
|
|
|
|
+ _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
|
|
|
|
|
+ return True, folder_data.get('name', 'Unknown')
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
|
|
|
|
|
+ return False, "Not a folder"
|
|
|
|
|
+ elif response.status_code == 404:
|
|
|
|
|
+ _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive")
|
|
|
|
|
+ return False, "Not found"
|
|
|
|
|
+ elif response.status_code == 403:
|
|
|
|
|
+ _logger.warning(f"❌ Access denied to folder ID {folder_id}")
|
|
|
|
|
+ return False, "Access denied"
|
|
|
|
|
+ elif response.status_code == 401:
|
|
|
|
|
+ _logger.error(f"❌ OAuth token expired or invalid")
|
|
|
|
|
+ return False, "Authentication error"
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"❌ Google Drive API error: {response.status_code}")
|
|
|
|
|
+ return False, f"API error: {response.status_code}"
|
|
|
|
|
+
|
|
|
|
|
+ except requests.exceptions.Timeout:
|
|
|
|
|
+ _logger.error(f"❌ Timeout validating folder ID {folder_id}")
|
|
|
|
|
+ return False, "Timeout"
|
|
|
|
|
+ except requests.exceptions.ConnectionError:
|
|
|
|
|
+ _logger.error(f"❌ Connection error validating folder ID {folder_id}")
|
|
|
|
|
+ return False, "Connection error"
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
|
|
|
|
|
+ return False, str(e)
|
|
|
|
|
+
|
|
|
|
|
+ def _create_google_drive_folder_structure(self):
|
|
|
|
|
+ """Create the complete Google Drive folder structure for this opportunity with optimization"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ # Validate prerequisites
|
|
|
|
|
+ if not self._validate_folder_creation_prerequisites():
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # Try to get existing folder ID first
|
|
|
|
|
+ existing_folder_id = self._try_get_existing_folder_id()
|
|
|
|
|
+ if existing_folder_id:
|
|
|
|
|
+ _logger.info(f"Found existing folder ID: {existing_folder_id}")
|
|
|
|
|
+
|
|
|
|
|
+ # Validate the folder ID against Google Drive
|
|
|
|
|
+ is_valid, error_message = self._validate_folder_id_with_google_drive(existing_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if is_valid:
|
|
|
|
|
+ _logger.info(f"✅ Folder ID {existing_folder_id} validated successfully")
|
|
|
|
|
+ # Update the record with the existing folder ID
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_id': existing_folder_id
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ # Get expected structure and store it
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+ expected_structure = self._build_structure_string(expected_components)
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_name': expected_structure
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return True
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"❌ Folder ID {existing_folder_id} validation failed: {error_message}")
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Folder ID from configured field is not accessible: %s. Creating new folder structure.") % error_message,
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ # Continue to create new folder structure
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Create folder structure in batch for better performance
|
|
|
|
|
+ folder_structure = self._create_folder_structure_batch(headers, components)
|
|
|
|
|
+
|
|
|
|
|
+ # Update the opportunity with the main folder ID
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_id': folder_structure['opportunity_folder_id'],
|
|
|
|
|
+ 'google_drive_folder_name': self._build_structure_string(components)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return folder_structure
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Failed to create folder structure for opportunity {self.id}: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def _create_folder_structure_batch(self, headers, components):
|
|
|
|
|
+ """Create folder structure in batch for better performance"""
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+
|
|
|
|
|
+ # Step 1: Create or get primary folder (Company/Contact)
|
|
|
|
|
+ primary_folder_id = self._create_or_get_folder(
|
|
|
|
|
+ headers, root_folder_id, components['primary_name']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 2: Create or get year folder
|
|
|
|
|
+ year_folder_id = self._create_or_get_folder(
|
|
|
|
|
+ headers, primary_folder_id, components['year']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 3: Create or get opportunity folder
|
|
|
|
|
+ opportunity_folder_id = self._create_or_get_folder(
|
|
|
|
|
+ headers, year_folder_id, components['opportunity_name']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 4: Create Meets and Archivos cliente folders (parallel creation)
|
|
|
|
|
+ meets_folder_id = self._create_or_get_folder(
|
|
|
|
|
+ headers, opportunity_folder_id, 'Meets'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ archivos_folder_id = self._create_or_get_folder(
|
|
|
|
|
+ headers, opportunity_folder_id, 'Archivos cliente'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'opportunity_folder_id': opportunity_folder_id,
|
|
|
|
|
+ 'meets_folder_id': meets_folder_id,
|
|
|
|
|
+ 'archivos_folder_id': archivos_folder_id
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def _create_or_get_folder(self, headers, parent_folder_id, folder_name):
|
|
|
|
|
+ """Create a folder or get existing one by name"""
|
|
|
|
|
+ # First, check if folder already exists
|
|
|
|
|
+ existing_folder = self._find_folder_by_name(headers, parent_folder_id, folder_name)
|
|
|
|
|
+ if existing_folder:
|
|
|
|
|
+ return existing_folder['id']
|
|
|
|
|
+
|
|
|
|
|
+ # Create new folder
|
|
|
|
|
+ folder_metadata = {
|
|
|
|
|
+ 'name': folder_name,
|
|
|
|
|
+ 'mimeType': 'application/vnd.google-apps.folder',
|
|
|
|
|
+ 'parents': [parent_folder_id]
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.post(
|
|
|
|
|
+ 'https://www.googleapis.com/drive/v3/files',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ json=folder_metadata,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ return folder_data['id']
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise UserError(_('Failed to create Google Drive folder "%s". Status: %s') % (folder_name, response.status_code))
|
|
|
|
|
+
|
|
|
|
|
+ def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
|
|
|
|
|
+ """Find a folder by name in the parent folder with caching"""
|
|
|
|
|
+ # Check cache first
|
|
|
|
|
+ cache_key = f'folder_{parent_folder_id}_{folder_name}'
|
|
|
|
|
+ if cache_key in _GOOGLE_DRIVE_CACHE:
|
|
|
|
|
+ cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
|
|
|
|
|
+ if datetime.now() < cache_entry['expires']:
|
|
|
|
|
+ return cache_entry['result']
|
|
|
|
|
+
|
|
|
|
|
+ params = {
|
|
|
|
|
+ 'q': f"'{parent_folder_id}' in parents and name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false",
|
|
|
|
|
+ 'fields': 'files(id,name)',
|
|
|
|
|
+ 'pageSize': 1
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ 'https://www.googleapis.com/drive/v3/files',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ params=params,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ data = response.json()
|
|
|
|
|
+ folders = data.get('files', [])
|
|
|
|
|
+ result = folders[0] if folders else None
|
|
|
|
|
+
|
|
|
|
|
+ # Cache the result for 2 minutes
|
|
|
|
|
+ _GOOGLE_DRIVE_CACHE[cache_key] = {
|
|
|
|
|
+ 'result': result,
|
|
|
|
|
+ 'expires': datetime.now() + timedelta(seconds=120)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"Failed to find folder '{folder_name}' in parent {parent_folder_id}. Status: {response.status_code}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error finding folder '{folder_name}' in parent {parent_folder_id}: {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _rename_google_drive_folder(self, new_name):
|
|
|
|
|
+ """Rename the Google Drive folder with optimization"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Sanitize the new name
|
|
|
|
|
+ sanitized_name = self._sanitize_folder_name(new_name)
|
|
|
|
|
+
|
|
|
|
|
+ # Check if the name is actually different
|
|
|
|
|
+ if self.google_drive_folder_name == sanitized_name:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ folder_metadata = {
|
|
|
|
|
+ 'name': sanitized_name
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.patch(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ json=folder_metadata,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ # Update the folder name in Odoo (with context to prevent loop)
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': sanitized_name})
|
|
|
|
|
+ _logger.info(f"Successfully renamed Google Drive folder to '{sanitized_name}'")
|
|
|
|
|
+
|
|
|
|
|
+ # Clear cache for this folder
|
|
|
|
|
+ cache_key = f'folder_{self.google_drive_folder_id}'
|
|
|
|
|
+ if cache_key in _GOOGLE_DRIVE_CACHE:
|
|
|
|
|
+ del _GOOGLE_DRIVE_CACHE[cache_key]
|
|
|
|
|
+
|
|
|
|
|
+ else:
|
|
|
|
|
+ error_msg = f'Failed to rename Google Drive folder. Status: {response.status_code}'
|
|
|
|
|
+ if response.text:
|
|
|
|
|
+ error_msg += f' - Response: {response.text}'
|
|
|
|
|
+ _logger.error(error_msg)
|
|
|
|
|
+ raise UserError(_(error_msg))
|
|
|
|
|
+
|
|
|
|
|
+ except requests.exceptions.Timeout:
|
|
|
|
|
+ raise UserError(_('Timeout while renaming Google Drive folder. Please try again.'))
|
|
|
|
|
+ except requests.exceptions.ConnectionError:
|
|
|
|
|
+ raise UserError(_('Connection error while renaming Google Drive folder. Please check your internet connection.'))
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error renaming folder {self.google_drive_folder_id}: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to rename Google Drive folder: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def _move_google_drive_folder(self, new_company_id):
|
|
|
|
|
+ """Move the Google Drive folder to a new company's structure"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Get new company's root folder
|
|
|
|
|
+ new_root_folder_id = new_company_id.google_drive_crm_folder_id
|
|
|
|
|
+ if not new_root_folder_id:
|
|
|
|
|
+ raise UserError(_('New company does not have Google Drive CRM folder configured'))
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder's parent
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to get current folder information'))
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ current_parents = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ # Remove from current parent and add to new parent
|
|
|
|
|
+ if current_parents:
|
|
|
|
|
+ # Remove from current parent
|
|
|
|
|
+ remove_response = requests.delete(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents/{current_parents[0]}',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if remove_response.status_code != 204:
|
|
|
|
|
+ raise UserError(_('Failed to remove folder from current parent'))
|
|
|
|
|
+
|
|
|
|
|
+ # Add to new parent
|
|
|
|
|
+ add_response = requests.post(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ json={'id': new_root_folder_id},
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if add_response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to move folder to new company'))
|
|
|
|
|
+
|
|
|
|
|
+ def _delete_google_drive_folder_structure(self):
|
|
|
|
|
+ """Delete the Google Drive folder structure when contact is removed"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Delete the folder (this will also delete subfolders)
|
|
|
|
|
+ response = requests.delete(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 204:
|
|
|
|
|
+ # Clear the folder ID from the record
|
|
|
|
|
+ self.write({
|
|
|
|
|
+ 'google_drive_folder_id': False,
|
|
|
|
|
+ 'google_drive_folder_name': False
|
|
|
|
|
+ })
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise UserError(_('Failed to delete Google Drive folder structure'))
|
|
|
|
|
+
|
|
|
|
|
+ def _recreate_google_drive_folder_structure(self):
|
|
|
|
|
+ """Recreate the Google Drive folder structure when contact changes"""
|
|
|
|
|
+ if not self.partner_id:
|
|
|
|
|
+ raise UserError(_('No contact associated with this opportunity. Cannot recreate folder structure.'))
|
|
|
|
|
+
|
|
|
|
|
+ # Store old folder information for reference
|
|
|
|
|
+ old_folder_id = self.google_drive_folder_id
|
|
|
|
|
+ old_folder_name = self.google_drive_folder_name
|
|
|
|
|
+
|
|
|
|
|
+ # Clear the folder ID but don't delete the actual folder
|
|
|
|
|
+ self.write({
|
|
|
|
|
+ 'google_drive_folder_id': False,
|
|
|
|
|
+ 'google_drive_folder_name': False
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ # Create new structure
|
|
|
|
|
+ try:
|
|
|
|
|
+ new_structure = self._create_google_drive_folder_structure()
|
|
|
|
|
+
|
|
|
|
|
+ # Log the recreation for audit purposes
|
|
|
|
|
+ if old_folder_id:
|
|
|
|
|
+ _logger.info(f"Recreated Google Drive folder structure for opportunity {self.id}: "
|
|
|
|
|
+ f"Old folder: {old_folder_id} ({old_folder_name}) -> "
|
|
|
|
|
+ f"New folder: {self.google_drive_folder_id} ({self.google_drive_folder_name})")
|
|
|
|
|
+
|
|
|
|
|
+ return new_structure
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ # Restore old values if recreation fails
|
|
|
|
|
+ self.write({
|
|
|
|
|
+ 'google_drive_folder_id': old_folder_id,
|
|
|
|
|
+ 'google_drive_folder_name': old_folder_name
|
|
|
|
|
+ })
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _rename_entire_folder_structure(self, old_components, new_components):
|
|
|
|
|
+ """Rename the entire folder structure instead of recreating it"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Get the current folder path to understand the structure
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Get folder information to find parent folders
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=parents,name',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to get current folder information'))
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ if not parent_ids:
|
|
|
|
|
+ raise UserError(_('Cannot rename folder structure: no parent folder found'))
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy to find the primary folder (company/contact)
|
|
|
|
|
+ primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
|
|
|
|
|
+ if not primary_folder_id:
|
|
|
|
|
+ raise UserError(_('Cannot find primary folder in the structure'))
|
|
|
|
|
+
|
|
|
|
|
+ # Rename the primary folder (company/contact name)
|
|
|
|
|
+ if old_components['primary_name'] != new_components['primary_name']:
|
|
|
|
|
+ self._rename_folder(headers, primary_folder_id, new_components['primary_name'])
|
|
|
|
|
+
|
|
|
|
|
+ # Find and rename year folder if needed
|
|
|
|
|
+ if old_components['year'] != new_components['year']:
|
|
|
|
|
+ year_folder_id = self._find_year_folder_id(headers, primary_folder_id, old_components['year'])
|
|
|
|
|
+ if year_folder_id:
|
|
|
|
|
+ self._rename_folder(headers, year_folder_id, new_components['year'])
|
|
|
|
|
+
|
|
|
|
|
+ # Rename opportunity folder if needed
|
|
|
|
|
+ if old_components['opportunity_name'] != new_components['opportunity_name']:
|
|
|
|
|
+ _logger.info(f"Renaming opportunity folder from '{old_components['opportunity_name']}' to '{new_components['opportunity_name']}'")
|
|
|
|
|
+ self._rename_folder(headers, current_folder_id, new_components['opportunity_name'])
|
|
|
|
|
+ # Update the folder name in Odoo (with context to prevent loop)
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': new_components['opportunity_name']})
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info(f"Opportunity folder name is already correct: '{new_components['opportunity_name']}'")
|
|
|
|
|
+
|
|
|
|
|
+ def _find_primary_folder_id(self, headers, current_folder_id):
|
|
|
|
|
+ """Find the primary folder (company/contact) by navigating up the hierarchy"""
|
|
|
|
|
+ max_depth = 5 # Prevent infinite loops
|
|
|
|
|
+ current_id = current_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ for depth in range(max_depth):
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=parents,name',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ if not parent_ids:
|
|
|
|
|
+ # This is the root folder, go back one level
|
|
|
|
|
+ return current_id
|
|
|
|
|
+
|
|
|
|
|
+ # Check if this folder is the primary folder (not a year or opportunity folder)
|
|
|
|
|
+ # Primary folder is typically the company/contact name
|
|
|
|
|
+ folder_name = folder_data.get('name', '')
|
|
|
|
|
+ if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
|
|
|
|
|
+ # This could be the primary folder, but let's check if it has a year folder as child
|
|
|
|
|
+ year_folders = self._find_folders_by_name(headers, current_id, r'^\d{4}$')
|
|
|
|
|
+ if year_folders:
|
|
|
|
|
+ # This is the primary folder
|
|
|
|
|
+ return current_id
|
|
|
|
|
+
|
|
|
|
|
+ current_id = parent_ids[0]
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _find_year_folder_id(self, headers, primary_folder_id, year):
|
|
|
|
|
+ """Find the year folder within the primary folder"""
|
|
|
|
|
+ year_folders = self._find_folders_by_name(headers, primary_folder_id, f'^{year}$')
|
|
|
|
|
+ return year_folders[0]['id'] if year_folders else None
|
|
|
|
|
+
|
|
|
|
|
+ def _find_folders_by_name(self, headers, parent_id, name_pattern):
|
|
|
|
|
+ """Find folders by name pattern in a parent folder"""
|
|
|
|
|
+ import re
|
|
|
|
|
+
|
|
|
|
|
+ params = {
|
|
|
|
|
+ 'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
|
|
|
|
|
+ 'fields': 'files(id,name)',
|
|
|
|
|
+ 'pageSize': 100
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ 'https://www.googleapis.com/drive/v3/files',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ params=params,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ data = response.json()
|
|
|
|
|
+ folders = data.get('files', [])
|
|
|
|
|
+
|
|
|
|
|
+ # Filter by name pattern
|
|
|
|
|
+ pattern = re.compile(name_pattern)
|
|
|
|
|
+ return [folder for folder in folders if pattern.match(folder.get('name', ''))]
|
|
|
|
|
+
|
|
|
|
|
+ def _rename_folder(self, headers, folder_id, new_name):
|
|
|
|
|
+ """Rename a specific folder"""
|
|
|
|
|
+ _logger.info(f"Attempting to rename folder {folder_id} to '{new_name}'")
|
|
|
|
|
+
|
|
|
|
|
+ folder_metadata = {
|
|
|
|
|
+ 'name': new_name
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = requests.patch(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{folder_id}',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ json=folder_metadata,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ _logger.info(f"Successfully renamed folder {folder_id} to '{new_name}'")
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.error(f"Failed to rename folder {folder_id}. Status: {response.status_code}, Response: {response.text}")
|
|
|
|
|
+ raise UserError(_('Failed to rename folder "%s" to "%s". Status: %s') % (folder_id, new_name, response.status_code))
|
|
|
|
|
+
|
|
|
|
|
+ except requests.exceptions.Timeout:
|
|
|
|
|
+ _logger.error(f"Timeout while renaming folder {folder_id}")
|
|
|
|
|
+ raise UserError(_('Timeout while renaming folder "%s"') % folder_id)
|
|
|
|
|
+ except requests.exceptions.ConnectionError:
|
|
|
|
|
+ _logger.error(f"Connection error while renaming folder {folder_id}")
|
|
|
|
|
+ raise UserError(_('Connection error while renaming folder "%s"') % folder_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error renaming folder {folder_id}: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to rename folder "%s" to "%s": %s') % (folder_id, new_name, str(e)))
|
|
|
|
|
+
|
|
|
|
|
+ def _update_google_drive_folder_structure(self, vals):
|
|
|
|
|
+ """Update the Google Drive folder structure based on changes"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ # Company doesn't have Google Drive configured, do nothing
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder information
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company changed - move folder to new company structure
|
|
|
|
|
+ if 'company_id' in vals and vals['company_id'] != self.company_id.id:
|
|
|
|
|
+ new_company = self.env['res.company'].browse(vals['company_id'])
|
|
|
|
|
+ if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
|
|
|
|
|
+ _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
|
|
|
|
|
+ self._move_google_drive_folder(new_company)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # If new company doesn't have Google Drive configured, keep the folder but log it
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if contact changed - this requires recreating the entire structure
|
|
|
|
|
+ if 'partner_id' in vals:
|
|
|
|
|
+ if not vals['partner_id']:
|
|
|
|
|
+ # Contact was removed, but we don't delete - just log it
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Contact changed, recreate the entire structure
|
|
|
|
|
+ _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
|
|
|
|
|
+ self._recreate_google_drive_folder_structure()
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if name changed - rename the opportunity folder
|
|
|
|
|
+ if 'name' in vals and vals['name'] != self.name:
|
|
|
|
|
+ _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
|
|
|
|
|
+ self._rename_google_drive_folder(vals['name'])
|
|
|
|
|
+
|
|
|
|
|
+ # Validate and update entire folder structure if needed (only for non-name changes)
|
|
|
|
|
+ # This will handle changes in company name, contact name, or year
|
|
|
|
|
+ if 'partner_id' in vals or 'create_date' in vals:
|
|
|
|
|
+ self._validate_and_update_folder_structure(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Check if stage changed and we need to create folder
|
|
|
|
|
+ if 'stage_id' in vals:
|
|
|
|
|
+ if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
|
|
|
|
|
+ if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ # Company doesn't have Google Drive configured, do nothing
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Validate contact exists before attempting to create folder
|
|
|
|
|
+ if not self.partner_id:
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ try:
|
|
|
|
|
+ self._create_google_drive_folder_structure()
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Failed to create Google Drive folder for opportunity {self.id}: {str(e)}")
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_and_update_folder_structure(self, vals):
|
|
|
|
|
+ """Validate and update the entire folder structure if any component changed"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Get current and new components
|
|
|
|
|
+ current_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Create a temporary record with new values to get new components
|
|
|
|
|
+ temp_vals = {}
|
|
|
|
|
+ if 'name' in vals:
|
|
|
|
|
+ temp_vals['name'] = vals['name']
|
|
|
|
|
+ if 'partner_id' in vals:
|
|
|
|
|
+ temp_vals['partner_id'] = vals['partner_id']
|
|
|
|
|
+ if 'create_date' in vals:
|
|
|
|
|
+ temp_vals['create_date'] = vals['create_date']
|
|
|
|
|
+
|
|
|
|
|
+ # Create a temporary record to get new components
|
|
|
|
|
+ temp_record = self.with_context(skip_google_drive_update=True)
|
|
|
|
|
+ for field, value in temp_vals.items():
|
|
|
|
|
+ setattr(temp_record, field, value)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ new_components = temp_record._get_folder_name_components()
|
|
|
|
|
+ except:
|
|
|
|
|
+ # If we can't get new components, skip validation
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if any component changed
|
|
|
|
|
+ components_changed = (
|
|
|
|
|
+ current_components['primary_name'] != new_components['primary_name'] or
|
|
|
|
|
+ current_components['year'] != new_components['year']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Also check if partner name changed (even if same partner, name might have changed)
|
|
|
|
|
+ partner_name_changed = self._check_partner_name_changes(vals)
|
|
|
|
|
+
|
|
|
|
|
+ if components_changed or partner_name_changed:
|
|
|
|
|
+ _logger.info(f"Folder structure components changed. Renaming entire structure.")
|
|
|
|
|
+ _logger.info(f"Old components: {current_components}")
|
|
|
|
|
+ _logger.info(f"New components: {new_components}")
|
|
|
|
|
+
|
|
|
|
|
+ # Store the old folder information for reference
|
|
|
|
|
+ old_folder_id = self.google_drive_folder_id
|
|
|
|
|
+ old_folder_name = self.google_drive_folder_name
|
|
|
|
|
+ old_structure = f"{current_components['primary_name']}/{current_components['year']}/{current_components['opportunity_name']}"
|
|
|
|
|
+ new_structure = f"{new_components['primary_name']}/{new_components['year']}/{new_components['opportunity_name']}"
|
|
|
|
|
+
|
|
|
|
|
+ # Rename the entire folder structure instead of recreating
|
|
|
|
|
+ try:
|
|
|
|
|
+ self._rename_entire_folder_structure(current_components, new_components)
|
|
|
|
|
+
|
|
|
|
|
+ # Log the change for audit
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("🔄 Google Drive folder structure renamed due to changes:<br/>"
|
|
|
|
|
+ "• Old structure: %s<br/>"
|
|
|
|
|
+ "• New structure: %s<br/>"
|
|
|
|
|
+ "• Folder ID: %s (same folder, renamed)") % (
|
|
|
|
|
+ old_structure,
|
|
|
|
|
+ new_structure,
|
|
|
|
|
+ self.google_drive_folder_id
|
|
|
|
|
+ ),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Failed to rename folder structure: {str(e)}")
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("❌ Failed to rename Google Drive folder structure: %s") % str(e),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ raise
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ # Company doesn't have Google Drive configured, do nothing
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder information
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company changed - move folder to new company structure
|
|
|
|
|
+ if 'company_id' in vals and vals['company_id'] != self.company_id.id:
|
|
|
|
|
+ new_company = self.env['res.company'].browse(vals['company_id'])
|
|
|
|
|
+ if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
|
|
|
|
|
+ _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
|
|
|
|
|
+ self._move_google_drive_folder(new_company)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # If new company doesn't have Google Drive configured, keep the folder but log it
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if contact changed - this requires recreating the entire structure
|
|
|
|
|
+ if 'partner_id' in vals:
|
|
|
|
|
+ if not vals['partner_id']:
|
|
|
|
|
+ # Contact was removed, but we don't delete - just log it
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Contact changed, recreate the entire structure
|
|
|
|
|
+ _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
|
|
|
|
|
+ self._recreate_google_drive_folder_structure()
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # Check if name changed - rename the opportunity folder
|
|
|
|
|
+ if 'name' in vals and vals['name'] != self.name:
|
|
|
|
|
+ _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
|
|
|
|
|
+ self._rename_google_drive_folder(vals['name'])
|
|
|
|
|
+
|
|
|
|
|
+ # Validate and update entire folder structure if needed
|
|
|
|
|
+ self._validate_and_update_folder_structure(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Check if stage changed and we need to create folder
|
|
|
|
|
+ if 'stage_id' in vals:
|
|
|
|
|
+ if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
|
|
|
|
|
+ if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
|
|
|
|
|
+ if self.partner_id:
|
|
|
|
|
+ self._create_google_drive_folder_structure()
|
|
|
|
|
+ else:
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ @api.model
|
|
|
|
|
+ def create(self, vals):
|
|
|
|
|
+ """Override create to handle Google Drive folder creation with optimization"""
|
|
|
|
|
+ record = super().create(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Check if we should create Google Drive folder (optimized conditions)
|
|
|
|
|
+ if (record.company_id.google_drive_crm_enabled and
|
|
|
|
|
+ record.company_id.google_drive_crm_stage_id and
|
|
|
|
|
+ record.stage_id.id == record.company_id.google_drive_crm_stage_id.id):
|
|
|
|
|
+
|
|
|
|
|
+ # Validate prerequisites before creating folder
|
|
|
|
|
+ if record._validate_folder_creation_prerequisites():
|
|
|
|
|
+ try:
|
|
|
|
|
+ record._create_google_drive_folder_structure()
|
|
|
|
|
+
|
|
|
|
|
+ # Store the initial structure and update URL
|
|
|
|
|
+ if record._store_initial_structure_and_update_url():
|
|
|
|
|
+ record.message_post(
|
|
|
|
|
+ body=_("✅ Google Drive folder created automatically"),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ # Log error but don't fail record creation
|
|
|
|
|
+ _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
|
|
|
|
|
+ record.message_post(
|
|
|
|
|
+ body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return record
|
|
|
|
|
+
|
|
|
|
|
+ def write(self, vals):
|
|
|
|
|
+ """Override write method to handle Google Drive folder updates"""
|
|
|
|
|
+ # Skip Google Drive updates if this is an internal update (to prevent loops)
|
|
|
|
|
+ if self.env.context.get('skip_google_drive_update'):
|
|
|
|
|
+ return super().write(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Clear cache before processing
|
|
|
|
|
+ _clear_google_drive_cache()
|
|
|
|
|
+
|
|
|
|
|
+ # Check if any relevant fields are being updated
|
|
|
|
|
+ relevant_fields = ['name', 'partner_id', 'create_date', 'stage_id', 'company_id']
|
|
|
|
|
+ needs_update = any(field in vals for field in relevant_fields)
|
|
|
|
|
+
|
|
|
|
|
+ if not needs_update:
|
|
|
|
|
+ return super().write(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Store current values for comparison - PROCESAR TODAS LAS OPORTUNIDADES
|
|
|
|
|
+ current_values = {}
|
|
|
|
|
+ for record in self:
|
|
|
|
|
+ current_values[record.id] = {
|
|
|
|
|
+ 'name': record.name,
|
|
|
|
|
+ 'partner_id': record.partner_id.id if record.partner_id else None,
|
|
|
|
|
+ 'create_date': record.create_date,
|
|
|
|
|
+ 'company_id': record.company_id.id if record.company_id else None,
|
|
|
|
|
+ 'google_drive_folder_name': record.google_drive_folder_name or ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Execute the write first
|
|
|
|
|
+ result = super().write(vals)
|
|
|
|
|
+
|
|
|
|
|
+ # Now process Google Drive updates with updated values - PROCESAR TODAS
|
|
|
|
|
+ for record in self:
|
|
|
|
|
+ record._process_google_drive_updates_immediate(vals, current_values[record.id])
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ def _process_google_drive_updates(self, vals):
|
|
|
|
|
+ """Process Google Drive updates for a single record"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Check if we need to create folder (stage-based creation)
|
|
|
|
|
+ if 'stage_id' in vals and not self.google_drive_folder_id:
|
|
|
|
|
+ if not self._validate_folder_creation_prerequisites():
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ # If we have a folder, verify and update structure if needed
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ self._verify_and_update_folder_structure(vals)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("❌ Error updating Google Drive: %s") % str(e),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _process_google_drive_updates_immediate(self, vals, old_values):
|
|
|
|
|
+ """Process Google Drive updates immediately after write"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ _logger.info(f"=== INICIO _process_google_drive_updates_immediate para oportunidad {self.id} ===")
|
|
|
|
|
+ _logger.info(f"Vals recibidos: {vals}")
|
|
|
|
|
+ _logger.info(f"Google Drive Folder ID actual: {self.google_drive_folder_id}")
|
|
|
|
|
+
|
|
|
|
|
+ # PASO 1: Verificar si necesitamos crear carpeta (stage-based creation)
|
|
|
|
|
+ should_create_folder = False
|
|
|
|
|
+
|
|
|
|
|
+ # Caso 1: Cambió stage_id y no tiene carpeta
|
|
|
|
|
+ if 'stage_id' in vals and not self.google_drive_folder_id:
|
|
|
|
|
+ should_create_folder = True
|
|
|
|
|
+ _logger.info(f"CASO 1: Stage changed for opportunity {self.id}. Checking if should create folder.")
|
|
|
|
|
+
|
|
|
|
|
+ # Caso 2: Ya está en el stage correcto, no tiene carpeta, y se actualizó cualquier campo
|
|
|
|
|
+ elif not self.google_drive_folder_id:
|
|
|
|
|
+ # Verificar si está en el stage correcto para crear automáticamente
|
|
|
|
|
+ company = self.company_id
|
|
|
|
|
+ _logger.info(f"CASO 2: Checking if opportunity {self.id} is in correct stage for auto-creation")
|
|
|
|
|
+ _logger.info(f"Company Google Drive enabled: {company.google_drive_crm_enabled}")
|
|
|
|
|
+ _logger.info(f"Company stage configured: {company.google_drive_crm_stage_id.name if company.google_drive_crm_stage_id else 'None'}")
|
|
|
|
|
+ _logger.info(f"Opportunity stage: {self.stage_id.name}")
|
|
|
|
|
+
|
|
|
|
|
+ if (company.google_drive_crm_enabled and
|
|
|
|
|
+ company.google_drive_crm_stage_id and
|
|
|
|
|
+ self.stage_id.id == company.google_drive_crm_stage_id.id):
|
|
|
|
|
+ should_create_folder = True
|
|
|
|
|
+ _logger.info(f"CASO 2: Opportunity {self.id} is in correct stage but has no folder. Will create.")
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info(f"CASO 2: Opportunity {self.id} is NOT in correct stage for auto-creation")
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"¿Debería crear carpeta?: {should_create_folder}")
|
|
|
|
|
+
|
|
|
|
|
+ # Crear carpeta si es necesario
|
|
|
|
|
+ if should_create_folder:
|
|
|
|
|
+ _logger.info(f"Intentando crear carpeta para oportunidad {self.id}")
|
|
|
|
|
+
|
|
|
|
|
+ if self._validate_folder_creation_prerequisites():
|
|
|
|
|
+ _logger.info(f"Prerrequisitos válidos. Creando Google Drive folder para opportunity {self.id}")
|
|
|
|
|
+ try:
|
|
|
|
|
+ self._create_google_drive_folder_structure()
|
|
|
|
|
+ _logger.info(f"Carpeta creada exitosamente para oportunidad {self.id}")
|
|
|
|
|
+
|
|
|
|
|
+ # Store the initial structure and update URL
|
|
|
|
|
+ if self._store_initial_structure_and_update_url():
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Google Drive folder created automatically"),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"ERROR creando carpeta para oportunidad {self.id}: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info(f"Prerrequisitos no cumplidos para oportunidad {self.id}. Skipping folder creation.")
|
|
|
|
|
+
|
|
|
|
|
+ # PASO 2: Si ya tiene carpeta, verificar y actualizar estructura si es necesario
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ _logger.info(f"Oportunidad {self.id} ya tiene carpeta. Verificando estructura.")
|
|
|
|
|
+ self._verify_and_update_folder_structure_immediate(vals, old_values)
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"=== FIN _process_google_drive_updates_immediate para oportunidad {self.id} ===")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("❌ Error updating Google Drive: %s") % str(e),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def _verify_and_update_folder_structure_immediate(self, vals, old_values):
|
|
|
|
|
+ """Verificar y actualizar estructura de Google Drive - SECUENCIAL"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
|
|
|
|
|
+
|
|
|
|
|
+ # PASO 1: Si cambia company_id y tiene folder_id → Mover
|
|
|
|
|
+ if 'company_id' in vals and self.google_drive_folder_id:
|
|
|
|
|
+ new_company_id = vals['company_id']
|
|
|
|
|
+ if new_company_id != old_values.get('company_id'):
|
|
|
|
|
+ _logger.info(f"Company changed from {old_values.get('company_id')} to {new_company_id}. Moving folder.")
|
|
|
|
|
+ self._move_folder_to_new_company(new_company_id)
|
|
|
|
|
+
|
|
|
|
|
+ # PASO 2: Si cambia otro campo → Verificar string de estructura
|
|
|
|
|
+ relevant_fields = ['name', 'partner_id', 'create_date']
|
|
|
|
|
+ if any(field in vals for field in relevant_fields):
|
|
|
|
|
+ expected_structure = self._build_structure_string(self._get_folder_name_components())
|
|
|
|
|
+ current_structure = old_values.get('google_drive_folder_name', '')
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Structure comparison: Current='{current_structure}' vs Expected='{expected_structure}'")
|
|
|
|
|
+
|
|
|
|
|
+ if expected_structure != current_structure:
|
|
|
|
|
+ _logger.info(f"Structure changed. Renaming folder structure.")
|
|
|
|
|
+ self._rename_entire_folder_structure_from_components(self._get_folder_name_components())
|
|
|
|
|
+
|
|
|
|
|
+ # Actualizar estructura local al final
|
|
|
|
|
+ expected_structure = self._build_structure_string(self._get_folder_name_components())
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_name': expected_structure
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Google Drive folder structure updated immediately"),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error verifying folder structure: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _verify_and_update_folder_structure(self, vals):
|
|
|
|
|
+ """Verify current structure vs expected and update if needed"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get expected structure components
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Build expected structure string
|
|
|
|
|
+ expected_structure = self._build_structure_string(expected_components)
|
|
|
|
|
+
|
|
|
|
|
+ # Get current structure from stored field
|
|
|
|
|
+ current_structure = self.google_drive_folder_name or ''
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Structure comparison for opportunity {self.id}:")
|
|
|
|
|
+ _logger.info(f"Current: '{current_structure}'")
|
|
|
|
|
+ _logger.info(f"Expected: '{expected_structure}'")
|
|
|
|
|
+
|
|
|
|
|
+ # Compare structures
|
|
|
|
|
+ if current_structure != expected_structure:
|
|
|
|
|
+ _logger.info(f"Structure mismatch detected. Updating Google Drive...")
|
|
|
|
|
+
|
|
|
|
|
+ # Determine what type of change occurred
|
|
|
|
|
+ if 'company_id' in vals:
|
|
|
|
|
+ self._handle_company_change(vals['company_id'])
|
|
|
|
|
+ else:
|
|
|
|
|
+ # For other changes, rename the structure
|
|
|
|
|
+ self._rename_entire_folder_structure_from_components(expected_components)
|
|
|
|
|
+
|
|
|
|
|
+ # Update the stored structure
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_name': expected_structure
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Google Drive folder structure updated successfully"),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info(f"Structure is up to date. No changes needed.")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error verifying folder structure: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _build_structure_string(self, components):
|
|
|
|
|
+ """Build a string representation of the folder structure"""
|
|
|
|
|
+ return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
|
|
|
|
|
+
|
|
|
|
|
+ def _rename_entire_folder_structure_from_components(self, expected_components):
|
|
|
|
|
+ """Rename entire folder structure based on expected components"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Get current structure from Google Drive
|
|
|
|
|
+ current_structure = self._analyze_complete_folder_structure(headers)
|
|
|
|
|
+
|
|
|
|
|
+ if not current_structure:
|
|
|
|
|
+ raise UserError(_('Could not analyze current folder structure'))
|
|
|
|
|
+
|
|
|
|
|
+ # Build current components from actual structure
|
|
|
|
|
+ current_components = {
|
|
|
|
|
+ 'primary_name': current_structure.get('primary_folder', {}).get('name', ''),
|
|
|
|
|
+ 'year': current_structure.get('year_folder', {}).get('name', ''),
|
|
|
|
|
+ 'opportunity_name': current_structure.get('opportunity_folder', {}).get('name', '')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Rename the structure
|
|
|
|
|
+ self._rename_entire_folder_structure(current_components, expected_components)
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error renaming folder structure: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _move_folder_to_new_company(self, new_company_id):
|
|
|
|
|
+ """Mover folder a nueva empresa - SIMPLE"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Obtener nueva empresa
|
|
|
|
|
+ new_company = self.env['res.company'].browse(new_company_id)
|
|
|
|
|
+ new_root_folder_id = new_company.google_drive_crm_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ if not new_root_folder_id:
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado."),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Moviendo folder de {self.company_id.name} a {new_company.name}")
|
|
|
|
|
+
|
|
|
|
|
+ # Obtener access token
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Mover el folder primario al nuevo root
|
|
|
|
|
+ self._move_folder_to_new_parent(headers, new_root_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Folder movido a nueva empresa: %s") % new_company.name,
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error moviendo folder: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _handle_structural_changes(self, vals):
|
|
|
|
|
+ """Handle structural changes (partner_id, create_date) - rename entire structure"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get current and new components
|
|
|
|
|
+ current_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Temporarily update the record to get new components
|
|
|
|
|
+ temp_record = self.with_context(skip_google_drive_update=True)
|
|
|
|
|
+ temp_vals = {}
|
|
|
|
|
+ if 'partner_id' in vals:
|
|
|
|
|
+ temp_vals['partner_id'] = vals['partner_id']
|
|
|
|
|
+ if 'create_date' in vals:
|
|
|
|
|
+ temp_vals['create_date'] = vals['create_date']
|
|
|
|
|
+
|
|
|
|
|
+ if temp_vals:
|
|
|
|
|
+ temp_record.write(temp_vals)
|
|
|
|
|
+ new_components = temp_record._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Check if any component changed
|
|
|
|
|
+ components_changed = (
|
|
|
|
|
+ current_components['primary_name'] != new_components['primary_name'] or
|
|
|
|
|
+ current_components['year'] != new_components['year']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if components_changed:
|
|
|
|
|
+ _logger.info(f"Structural changes detected. Renaming folder structure.")
|
|
|
|
|
+ self._rename_entire_folder_structure(current_components, new_components)
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.info(f"No structural changes detected. Skipping rename.")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error handling structural changes: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _handle_name_change(self, new_name):
|
|
|
|
|
+ """Handle simple name change - only rename opportunity folder"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ sanitized_new_name = self._sanitize_folder_name(new_name)
|
|
|
|
|
+ self._rename_google_drive_folder(sanitized_new_name)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error handling name change: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _move_folder_to_new_parent(self, headers, new_parent_id):
|
|
|
|
|
+ """Move entire folder structure to new parent in Google Drive"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get current folder info and navigate up to find the primary folder (company/contact)
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy to find the primary folder
|
|
|
|
|
+ primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not primary_folder_id:
|
|
|
|
|
+ raise UserError(_('Could not find primary folder in hierarchy'))
|
|
|
|
|
+
|
|
|
|
|
+ # Get current parent of primary folder
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?fields=parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to get primary folder information'))
|
|
|
|
|
+
|
|
|
|
|
+ current_data = response.json()
|
|
|
|
|
+ current_parents = current_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ if not current_parents:
|
|
|
|
|
+ raise UserError(_('Primary folder has no parent'))
|
|
|
|
|
+
|
|
|
|
|
+ current_parent_id = current_parents[0]
|
|
|
|
|
+
|
|
|
|
|
+ # Move the entire structure by moving the primary folder
|
|
|
|
|
+ move_response = requests.patch(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?addParents={new_parent_id}&removeParents={current_parent_id}',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if move_response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to move folder structure to new parent'))
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Successfully moved entire folder structure from {current_parent_id} to {new_parent_id}")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error moving folder structure: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _find_primary_folder_id(self, headers, start_folder_id):
|
|
|
|
|
+ """Find the primary folder (company/contact level) in the hierarchy"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ current_id = start_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up to 3 levels to find the primary folder
|
|
|
|
|
+ for _ in range(3):
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=name,parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ folder_name = folder_data.get('name', '')
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ # Check if this is the primary folder (not year, not opportunity)
|
|
|
|
|
+ # Primary folder is typically the company/contact name
|
|
|
|
|
+ if not parent_ids:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ # If this folder's name looks like a year (4 digits), continue up
|
|
|
|
|
+ if folder_name.isdigit() and len(folder_name) == 4:
|
|
|
|
|
+ current_id = parent_ids[0]
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # If this folder's name contains opportunity ID pattern, continue up
|
|
|
|
|
+ if ' - ' in folder_name and folder_name.split(' - ')[0].isdigit():
|
|
|
|
|
+ current_id = parent_ids[0]
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # This should be the primary folder
|
|
|
|
|
+ return current_id
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error finding primary folder: {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def action_open_google_drive_folder(self):
|
|
|
|
|
+ """Open Google Drive folder for this opportunity"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ raise UserError(_('No Google Drive folder configured for this opportunity'))
|
|
|
|
|
+
|
|
|
|
|
+ folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'type': 'ir.actions.act_url',
|
|
|
|
|
+ 'url': folder_url,
|
|
|
|
|
+ 'target': 'new',
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ def action_create_google_drive_folder(self):
|
|
|
|
|
+ """Create Google Drive folder structure for this opportunity"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ raise UserError(_('Google Drive folder already exists for this opportunity'))
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ folder_structure = self._create_google_drive_folder_structure()
|
|
|
|
|
+
|
|
|
|
|
+ # Store the initial structure and update URL
|
|
|
|
|
+ self._store_initial_structure_and_update_url()
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'type': 'ir.actions.client',
|
|
|
|
|
+ 'tag': 'display_notification',
|
|
|
|
|
+ 'params': {
|
|
|
|
|
+ 'title': _('Success'),
|
|
|
|
|
+ 'message': _('Google Drive folder structure created successfully!'),
|
|
|
|
|
+ 'type': 'success',
|
|
|
|
|
+ 'sticky': False,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def action_upload_to_google_drive(self):
|
|
|
|
|
+ """Upload documents to Google Drive"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ raise UserError(_('Please create a Google Drive folder for this opportunity first'))
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # TODO: Implement Google Drive API call to upload documents
|
|
|
|
|
+ # For now, just show a message
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'type': 'ir.actions.client',
|
|
|
|
|
+ 'tag': 'display_notification',
|
|
|
|
|
+ 'params': {
|
|
|
|
|
+ 'title': _('Info'),
|
|
|
|
|
+ 'message': _('Document upload to Google Drive will be implemented soon.'),
|
|
|
|
|
+ 'type': 'info',
|
|
|
|
|
+ 'sticky': False,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise UserError(_('Failed to upload to Google Drive: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def action_recreate_google_drive_structure(self):
|
|
|
|
|
+ """Manually rename the Google Drive folder structure"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
|
|
|
|
|
+
|
|
|
|
|
+ # Check if company has Google Drive folder configured
|
|
|
|
|
+ root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not root_folder_id:
|
|
|
|
|
+ raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get expected components
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder name from Google Drive
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=name',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ raise UserError(_('Failed to get current folder information from Google Drive'))
|
|
|
|
|
+
|
|
|
|
|
+ current_folder_data = response.json()
|
|
|
|
|
+ current_folder_name = current_folder_data.get('name', '')
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Current folder name in Google Drive: '{current_folder_name}'")
|
|
|
|
|
+ _logger.info(f"Expected folder name: '{expected_components['opportunity_name']}'")
|
|
|
|
|
+
|
|
|
|
|
+ # Create old components with current Google Drive name
|
|
|
|
|
+ old_components = expected_components.copy()
|
|
|
|
|
+ old_components['opportunity_name'] = current_folder_name
|
|
|
|
|
+
|
|
|
|
|
+ # Rename the structure
|
|
|
|
|
+ self._rename_entire_folder_structure(old_components, expected_components)
|
|
|
|
|
+
|
|
|
|
|
+ # Update the stored structure
|
|
|
|
|
+ expected_structure = self._build_structure_string(expected_components)
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_name': expected_structure
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'type': 'ir.actions.client',
|
|
|
|
|
+ 'tag': 'display_notification',
|
|
|
|
|
+ 'params': {
|
|
|
|
|
+ 'title': _('Success'),
|
|
|
|
|
+ 'message': _('Google Drive folder structure renamed successfully!<br/>'
|
|
|
|
|
+ 'Folder ID: %s<br/>'
|
|
|
|
|
+ 'Old name: %s<br/>'
|
|
|
|
|
+ 'New name: %s') % (self.google_drive_folder_id, current_folder_name, expected_components['opportunity_name']),
|
|
|
|
|
+ 'type': 'success',
|
|
|
|
|
+ 'sticky': False,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Failed to rename Google Drive folder structure: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to rename Google Drive folder structure: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def action_analyze_folder_structure(self):
|
|
|
|
|
+ """Analyze current vs expected folder structure"""
|
|
|
|
|
+ self.ensure_one()
|
|
|
|
|
+
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get expected components
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder information
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Analyze current structure
|
|
|
|
|
+ current_structure = self._analyze_complete_folder_structure(headers)
|
|
|
|
|
+
|
|
|
|
|
+ # Compare structures
|
|
|
|
|
+ analysis = self._compare_folder_structures(expected_components, current_structure, headers)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'type': 'ir.actions.client',
|
|
|
|
|
+ 'tag': 'display_notification',
|
|
|
|
|
+ 'params': {
|
|
|
|
|
+ 'title': _('Folder Structure Analysis'),
|
|
|
|
|
+ 'message': analysis,
|
|
|
|
|
+ 'type': 'info',
|
|
|
|
|
+ 'sticky': True,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ raise UserError(_('Failed to analyze folder structure: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def _analyze_current_folder_structure(self, headers):
|
|
|
|
|
+ """Analyze the current folder structure in Google Drive"""
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder info
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ current_name = folder_data.get('name', '')
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy
|
|
|
|
|
+ structure = {
|
|
|
|
|
+ 'opportunity_folder': {
|
|
|
|
|
+ 'id': current_folder_id,
|
|
|
|
|
+ 'name': current_name
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parent_ids:
|
|
|
|
|
+ # Get year folder
|
|
|
|
|
+ year_response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{parent_ids[0]}?fields=name,parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if year_response.status_code == 200:
|
|
|
|
|
+ year_data = year_response.json()
|
|
|
|
|
+ structure['year_folder'] = {
|
|
|
|
|
+ 'id': parent_ids[0],
|
|
|
|
|
+ 'name': year_data.get('name', '')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ year_parent_ids = year_data.get('parents', [])
|
|
|
|
|
+ if year_parent_ids:
|
|
|
|
|
+ # Get primary folder (company/contact)
|
|
|
|
|
+ primary_response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{year_parent_ids[0]}?fields=name',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if primary_response.status_code == 200:
|
|
|
|
|
+ primary_data = primary_response.json()
|
|
|
|
|
+ structure['primary_folder'] = {
|
|
|
|
|
+ 'id': year_parent_ids[0],
|
|
|
|
|
+ 'name': primary_data.get('name', '')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return structure
|
|
|
|
|
+
|
|
|
|
|
+ def _analyze_complete_folder_structure(self, headers):
|
|
|
|
|
+ """Analyze the complete folder structure from root to opportunity"""
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder info
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code != 200:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ current_name = folder_data.get('name', '')
|
|
|
|
|
+ parent_ids = folder_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ # Build complete structure
|
|
|
|
|
+ complete_structure = {
|
|
|
|
|
+ 'opportunity_folder': {
|
|
|
|
|
+ 'id': current_folder_id,
|
|
|
|
|
+ 'name': current_name,
|
|
|
|
|
+ 'level': 3
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ current_id = current_folder_id
|
|
|
|
|
+ level = 3 # Opportunity level
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy
|
|
|
|
|
+ for _ in range(5): # Max 5 levels up
|
|
|
|
|
+ if not parent_ids:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ parent_id = parent_ids[0]
|
|
|
|
|
+
|
|
|
|
|
+ # Get parent folder info
|
|
|
|
|
+ parent_response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{parent_id}?fields=name,parents',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if parent_response.status_code != 200:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ parent_data = parent_response.json()
|
|
|
|
|
+ parent_name = parent_data.get('name', '')
|
|
|
|
|
+ parent_ids = parent_data.get('parents', [])
|
|
|
|
|
+
|
|
|
|
|
+ level -= 1
|
|
|
|
|
+
|
|
|
|
|
+ if level == 2: # Year level
|
|
|
|
|
+ complete_structure['year_folder'] = {
|
|
|
|
|
+ 'id': parent_id,
|
|
|
|
|
+ 'name': parent_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+ elif level == 1: # Primary level (company/contact)
|
|
|
|
|
+ complete_structure['primary_folder'] = {
|
|
|
|
|
+ 'id': parent_id,
|
|
|
|
|
+ 'name': parent_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+ elif level == 0: # Root level
|
|
|
|
|
+ complete_structure['root_folder'] = {
|
|
|
|
|
+ 'id': parent_id,
|
|
|
|
|
+ 'name': parent_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ current_id = parent_id
|
|
|
|
|
+
|
|
|
|
|
+ return complete_structure
|
|
|
|
|
+
|
|
|
|
|
+ def _compare_folder_structures(self, expected_components, current_structure, headers):
|
|
|
|
|
+ """Compare expected vs current folder structure"""
|
|
|
|
|
+ if not current_structure:
|
|
|
|
|
+ return _('❌ Could not analyze current folder structure')
|
|
|
|
|
+
|
|
|
|
|
+ analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Expected structure
|
|
|
|
|
+ analysis += f"<strong>Expected Structure:</strong><br/>"
|
|
|
|
|
+ analysis += f"📁 [Root Folder] (MC Team)<br/>"
|
|
|
|
|
+ analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
|
|
|
|
|
+ analysis += f" └── 📁 {expected_components['year']} (Year)<br/>"
|
|
|
|
|
+ analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
|
|
|
|
|
+ analysis += f" ├── 📁 Meets<br/>"
|
|
|
|
|
+ analysis += f" └── 📁 Archivos cliente<br/><br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Current structure
|
|
|
|
|
+ analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Root folder
|
|
|
|
|
+ if 'root_folder' in current_structure:
|
|
|
|
|
+ root_name = current_structure['root_folder']['name']
|
|
|
|
|
+ analysis += f"📁 {root_name} (Root)<br/>"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f"📁 [Unknown Root]<br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Primary folder
|
|
|
|
|
+ if 'primary_folder' in current_structure:
|
|
|
|
|
+ primary_name = current_structure['primary_folder']['name']
|
|
|
|
|
+ analysis += f"└── 📁 {primary_name}"
|
|
|
|
|
+ if primary_name != expected_components['primary_name']:
|
|
|
|
|
+ analysis += f" ❌ (Expected: {expected_components['primary_name']})"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += " ✅"
|
|
|
|
|
+ analysis += "<br/>"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Year folder
|
|
|
|
|
+ if 'year_folder' in current_structure:
|
|
|
|
|
+ year_name = current_structure['year_folder']['name']
|
|
|
|
|
+ analysis += f" └── 📁 {year_name}"
|
|
|
|
|
+ if year_name != expected_components['year']:
|
|
|
|
|
+ analysis += f" ❌ (Expected: {expected_components['year']})"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += " ✅"
|
|
|
|
|
+ analysis += "<br/>"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f" └── 📁 [Missing Year Folder] ❌<br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Opportunity folder
|
|
|
|
|
+ if 'opportunity_folder' in current_structure:
|
|
|
|
|
+ opp_name = current_structure['opportunity_folder']['name']
|
|
|
|
|
+ analysis += f" └── 📁 {opp_name}"
|
|
|
|
|
+ if opp_name != expected_components['opportunity_name']:
|
|
|
|
|
+ analysis += f" ❌ (Expected: {expected_components['opportunity_name']})"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += " ✅"
|
|
|
|
|
+ analysis += "<br/>"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f" └── 📁 [Missing Opportunity Folder] ❌<br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Check subfolders
|
|
|
|
|
+ if 'opportunity_folder' in current_structure:
|
|
|
|
|
+ opp_id = current_structure['opportunity_folder']['id']
|
|
|
|
|
+ subfolders = self._get_subfolders(headers, opp_id)
|
|
|
|
|
+ if subfolders:
|
|
|
|
|
+ analysis += f" ├── 📁 Meets ✅<br/>"
|
|
|
|
|
+ analysis += f" └── 📁 Archivos cliente ✅<br/>"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f" ├── 📁 Meets ❌ (Missing)<br/>"
|
|
|
|
|
+ analysis += f" └── 📁 Archivos cliente ❌ (Missing)<br/>"
|
|
|
|
|
+
|
|
|
|
|
+ # Summary
|
|
|
|
|
+ analysis += f"<br/><strong>Summary:</strong><br/>"
|
|
|
|
|
+ correct_count = 0
|
|
|
|
|
+ total_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ if 'primary_folder' in current_structure:
|
|
|
|
|
+ total_count += 1
|
|
|
|
|
+ if current_structure['primary_folder']['name'] == expected_components['primary_name']:
|
|
|
|
|
+ correct_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ if 'year_folder' in current_structure:
|
|
|
|
|
+ total_count += 1
|
|
|
|
|
+ if current_structure['year_folder']['name'] == expected_components['year']:
|
|
|
|
|
+ correct_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ if 'opportunity_folder' in current_structure:
|
|
|
|
|
+ total_count += 1
|
|
|
|
|
+ if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']:
|
|
|
|
|
+ correct_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ if total_count == 3 and correct_count == 3:
|
|
|
|
|
+ analysis += "✅ Complete structure is correct"
|
|
|
|
|
+ else:
|
|
|
|
|
+ analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct). Use 'Rename Folder Structure' button to fix."
|
|
|
|
|
+
|
|
|
|
|
+ return analysis
|
|
|
|
|
+
|
|
|
|
|
+ def _get_subfolders(self, headers, parent_id):
|
|
|
|
|
+ """Get subfolders of a parent folder"""
|
|
|
|
|
+ params = {
|
|
|
|
|
+ 'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
|
|
|
|
|
+ 'fields': 'files(id,name)',
|
|
|
|
|
+ 'pageSize': 100
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ 'https://www.googleapis.com/drive/v3/files',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ params=params,
|
|
|
|
|
+ timeout=30
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ data = response.json()
|
|
|
|
|
+ return data.get('files', [])
|
|
|
|
|
+ else:
|
|
|
|
|
+ return []
|
|
|
|
|
+ except:
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ def _store_initial_structure_and_update_url(self):
|
|
|
|
|
+ """Centralized method to store initial structure and update URL"""
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+ expected_structure = self._build_structure_string(expected_components)
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_folder_name': expected_structure
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ # Update Google Drive URL if empty
|
|
|
|
|
+ self._update_google_drive_url()
|
|
|
|
|
+
|
|
|
|
|
+ # Copy URL to configured field if it's empty
|
|
|
|
|
+ self._copy_google_drive_url_to_configured_field()
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Estructura inicial almacenada para oportunidad {self.id}")
|
|
|
|
|
+ return True
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.error(f"ERROR: _create_google_drive_folder_structure no asignó google_drive_folder_id para oportunidad {self.id}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _generate_google_drive_url(self):
|
|
|
|
|
+ """Generate Google Drive URL for the opportunity folder"""
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ return f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _update_google_drive_url(self):
|
|
|
|
|
+ """Update the google_drive_url field if it's empty and we have a folder ID"""
|
|
|
|
|
+ if self.google_drive_folder_id and not self.google_drive_url:
|
|
|
|
|
+ url = self._generate_google_drive_url()
|
|
|
|
|
+ if url:
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({
|
|
|
|
|
+ 'google_drive_url': url
|
|
|
|
|
+ })
|
|
|
|
|
+ _logger.info(f"Updated Google Drive URL for opportunity {self.id}: {url}")
|
|
|
|
|
+
|
|
|
|
|
+ def _extract_folder_id_from_url(self, url):
|
|
|
|
|
+ """Extract folder ID from Google Drive URL"""
|
|
|
|
|
+ if not url:
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ # Handle different Google Drive URL formats
|
|
|
|
|
+ import re
|
|
|
|
|
+
|
|
|
|
|
+ # Format: https://drive.google.com/drive/folders/FOLDER_ID
|
|
|
|
|
+ folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
|
|
|
|
|
+ match = re.search(folder_pattern, url)
|
|
|
|
|
+
|
|
|
|
|
+ if match:
|
|
|
|
|
+ return match.group(1)
|
|
|
|
|
+
|
|
|
|
|
+ # Format: https://drive.google.com/open?id=FOLDER_ID
|
|
|
|
|
+ open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
|
|
|
|
|
+ match = re.search(open_pattern, url)
|
|
|
|
|
+
|
|
|
|
|
+ if match:
|
|
|
|
|
+ return match.group(1)
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _get_configured_field_name(self):
|
|
|
|
|
+ """Get the field name configured in company settings"""
|
|
|
|
|
+ if not self.company_id or not self.company_id.google_drive_crm_field_id:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return self.company_id.google_drive_crm_field_id.name
|
|
|
|
|
+
|
|
|
|
|
+ def _get_configured_field_value(self):
|
|
|
|
|
+ """Get the value of the configured field"""
|
|
|
|
|
+ field_name = self._get_configured_field_name()
|
|
|
|
|
+ if not field_name:
|
|
|
|
|
+ return None
|
|
|
|
|
+ return getattr(self, field_name, None)
|
|
|
|
|
+
|
|
|
|
|
+ def _set_configured_field_value(self, value):
|
|
|
|
|
+ """Set the value of the configured field"""
|
|
|
|
|
+ field_name = self._get_configured_field_name()
|
|
|
|
|
+ if not field_name:
|
|
|
|
|
+ return False
|
|
|
|
|
+ self.with_context(skip_google_drive_update=True).write({field_name: value})
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ def _copy_google_drive_url_to_configured_field(self):
|
|
|
|
|
+ """Copy google_drive_url to the configured field if it's empty"""
|
|
|
|
|
+ if not self.google_drive_url:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ field_name = self._get_configured_field_name()
|
|
|
|
|
+ if not field_name:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ current_value = getattr(self, field_name, None)
|
|
|
|
|
+ if not current_value: # Solo si está vacío
|
|
|
|
|
+ self._set_configured_field_value(self.google_drive_url)
|
|
|
|
|
+ _logger.info(f"Copied google_drive_url to {field_name} for opportunity {self.id}")
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _validate_folder_id_with_google_drive(self, folder_id):
|
|
|
|
|
+ """Validate if the folder ID exists and is accessible in Google Drive"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ access_token = self._get_google_drive_access_token()
|
|
|
|
|
+ headers = {
|
|
|
|
|
+ 'Authorization': f'Bearer {access_token}',
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Test access to the specific folder
|
|
|
|
|
+ response = requests.get(
|
|
|
|
|
+ f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
|
|
|
|
|
+ headers=headers,
|
|
|
|
|
+ timeout=10
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if response.status_code == 200:
|
|
|
|
|
+ folder_data = response.json()
|
|
|
|
|
+ if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
|
|
|
|
|
+ _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
|
|
|
|
|
+ return True, folder_data.get('name', 'Unknown')
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
|
|
|
|
|
+ return False, "Not a folder"
|
|
|
|
|
+ elif response.status_code == 404:
|
|
|
|
|
+ _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive")
|
|
|
|
|
+ return False, "Not found"
|
|
|
|
|
+ elif response.status_code == 403:
|
|
|
|
|
+ _logger.warning(f"❌ Access denied to folder ID {folder_id}")
|
|
|
|
|
+ return False, "Access denied"
|
|
|
|
|
+ elif response.status_code == 401:
|
|
|
|
|
+ _logger.error(f"❌ OAuth token expired or invalid")
|
|
|
|
|
+ return False, "Authentication error"
|
|
|
|
|
+ else:
|
|
|
|
|
+ _logger.warning(f"❌ Google Drive API error: {response.status_code}")
|
|
|
|
|
+ return False, f"API error: {response.status_code}"
|
|
|
|
|
+
|
|
|
|
|
+ except requests.exceptions.Timeout:
|
|
|
|
|
+ _logger.error(f"❌ Timeout validating folder ID {folder_id}")
|
|
|
|
|
+ return False, "Timeout"
|
|
|
|
|
+ except requests.exceptions.ConnectionError:
|
|
|
|
|
+ _logger.error(f"❌ Connection error validating folder ID {folder_id}")
|
|
|
|
|
+ return False, "Connection error"
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
|
|
|
|
|
+ return False, str(e)
|
|
|
|
|
+
|
|
|
|
|
+
|