| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704 |
- # -*- coding: utf-8 -*-
- # Part of Odoo. See LICENSE file for full copyright and licensing details.
- import requests
- import json
- import logging
- import re
- from datetime import datetime, timedelta
- from odoo import fields, models, api, _
- from odoo.exceptions import UserError, ValidationError
- _logger = logging.getLogger(__name__)
- # Cache for Google Drive API responses (5 minutes)
- _GOOGLE_DRIVE_CACHE = {}
- _CACHE_TIMEOUT = 300 # 5 minutes
- def _clear_google_drive_cache():
- """Clear expired cache entries"""
- global _GOOGLE_DRIVE_CACHE
- current_time = datetime.now()
- expired_keys = [
- key for key, entry in _GOOGLE_DRIVE_CACHE.items()
- if current_time >= entry['expires']
- ]
- for key in expired_keys:
- del _GOOGLE_DRIVE_CACHE[key]
- class CrmLead(models.Model):
- _inherit = 'crm.lead'
- google_drive_documents_count = fields.Integer(
- string='Google Drive Documents',
- compute='_compute_google_drive_documents_count',
- help='Number of documents in Google Drive for this opportunity'
- )
- google_drive_folder_id = fields.Char(
- string='Google Drive Folder ID',
- help='ID del folder específico en Google Drive para esta oportunidad',
- readonly=True
- )
- google_drive_folder_name = fields.Char(
- string='Google Drive Folder Name',
- help='Nombre del folder en Google Drive para esta oportunidad',
- readonly=True
- )
- google_drive_url = fields.Char(
- string='URL Drive',
- help='URL de la carpeta de Google Drive de la oportunidad'
- )
- @api.depends('google_drive_folder_id')
- def _compute_google_drive_documents_count(self):
- """Compute the number of documents in Google Drive with caching"""
- for record in self:
- if record.google_drive_folder_id:
- # Check cache first
- cache_key = f'doc_count_{record.google_drive_folder_id}'
- if cache_key in _GOOGLE_DRIVE_CACHE:
- cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
- if datetime.now() < cache_entry['expires']:
- record.google_drive_documents_count = cache_entry['count']
- continue
-
- # TODO: Implement Google Drive API call to count documents
- # For now, return 0 but cache the result
- count = 0
- _GOOGLE_DRIVE_CACHE[cache_key] = {
- 'count': count,
- 'expires': datetime.now() + timedelta(seconds=300) # 5 minutes
- }
- record.google_drive_documents_count = count
- else:
- record.google_drive_documents_count = 0
- def _get_company_root_folder_id(self):
- """Get the company's root Google Drive folder ID"""
- if not self.company_id or not self.company_id.google_drive_crm_enabled:
- return None
-
- if not self.company_id.google_drive_crm_folder_id:
- return None
-
- return self.company_id.google_drive_crm_folder_id
- def _extract_folder_id_from_url(self, url):
- """Extract folder ID from Google Drive URL"""
- if not url:
- return None
-
- # Handle different Google Drive URL formats
- import re
-
- # Format: https://drive.google.com/drive/folders/FOLDER_ID
- folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
- match = re.search(folder_pattern, url)
-
- if match:
- return match.group(1)
-
- # Format: https://drive.google.com/open?id=FOLDER_ID
- open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
- match = re.search(open_pattern, url)
-
- if match:
- return match.group(1)
-
- return None
- def _get_google_drive_field_value(self):
- """Get value from the configured Google Drive field in company settings"""
- if not self.company_id.google_drive_crm_field_id:
- return None
-
- field_name = self.company_id.google_drive_crm_field_id.name
- if not field_name:
- return None
-
- # Get the field value from the record
- field_value = getattr(self, field_name, None)
- return field_value
- def _try_extract_folder_id_from_field(self):
- """Try to extract folder ID from the configured field value"""
- field_value = self._get_google_drive_field_value()
- if not field_value:
- return None
-
- # Try to extract folder ID from the field value (could be URL or direct ID)
- folder_id = self._extract_folder_id_from_url(field_value)
- if folder_id:
- return folder_id
-
- # If it's not a URL, check if it looks like a folder ID
- if isinstance(field_value, str) and len(field_value) >= 10 and len(field_value) <= 50:
- # Basic validation for Google Drive folder ID format
- import re
- if re.match(r'^[a-zA-Z0-9_-]+$', field_value):
- return field_value
-
- return None
- def _validate_folder_creation_prerequisites(self):
- """Validate prerequisites before creating Google Drive folder"""
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- self.message_post(
- body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
- message_type='comment'
- )
- return False
-
- # Validate contact exists
- if not self.partner_id:
- self.message_post(
- body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
- message_type='comment'
- )
- return False
-
- return True
- def _try_get_existing_folder_id(self):
- """Try to get existing folder ID from various sources"""
- # First, check if we already have a folder ID
- if self.google_drive_folder_id:
- return self.google_drive_folder_id
-
- # Second, try to extract from the configured field
- field_folder_id = self._try_extract_folder_id_from_field()
- if field_folder_id:
- _logger.info(f"Found folder ID from configured field: {field_folder_id}")
- return field_folder_id
-
- # Third, try to extract from google_drive_url field
- if self.google_drive_url:
- url_folder_id = self._extract_folder_id_from_url(self.google_drive_url)
- if url_folder_id:
- _logger.info(f"Found folder ID from URL field: {url_folder_id}")
- return url_folder_id
-
- return None
- def _check_folder_company_mismatch(self):
- """Check if the current folder belongs to the correct company using the new service"""
- if not self.google_drive_folder_id or not self.company_id:
- return False
-
- try:
- # Get company root folder ID
- company_root_folder_id = self._get_company_root_folder_id()
- if not company_root_folder_id:
- return False
-
- # Use the new Google Drive service
- drive_service = self.env['google.drive.service']
-
- # Check if folder belongs to the company root
- belongs_to_company = drive_service.check_folder_belongs_to_parent(
- self.google_drive_folder_id,
- company_root_folder_id
- )
-
- # Return True if there's a mismatch (folder doesn't belong to company)
- return not belongs_to_company
-
- except Exception as e:
- _logger.error(f"Error checking folder company mismatch: {str(e)}")
- return False
- def _get_folder_name_components(self):
- """Get the components for folder naming with priority for partner_id"""
- primary_name = None
-
- if self.partner_id:
- _logger.info(f"Contact found: {self.partner_id.name} (ID: {self.partner_id.id})")
-
- # Prioridad 1: partner_id.company_id.name
- if self.partner_id.company_id and self.partner_id.company_id.name:
- primary_name = self.partner_id.company_id.name
- _logger.info(f"Using company_id.name: {primary_name}")
- # Prioridad 2: partner_id.company_name
- elif self.partner_id.company_name:
- primary_name = self.partner_id.company_name
- _logger.info(f"Using company_name: {primary_name}")
- # Prioridad 3: partner_id.name
- else:
- primary_name = self.partner_id.name
- _logger.info(f"Using partner name: {primary_name}")
- else:
- _logger.warning("No contact assigned to opportunity")
- primary_name = "Sin Contacto"
-
- if not primary_name:
- raise UserError(_('No company or contact name available. Please assign a contact with company information before creating Google Drive folders.'))
-
- # Validate and sanitize the primary name
- sanitized_primary_name = self._sanitize_folder_name(primary_name)
- sanitized_opportunity_name = self._sanitize_folder_name(self.name)
- year = str(self.create_date.year) if self.create_date else str(datetime.now().year)
-
- _logger.info(f"Folder components - Primary: '{sanitized_primary_name}', Opportunity: '{sanitized_opportunity_name}', Year: {year}")
-
- return {
- 'primary_name': sanitized_primary_name,
- 'opportunity_name': sanitized_opportunity_name,
- 'year': year
- }
- def _check_partner_name_changes(self, vals):
- """Check if partner or partner's company name has changed"""
- if 'partner_id' not in vals:
- return False
-
- old_partner = self.partner_id
- new_partner_id = vals['partner_id']
-
- if not new_partner_id:
- return False
-
- new_partner = self.env['res.partner'].browse(new_partner_id)
-
- # Check if partner changed
- if old_partner.id != new_partner.id:
- return True
-
- # Check if partner's company name changed
- old_company_name = old_partner.parent_id.name if old_partner.parent_id else old_partner.name
- new_company_name = new_partner.parent_id.name if new_partner.parent_id else new_partner.name
-
- return old_company_name != new_company_name
- def _sanitize_folder_name(self, name):
- """Sanitize folder name to be Google Drive compatible with optimization"""
- if not name:
- return 'Sin nombre'
-
- # Use regex for better performance
- sanitized_name = re.sub(r'[<>:"|?*/\\]', '_', name)
-
- # Remove leading/trailing spaces and dots
- sanitized_name = sanitized_name.strip(' .')
-
- # Ensure it's not empty after sanitization
- if not sanitized_name:
- return 'Sin nombre'
-
- # Limit length to 255 characters (Google Drive limit)
- if len(sanitized_name) > 255:
- sanitized_name = sanitized_name[:252] + '...'
-
- return sanitized_name
- def _validate_folder_id_with_google_drive(self, folder_id):
- """Validate if the folder ID exists and is accessible in Google Drive using the new service"""
- try:
- drive_service = self.env['google.drive.service']
- validation = drive_service.validate_folder_id(folder_id)
-
- if validation.get('valid'):
- folder_name = validation.get('name', 'Unknown')
- _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
- return True, folder_name
- else:
- error_message = validation.get('error', 'Unknown error')
- _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
- return False, error_message
-
- except Exception as e:
- _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
- return False, str(e)
- def _create_google_drive_folder_structure(self):
- """Create the complete Google Drive folder structure for this opportunity with optimization"""
- self.ensure_one()
-
- # Validate prerequisites
- if not self._validate_folder_creation_prerequisites():
- return None
-
- # Try to get existing folder ID first
- existing_folder_id = self._try_get_existing_folder_id()
- if existing_folder_id:
- _logger.info(f"Found existing folder ID: {existing_folder_id}")
-
- # Validate the folder ID against Google Drive
- is_valid, error_message = self._validate_folder_id_with_google_drive(existing_folder_id)
-
- if is_valid:
- _logger.info(f"✅ Folder ID {existing_folder_id} validated successfully")
- # Update the record with the existing folder ID
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_id': existing_folder_id
- })
-
- # Get expected structure and store it
- expected_components = self._get_folder_name_components()
- expected_structure = self._build_structure_string(expected_components)
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': expected_structure
- })
-
- self.message_post(
- body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
- message_type='comment'
- )
- return True
- else:
- _logger.warning(f"❌ Folder ID {existing_folder_id} validation failed: {error_message}")
- self.message_post(
- body=_("⚠️ Folder ID from configured field is not accessible: %s. Creating new folder structure.") % error_message,
- message_type='comment'
- )
- # Continue to create new folder structure
-
- components = self._get_folder_name_components()
-
- try:
- # Create folder structure in batch for better performance
- folder_structure = self._create_folder_structure_batch(components)
-
- # Update the opportunity with the main folder ID
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_id': folder_structure['opportunity_folder_id'],
- 'google_drive_folder_name': self._build_structure_string(components)
- })
-
- return folder_structure
-
- except Exception as e:
- _logger.error(f"Failed to create folder structure for opportunity {self.id}: {str(e)}")
- raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
- def _create_folder_structure_batch(self, components):
- """Create folder structure in batch for better performance using the new service"""
- try:
- drive_service = self.env['google.drive.service']
- root_folder_id = self._get_company_root_folder_id()
-
- # Step 1: Create or get primary folder (Company/Contact)
- primary_folder_id = self._create_or_get_folder_crm(
- root_folder_id, components['primary_name']
- )
-
- # Step 2: Create or get year folder
- year_folder_id = self._create_or_get_folder_crm(
- primary_folder_id, components['year']
- )
-
- # Step 3: Create or get opportunity folder
- opportunity_folder_id = self._create_or_get_folder_crm(
- year_folder_id, components['opportunity_name']
- )
-
- # Step 4: Create Meets and Archivos cliente folders (parallel creation)
- meets_folder_id = self._create_or_get_folder_crm(
- opportunity_folder_id, 'Meets'
- )
-
- archivos_folder_id = self._create_or_get_folder_crm(
- opportunity_folder_id, 'Archivos cliente'
- )
-
- return {
- 'opportunity_folder_id': opportunity_folder_id,
- 'meets_folder_id': meets_folder_id,
- 'archivos_folder_id': archivos_folder_id
- }
- except Exception as e:
- _logger.error(f"Error creating folder structure batch: {str(e)}")
- raise
- def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
- """Create a folder or get existing one by name using the new service"""
- try:
- drive_service = self.env['google.drive.service']
-
- # First, check if folder already exists
- existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
- if existing_folders:
- return existing_folders[0]['id']
-
- # Create new folder
- result = drive_service.create_folder(folder_name, parent_folder_id)
-
- if result.get('success'):
- return result.get('folder_id')
- else:
- error_msg = result.get('error', 'Unknown error')
- raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
-
- except Exception as e:
- _logger.error(f"Error creating or getting folder {folder_name}: {str(e)}")
- raise
- def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
- """Find a folder by name in the parent folder with caching using the new service"""
- try:
- drive_service = self.env['google.drive.service']
-
- # Use the new service method
- folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
-
- if folders:
- result = folders[0]
-
- # Cache the result for 2 minutes
- cache_key = f'folder_{parent_folder_id}_{folder_name}'
- _GOOGLE_DRIVE_CACHE[cache_key] = {
- 'result': result,
- 'expires': datetime.now() + timedelta(seconds=120)
- }
-
- return result
- else:
- return None
-
- except Exception as e:
- _logger.error(f"Error finding folder '{folder_name}' in parent {parent_folder_id}: {str(e)}")
- return None
- def _rename_google_drive_folder(self, new_name):
- """Rename the Google Drive folder with optimization using the new service"""
- if not self.google_drive_folder_id:
- return
-
- # Sanitize the new name
- sanitized_name = self._sanitize_folder_name(new_name)
-
- # Check if the name is actually different
- if self.google_drive_folder_name == sanitized_name:
- return
-
- try:
- drive_service = self.env['google.drive.service']
-
- _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
-
- result = drive_service.rename_folder(self.google_drive_folder_id, sanitized_name)
-
- if result.get('success'):
- # Update the folder name in Odoo (with context to prevent loop)
- self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': sanitized_name})
- _logger.info(f"Successfully renamed Google Drive folder to '{sanitized_name}'")
-
- # Clear cache for this folder
- cache_key = f'folder_{self.google_drive_folder_id}'
- if cache_key in _GOOGLE_DRIVE_CACHE:
- del _GOOGLE_DRIVE_CACHE[cache_key]
- else:
- error_msg = f'Failed to rename Google Drive folder: {result.get("error", "Unknown error")}'
- _logger.error(error_msg)
- raise UserError(_(error_msg))
-
- except Exception as e:
- _logger.error(f"Error renaming folder {self.google_drive_folder_id}: {str(e)}")
- raise UserError(_('Failed to rename Google Drive folder: %s') % str(e))
- def _move_google_drive_folder(self, new_company_id):
- """Move the Google Drive folder to a new company's structure"""
- if not self.google_drive_folder_id:
- return
-
- # Get new company's root folder
- new_root_folder_id = new_company_id.google_drive_crm_folder_id
- if not new_root_folder_id:
- raise UserError(_('New company does not have Google Drive CRM folder configured'))
-
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- try:
- drive_service = self.env['google.drive.service']
-
- # Move the folder to the new company root
- result = drive_service.move_folder(self.google_drive_folder_id, new_root_folder_id)
-
- if not result.get('success'):
- raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
-
- except Exception as e:
- _logger.error(f"Error moving folder to new company: {str(e)}")
- raise UserError(_('Failed to move folder to new company: %s') % str(e))
- def _delete_google_drive_folder_structure(self):
- """Delete the Google Drive folder structure when contact is removed using the new service"""
- if not self.google_drive_folder_id:
- return
-
- try:
- drive_service = self.env['google.drive.service']
-
- # Delete the folder (this will also delete subfolders)
- result = drive_service.delete_folder(self.google_drive_folder_id)
-
- if result.get('success'):
- # Clear the folder ID from the record
- self.write({
- 'google_drive_folder_id': False,
- 'google_drive_folder_name': False
- })
- else:
- raise UserError(_('Failed to delete Google Drive folder structure: %s') % result.get('error', 'Unknown error'))
-
- except Exception as e:
- _logger.error(f"Error deleting folder structure: {str(e)}")
- raise UserError(_('Failed to delete Google Drive folder structure: %s') % str(e))
- def _recreate_google_drive_folder_structure(self):
- """Recreate the Google Drive folder structure when contact changes"""
- if not self.partner_id:
- raise UserError(_('No contact associated with this opportunity. Cannot recreate folder structure.'))
-
- # Store old folder information for reference
- old_folder_id = self.google_drive_folder_id
- old_folder_name = self.google_drive_folder_name
-
- # Clear the folder ID but don't delete the actual folder
- self.write({
- 'google_drive_folder_id': False,
- 'google_drive_folder_name': False
- })
-
- # Create new structure
- try:
- new_structure = self._create_google_drive_folder_structure()
-
- # Log the recreation for audit purposes
- if old_folder_id:
- _logger.info(f"Recreated Google Drive folder structure for opportunity {self.id}: "
- f"Old folder: {old_folder_id} ({old_folder_name}) -> "
- f"New folder: {self.google_drive_folder_id} ({self.google_drive_folder_name})")
-
- return new_structure
- except Exception as e:
- # Restore old values if recreation fails
- self.write({
- 'google_drive_folder_id': old_folder_id,
- 'google_drive_folder_name': old_folder_name
- })
- raise
- def _rename_entire_folder_structure(self, old_components=None, new_components=None):
- """Unified method to rename folder structure"""
- if not self.google_drive_folder_id:
- return
-
- try:
- # If only new_components provided, get current from Google Drive
- if old_components is None and new_components is not None:
- current_structure = self._analyze_complete_folder_structure()
- if not current_structure:
- raise UserError(_('Could not analyze current folder structure'))
-
- old_components = {
- 'primary_name': current_structure.get('primary_folder', {}).get('name', ''),
- 'year': current_structure.get('year_folder', {}).get('name', ''),
- 'opportunity_name': current_structure.get('opportunity_folder', {}).get('name', '')
- }
-
- # If only old_components provided, get new from current record
- if new_components is None and old_components is not None:
- new_components = self._get_folder_name_components()
-
- drive_service = self.env['google.drive.service']
- current_folder_id = self.google_drive_folder_id
-
- # Navigate up to find primary folder
- primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
- if not primary_folder_id:
- raise UserError(_('Cannot find primary folder in the structure'))
-
- # Rename primary folder if needed
- if old_components['primary_name'] != new_components['primary_name']:
- result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
- if not result.get('success'):
- raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
-
- # Rename year folder if needed
- if old_components['year'] != new_components['year']:
- year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
- if year_folder_id:
- result = drive_service.rename_folder(year_folder_id, new_components['year'])
- if not result.get('success'):
- raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
-
- # Rename opportunity folder if needed
- if old_components['opportunity_name'] != new_components['opportunity_name']:
- _logger.info(f"Renaming opportunity folder from '{old_components['opportunity_name']}' to '{new_components['opportunity_name']}'")
- result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
- if not result.get('success'):
- raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
-
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': new_components['opportunity_name']
- })
- else:
- _logger.info(f"Opportunity folder name is already correct: '{new_components['opportunity_name']}'")
- except Exception as e:
- _logger.error(f"Error renaming folder structure: {str(e)}")
- raise
- def _update_google_drive_folder_structure(self, vals):
- """Update the Google Drive folder structure based on changes"""
- if not self.google_drive_folder_id:
- return
-
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- # Company doesn't have Google Drive configured, do nothing
- return
-
- # Get current folder information
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Check if company changed - move folder to new company structure
- if 'company_id' in vals and vals['company_id'] != self.company_id.id:
- new_company = self.env['res.company'].browse(vals['company_id'])
- if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
- _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
- self._move_google_drive_folder(new_company)
- else:
- # If new company doesn't have Google Drive configured, keep the folder but log it
- self.message_post(
- body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
- message_type='comment'
- )
- return
-
- # Check if contact changed - this requires recreating the entire structure
- if 'partner_id' in vals:
- if not vals['partner_id']:
- # Contact was removed, but we don't delete - just log it
- self.message_post(
- body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
- message_type='comment'
- )
- return
- else:
- # Contact changed, recreate the entire structure
- _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
- self._recreate_google_drive_folder_structure()
- return
-
- # Check if name changed - rename the opportunity folder
- if 'name' in vals and vals['name'] != self.name:
- _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
- self._rename_google_drive_folder(vals['name'])
-
- # Validate and update entire folder structure if needed (only for non-name changes)
- # This will handle changes in company name, contact name, or year
- if 'partner_id' in vals or 'create_date' in vals:
- self._validate_and_update_folder_structure(vals)
-
- # Check if stage changed and we need to create folder
- if 'stage_id' in vals:
- if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
- if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- # Company doesn't have Google Drive configured, do nothing
- return
-
- # Validate contact exists before attempting to create folder
- if not self.partner_id:
- self.message_post(
- body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
- message_type='comment'
- )
- else:
- try:
- self._create_google_drive_folder_structure()
- except Exception as e:
- _logger.error(f"Failed to create Google Drive folder for opportunity {self.id}: {str(e)}")
- self.message_post(
- body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
- message_type='comment'
- )
-
- def _validate_and_update_folder_structure(self, vals):
- """Validate and update the entire folder structure if any component changed"""
- if not self.google_drive_folder_id:
- return
-
- # Get current and new components
- current_components = self._get_folder_name_components()
-
- # Create a temporary record with new values to get new components
- temp_vals = {}
- if 'name' in vals:
- temp_vals['name'] = vals['name']
- if 'partner_id' in vals:
- temp_vals['partner_id'] = vals['partner_id']
- if 'create_date' in vals:
- temp_vals['create_date'] = vals['create_date']
-
- # Create a temporary record to get new components
- temp_record = self.with_context(skip_google_drive_update=True)
- for field, value in temp_vals.items():
- setattr(temp_record, field, value)
-
- try:
- new_components = temp_record._get_folder_name_components()
- except:
- # If we can't get new components, skip validation
- return
-
- # Check if any component changed
- components_changed = (
- current_components['primary_name'] != new_components['primary_name'] or
- current_components['year'] != new_components['year']
- )
-
- # Also check if partner name changed (even if same partner, name might have changed)
- partner_name_changed = self._check_partner_name_changes(vals)
-
- if components_changed or partner_name_changed:
- _logger.info(f"Folder structure components changed. Renaming entire structure.")
- _logger.info(f"Old components: {current_components}")
- _logger.info(f"New components: {new_components}")
-
- # Store the old folder information for reference
- old_folder_id = self.google_drive_folder_id
- old_folder_name = self.google_drive_folder_name
- old_structure = f"{current_components['primary_name']}/{current_components['year']}/{current_components['opportunity_name']}"
- new_structure = f"{new_components['primary_name']}/{new_components['year']}/{new_components['opportunity_name']}"
-
- # Rename the entire folder structure instead of recreating
- try:
- self._rename_entire_folder_structure(current_components, new_components)
-
- # Log the change for audit
- self.message_post(
- body=_("🔄 Google Drive folder structure renamed due to changes:<br/>"
- "• Old structure: %s<br/>"
- "• New structure: %s<br/>"
- "• Folder ID: %s (same folder, renamed)") % (
- old_structure,
- new_structure,
- self.google_drive_folder_id
- ),
- message_type='comment'
- )
-
- except Exception as e:
- _logger.error(f"Failed to rename folder structure: {str(e)}")
- self.message_post(
- body=_("❌ Failed to rename Google Drive folder structure: %s") % str(e),
- message_type='comment'
- )
- raise
- return
-
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- # Company doesn't have Google Drive configured, do nothing
- return
-
- # Get current folder information
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Check if company changed - move folder to new company structure
- if 'company_id' in vals and vals['company_id'] != self.company_id.id:
- new_company = self.env['res.company'].browse(vals['company_id'])
- if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
- _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
- self._move_google_drive_folder(new_company)
- else:
- # If new company doesn't have Google Drive configured, keep the folder but log it
- self.message_post(
- body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
- message_type='comment'
- )
- return
-
- # Check if contact changed - this requires recreating the entire structure
- if 'partner_id' in vals:
- if not vals['partner_id']:
- # Contact was removed, but we don't delete - just log it
- self.message_post(
- body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
- message_type='comment'
- )
- return
- else:
- # Contact changed, recreate the entire structure
- _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
- self._recreate_google_drive_folder_structure()
- return
-
- # Check if name changed - rename the opportunity folder
- if 'name' in vals and vals['name'] != self.name:
- _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
- self._rename_google_drive_folder(vals['name'])
-
- # Validate and update entire folder structure if needed
- self._validate_and_update_folder_structure(vals)
-
- # Check if stage changed and we need to create folder
- if 'stage_id' in vals:
- if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
- if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
- if self.partner_id:
- self._create_google_drive_folder_structure()
- else:
- self.message_post(
- body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
- message_type='comment'
- )
- @api.model
- def create(self, vals):
- """Override create to handle Google Drive folder creation with optimization"""
- record = super().create(vals)
-
- # Check if we should create Google Drive folder (optimized conditions)
- if (record.company_id.google_drive_crm_enabled and
- record.company_id.google_drive_crm_stage_id and
- record.stage_id.id == record.company_id.google_drive_crm_stage_id.id):
-
- # Validate prerequisites before creating folder
- if record._validate_folder_creation_prerequisites():
- try:
- record._create_google_drive_folder_structure()
-
- # Store the initial structure and update URL
- if record._store_initial_structure_and_update_url():
- record.message_post(
- body=_("✅ Google Drive folder created automatically"),
- message_type='comment'
- )
- except Exception as e:
- # Log error but don't fail record creation
- _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
- record.message_post(
- body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
- message_type='comment'
- )
-
- return record
- def write(self, vals):
- """Override write method to handle Google Drive folder updates"""
- # Skip Google Drive updates if this is an internal update (to prevent loops)
- if self.env.context.get('skip_google_drive_update'):
- return super().write(vals)
-
- # Clear cache before processing
- _clear_google_drive_cache()
-
- # Check if any relevant fields are being updated
- relevant_fields = ['name', 'partner_id', 'create_date', 'stage_id', 'company_id']
- needs_update = any(field in vals for field in relevant_fields)
-
- if not needs_update:
- return super().write(vals)
-
- # Store current values for comparison - PROCESAR TODAS LAS OPORTUNIDADES
- current_values = {}
- for record in self:
- current_values[record.id] = {
- 'name': record.name,
- 'partner_id': record.partner_id.id if record.partner_id else None,
- 'create_date': record.create_date,
- 'company_id': record.company_id.id if record.company_id else None,
- 'google_drive_folder_name': record.google_drive_folder_name or ''
- }
-
- # Execute the write first
- result = super().write(vals)
-
- # Now process Google Drive updates with updated values - PROCESAR TODAS
- for record in self:
- record._process_google_drive_updates(vals, current_values[record.id])
-
- return result
- def _process_google_drive_updates(self, vals, old_values=None):
- """Unified method to process Google Drive updates"""
- try:
- _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
-
- # Check if we need to create folder
- should_create = self._should_create_folder(vals)
- if should_create and self._validate_folder_creation_prerequisites():
- _logger.info(f"Creating Google Drive folder for opportunity {self.id}")
- self._create_google_drive_folder_structure()
- if self._store_initial_structure_and_update_url():
- self.message_post(
- body=_("✅ Google Drive folder created automatically"),
- message_type='comment'
- )
-
- # If we have a folder, verify and update structure
- if self.google_drive_folder_id:
- self._verify_and_update_folder_structure(vals, old_values)
-
- except Exception as e:
- _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
- self.message_post(
- body=_("❌ Error updating Google Drive: %s") % str(e),
- message_type='comment'
- )
- def _should_create_folder(self, vals):
- """Helper method to determine if folder should be created"""
- # Stage-based creation logic
- if 'stage_id' in vals and not self.google_drive_folder_id:
- return True
-
- # Auto-creation logic
- if not self.google_drive_folder_id:
- company = self.company_id
- return (company.google_drive_crm_enabled and
- company.google_drive_crm_stage_id and
- self.stage_id.id == company.google_drive_crm_stage_id.id)
-
- return False
- def _verify_and_update_folder_structure(self, vals, old_values=None):
- """Unified method to verify and update folder structure"""
- try:
- _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
-
- # Get expected structure components
- expected_components = self._get_folder_name_components()
- expected_structure = self._build_structure_string(expected_components)
-
- # Get current structure from old_values or stored field
- if old_values:
- current_structure = old_values.get('google_drive_folder_name', '')
- else:
- current_structure = self.google_drive_folder_name or ''
-
- _logger.info(f"Structure comparison: Current='{current_structure}' vs Expected='{expected_structure}'")
-
- # Compare structures
- if current_structure != expected_structure:
- _logger.info(f"Structure mismatch detected. Updating Google Drive...")
-
- # Determine what type of change occurred
- if 'company_id' in vals and self.google_drive_folder_id:
- new_company_id = vals['company_id']
- if old_values and new_company_id != old_values.get('company_id'):
- _logger.info(f"Company changed from {old_values.get('company_id')} to {new_company_id}. Moving folder.")
- self._move_folder_to_new_company(new_company_id)
- else:
- # For other changes, rename the structure
- self._rename_entire_folder_structure_from_components(expected_components)
-
- # Update the stored structure
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': expected_structure
- })
-
- self.message_post(
- body=_("✅ Google Drive folder structure updated"),
- message_type='comment'
- )
- else:
- _logger.info(f"Structure is up to date. No changes needed.")
-
- except Exception as e:
- _logger.error(f"Error verifying folder structure: {str(e)}")
- raise
- def _build_structure_string(self, components):
- """Build a string representation of the folder structure"""
- return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
- def _rename_entire_folder_structure_from_components(self, expected_components):
- """Rename entire folder structure based on expected components"""
- try:
- # Use the unified method with only new_components
- self._rename_entire_folder_structure(new_components=expected_components)
- except Exception as e:
- _logger.error(f"Error renaming folder structure: {str(e)}")
- raise
- def _move_folder_to_new_company(self, new_company_id):
- """Mover folder a nueva empresa - SIMPLE"""
- if not self.google_drive_folder_id:
- return
-
- try:
- # Obtener nueva empresa
- new_company = self.env['res.company'].browse(new_company_id)
- new_root_folder_id = new_company.google_drive_crm_folder_id
-
- if not new_root_folder_id:
- self.message_post(
- body=_("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado."),
- message_type='comment'
- )
- return
-
- _logger.info(f"Moviendo folder de {self.company_id.name} a {new_company.name}")
-
- # Obtener access token
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Mover el folder primario al nuevo root
- self._move_folder_to_new_parent(headers, new_root_folder_id)
-
- self.message_post(
- body=_("✅ Folder movido a nueva empresa: %s") % new_company.name,
- message_type='comment'
- )
-
- except Exception as e:
- _logger.error(f"Error moviendo folder: {str(e)}")
- raise
- def _handle_structural_changes(self, vals):
- """Handle structural changes (partner_id, create_date) - rename entire structure"""
- if not self.google_drive_folder_id:
- return
-
- try:
- # Get current and new components
- current_components = self._get_folder_name_components()
-
- # Temporarily update the record to get new components
- temp_record = self.with_context(skip_google_drive_update=True)
- temp_vals = {}
- if 'partner_id' in vals:
- temp_vals['partner_id'] = vals['partner_id']
- if 'create_date' in vals:
- temp_vals['create_date'] = vals['create_date']
-
- if temp_vals:
- temp_record.write(temp_vals)
- new_components = temp_record._get_folder_name_components()
-
- # Check if any component changed
- components_changed = (
- current_components['primary_name'] != new_components['primary_name'] or
- current_components['year'] != new_components['year']
- )
-
- if components_changed:
- _logger.info(f"Structural changes detected. Renaming folder structure.")
- self._rename_entire_folder_structure(current_components, new_components)
- else:
- _logger.info(f"No structural changes detected. Skipping rename.")
-
- except Exception as e:
- _logger.error(f"Error handling structural changes: {str(e)}")
- raise
- def _handle_name_change(self, new_name):
- """Handle simple name change - only rename opportunity folder"""
- if not self.google_drive_folder_id:
- return
-
- try:
- sanitized_new_name = self._sanitize_folder_name(new_name)
- self._rename_google_drive_folder(sanitized_new_name)
- except Exception as e:
- _logger.error(f"Error handling name change: {str(e)}")
- raise
- def _move_folder_to_new_parent(self, headers, new_parent_id):
- """Move entire folder structure to new parent in Google Drive using the new service"""
- try:
- # Use the new Google Drive service
- drive_service = self.env['google.drive.service']
-
- # Get current folder info and navigate up to find the primary folder (company/contact)
- current_folder_id = self.google_drive_folder_id
-
- # Validate current folder using the service
- validation = drive_service.validate_folder_id(current_folder_id)
- if not validation.get('valid'):
- raise UserError(_('Failed to get current folder information'))
-
- # Navigate up the hierarchy to find the primary folder
- primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
-
- if not primary_folder_id:
- raise UserError(_('Could not find primary folder in hierarchy'))
-
- # Move the primary folder to the new parent
- result = drive_service.move_folder(primary_folder_id, new_parent_id)
-
- if not result.get('success'):
- raise UserError(_('Failed to move folder structure to new parent: %s') % result.get('error', 'Unknown error'))
-
- _logger.info(f"Successfully moved entire folder structure to {new_parent_id}")
-
- except Exception as e:
- _logger.error(f"Error moving folder structure: {str(e)}")
- raise
- def _find_primary_folder_id(self, headers, start_folder_id):
- """Find the primary folder (company/contact level) in the hierarchy using the new service"""
- return self._find_primary_folder_id_crm(start_folder_id)
- def action_open_google_drive_folder(self):
- """Open Google Drive folder for this opportunity"""
- self.ensure_one()
-
- if not self.google_drive_folder_id:
- raise UserError(_('No Google Drive folder configured for this opportunity'))
-
- folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
-
- return {
- 'type': 'ir.actions.act_url',
- 'url': folder_url,
- 'target': 'new',
- }
- def action_create_google_drive_folder(self):
- """Create Google Drive folder structure for this opportunity"""
- self.ensure_one()
-
- if self.google_drive_folder_id:
- raise UserError(_('Google Drive folder already exists for this opportunity'))
-
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
-
- try:
- folder_structure = self._create_google_drive_folder_structure()
-
- # Store the initial structure and update URL
- self._store_initial_structure_and_update_url()
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Google Drive folder structure created successfully!'),
- 'type': 'success',
- 'sticky': False,
- }
- }
- except Exception as e:
- raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
- def action_upload_to_google_drive(self):
- """Upload documents to Google Drive"""
- self.ensure_one()
-
- if not self.google_drive_folder_id:
- raise UserError(_('Please create a Google Drive folder for this opportunity first'))
-
- try:
- # TODO: Implement Google Drive API call to upload documents
- # For now, just show a message
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Info'),
- 'message': _('Document upload to Google Drive will be implemented soon.'),
- 'type': 'info',
- 'sticky': False,
- }
- }
-
- except Exception as e:
- raise UserError(_('Failed to upload to Google Drive: %s') % str(e))
- def action_recreate_google_drive_structure(self):
- """Manually rename the Google Drive folder structure"""
- self.ensure_one()
-
- if not self.google_drive_folder_id:
- raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
-
- # Check if company has Google Drive folder configured
- root_folder_id = self._get_company_root_folder_id()
- if not root_folder_id:
- raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
-
- try:
- # Get expected components
- expected_components = self._get_folder_name_components()
-
- # Get current folder name from Google Drive
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Use the new Google Drive service to get folder information
- drive_service = self.env['google.drive.service']
- validation = drive_service.validate_folder_id(self.google_drive_folder_id)
-
- if not validation.get('valid'):
- raise UserError(_('Failed to get current folder information from Google Drive'))
-
- current_folder_name = validation.get('name', '')
-
- _logger.info(f"Current folder name in Google Drive: '{current_folder_name}'")
- _logger.info(f"Expected folder name: '{expected_components['opportunity_name']}'")
-
- # Create old components with current Google Drive name
- old_components = expected_components.copy()
- old_components['opportunity_name'] = current_folder_name
-
- # Rename the structure
- self._rename_entire_folder_structure(old_components, expected_components)
-
- # Update the stored structure
- expected_structure = self._build_structure_string(expected_components)
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': expected_structure
- })
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Google Drive folder structure renamed successfully!<br/>'
- 'Folder ID: %s<br/>'
- 'Old name: %s<br/>'
- 'New name: %s') % (self.google_drive_folder_id, current_folder_name, expected_components['opportunity_name']),
- 'type': 'success',
- 'sticky': False,
- }
- }
- except Exception as e:
- _logger.error(f"Failed to rename Google Drive folder structure: {str(e)}")
- raise UserError(_('Failed to rename Google Drive folder structure: %s') % str(e))
- def action_analyze_folder_structure(self):
- """Analyze current vs expected folder structure"""
- self.ensure_one()
-
- if not self.google_drive_folder_id:
- raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
-
- try:
- # Get expected components
- expected_components = self._get_folder_name_components()
-
- # Get current folder information
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Analyze current structure
- current_structure = self._analyze_complete_folder_structure(headers)
-
- # Compare structures
- analysis = self._compare_folder_structures(expected_components, current_structure, headers)
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Folder Structure Analysis'),
- 'message': analysis,
- 'type': 'info',
- 'sticky': True,
- }
- }
- except Exception as e:
- raise UserError(_('Failed to analyze folder structure: %s') % str(e))
- def _analyze_current_folder_structure(self, headers):
- """Analyze the current folder structure in Google Drive using the new service"""
- current_folder_id = self.google_drive_folder_id
-
- try:
- # Use the new CRM-specific method
- return self._analyze_crm_folder_structure(current_folder_id)
- except Exception as e:
- _logger.error(f"Error analyzing current folder structure: {str(e)}")
- return None
- def _analyze_complete_folder_structure(self, headers):
- """Analyze the complete folder structure from root to opportunity using the new CRM-specific method"""
- current_folder_id = self.google_drive_folder_id
-
- try:
- # Use the new CRM-specific method
- return self._analyze_crm_folder_structure(current_folder_id)
- except Exception as e:
- _logger.error(f"Error in _analyze_complete_folder_structure: {str(e)}")
- return None
- def _compare_folder_structures(self, expected_components, current_structure, headers):
- """Compare expected vs current folder structure"""
- if not current_structure:
- return _('❌ Could not analyze current folder structure')
-
- analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
-
- # Expected structure
- analysis += f"<strong>Expected Structure:</strong><br/>"
- analysis += f"📁 [Root Folder] (MC Team)<br/>"
- analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
- analysis += f" └── 📁 {expected_components['year']} (Year)<br/>"
- analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
- analysis += f" ├── 📁 Meets<br/>"
- analysis += f" └── 📁 Archivos cliente<br/><br/>"
-
- # Current structure
- analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
-
- # Root folder
- if 'root_folder' in current_structure:
- root_name = current_structure['root_folder']['name']
- analysis += f"📁 {root_name} (Root)<br/>"
- else:
- analysis += f"📁 [Unknown Root]<br/>"
-
- # Primary folder
- if 'primary_folder' in current_structure:
- primary_name = current_structure['primary_folder']['name']
- analysis += f"└── 📁 {primary_name}"
- if primary_name != expected_components['primary_name']:
- analysis += f" ❌ (Expected: {expected_components['primary_name']})"
- else:
- analysis += " ✅"
- analysis += "<br/>"
- else:
- analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
-
- # Year folder
- if 'year_folder' in current_structure:
- year_name = current_structure['year_folder']['name']
- analysis += f" └── 📁 {year_name}"
- if year_name != expected_components['year']:
- analysis += f" ❌ (Expected: {expected_components['year']})"
- else:
- analysis += " ✅"
- analysis += "<br/>"
- else:
- analysis += f" └── 📁 [Missing Year Folder] ❌<br/>"
-
- # Opportunity folder
- if 'opportunity_folder' in current_structure:
- opp_name = current_structure['opportunity_folder']['name']
- analysis += f" └── 📁 {opp_name}"
- if opp_name != expected_components['opportunity_name']:
- analysis += f" ❌ (Expected: {expected_components['opportunity_name']})"
- else:
- analysis += " ✅"
- analysis += "<br/>"
- else:
- analysis += f" └── 📁 [Missing Opportunity Folder] ❌<br/>"
-
- # Check subfolders
- if 'opportunity_folder' in current_structure:
- opp_id = current_structure['opportunity_folder']['id']
- subfolders = self._get_subfolders(headers, opp_id)
- if subfolders:
- analysis += f" ├── 📁 Meets ✅<br/>"
- analysis += f" └── 📁 Archivos cliente ✅<br/>"
- else:
- analysis += f" ├── 📁 Meets ❌ (Missing)<br/>"
- analysis += f" └── 📁 Archivos cliente ❌ (Missing)<br/>"
-
- # Summary
- analysis += f"<br/><strong>Summary:</strong><br/>"
- correct_count = 0
- total_count = 0
-
- if 'primary_folder' in current_structure:
- total_count += 1
- if current_structure['primary_folder']['name'] == expected_components['primary_name']:
- correct_count += 1
-
- if 'year_folder' in current_structure:
- total_count += 1
- if current_structure['year_folder']['name'] == expected_components['year']:
- correct_count += 1
-
- if 'opportunity_folder' in current_structure:
- total_count += 1
- if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']:
- correct_count += 1
-
- if total_count == 3 and correct_count == 3:
- analysis += "✅ Complete structure is correct"
- else:
- analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct). Use 'Rename Folder Structure' button to fix."
-
- return analysis
- def _get_subfolders(self, headers, parent_id):
- """Get subfolders of a parent folder using the new service"""
- try:
- drive_service = self.env['google.drive.service']
- return drive_service.find_folders_by_name(parent_id, r'.*')
- except Exception as e:
- _logger.error(f"Error getting subfolders: {str(e)}")
- return []
- def _store_initial_structure_and_update_url(self):
- """Centralized method to store initial structure and update URL"""
- if self.google_drive_folder_id:
- expected_components = self._get_folder_name_components()
- expected_structure = self._build_structure_string(expected_components)
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': expected_structure
- })
-
- # Update Google Drive URL if empty
- self._update_google_drive_url()
-
- # Copy URL to configured field if it's empty
- self._copy_google_drive_url_to_configured_field()
-
- _logger.info(f"Estructura inicial almacenada para oportunidad {self.id}")
- return True
- else:
- _logger.error(f"ERROR: _create_google_drive_folder_structure no asignó google_drive_folder_id para oportunidad {self.id}")
- return False
- def _generate_google_drive_url(self):
- """Generate Google Drive URL for the opportunity folder"""
- if self.google_drive_folder_id:
- return f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
- return False
- def _update_google_drive_url(self):
- """Update the google_drive_url field if it's empty and we have a folder ID"""
- if self.google_drive_folder_id and not self.google_drive_url:
- url = self._generate_google_drive_url()
- if url:
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_url': url
- })
- _logger.info(f"Updated Google Drive URL for opportunity {self.id}: {url}")
- def _extract_folder_id_from_url(self, url):
- """Extract folder ID from Google Drive URL"""
- if not url:
- return None
-
- # Handle different Google Drive URL formats
- import re
-
- # Format: https://drive.google.com/drive/folders/FOLDER_ID
- folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
- match = re.search(folder_pattern, url)
-
- if match:
- return match.group(1)
-
- # Format: https://drive.google.com/open?id=FOLDER_ID
- open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
- match = re.search(open_pattern, url)
-
- if match:
- return match.group(1)
-
- return None
- def _get_configured_field_name(self):
- """Get the field name configured in company settings"""
- if not self.company_id or not self.company_id.google_drive_crm_field_id:
- return None
- return self.company_id.google_drive_crm_field_id.name
- def _get_configured_field_value(self):
- """Get the value of the configured field"""
- field_name = self._get_configured_field_name()
- if not field_name:
- return None
- return getattr(self, field_name, None)
- def _set_configured_field_value(self, value):
- """Set the value of the configured field"""
- field_name = self._get_configured_field_name()
- if not field_name:
- return False
- self.with_context(skip_google_drive_update=True).write({field_name: value})
- return True
- def _copy_google_drive_url_to_configured_field(self):
- """Copy google_drive_url to the configured field if it's empty"""
- if not self.google_drive_url:
- return False
-
- field_name = self._get_configured_field_name()
- if not field_name:
- return False
-
- current_value = getattr(self, field_name, None)
- if not current_value: # Solo si está vacío
- self._set_configured_field_value(self.google_drive_url)
- _logger.info(f"Copied google_drive_url to {field_name} for opportunity {self.id}")
- return True
-
- return False
- def _validate_folder_id_with_google_drive(self, folder_id):
- """Validate if the folder ID exists and is accessible in Google Drive using the new service"""
- try:
- drive_service = self.env['google.drive.service']
- validation = drive_service.validate_folder_id(folder_id)
-
- if validation.get('valid'):
- folder_name = validation.get('name', 'Unknown')
- _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
- return True, folder_name
- else:
- error_message = validation.get('error', 'Unknown error')
- _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
- return False, error_message
-
- except Exception as e:
- _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
- return False, str(e)
- # ============================================================================
- # MÉTODOS ESPECÍFICOS DE CRM QUE USAN SERVICIOS GENÉRICOS
- # ============================================================================
- def _find_primary_folder_id_crm(self, start_folder_id):
- """Find the primary folder (company/contact level) in the hierarchy - CRM specific logic"""
- try:
- # Use the generic service to navigate the hierarchy
- drive_service = self.env['google.drive.service']
- hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
-
- # Apply CRM-specific logic to identify the primary folder
- for folder_info in hierarchy:
- folder_name = folder_info.get('name', '')
- level = folder_info.get('level', 0)
-
- # Skip root level folders
- if level == 0:
- continue
-
- # Check if this folder is the primary folder (not a year or opportunity folder)
- # Primary folder is typically the company/contact name
- if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
- # This could be the primary folder, but let's check if it has a year folder as child
- year_folders = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
- if year_folders:
- # This is the primary folder
- return folder_info['id']
-
- return None
-
- except Exception as e:
- _logger.error(f"Error finding primary folder (CRM): {str(e)}")
- return None
- def _find_year_folder_id_crm(self, primary_folder_id, year):
- """Find the year folder within the primary folder - CRM specific"""
- try:
- drive_service = self.env['google.drive.service']
- year_folders = drive_service.find_folders_by_name(primary_folder_id, f'^{year}$')
- return year_folders[0]['id'] if year_folders else None
- except Exception as e:
- _logger.error(f"Error finding year folder (CRM): {str(e)}")
- return None
- def _analyze_crm_folder_structure(self, folder_id):
- """Analyze the complete folder structure from root to opportunity - CRM specific"""
- try:
- drive_service = self.env['google.drive.service']
-
- # Get current folder info
- validation = drive_service.validate_folder_id(folder_id)
- if not validation.get('valid'):
- return None
-
- current_name = validation.get('name', '')
-
- # Build complete structure
- complete_structure = {
- 'opportunity_folder': {
- 'id': folder_id,
- 'name': current_name,
- 'level': 3
- }
- }
-
- # Navigate up the hierarchy using the generic service
- hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
-
- # Apply CRM-specific logic to categorize folders
- for folder_info in hierarchy:
- folder_name = folder_info.get('name', '')
- level = folder_info.get('level', 0)
-
- if level == 2: # Year level
- if folder_name.isdigit() and len(folder_name) == 4:
- complete_structure['year_folder'] = {
- 'id': folder_info['id'],
- 'name': folder_name,
- 'level': level
- }
- elif level == 1: # Primary level (company/contact)
- if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
- complete_structure['primary_folder'] = {
- 'id': folder_info['id'],
- 'name': folder_name,
- 'level': level
- }
- elif level == 0: # Root level
- complete_structure['root_folder'] = {
- 'id': folder_info['id'],
- 'name': folder_name,
- 'level': level
- }
-
- return complete_structure
-
- except Exception as e:
- _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}")
- return None
- def _check_crm_folder_company_mismatch(self, folder_id, company_root_id):
- """Check if folder belongs to the correct company root - CRM specific"""
- try:
- drive_service = self.env['google.drive.service']
-
- # Use the generic service to check folder parent relationship
- belongs_to_company = drive_service.check_folder_belongs_to_parent(
- folder_id,
- company_root_id
- )
-
- # Return True if there's a mismatch (folder doesn't belong to company)
- return not belongs_to_company
-
- except Exception as e:
- _logger.error(f"Error checking CRM folder company mismatch: {str(e)}")
- return False
|