|
@@ -0,0 +1,1704 @@
|
|
|
|
|
+# -*- 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_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 using the new service"""
|
|
|
|
|
+ if not self.google_drive_folder_id or not self.company_id:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Get company root folder ID
|
|
|
|
|
+ company_root_folder_id = self._get_company_root_folder_id()
|
|
|
|
|
+ if not company_root_folder_id:
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Use the new Google Drive service
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Check if folder belongs to the company root
|
|
|
|
|
+ belongs_to_company = drive_service.check_folder_belongs_to_parent(
|
|
|
|
|
+ self.google_drive_folder_id,
|
|
|
|
|
+ company_root_folder_id
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Return True if there's a mismatch (folder doesn't belong to company)
|
|
|
|
|
+ return not belongs_to_company
|
|
|
|
|
+
|
|
|
|
|
+ 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 using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ validation = drive_service.validate_folder_id(folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if validation.get('valid'):
|
|
|
|
|
+ folder_name = validation.get('name', 'Unknown')
|
|
|
|
|
+ _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
|
|
|
|
|
+ return True, folder_name
|
|
|
|
|
+ else:
|
|
|
|
|
+ error_message = validation.get('error', 'Unknown error')
|
|
|
|
|
+ _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
|
|
|
|
|
+ return False, error_message
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Create folder structure in batch for better performance
|
|
|
|
|
+ folder_structure = self._create_folder_structure_batch(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, components):
|
|
|
|
|
+ """Create folder structure in batch for better performance using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ 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_crm(
|
|
|
|
|
+ root_folder_id, components['primary_name']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 2: Create or get year folder
|
|
|
|
|
+ year_folder_id = self._create_or_get_folder_crm(
|
|
|
|
|
+ primary_folder_id, components['year']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 3: Create or get opportunity folder
|
|
|
|
|
+ opportunity_folder_id = self._create_or_get_folder_crm(
|
|
|
|
|
+ year_folder_id, components['opportunity_name']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Step 4: Create Meets and Archivos cliente folders (parallel creation)
|
|
|
|
|
+ meets_folder_id = self._create_or_get_folder_crm(
|
|
|
|
|
+ opportunity_folder_id, 'Meets'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ archivos_folder_id = self._create_or_get_folder_crm(
|
|
|
|
|
+ opportunity_folder_id, 'Archivos cliente'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'opportunity_folder_id': opportunity_folder_id,
|
|
|
|
|
+ 'meets_folder_id': meets_folder_id,
|
|
|
|
|
+ 'archivos_folder_id': archivos_folder_id
|
|
|
|
|
+ }
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error creating folder structure batch: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
|
|
|
|
|
+ """Create a folder or get existing one by name using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # First, check if folder already exists
|
|
|
|
|
+ existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
|
|
|
|
|
+ if existing_folders:
|
|
|
|
|
+ return existing_folders[0]['id']
|
|
|
|
|
+
|
|
|
|
|
+ # Create new folder
|
|
|
|
|
+ result = drive_service.create_folder(folder_name, parent_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if result.get('success'):
|
|
|
|
|
+ return result.get('folder_id')
|
|
|
|
|
+ else:
|
|
|
|
|
+ error_msg = result.get('error', 'Unknown error')
|
|
|
|
|
+ raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error creating or getting folder {folder_name}: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+ def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
|
|
|
|
|
+ """Find a folder by name in the parent folder with caching using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Use the new service method
|
|
|
|
|
+ folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
|
|
|
|
|
+
|
|
|
|
|
+ if folders:
|
|
|
|
|
+ result = folders[0]
|
|
|
|
|
+
|
|
|
|
|
+ # Cache the result for 2 minutes
|
|
|
|
|
+ cache_key = f'folder_{parent_folder_id}_{folder_name}'
|
|
|
|
|
+ _GOOGLE_DRIVE_CACHE[cache_key] = {
|
|
|
|
|
+ 'result': result,
|
|
|
|
|
+ 'expires': datetime.now() + timedelta(seconds=120)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+ else:
|
|
|
|
|
+ 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 using the new service"""
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
|
|
|
|
|
+
|
|
|
|
|
+ result = drive_service.rename_folder(self.google_drive_folder_id, sanitized_name)
|
|
|
|
|
+
|
|
|
|
|
+ if result.get('success'):
|
|
|
|
|
+ # 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: {result.get("error", "Unknown error")}'
|
|
|
|
|
+ _logger.error(error_msg)
|
|
|
|
|
+ raise UserError(_(error_msg))
|
|
|
|
|
+
|
|
|
|
|
+ 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'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Move the folder to the new company root
|
|
|
|
|
+ result = drive_service.move_folder(self.google_drive_folder_id, new_root_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not result.get('success'):
|
|
|
|
|
+ raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error moving folder to new company: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to move folder to new company: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ def _delete_google_drive_folder_structure(self):
|
|
|
|
|
+ """Delete the Google Drive folder structure when contact is removed using the new service"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Delete the folder (this will also delete subfolders)
|
|
|
|
|
+ result = drive_service.delete_folder(self.google_drive_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if result.get('success'):
|
|
|
|
|
+ # 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: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error deleting folder structure: {str(e)}")
|
|
|
|
|
+ raise UserError(_('Failed to delete Google Drive folder structure: %s') % str(e))
|
|
|
|
|
+
|
|
|
|
|
+ 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=None, new_components=None):
|
|
|
|
|
+ """Unified method to rename folder structure"""
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # If only new_components provided, get current from Google Drive
|
|
|
|
|
+ if old_components is None and new_components is not None:
|
|
|
|
|
+ current_structure = self._analyze_complete_folder_structure()
|
|
|
|
|
+ if not current_structure:
|
|
|
|
|
+ raise UserError(_('Could not analyze current folder structure'))
|
|
|
|
|
+
|
|
|
|
|
+ old_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', '')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # If only old_components provided, get new from current record
|
|
|
|
|
+ if new_components is None and old_components is not None:
|
|
|
|
|
+ new_components = self._get_folder_name_components()
|
|
|
|
|
+
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up to find primary folder
|
|
|
|
|
+ primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
|
|
|
|
|
+ if not primary_folder_id:
|
|
|
|
|
+ raise UserError(_('Cannot find primary folder in the structure'))
|
|
|
|
|
+
|
|
|
|
|
+ # Rename primary folder if needed
|
|
|
|
|
+ if old_components['primary_name'] != new_components['primary_name']:
|
|
|
|
|
+ result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
|
|
|
|
|
+ if not result.get('success'):
|
|
|
|
|
+ raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ # Rename year folder if needed
|
|
|
|
|
+ if old_components['year'] != new_components['year']:
|
|
|
|
|
+ year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
|
|
|
|
|
+ if year_folder_id:
|
|
|
|
|
+ result = drive_service.rename_folder(year_folder_id, new_components['year'])
|
|
|
|
|
+ if not result.get('success'):
|
|
|
|
|
+ raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ # 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']}'")
|
|
|
|
|
+ result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
|
|
|
|
|
+ if not result.get('success'):
|
|
|
|
|
+ raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ 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']}'")
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error renaming folder structure: {str(e)}")
|
|
|
|
|
+ raise
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ 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(vals, current_values[record.id])
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ def _process_google_drive_updates(self, vals, old_values=None):
|
|
|
|
|
+ """Unified method to process Google Drive updates"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
|
|
|
|
|
+
|
|
|
|
|
+ # Check if we need to create folder
|
|
|
|
|
+ should_create = self._should_create_folder(vals)
|
|
|
|
|
+ if should_create and self._validate_folder_creation_prerequisites():
|
|
|
|
|
+ _logger.info(f"Creating Google Drive folder for opportunity {self.id}")
|
|
|
|
|
+ self._create_google_drive_folder_structure()
|
|
|
|
|
+ if self._store_initial_structure_and_update_url():
|
|
|
|
|
+ self.message_post(
|
|
|
|
|
+ body=_("✅ Google Drive folder created automatically"),
|
|
|
|
|
+ message_type='comment'
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # If we have a folder, verify and update structure
|
|
|
|
|
+ if self.google_drive_folder_id:
|
|
|
|
|
+ self._verify_and_update_folder_structure(vals, old_values)
|
|
|
|
|
+
|
|
|
|
|
+ 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 _should_create_folder(self, vals):
|
|
|
|
|
+ """Helper method to determine if folder should be created"""
|
|
|
|
|
+ # Stage-based creation logic
|
|
|
|
|
+ if 'stage_id' in vals and not self.google_drive_folder_id:
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ # Auto-creation logic
|
|
|
|
|
+ if not self.google_drive_folder_id:
|
|
|
|
|
+ company = self.company_id
|
|
|
|
|
+ return (company.google_drive_crm_enabled and
|
|
|
|
|
+ company.google_drive_crm_stage_id and
|
|
|
|
|
+ self.stage_id.id == company.google_drive_crm_stage_id.id)
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def _verify_and_update_folder_structure(self, vals, old_values=None):
|
|
|
|
|
+ """Unified method to verify and update folder structure"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
|
|
|
|
|
+
|
|
|
|
|
+ # Get expected structure components
|
|
|
|
|
+ expected_components = self._get_folder_name_components()
|
|
|
|
|
+ expected_structure = self._build_structure_string(expected_components)
|
|
|
|
|
+
|
|
|
|
|
+ # Get current structure from old_values or stored field
|
|
|
|
|
+ if old_values:
|
|
|
|
|
+ current_structure = old_values.get('google_drive_folder_name', '')
|
|
|
|
|
+ else:
|
|
|
|
|
+ current_structure = self.google_drive_folder_name or ''
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Structure comparison: Current='{current_structure}' vs 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 and self.google_drive_folder_id:
|
|
|
|
|
+ new_company_id = vals['company_id']
|
|
|
|
|
+ if old_values and 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)
|
|
|
|
|
+ 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"),
|
|
|
|
|
+ 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:
|
|
|
|
|
+ # Use the unified method with only new_components
|
|
|
|
|
+ self._rename_entire_folder_structure(new_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 using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Use the new Google Drive service
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder info and navigate up to find the primary folder (company/contact)
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ # Validate current folder using the service
|
|
|
|
|
+ validation = drive_service.validate_folder_id(current_folder_id)
|
|
|
|
|
+ if not validation.get('valid'):
|
|
|
|
|
+ raise UserError(_('Failed to get current folder information'))
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy to find the primary folder
|
|
|
|
|
+ primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not primary_folder_id:
|
|
|
|
|
+ raise UserError(_('Could not find primary folder in hierarchy'))
|
|
|
|
|
+
|
|
|
|
|
+ # Move the primary folder to the new parent
|
|
|
|
|
+ result = drive_service.move_folder(primary_folder_id, new_parent_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not result.get('success'):
|
|
|
|
|
+ raise UserError(_('Failed to move folder structure to new parent: %s') % result.get('error', 'Unknown error'))
|
|
|
|
|
+
|
|
|
|
|
+ _logger.info(f"Successfully moved entire folder structure 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 using the new service"""
|
|
|
|
|
+ return self._find_primary_folder_id_crm(start_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ 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'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Use the new Google Drive service to get folder information
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ validation = drive_service.validate_folder_id(self.google_drive_folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if not validation.get('valid'):
|
|
|
|
|
+ raise UserError(_('Failed to get current folder information from Google Drive'))
|
|
|
|
|
+
|
|
|
|
|
+ current_folder_name = validation.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 using the new service"""
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Use the new CRM-specific method
|
|
|
|
|
+ return self._analyze_crm_folder_structure(current_folder_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error analyzing current folder structure: {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _analyze_complete_folder_structure(self, headers):
|
|
|
|
|
+ """Analyze the complete folder structure from root to opportunity using the new CRM-specific method"""
|
|
|
|
|
+ current_folder_id = self.google_drive_folder_id
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Use the new CRM-specific method
|
|
|
|
|
+ return self._analyze_crm_folder_structure(current_folder_id)
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error in _analyze_complete_folder_structure: {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ 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 using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ return drive_service.find_folders_by_name(parent_id, r'.*')
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error getting subfolders: {str(e)}")
|
|
|
|
|
+ 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 using the new service"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ validation = drive_service.validate_folder_id(folder_id)
|
|
|
|
|
+
|
|
|
|
|
+ if validation.get('valid'):
|
|
|
|
|
+ folder_name = validation.get('name', 'Unknown')
|
|
|
|
|
+ _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
|
|
|
|
|
+ return True, folder_name
|
|
|
|
|
+ else:
|
|
|
|
|
+ error_message = validation.get('error', 'Unknown error')
|
|
|
|
|
+ _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
|
|
|
|
|
+ return False, error_message
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
|
|
|
|
|
+ return False, str(e)
|
|
|
|
|
+
|
|
|
|
|
+ # ============================================================================
|
|
|
|
|
+ # MÉTODOS ESPECÍFICOS DE CRM QUE USAN SERVICIOS GENÉRICOS
|
|
|
|
|
+ # ============================================================================
|
|
|
|
|
+
|
|
|
|
|
+ def _find_primary_folder_id_crm(self, start_folder_id):
|
|
|
|
|
+ """Find the primary folder (company/contact level) in the hierarchy - CRM specific logic"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Use the generic service to navigate the hierarchy
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
|
|
|
|
|
+
|
|
|
|
|
+ # Apply CRM-specific logic to identify the primary folder
|
|
|
|
|
+ for folder_info in hierarchy:
|
|
|
|
|
+ folder_name = folder_info.get('name', '')
|
|
|
|
|
+ level = folder_info.get('level', 0)
|
|
|
|
|
+
|
|
|
|
|
+ # Skip root level folders
|
|
|
|
|
+ if level == 0:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # Check if this folder is the primary folder (not a year or opportunity folder)
|
|
|
|
|
+ # Primary folder is typically the company/contact 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 = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
|
|
|
|
|
+ if year_folders:
|
|
|
|
|
+ # This is the primary folder
|
|
|
|
|
+ return folder_info['id']
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error finding primary folder (CRM): {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _find_year_folder_id_crm(self, primary_folder_id, year):
|
|
|
|
|
+ """Find the year folder within the primary folder - CRM specific"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+ year_folders = drive_service.find_folders_by_name(primary_folder_id, f'^{year}$')
|
|
|
|
|
+ return year_folders[0]['id'] if year_folders else None
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error finding year folder (CRM): {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _analyze_crm_folder_structure(self, folder_id):
|
|
|
|
|
+ """Analyze the complete folder structure from root to opportunity - CRM specific"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Get current folder info
|
|
|
|
|
+ validation = drive_service.validate_folder_id(folder_id)
|
|
|
|
|
+ if not validation.get('valid'):
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ current_name = validation.get('name', '')
|
|
|
|
|
+
|
|
|
|
|
+ # Build complete structure
|
|
|
|
|
+ complete_structure = {
|
|
|
|
|
+ 'opportunity_folder': {
|
|
|
|
|
+ 'id': folder_id,
|
|
|
|
|
+ 'name': current_name,
|
|
|
|
|
+ 'level': 3
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Navigate up the hierarchy using the generic service
|
|
|
|
|
+ hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
|
|
|
|
|
+
|
|
|
|
|
+ # Apply CRM-specific logic to categorize folders
|
|
|
|
|
+ for folder_info in hierarchy:
|
|
|
|
|
+ folder_name = folder_info.get('name', '')
|
|
|
|
|
+ level = folder_info.get('level', 0)
|
|
|
|
|
+
|
|
|
|
|
+ if level == 2: # Year level
|
|
|
|
|
+ if folder_name.isdigit() and len(folder_name) == 4:
|
|
|
|
|
+ complete_structure['year_folder'] = {
|
|
|
|
|
+ 'id': folder_info['id'],
|
|
|
|
|
+ 'name': folder_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+ elif level == 1: # Primary level (company/contact)
|
|
|
|
|
+ if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
|
|
|
|
|
+ complete_structure['primary_folder'] = {
|
|
|
|
|
+ 'id': folder_info['id'],
|
|
|
|
|
+ 'name': folder_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+ elif level == 0: # Root level
|
|
|
|
|
+ complete_structure['root_folder'] = {
|
|
|
|
|
+ 'id': folder_info['id'],
|
|
|
|
|
+ 'name': folder_name,
|
|
|
|
|
+ 'level': level
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return complete_structure
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}")
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def _check_crm_folder_company_mismatch(self, folder_id, company_root_id):
|
|
|
|
|
+ """Check if folder belongs to the correct company root - CRM specific"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ drive_service = self.env['google.drive.service']
|
|
|
|
|
+
|
|
|
|
|
+ # Use the generic service to check folder parent relationship
|
|
|
|
|
+ belongs_to_company = drive_service.check_folder_belongs_to_parent(
|
|
|
|
|
+ folder_id,
|
|
|
|
|
+ company_root_id
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Return True if there's a mismatch (folder doesn't belong to company)
|
|
|
|
|
+ return not belongs_to_company
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ _logger.error(f"Error checking CRM folder company mismatch: {str(e)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+
|