| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648 |
- # -*- 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']}"
- def _update_folder_structure_fields(self, folder_id=None, structure_components=None):
- """Helper method to update folder structure fields without triggering loops"""
- update_vals = {}
-
- if folder_id:
- update_vals['google_drive_folder_id'] = folder_id
- update_vals['google_drive_url'] = f"https://drive.google.com/drive/folders/{folder_id}"
-
- if structure_components:
- update_vals['google_drive_folder_name'] = self._build_structure_string(structure_components)
-
- if update_vals:
- self.with_context(skip_google_drive_update=True).write(update_vals)
- def _post_folder_message(self, message, message_type='comment'):
- """Helper method to post folder-related messages"""
- self.message_post(
- body=_(message),
- message_type=message_type
- )
- def _get_current_folder_components(self):
- """Helper method to get current folder components"""
- return self._get_folder_name_components()
- def _get_expected_folder_structure(self):
- """Helper method to get expected folder structure string"""
- components = self._get_current_folder_components()
- return self._build_structure_string(components)
- # ============================================================================
- # 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.parent_id.name (empresa padre del contacto)
- if self.partner_id and self.partner_id.parent_id and self.partner_id.parent_id.name:
- primary_name = self.partner_id.parent_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_current_folder_components()
-
- # Update folder structure fields
- self._update_folder_structure_fields(
- folder_id=folder_id,
- structure_components=expected_components
- )
-
- # 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._post_folder_message("✅ Google Drive folder created automatically")
-
- # 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._post_folder_message(f"❌ Error updating Google Drive: {str(e)}")
- 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_current_folder_components()
- self._update_folder_structure_fields(structure_components=expected_components)
-
- self._post_folder_message("✅ Google Drive folder renamed due to partner change")
- return
-
- # Handle partner parent changes (when contact's company changes)
- 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:
- # Same partner, but check if their parent (company) changed
- old_partner = self.env['res.partner'].browse(old_partner_id) if old_partner_id else None
- new_partner = self.env['res.partner'].browse(new_partner_id) if new_partner_id else None
-
- if old_partner and new_partner:
- old_parent_id = old_partner.parent_id.id if old_partner.parent_id else None
- new_parent_id = new_partner.parent_id.id if new_partner.parent_id else None
-
- if old_parent_id != new_parent_id:
- # Partner's parent (company) changed - rename folder structure
- _logger.info(f"🔄 Partner's parent company changed from {old_parent_id} to {new_parent_id}. Renaming folder structure.")
- self._rename_entire_folder_structure()
-
- # Update stored structure
- expected_components = self._get_current_folder_components()
- self._update_folder_structure_fields(structure_components=expected_components)
-
- self._post_folder_message("✅ Google Drive folder renamed due to contact's company change")
- return
-
- # Handle other structure changes (name changes, etc.)
- expected_components = self._get_current_folder_components()
- expected_structure = self._get_expected_folder_structure()
-
- 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._update_folder_structure_fields(structure_components=expected_components)
-
- self._post_folder_message("✅ Google Drive folder structure updated")
- 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:
- new_components = self._get_current_folder_components()
-
- # Validate that we have valid components
- if not new_components:
- raise UserError(_('Could not determine new folder structure 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._update_folder_structure_fields(
- folder_id=new_opportunity_folder_id,
- structure_components=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
- self._update_folder_structure_fields(structure_components=new_components)
-
- _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._post_folder_message("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado.")
- 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._post_folder_message("⚠️ No se puede mover: La carpeta actual no existe o no es accesible.")
- return
-
- # Get current opportunity folder components
- current_components = self._get_current_folder_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._update_folder_structure_fields(
- folder_id=self.google_drive_folder_id, # Same ID, new location
- structure_components=current_components
- )
-
- self._post_folder_message(f"✅ Oportunidad movida a nueva empresa: {new_company.name}")
-
- except Exception as e:
- _logger.error(f"Error moviendo folder: {str(e)}")
- # Don't raise the exception, just log it and continue
- self._post_folder_message(f"⚠️ Error moviendo carpeta: {str(e)}")
- # ============================================================================
- # 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 - COMPLETELY REWRITTEN"""
- try:
- drive_service = self.env['google.drive.service']
-
- # Step 1: Validate the current folder
- 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', '')
- _logger.info(f"🔍 Starting analysis for folder: {current_name} (ID: {folder_id})")
-
- # Step 2: Get the complete hierarchy
- hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=10)
- _logger.info(f"📊 Retrieved hierarchy: {len(hierarchy)} levels")
-
- if not hierarchy:
- _logger.warning(f"Could not retrieve hierarchy for folder {folder_id}")
- return {
- 'opportunity_folder': {
- 'id': folder_id,
- 'name': current_name,
- 'level': 0,
- 'found': True
- },
- 'year_folder': {'found': False},
- 'primary_folder': {'found': False},
- 'root_folder': {'found': False}
- }
-
- # Step 3: Initialize structure with all components as not found
- structure = {
- 'opportunity_folder': {'found': False},
- 'year_folder': {'found': False},
- 'primary_folder': {'found': False},
- 'root_folder': {'found': False}
- }
-
- # Step 4: Analyze each level in the hierarchy
- for folder_info in hierarchy:
- folder_name = folder_info.get('name', '')
- level = folder_info.get('level', 0)
- folder_id_info = folder_info.get('id', '')
-
- _logger.info(f"📂 Analyzing Level {level}: '{folder_name}' (ID: {folder_id_info})")
-
- # Identify folder type based on level and name patterns
- if level == 0:
- # This is the opportunity folder (the one we're analyzing)
- structure['opportunity_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified opportunity folder: {folder_name}")
-
- elif level == 1:
- # This could be year folder or primary folder
- if folder_name.isdigit() and len(folder_name) == 4:
- # It's a year folder
- structure['year_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified year folder: {folder_name}")
- elif folder_name not in ['Meets', 'Archivos cliente']:
- # It's a primary folder (company/contact)
- structure['primary_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified primary folder: {folder_name}")
-
- elif level == 2:
- # This could be year folder or primary folder (depending on what was at level 1)
- if folder_name.isdigit() and len(folder_name) == 4 and not structure['year_folder']['found']:
- # It's a year folder
- structure['year_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified year folder at level 2: {folder_name}")
- elif folder_name not in ['Meets', 'Archivos cliente'] and not structure['primary_folder']['found']:
- # It's a primary folder
- structure['primary_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified primary folder at level 2: {folder_name}")
-
- elif level >= 3:
- # This is likely the root folder
- if not structure['root_folder']['found']:
- structure['root_folder'] = {
- 'id': folder_id_info,
- 'name': folder_name,
- 'level': level,
- 'found': True
- }
- _logger.info(f"✅ Identified root folder: {folder_name}")
-
- _logger.info(f"📊 Final structure analysis: {structure}")
- return 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_list):
- """Override create to handle Google Drive folder creation - supports batch creation"""
- # Handle both single record and batch creation
- if isinstance(vals_list, dict):
- vals_list = [vals_list]
-
- records = super().create(vals_list)
-
- # Process each record individually for Google Drive folder creation
- for record in records:
- 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._post_folder_message("✅ Google Drive folder created automatically")
- except Exception as e:
- _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
- record._post_folder_message(f"⚠️ Google Drive folder creation failed: {str(e)}")
-
- return records
- 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_current_folder_components()
- self._rename_entire_folder_structure(new_components=expected_components)
-
- self._update_folder_structure_fields(structure_components=expected_components)
-
- 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_current_folder_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"<strong>❌ Cannot Analyze Folder Structure</strong><br/><br/>"
- error_msg += f"<strong>Folder ID:</strong> {self.google_drive_folder_id}<br/>"
- error_msg += f"<strong>Validation Result:</strong> {validation}<br/>"
- error_msg += f"<strong>Possible Issues:</strong><br/>"
- error_msg += f"• Folder may not exist or be accessible<br/>"
- error_msg += f"• Insufficient permissions to access the folder<br/>"
- error_msg += f"• Folder may be in a Shared Drive without proper access<br/>"
- error_msg += f"• Network connectivity issues<br/><br/>"
- error_msg += f"<strong>Expected Structure:</strong><br/>"
- error_msg += f"📁 [Root Folder] (MC Team)<br/>"
- error_msg += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
- error_msg += f" └── 📁 {expected_components['year']} (Year)<br/>"
- error_msg += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
- error_msg += f" ├── 📁 Meets<br/>"
- error_msg += f" └── 📁 Archivos cliente<br/><br/>"
- error_msg += f"<strong>Recommendation:</strong> 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 - EXTRA SPACING FORMAT"""
- try:
- # Create text analysis with EXTRA spacing for better readability
- analysis = "📁 FOLDER STRUCTURE ANALYSIS\n"
- analysis += "=" * 60 + "\n\n\n"
-
- # Expected structure
- analysis += "✅ EXPECTED STRUCTURE:\n"
- analysis += "=" * 30 + "\n\n"
- analysis += f"📁 [Root Folder] (MC Team)\n"
- analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)\n"
- analysis += f" └── 📁 {expected_components['year']} (Year)\n"
- analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)\n"
- analysis += f" ├── 📁 Meets\n"
- analysis += f" └── 📁 Archivos cliente\n\n\n"
-
- # Current structure
- analysis += "🔍 CURRENT STRUCTURE:\n"
- analysis += "=" * 30 + "\n\n"
-
- # Root folder
- if current_structure.get('root_folder', {}).get('found', False):
- root_name = current_structure['root_folder']['name']
- analysis += f"📁 {root_name} (Root) ✅\n"
- else:
- analysis += "📁 [Unknown Root] ❌\n"
-
- # Primary folder
- if current_structure.get('primary_folder', {}).get('found', False):
- primary_name = current_structure['primary_folder']['name']
- if primary_name == expected_components['primary_name']:
- analysis += f"└── 📁 {primary_name} ✅\n"
- else:
- analysis += f"└── 📁 {primary_name} ❌\n"
- analysis += f" (Expected: {expected_components['primary_name']})\n"
- else:
- analysis += "└── 📁 [Missing Primary Folder] ❌\n"
-
- # Year folder
- if current_structure.get('year_folder', {}).get('found', False):
- year_name = current_structure['year_folder']['name']
- if year_name == expected_components['year']:
- analysis += f" └── 📁 {year_name} ✅\n"
- else:
- analysis += f" └── 📁 {year_name} ❌\n"
- analysis += f" (Expected: {expected_components['year']})\n"
- else:
- analysis += " └── 📁 [Missing Year Folder] ❌\n"
-
- # Opportunity folder
- if current_structure.get('opportunity_folder', {}).get('found', False):
- opp_name = current_structure['opportunity_folder']['name']
- if opp_name == expected_components['opportunity_name']:
- analysis += f" └── 📁 {opp_name} ✅\n"
- else:
- analysis += f" └── 📁 {opp_name} ❌\n"
- analysis += f" (Expected: {expected_components['opportunity_name']})\n"
- else:
- analysis += " └── 📁 [Missing Opportunity Folder] ❌\n"
-
- # Add subfolders to current structure (always show them)
- analysis += f" ├── 📁 Meets ✅\n"
- analysis += f" └── 📁 Archivos cliente ✅\n\n\n"
-
- # Calculate summary
- correct_count = 0
- total_count = 0
-
- # Check primary folder
- if current_structure.get('primary_folder', {}).get('found', False):
- total_count += 1
- if current_structure['primary_folder']['name'] == expected_components['primary_name']:
- correct_count += 1
-
- # Check year folder
- if current_structure.get('year_folder', {}).get('found', False):
- total_count += 1
- if current_structure['year_folder']['name'] == expected_components['year']:
- correct_count += 1
-
- # Check opportunity folder
- if current_structure.get('opportunity_folder', {}).get('found', False):
- total_count += 1
- if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']:
- correct_count += 1
-
- # Summary section with EXTRA spacing
- analysis += "=" * 60 + "\n\n"
- analysis += "📊 SUMMARY:\n"
- analysis += "=" * 20 + "\n\n"
-
- if total_count == 3 and correct_count == 3:
- analysis += "🎉 PERFECT STRUCTURE!\n\n"
- analysis += "✅ All folders are in the right place\n"
- analysis += "✅ All folder names are correct\n"
- analysis += f"✅ {correct_count}/{total_count} Components Correct\n\n"
- analysis += "🎯 Status: Ready to use!\n"
- else:
- analysis += "⚠️ STRUCTURE ISSUES DETECTED\n\n"
- analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct)\n\n"
- analysis += "💡 RECOMMENDATION:\n"
- analysis += "Use the 'Rename Folder Structure' button to fix the folder hierarchy.\n\n"
- analysis += "🔧 Action Required: Manual intervention needed\n"
-
- return analysis
-
- except Exception as e:
- _logger.error(f"Error in _compare_folder_structures: {str(e)}")
- return f"❌ ERROR ANALYZING FOLDER STRUCTURE\n{str(e)}"
- # ============================================================================
- # 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"❌ <strong>CRM Calendar Sync Error</strong><br/>"
- f"Error: {str(e)}<br/>"
- f"Time: {fields.Datetime.now()}<br/>"
- 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"<strong>🔄 Sincronización CRM Completada</strong><br/><br/>"
- summary += f"📊 <strong>Resumen:</strong><br/>"
- summary += f"• Reuniones procesadas: {sync_results['total_meetings']}<br/>"
- summary += f"• Oportunidades encontradas: {sync_results['opportunities_found']}<br/>"
- summary += f"• Carpetas creadas: {sync_results['folders_created']}<br/>"
- summary += f"• Archivos movidos: {sync_results['files_moved']}<br/>"
-
- if sync_results['errors']:
- summary += f"<br/>❌ <strong>Errores ({len(sync_results['errors'])}):</strong><br/>"
- for error in sync_results['errors'][:5]: # Show first 5 errors
- summary += f"• {error}<br/>"
- if len(sync_results['errors']) > 5:
- summary += f"• ... y {len(sync_results['errors']) - 5} errores más<br/>"
-
- return summary
|