# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import re
from datetime import datetime, timedelta
from odoo import fields, models, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
# Cache for Google Drive API responses (5 minutes)
_GOOGLE_DRIVE_CACHE = {}
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:
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
count = 0
_GOOGLE_DRIVE_CACHE[cache_key] = {
'count': count,
'expires': datetime.now() + timedelta(seconds=300)
}
record.google_drive_documents_count = count
else:
record.google_drive_documents_count = 0
# ============================================================================
# MÉTODOS DE CONFIGURACIÓN Y UTILIDADES
# ============================================================================
def _get_company_root_folder_id(self):
"""Get the company's root Google Drive folder ID"""
company = self.company_id
return (company.google_drive_crm_folder_id
if company and company.google_drive_crm_enabled else None)
def _extract_folder_id_from_url(self, url):
"""Extract folder ID from Google Drive URL - Now uses generic service"""
drive_service = self.env['google.drive.service']
return drive_service.extract_folder_id_from_url(url)
def _get_configured_field_value(self):
"""Get value from the configured Google Drive field in company settings"""
field_id = self.company_id.google_drive_crm_field_id
return getattr(self, field_id.name, None) if field_id else None
def _set_configured_field_value(self, value):
"""Set the value of the configured field"""
field_id = self.company_id.google_drive_crm_field_id
if field_id:
self.with_context(skip_google_drive_update=True).write({field_id.name: value})
return True
return False
def _sanitize_folder_name(self, name):
"""Sanitize folder name to be Google Drive compatible - Now uses generic service"""
drive_service = self.env['google.drive.service']
return drive_service.sanitize_folder_name(name)
def _build_structure_string(self, components):
"""Build a string representation of the folder structure"""
return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
# ============================================================================
# MÉTODOS DE VALIDACIÓN Y PRERREQUISITOS
# ============================================================================
def _validate_folder_creation_prerequisites(self):
"""Validate prerequisites before creating Google Drive folder"""
if not self._get_company_root_folder_id():
self.message_post(
body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
message_type='comment'
)
return False
if not self.partner_id:
self.message_post(
body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
message_type='comment'
)
return False
return True
def _try_get_existing_folder_id(self):
"""Try to get existing folder ID from various sources"""
# Check if we already have a folder ID
if self.google_drive_folder_id:
return self.google_drive_folder_id
# Try to extract from configured field
field_value = self._get_configured_field_value()
if field_value:
folder_id = self._extract_folder_id_from_url(field_value)
if folder_id:
_logger.info(f"Found folder ID from configured field: {folder_id}")
return folder_id
# Try to extract from google_drive_url field
if self.google_drive_url:
folder_id = self._extract_folder_id_from_url(self.google_drive_url)
if folder_id:
_logger.info(f"Found folder ID from URL field: {folder_id}")
return folder_id
return None
def _validate_folder_id_with_google_drive(self, folder_id):
"""Validate if the folder ID exists and is accessible in Google Drive - Now uses generic service"""
drive_service = self.env['google.drive.service']
return drive_service.validate_folder_id_with_google_drive(folder_id)
# ============================================================================
# MÉTODOS DE COMPONENTES DE CARPETA
# ============================================================================
def _get_folder_name_components(self):
"""Get the components for folder naming based on partner/contact information"""
# Priority 1: partner_id.company_id.name (empresa del cliente)
if self.partner_id and self.partner_id.company_id and self.partner_id.company_id.name:
primary_name = self.partner_id.company_id.name
# Priority 2: partner_id.company_name
elif self.partner_id and self.partner_id.company_name:
primary_name = self.partner_id.company_name
# Priority 3: partner_id.name
elif self.partner_id:
primary_name = self.partner_id.name
else:
primary_name = "Sin Contacto"
if not primary_name:
raise UserError(_('No company or contact name available. Please assign a contact with company information.'))
return {
'primary_name': self._sanitize_folder_name(primary_name),
'opportunity_name': self._sanitize_folder_name(self.name),
'year': str(self.create_date.year) if self.create_date else str(datetime.now().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 or old_partner.id == new_partner_id:
return False
new_partner = self.env['res.partner'].browse(new_partner_id)
# 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
# ============================================================================
# MÉTODOS DE CREACIÓN DE CARPETAS
# ============================================================================
def _create_google_drive_folder_structure(self):
"""Create the complete Google Drive folder structure for this opportunity"""
self.ensure_one()
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:
is_valid, _ = self._validate_folder_id_with_google_drive(existing_folder_id)
if is_valid:
self._store_folder_info(existing_folder_id)
self.message_post(
body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
message_type='comment'
)
return True
# Check if structure already exists before creating
existing_structure = self._find_existing_folder_structure()
if existing_structure:
self._store_folder_info(existing_structure['opportunity_folder_id'])
self.message_post(
body=_("✅ Using existing folder structure: %s") % existing_structure['opportunity_folder_id'],
message_type='comment'
)
return existing_structure
# Create new folder structure
components = self._get_folder_name_components()
try:
folder_structure = self._create_folder_structure_batch(components)
self._store_folder_info(folder_structure['opportunity_folder_id'])
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, reusing existing levels to avoid duplicates"""
drive_service = self.env['google.drive.service']
root_folder_id = self._get_company_root_folder_id()
# Level 1: Primary folder (empresa cliente) - reutilizar si existe
primary_folder_id, was_reused = self._create_or_get_folder_crm(root_folder_id, components['primary_name'])
_logger.info(f"✅ Primary folder '{components['primary_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {primary_folder_id})")
# Level 2: Year folder - reutilizar si existe
year_folder_id, was_reused = self._create_or_get_folder_crm(primary_folder_id, components['year'])
_logger.info(f"✅ Year folder '{components['year']}': {'REUSED' if was_reused else 'CREATED'} (ID: {year_folder_id})")
# Level 3: Opportunity folder - siempre crear (único por oportunidad)
opportunity_folder_id, was_reused = self._create_or_get_folder_crm(year_folder_id, components['opportunity_name'])
_logger.info(f"✅ Opportunity folder '{components['opportunity_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {opportunity_folder_id})")
# Subfolders - reutilizar si existen
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
}
def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
"""Create a folder or get existing one by name, avoiding duplicates - Now uses generic service"""
drive_service = self.env['google.drive.service']
_logger.info(f"🔍 Checking for existing folder '{folder_name}' in parent {parent_folder_id}")
# First check if folder already exists
existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
was_reused = len(existing_folders) > 0
if was_reused:
folder_id = existing_folders[0]['id']
_logger.info(f"🔄 REUSING existing folder '{folder_name}' (ID: {folder_id})")
else:
# Create new folder
result = drive_service.create_folder(folder_name, parent_folder_id)
if result.get('success'):
folder_id = result.get('folder_id')
_logger.info(f"📁 CREATED new folder '{folder_name}' (ID: {folder_id})")
else:
error_msg = result.get('error', 'Unknown error')
raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
return folder_id, was_reused
def _find_existing_folder_structure(self):
"""Find existing folder structure to avoid creating duplicates"""
components = self._get_folder_name_components()
drive_service = self.env['google.drive.service']
root_folder_id = self._get_company_root_folder_id()
try:
# Level 1: Find primary folder (empresa cliente)
primary_folders = drive_service.find_folders_by_name(root_folder_id, f"^{components['primary_name']}$")
if not primary_folders:
return None
primary_folder_id = primary_folders[0]['id']
# Level 2: Find year folder
year_folders = drive_service.find_folders_by_name(primary_folder_id, f"^{components['year']}$")
if not year_folders:
return None
year_folder_id = year_folders[0]['id']
# Level 3: Find opportunity folder
opportunity_folders = drive_service.find_folders_by_name(year_folder_id, f"^{components['opportunity_name']}$")
if not opportunity_folders:
return None
opportunity_folder_id = opportunity_folders[0]['id']
# Check if subfolders exist
meets_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Meets$')
archivos_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Archivos cliente$')
_logger.info(f"🔄 Found existing complete folder structure for opportunity '{components['opportunity_name']}'")
return {
'opportunity_folder_id': opportunity_folder_id,
'meets_folder_id': meets_folders[0]['id'] if meets_folders else None,
'archivos_folder_id': archivos_folders[0]['id'] if archivos_folders else None
}
except Exception as e:
_logger.warning(f"Error finding existing folder structure: {str(e)}")
return None
def _store_folder_info(self, folder_id):
"""Store folder information and update URL"""
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_id': folder_id,
'google_drive_folder_name': expected_structure,
'google_drive_url': f"https://drive.google.com/drive/folders/{folder_id}"
})
# Copy URL to configured field if empty
if not self._get_configured_field_value():
self._set_configured_field_value(self.google_drive_url)
# ============================================================================
# MÉTODOS DE ACTUALIZACIÓN Y RENOMBRADO
# ============================================================================
def _process_google_drive_updates(self, vals, old_values=None):
"""Unified method to process Google Drive updates"""
try:
# Check if we need to create folder
if self._should_create_folder(vals) and self._validate_folder_creation_prerequisites():
self._create_google_drive_folder_structure()
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"""
if 'stage_id' in vals and not self.google_drive_folder_id:
return True
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"""
# Handle company change (move folder to new company, don't rename)
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'):
self._move_folder_to_new_company(new_company_id)
# Don't update folder name - just move it
return # Exit early after moving folder
# Handle partner changes (this should trigger folder rename)
if 'partner_id' in vals and self.google_drive_folder_id:
old_partner_id = old_values.get('partner_id') if old_values else self.partner_id.id
new_partner_id = vals['partner_id']
if old_partner_id != new_partner_id:
# Partner changed - rename folder structure
self._rename_entire_folder_structure()
# Update stored structure
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=_("✅ Google Drive folder renamed due to partner change"),
message_type='comment'
)
return
# Handle other structure changes (name changes, etc.)
expected_components = self._get_folder_name_components()
expected_structure = self._build_structure_string(expected_components)
current_structure = (old_values.get('google_drive_folder_name', '') if old_values
else self.google_drive_folder_name or '')
if current_structure != expected_structure:
# Rename structure
self._rename_entire_folder_structure(new_components=expected_components)
# Update 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'
)
def _rename_entire_folder_structure(self, old_components=None, new_components=None):
"""Unified method to rename folder structure with robust error handling"""
if not self.google_drive_folder_id:
return
try:
drive_service = self.env['google.drive.service']
_logger.info(f"🔄 Starting folder structure rename for opportunity {self.id}")
# First validate that the current folder exists
validation = drive_service.validate_folder_id(self.google_drive_folder_id)
if not validation.get('valid'):
_logger.warning(f"Cannot rename folder {self.google_drive_folder_id}: folder not found or not accessible")
raise UserError(_('Cannot rename folder: folder not found or not accessible'))
# Get current structure if needed
if old_components is None and new_components is not None:
current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id)
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', '')
}
# Get new components if needed
if new_components is None and old_components is not None:
new_components = self._get_folder_name_components()
current_folder_id = self.google_drive_folder_id
# SOLUCIÓN ROBUSTA: En lugar de buscar el primary folder, vamos a recrear la estructura
# Esto evita problemas con folders que ya no existen
_logger.info(f"🔄 Using robust approach: recreating folder structure")
# Get company root folder
company_root_folder_id = self._get_company_root_folder_id()
if not company_root_folder_id:
raise UserError(_('Company root folder not configured'))
# Create new structure with new components
_logger.info(f"🔄 Creating new folder structure with components: {new_components}")
# Create primary folder
new_primary_folder_id, was_reused = self._create_or_get_folder_crm(company_root_folder_id, new_components['primary_name'])
_logger.info(f"✅ Primary folder created/found: {new_primary_folder_id}")
# Create year folder
new_year_folder_id, was_reused = self._create_or_get_folder_crm(new_primary_folder_id, new_components['year'])
_logger.info(f"✅ Year folder created/found: {new_year_folder_id}")
# Validate current opportunity folder exists before moving
_logger.info(f"🔍 Validating current opportunity folder: {current_folder_id}")
current_validation = drive_service.validate_folder_id(current_folder_id)
if not current_validation.get('valid'):
_logger.warning(f"⚠️ Current opportunity folder {current_folder_id} is not accessible: {current_validation.get('error')}")
# If folder doesn't exist, we need to create a new one
_logger.info(f"🔄 Creating new opportunity folder in the correct structure")
# Create new opportunity folder with correct name
new_opportunity_folder_id, was_reused = self._create_or_get_folder_crm(new_year_folder_id, new_components['opportunity_name'])
# Update the stored folder ID to the new folder
self.with_context(skip_google_drive_update=True).write({
'google_drive_folder_id': new_opportunity_folder_id,
'google_drive_folder_name': self._build_structure_string(new_components)
})
_logger.info(f"✅ Created new opportunity folder: {new_opportunity_folder_id}")
return
# Move current opportunity folder to new structure
_logger.info(f"🔄 Moving opportunity folder from old structure to new structure")
move_result = drive_service.move_folder(current_folder_id, new_year_folder_id)
if not move_result.get('success'):
_logger.error(f"❌ Failed to move opportunity folder: {move_result.get('error')}")
raise UserError(_('Failed to move opportunity folder: %s') % move_result.get('error', 'Unknown error'))
_logger.info(f"✅ Successfully moved opportunity folder to new structure")
# Update the stored folder information
new_structure = self._build_structure_string(new_components)
self.with_context(skip_google_drive_update=True).write({
'google_drive_folder_name': new_structure
})
_logger.info(f"✅ Folder structure rename completed successfully")
except Exception as e:
_logger.error(f"Error renaming folder structure: {str(e)}")
raise
def _move_folder_to_new_company(self, new_company_id):
"""Move only this opportunity's folder to new company"""
if not self.google_drive_folder_id:
return
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
try:
drive_service = self.env['google.drive.service']
# First validate that the current folder exists
validation = drive_service.validate_folder_id(self.google_drive_folder_id)
if not validation.get('valid'):
_logger.warning(f"Cannot move folder {self.google_drive_folder_id}: folder not found or not accessible")
self.message_post(
body=_("⚠️ No se puede mover: La carpeta actual no existe o no es accesible."),
message_type='comment'
)
return
# Get current opportunity folder components
current_components = self._get_folder_name_components()
# Create new structure in the new company
new_primary_folder_id, _ = self._create_or_get_folder_crm(new_root_folder_id, current_components['primary_name'])
new_year_folder_id, _ = self._create_or_get_folder_crm(new_primary_folder_id, current_components['year'])
# Move only the opportunity folder (not the entire primary folder)
result = drive_service.move_folder(self.google_drive_folder_id, new_year_folder_id)
if not result.get('success'):
raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
# Update the stored folder ID to the new location
self.with_context(skip_google_drive_update=True).write({
'google_drive_folder_id': self.google_drive_folder_id, # Same ID, new location
'google_drive_folder_name': self._build_structure_string(current_components)
})
self.message_post(
body=_("✅ Oportunidad movida a nueva empresa: %s") % new_company.name,
message_type='comment'
)
except Exception as e:
_logger.error(f"Error moviendo folder: {str(e)}")
# Don't raise the exception, just log it and continue
self.message_post(
body=_("⚠️ Error moviendo carpeta: %s") % str(e),
message_type='comment'
)
# ============================================================================
# MÉTODOS ESPECÍFICOS DE CRM
# ============================================================================
def _find_primary_folder_id_crm(self, start_folder_id):
"""Find the primary folder (company/contact level) in the hierarchy"""
try:
drive_service = self.env['google.drive.service']
_logger.info(f"🔍 Finding primary folder starting from: {start_folder_id}")
# First validate the start folder exists
validation = drive_service.validate_folder_id(start_folder_id)
if not validation.get('valid'):
_logger.error(f"❌ Start folder {start_folder_id} is not valid: {validation.get('error')}")
return None
hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
_logger.info(f"📁 Hierarchy found: {len(hierarchy)} levels")
for folder_info in hierarchy:
folder_name = folder_info.get('name', '')
level = folder_info.get('level', 0)
folder_id = folder_info.get('id', '')
_logger.info(f"📂 Level {level}: {folder_name} (ID: {folder_id})")
if level == 0:
continue
if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
# Validate this folder exists before checking its children
folder_validation = drive_service.validate_folder_id(folder_id)
if not folder_validation.get('valid'):
_logger.warning(f"⚠️ Folder {folder_id} ({folder_name}) is not accessible, skipping")
continue
year_folders = drive_service.find_folders_by_name(folder_id, r'^\d{4}$')
if year_folders:
_logger.info(f"✅ Found primary folder: {folder_name} (ID: {folder_id})")
return folder_id
else:
_logger.info(f"ℹ️ Folder {folder_name} has no year folders, not primary")
_logger.warning(f"❌ No primary folder found in hierarchy")
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"""
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"""
try:
drive_service = self.env['google.drive.service']
validation = drive_service.validate_folder_id(folder_id)
if not validation.get('valid'):
_logger.warning(f"Folder {folder_id} is not valid or accessible")
return None
current_name = validation.get('name', '')
complete_structure = {
'opportunity_folder': {
'id': folder_id,
'name': current_name,
'level': 3
}
}
hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
# If hierarchy is empty, return basic structure with just the opportunity folder
if not hierarchy:
_logger.warning(f"Could not navigate hierarchy for folder {folder_id}")
return complete_structure
for folder_info in hierarchy:
folder_name = folder_info.get('name', '')
level = folder_info.get('level', 0)
if level == 2 and folder_name.isdigit() and len(folder_name) == 4:
complete_structure['year_folder'] = {
'id': folder_info['id'],
'name': folder_name,
'level': level
}
elif level == 1 and 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:
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
# ============================================================================
# MÉTODOS DE ACCIÓN
# ============================================================================
@api.model
def create(self, vals):
"""Override create to handle Google Drive folder creation"""
record = super().create(vals)
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):
if record._validate_folder_creation_prerequisites():
try:
record._create_google_drive_folder_structure()
record.message_post(
body=_("✅ Google Drive folder created automatically"),
message_type='comment'
)
except Exception as e:
_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"""
if self.env.context.get('skip_google_drive_update'):
return super().write(vals)
_clear_google_drive_cache()
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
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 ''
}
result = super().write(vals)
# Process Google Drive updates
for record in self:
record._process_google_drive_updates(vals, current_values[record.id])
return result
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'))
return {
'type': 'ir.actions.act_url',
'url': f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}",
'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'))
if not self._get_company_root_folder_id():
raise UserError(_('Google Drive CRM folder is not configured for this company.'))
try:
self._create_google_drive_folder_structure()
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_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.'))
if not self._get_company_root_folder_id():
raise UserError(_('Google Drive CRM folder is not configured for this company.'))
try:
expected_components = self._get_folder_name_components()
self._rename_entire_folder_structure(new_components=expected_components)
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!'),
'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.'))
try:
# Add detailed diagnostic information
_logger.info(f"🔍 Analyzing folder structure for opportunity {self.id} (ID: {self.name})")
_logger.info(f"📁 Current folder ID: {self.google_drive_folder_id}")
expected_components = self._get_folder_name_components()
_logger.info(f"📋 Expected components: {expected_components}")
# Test folder validation first
drive_service = self.env['google.drive.service']
validation = drive_service.validate_folder_id(self.google_drive_folder_id)
_logger.info(f"✅ Folder validation result: {validation}")
current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id)
_logger.info(f"📊 Current structure analysis: {current_structure}")
if not current_structure:
# Provide a more helpful error message
error_msg = f"❌ Cannot Analyze Folder Structure
"
error_msg += f"Folder ID: {self.google_drive_folder_id}
"
error_msg += f"Validation Result: {validation}
"
error_msg += f"Possible Issues:
"
error_msg += f"• Folder may not exist or be accessible
"
error_msg += f"• Insufficient permissions to access the folder
"
error_msg += f"• Folder may be in a Shared Drive without proper access
"
error_msg += f"• Network connectivity issues
"
error_msg += f"Expected Structure:
"
error_msg += f"📁 [Root Folder] (MC Team)
"
error_msg += f"└── 📁 {expected_components['primary_name']} (Company/Contact)
"
error_msg += f" └── 📁 {expected_components['year']} (Year)
"
error_msg += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)
"
error_msg += f" ├── 📁 Meets
"
error_msg += f" └── 📁 Archivos cliente
"
error_msg += f"Recommendation: Try using the 'Rename Folder Structure' button to recreate the structure."
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Folder Structure Analysis'),
'message': error_msg,
'type': 'warning',
'sticky': True,
}
}
analysis = self._compare_folder_structures(expected_components, current_structure)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Folder Structure Analysis'),
'message': analysis,
'type': 'info',
'sticky': True,
}
}
except Exception as e:
_logger.error(f"Error in action_analyze_folder_structure: {str(e)}")
raise UserError(_('Failed to analyze folder structure: %s') % str(e))
def _compare_folder_structures(self, expected_components, current_structure):
"""Compare expected vs current folder structure"""
analysis = f"📁 Complete Folder Structure Analysis
"
# Expected structure
analysis += f"Expected Structure:
"
analysis += f"📁 [Root Folder] (MC Team)
"
analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)
"
analysis += f" └── 📁 {expected_components['year']} (Year)
"
analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)
"
analysis += f" ├── 📁 Meets
"
analysis += f" └── 📁 Archivos cliente
"
# Current structure
analysis += f"Current Structure in Google Drive:
"
if 'root_folder' in current_structure:
analysis += f"📁 {current_structure['root_folder']['name']} (Root)
"
else:
analysis += f"📁 [Unknown Root]
"
if 'primary_folder' in current_structure:
primary_name = current_structure['primary_folder']['name']
analysis += f"└── 📁 {primary_name}"
analysis += " ✅" if primary_name == expected_components['primary_name'] else f" ❌ (Expected: {expected_components['primary_name']})"
analysis += "
"
else:
analysis += f"└── 📁 [Missing Primary Folder] ❌
"
if 'year_folder' in current_structure:
year_name = current_structure['year_folder']['name']
analysis += f" └── 📁 {year_name}"
analysis += " ✅" if year_name == expected_components['year'] else f" ❌ (Expected: {expected_components['year']})"
analysis += "
"
else:
analysis += f" └── 📁 [Missing Year Folder] ❌
"
if 'opportunity_folder' in current_structure:
opp_name = current_structure['opportunity_folder']['name']
analysis += f" └── 📁 {opp_name}"
analysis += " ✅" if opp_name == expected_components['opportunity_name'] else f" ❌ (Expected: {expected_components['opportunity_name']})"
analysis += "
"
else:
analysis += f" └── 📁 [Missing Opportunity Folder] ❌
"
# Summary
correct_count = 0
total_count = 0
for folder_type in ['primary_folder', 'year_folder', 'opportunity_folder']:
if folder_type in current_structure:
total_count += 1
current_name = current_structure[folder_type]['name']
expected_name = expected_components[folder_type.replace('_folder', '_name')]
if current_name == expected_name:
correct_count += 1
analysis += f"
Summary:
"
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
# ============================================================================
# MÉTODOS DE SINCRONIZACIÓN DE MEETS
# ============================================================================
@api.model
def _sync_meetings_with_opportunities(self, time_filter=15):
"""Sync Google Meet recordings with CRM opportunities
Args:
time_filter: Can be:
- int: Number of days back (e.g., 15 for last 15 days)
- str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day)
"""
# Parse time_filter parameter
if isinstance(time_filter, str):
# Try to parse as date
try:
from datetime import datetime
target_date = datetime.strptime(time_filter, '%Y-%m-%d').date()
_logger.info(f"Starting CRM meetings synchronization for specific date: {time_filter}")
days_back = None
specific_date = target_date
except ValueError:
# If not a valid date, try to convert to int
try:
days_back = int(time_filter)
specific_date = None
_logger.info(f"Starting CRM meetings synchronization for last {days_back} days...")
except ValueError:
raise UserError(_('Invalid time_filter parameter. Use integer (days back) or date string (YYYY-MM-DD)'))
else:
# Assume it's an integer
days_back = int(time_filter)
specific_date = None
_logger.info(f"Starting CRM meetings synchronization for last {days_back} days...")
try:
# Get Google Drive service
drive_service = self.env['google.drive.service']
# Get meetings based on filter type
if specific_date:
meetings = self._get_meetings_with_recordings_for_date(specific_date)
else:
meetings = self._get_meetings_with_recordings(days_back=days_back)
_logger.info(f"Found {len(meetings)} meetings with recordings")
# Limit to first 10 meetings for performance
meetings = meetings[:10]
_logger.info(f"Processing first {len(meetings)} meetings for performance")
sync_results = {
'total_meetings': len(meetings),
'opportunities_found': 0,
'folders_created': 0,
'files_moved': 0,
'errors': []
}
for meeting in meetings:
try:
result = self._process_meeting_for_opportunities(meeting, drive_service)
sync_results['opportunities_found'] += result.get('opportunities_found', 0)
sync_results['folders_created'] += result.get('folders_created', 0)
sync_results['files_moved'] += result.get('files_moved', 0)
except Exception as e:
error_msg = f"Error processing meeting {meeting.get('id', 'unknown')}: {str(e)}"
_logger.error(error_msg)
sync_results['errors'].append(error_msg)
# Generate summary message
summary = self._generate_sync_summary(sync_results)
_logger.info(f"CRM sync completed: {summary}")
return summary
except Exception as e:
_logger.error(f"Failed to sync meetings with opportunities: {str(e)}")
raise UserError(_('Failed to sync meetings: %s') % str(e))
@api.model
def _sync_meetings_with_opportunities_cron(self, time_filter=15):
"""Cron job method for automatic CRM calendar sync - handles errors gracefully
Args:
time_filter: Can be:
- int: Number of days back (e.g., 15 for last 15 days)
- str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day)
"""
# Parse time_filter parameter for logging
if isinstance(time_filter, str):
try:
from datetime import datetime
target_date = datetime.strptime(time_filter, '%Y-%m-%d').date()
log_message = f"🔄 Starting automatic CRM calendar sync (cron) for specific date: {time_filter}"
except ValueError:
try:
days_back = int(time_filter)
log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..."
except ValueError:
log_message = f"🔄 Starting automatic CRM calendar sync (cron) with invalid time_filter: {time_filter}"
else:
days_back = int(time_filter)
log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..."
_logger.info(log_message)
try:
# Check if any company has Google Drive CRM enabled
companies_with_google = self.env['res.company'].search([
('google_drive_crm_enabled', '=', True),
('google_drive_crm_folder_id', '!=', False)
])
if not companies_with_google:
_logger.info("🔄 No companies configured for Google Drive CRM. Skipping sync.")
return
# Check if there are any opportunities to process
opportunities_count = self.env['crm.lead'].search_count([
('type', '=', 'opportunity'),
('stage_id.is_won', '=', False)
])
if opportunities_count == 0:
_logger.info("🔄 No active opportunities found. Skipping sync.")
return
# Find users with Google authentication
authenticated_users = self.env['res.users'].search([
('res_users_settings_id.google_rtoken', '!=', False),
('res_users_settings_id.google_token', '!=', False)
])
if not authenticated_users:
_logger.warning("🔄 No users with Google authentication found. Skipping sync.")
return
_logger.info(f"🔄 Found {len(authenticated_users)} users with Google authentication")
# Execute sync for each authenticated user
total_results = {
'total_meetings': 0,
'opportunities_found': 0,
'folders_created': 0,
'files_moved': 0,
'errors': []
}
for user in authenticated_users:
try:
_logger.info(f"🔄 Executing sync for user: {user.name} ({user.login})")
# Execute sync with user context
result = self.with_user(user.id)._sync_meetings_with_opportunities(time_filter=time_filter)
# Parse result if it's a string (summary)
if isinstance(result, str):
# Extract numbers from summary (basic parsing)
import re
meetings_match = re.search(r'Reuniones procesadas: (\d+)', result)
opportunities_match = re.search(r'Oportunidades encontradas: (\d+)', result)
folders_match = re.search(r'Carpetas creadas: (\d+)', result)
files_match = re.search(r'Archivos movidos: (\d+)', result)
if meetings_match:
total_results['total_meetings'] += int(meetings_match.group(1))
if opportunities_match:
total_results['opportunities_found'] += int(opportunities_match.group(1))
if folders_match:
total_results['folders_created'] += int(folders_match.group(1))
if files_match:
total_results['files_moved'] += int(files_match.group(1))
_logger.info(f"✅ Sync completed for user {user.name}")
except Exception as e:
error_msg = f"Error syncing for user {user.name}: {str(e)}"
_logger.error(error_msg)
total_results['errors'].append(error_msg)
continue
# Generate final summary
final_summary = self._generate_sync_summary(total_results)
_logger.info(f"✅ Automatic CRM calendar sync completed for all users: {final_summary}")
return final_summary
except Exception as e:
# Log error but don't fail the cron job
_logger.error(f"❌ Automatic CRM calendar sync failed: {str(e)}")
# Create a system message for administrators
try:
admin_users = self.env['res.users'].search([('groups_id', 'in', self.env.ref('base.group_system').id)])
for admin in admin_users:
admin.message_post(
body=f"❌ CRM Calendar Sync Error
"
f"Error: {str(e)}
"
f"Time: {fields.Datetime.now()}
"
f"Time Filter: {time_filter}",
message_type='comment',
subtype_xmlid='mail.mt_comment'
)
except:
pass # Don't fail if we can't create messages
return False
def _get_meetings_with_recordings(self, days_back=15):
"""Get Google Meet meetings from last N days that have recordings"""
try:
# Use Google Calendar service to get real meetings
calendar_service = self.env['google.calendar.service']
meetings = calendar_service.get_meetings_with_recordings(days_back=days_back)
_logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar")
return meetings
except Exception as e:
_logger.error(f"Failed to get meetings from Google Calendar: {str(e)}")
# Fallback to empty list if API fails
return []
def _get_meetings_with_recordings_for_date(self, target_date):
"""Get Google Meet meetings from a specific date that have recordings"""
try:
# Use Google Calendar service to get meetings for specific date
calendar_service = self.env['google.calendar.service']
meetings = calendar_service.get_meetings_with_recordings_for_date(target_date)
_logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar for date: {target_date}")
return meetings
except Exception as e:
_logger.error(f"Failed to get meetings from Google Calendar for date {target_date}: {str(e)}")
# Fallback to empty list if API fails
return []
def _process_meeting_for_opportunities(self, meeting, drive_service):
"""Process a single meeting for opportunities"""
result = {
'opportunities_found': 0,
'folders_created': 0,
'files_moved': 0
}
# Find opportunities by participant emails
opportunities = self._find_opportunities_by_participants(meeting['participants'])
result['opportunities_found'] = len(opportunities)
if not opportunities:
_logger.info(f"❌ No opportunities found for meeting '{meeting['title']}' with participants: {meeting['participants']}")
return result
for opportunity in opportunities:
try:
_logger.info(f"🎯 Processing meeting '{meeting['title']}' for opportunity '{opportunity.name}' (ID: {opportunity.id})")
# Ensure opportunity has Google Drive folder structure
if not opportunity.google_drive_folder_id:
_logger.info(f"📁 Creating Google Drive folder for opportunity {opportunity.name}")
opportunity._create_google_drive_folder_structure()
result['folders_created'] += 1
# Get or create Meets folder
meets_folder_id = self._get_or_create_meets_folder(opportunity, drive_service)
# Move recording files to Meets folder
files_moved = self._move_recording_files_to_meets_folder(
meeting['recording_files'],
meets_folder_id,
drive_service,
meeting['title']
)
result['files_moved'] += files_moved
# Search for additional files in the user's configured CRM meets folder
additional_files_moved = self._search_and_move_crm_meets_files(meeting['title'], meets_folder_id, drive_service)
result['files_moved'] += additional_files_moved
_logger.info(f"✅ Successfully processed meeting '{meeting['title']}' for opportunity '{opportunity.name}': {files_moved + additional_files_moved} files moved")
except Exception as e:
_logger.error(f"❌ Error processing opportunity {opportunity.name}: {str(e)}")
continue
return result
def _find_opportunities_by_participants(self, participant_emails):
"""Find opportunities where participants are partners - returns the most recent one"""
opportunities = self.env['crm.lead'].search([
('partner_id.email', 'in', participant_emails),
('type', '=', 'opportunity'),
('stage_id.is_won', '=', False) # Only active opportunities
], order='create_date desc', limit=1)
if opportunities:
opportunity = opportunities[0]
_logger.info(f"Found most recent opportunity '{opportunity.name}' (ID: {opportunity.id}, created: {opportunity.create_date}) for participants: {participant_emails}")
else:
_logger.info(f"No opportunities found for participants: {participant_emails}")
return opportunities
def _get_or_create_meets_folder(self, opportunity, drive_service):
"""Get or create Meets folder within opportunity folder"""
try:
# List folders in opportunity folder
folders = drive_service._do_request(
'/drive/v3/files',
params={
'q': f"'{opportunity.google_drive_folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
'fields': 'files(id,name)',
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
)
# Look for existing Meets folder
meets_folder_id = None
for folder in folders.get('files', []):
if folder['name'] == 'Meets':
meets_folder_id = folder['id']
_logger.info(f"Found existing Meets folder: {meets_folder_id}")
break
# Create Meets folder if it doesn't exist
if not meets_folder_id:
_logger.info(f"Creating Meets folder for opportunity {opportunity.name}")
meets_folder = drive_service._do_request(
'/drive/v3/files',
method='POST',
json_data={
'name': 'Meets',
'mimeType': 'application/vnd.google-apps.folder',
'parents': [opportunity.google_drive_folder_id]
}
)
meets_folder_id = meets_folder['id']
_logger.info(f"Created Meets folder: {meets_folder_id}")
return meets_folder_id
except Exception as e:
_logger.error(f"Error getting/creating Meets folder: {str(e)}")
raise
def _move_recording_files_to_meets_folder(self, file_ids, meets_folder_id, drive_service, meeting_title):
"""Move recording files to Meets folder"""
files_moved = 0
_logger.info(f"🔍 Starting to move {len(file_ids)} files to Meets folder: {meets_folder_id}")
_logger.info(f"📁 Target Meets folder ID: {meets_folder_id}")
for file_id in file_ids:
try:
_logger.info(f"📄 Processing file ID: {file_id}")
# Check if file is already in the Meets folder
file_info = drive_service._do_request(
f'/drive/v3/files/{file_id}',
params={
'fields': 'id,name,parents',
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
)
file_name = file_info.get('name', 'Unknown')
current_parents = file_info.get('parents', [])
_logger.info(f"📄 File: {file_name} (ID: {file_id})")
_logger.info(f"📍 Current parents: {current_parents}")
_logger.info(f"🎯 Target folder: {meets_folder_id}")
# Check if file is already in the target folder
if meets_folder_id in current_parents:
_logger.info(f"✅ File {file_name} already in Meets folder")
continue
# Move file to Meets folder
_logger.info(f"🚚 Moving file {file_name} to Meets folder...")
# Prepare parameters for Shared Drive support
move_params = {
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
# Try alternative approach: use addParents and removeParents as URL parameters
move_params['addParents'] = meets_folder_id
move_params['removeParents'] = ','.join(current_parents)
_logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}")
drive_service._do_request(
f'/drive/v3/files/{file_id}',
method='PATCH',
params=move_params
)
# Verify the move was successful
updated_file_info = drive_service._do_request(
f'/drive/v3/files/{file_id}',
params={
'fields': 'id,name,parents',
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
)
updated_parents = updated_file_info.get('parents', [])
_logger.info(f"✅ File moved! New parents: {updated_parents}")
files_moved += 1
_logger.info(f"✅ Successfully moved file {file_name} to Meets folder for meeting: {meeting_title}")
except Exception as e:
_logger.error(f"❌ Error moving file {file_id}: {str(e)}")
continue
_logger.info(f"📊 Total files moved: {files_moved} out of {len(file_ids)}")
return files_moved
def _search_and_move_crm_meets_files(self, meeting_title, meets_folder_id, drive_service):
"""Search for files in the user's configured CRM meets folder and move them to the opportunity's Meets folder"""
files_moved = 0
try:
# Get the current user's CRM meets folder configuration
current_user = self.env.user
user_settings = current_user.res_users_settings_id
if not user_settings or not user_settings.google_crm_meets_folder_id:
_logger.info(f"🔍 No CRM meets folder configured for user {current_user.name}. Skipping additional file search.")
return 0
crm_meets_folder_id = user_settings.google_crm_meets_folder_id
_logger.info(f"🔍 Searching for files in user's CRM meets folder ({crm_meets_folder_id}) containing meeting title: '{meeting_title}'")
# Search for files in the user's CRM meets folder that contain the meeting title
files_in_crm_meets = drive_service._do_request(
'/drive/v3/files',
params={
'q': f"'{crm_meets_folder_id}' in parents and trashed=false and name contains '{meeting_title}'",
'fields': 'files(id,name,parents)',
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
)
found_files = files_in_crm_meets.get('files', [])
_logger.info(f"🔍 Found {len(found_files)} files in CRM meets folder containing meeting title: '{meeting_title}'")
for file_info in found_files:
file_id = file_info['id']
file_name = file_info['name']
current_parents = file_info.get('parents', [])
_logger.info(f"📄 Processing file '{file_name}' (ID: {file_id}) from CRM meets folder")
# Check if file is already in the target folder
if meets_folder_id in current_parents:
_logger.info(f"✅ File '{file_name}' already in opportunity's Meets folder")
continue
# Move file to the opportunity's Meets folder
_logger.info(f"🚚 Moving file '{file_name}' to opportunity's Meets folder...")
# Prepare parameters for Shared Drive support
move_params = {
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
# Try alternative approach: use addParents and removeParents as URL parameters
move_params['addParents'] = meets_folder_id
move_params['removeParents'] = ','.join(current_parents)
_logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}")
drive_service._do_request(
f'/drive/v3/files/{file_id}',
method='PATCH',
params=move_params
)
# Verify the move was successful
updated_file_info = drive_service._do_request(
f'/drive/v3/files/{file_id}',
params={
'fields': 'id,name,parents',
'supportsAllDrives': 'true',
'includeItemsFromAllDrives': 'true'
}
)
updated_parents = updated_file_info.get('parents', [])
_logger.info(f"✅ File moved! New parents: {updated_parents}")
files_moved += 1
_logger.info(f"✅ Successfully moved file '{file_name}' to opportunity's Meets folder for meeting: {meeting_title}")
_logger.info(f"📊 Total additional files moved from CRM meets folder: {files_moved}")
return files_moved
except Exception as e:
_logger.error(f"❌ Error searching or moving files from CRM meets folder: {str(e)}")
return 0
def _generate_sync_summary(self, sync_results):
"""Generate summary message for sync results"""
summary = f"🔄 Sincronización CRM Completada
"
summary += f"📊 Resumen:
"
summary += f"• Reuniones procesadas: {sync_results['total_meetings']}
"
summary += f"• Oportunidades encontradas: {sync_results['opportunities_found']}
"
summary += f"• Carpetas creadas: {sync_results['folders_created']}
"
summary += f"• Archivos movidos: {sync_results['files_moved']}
"
if sync_results['errors']:
summary += f"
❌ Errores ({len(sync_results['errors'])}):
"
for error in sync_results['errors'][:5]: # Show first 5 errors
summary += f"• {error}
"
if len(sync_results['errors']) > 5:
summary += f"• ... y {len(sync_results['errors']) - 5} errores más
"
return summary