| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412 |
- # -*- 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}")
-
- folder_id = drive_service.create_or_get_folder(parent_folder_id, folder_name)
-
- # Check if it was reused or created
- existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
- was_reused = len(existing_folders) > 0
-
- if was_reused:
- _logger.info(f"🔄 REUSING existing folder '{folder_name}' (ID: {folder_id})")
- else:
- _logger.info(f"📁 CREATED new folder '{folder_name}' (ID: {folder_id})")
-
- 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"""
- if not self.google_drive_folder_id:
- return
-
- # 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()
-
- drive_service = self.env['google.drive.service']
- current_folder_id = self.google_drive_folder_id
-
- # Navigate up to find primary folder
- primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
- if not primary_folder_id:
- raise UserError(_('Cannot find primary folder in the structure'))
-
- # Rename folders if needed
- if old_components['primary_name'] != new_components['primary_name']:
- result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
- if not result.get('success'):
- raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
-
- if old_components['year'] != new_components['year']:
- year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
- if year_folder_id:
- result = drive_service.rename_folder(year_folder_id, new_components['year'])
- if not result.get('success'):
- raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
-
- if old_components['opportunity_name'] != new_components['opportunity_name']:
- result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
- if not result.get('success'):
- raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
-
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': new_components['opportunity_name']
- })
- 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']
-
- # 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)}")
- raise
- # ============================================================================
- # 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']
- hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
-
- for folder_info in hierarchy:
- folder_name = folder_info.get('name', '')
- level = folder_info.get('level', 0)
-
- if level == 0:
- continue
-
- if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
- year_folders = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
- if year_folders:
- return folder_info['id']
-
- return None
- except Exception as e:
- _logger.error(f"Error finding primary folder (CRM): {str(e)}")
- return None
- def _find_year_folder_id_crm(self, primary_folder_id, year):
- """Find the year folder within the primary folder"""
- 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:
- expected_components = self._get_folder_name_components()
- current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id)
-
- 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>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"""
- analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
-
- # Expected structure
- analysis += f"<strong>Expected Structure:</strong><br/>"
- analysis += f"📁 [Root Folder] (MC Team)<br/>"
- analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
- analysis += f" └── 📁 {expected_components['year']} (Year)<br/>"
- analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
- analysis += f" ├── 📁 Meets<br/>"
- analysis += f" └── 📁 Archivos cliente<br/><br/>"
-
- # Current structure
- analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
-
- if 'root_folder' in current_structure:
- analysis += f"📁 {current_structure['root_folder']['name']} (Root)<br/>"
- else:
- analysis += f"📁 [Unknown Root]<br/>"
-
- 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 += "<br/>"
- else:
- analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
-
- 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 += "<br/>"
- else:
- analysis += f" └── 📁 [Missing Year Folder] ❌<br/>"
-
- 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 += "<br/>"
- else:
- analysis += f" └── 📁 [Missing Opportunity Folder] ❌<br/>"
-
- # 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"<br/><strong>Summary:</strong><br/>"
- 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"❌ <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
|