| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265 |
- # -*- 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_google_drive_access_token(self):
- """Get Google Drive access token from system parameters with caching"""
- # Check cache first
- cache_key = 'access_token'
- if cache_key in _GOOGLE_DRIVE_CACHE:
- cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
- if datetime.now() < cache_entry['expires']:
- return cache_entry['token']
-
- access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
- if not access_token:
- raise UserError(_('No OAuth token found. Please connect your Google account in Google API settings first.'))
-
- # Test if the token is still valid
- if not self._test_access_token(access_token):
- # Try to refresh the token
- if self._refresh_access_token():
- access_token = self.env['ir.config_parameter'].sudo().get_param('google_api.access_token')
- else:
- raise UserError(_('Google Drive access token has expired and could not be refreshed. Please reconnect your Google account.'))
-
- # Cache the token for 5 minutes
- _GOOGLE_DRIVE_CACHE[cache_key] = {
- 'token': access_token,
- 'expires': datetime.now() + timedelta(seconds=_CACHE_TIMEOUT)
- }
-
- return access_token
- def _test_access_token(self, access_token):
- """Test if the access token is still valid with better error handling"""
- try:
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- response = requests.get(
- 'https://www.googleapis.com/drive/v3/about',
- headers=headers,
- params={'fields': 'user'},
- timeout=10
- )
-
- if response.status_code == 200:
- return True
- elif response.status_code == 401:
- _logger.warning("Google Drive access token is invalid (401)")
- return False
- else:
- _logger.warning(f"Google Drive API test failed with status {response.status_code}")
- return False
-
- except requests.exceptions.Timeout:
- _logger.error("Google Drive API test timeout")
- return False
- except requests.exceptions.ConnectionError:
- _logger.error("Google Drive API connection error")
- return False
- except Exception as e:
- _logger.error(f"Google Drive API test error: {str(e)}")
- return False
- def _refresh_access_token(self):
- """Refresh the access token using the refresh token"""
- try:
- refresh_token = self.env['ir.config_parameter'].sudo().get_param('google_api.refresh_token')
- client_id = self.env['ir.config_parameter'].sudo().get_param('google_api.client_id')
- client_secret = self.env['ir.config_parameter'].sudo().get_param('google_api.client_secret')
-
- if not all([refresh_token, client_id, client_secret]):
- return False
-
- # Exchange refresh token for new access token
- token_url = 'https://oauth2.googleapis.com/token'
- data = {
- 'client_id': client_id,
- 'client_secret': client_secret,
- 'refresh_token': refresh_token,
- 'grant_type': 'refresh_token'
- }
-
- response = requests.post(token_url, data=data, timeout=30)
-
- if response.status_code == 200:
- token_data = response.json()
- new_access_token = token_data.get('access_token')
-
- if new_access_token:
- # Store the new access token
- self.env['ir.config_parameter'].sudo().set_param('google_api.access_token', new_access_token)
- return True
-
- return False
-
- except Exception as e:
- _logger.error(f"Failed to refresh access token: {str(e)}")
- return False
- 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"""
- if not self.google_drive_folder_id or not self.company_id:
- return False
-
- try:
- # Get access token
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Get company root folder ID
- company_root_folder_id = self._get_company_root_folder_id()
- if not company_root_folder_id:
- return False
-
- # Get current folder info
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- return False
-
- folder_data = response.json()
- parent_ids = folder_data.get('parents', [])
-
- if not parent_ids:
- return False
-
- # Navigate up to find if the folder is under the correct company root
- current_parent_id = parent_ids[0]
- max_levels = 5 # Prevent infinite loop
-
- for _ in range(max_levels):
- # Check if current parent is the company root folder
- if current_parent_id == company_root_folder_id:
- # Folder is under the correct company root
- return False
-
- parent_response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_parent_id}?fields=parents',
- headers=headers,
- timeout=30
- )
-
- if parent_response.status_code != 200:
- break
-
- parent_data = parent_response.json()
- parent_parent_ids = parent_data.get('parents', [])
-
- if not parent_parent_ids:
- # Reached the top level (My Drive), folder is not under the correct company root
- _logger.info(f"Folder belongs to wrong company. Current root: {current_parent_id}, Expected: {company_root_folder_id}")
- return True
-
- current_parent_id = parent_parent_ids[0]
-
- # If we reach here, folder is not under the correct company root
- _logger.info(f"Folder belongs to wrong company. Could not find company root: {company_root_folder_id}")
- return True
-
- 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"""
- try:
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Test access to the specific folder
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
- headers=headers,
- timeout=10
- )
-
- if response.status_code == 200:
- folder_data = response.json()
- if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
- _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
- return True, folder_data.get('name', 'Unknown')
- else:
- _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
- return False, "Not a folder"
- elif response.status_code == 404:
- _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive")
- return False, "Not found"
- elif response.status_code == 403:
- _logger.warning(f"❌ Access denied to folder ID {folder_id}")
- return False, "Access denied"
- elif response.status_code == 401:
- _logger.error(f"❌ OAuth token expired or invalid")
- return False, "Authentication error"
- else:
- _logger.warning(f"❌ Google Drive API error: {response.status_code}")
- return False, f"API error: {response.status_code}"
-
- except requests.exceptions.Timeout:
- _logger.error(f"❌ Timeout validating folder ID {folder_id}")
- return False, "Timeout"
- except requests.exceptions.ConnectionError:
- _logger.error(f"❌ Connection error validating folder ID {folder_id}")
- return False, "Connection error"
- 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
-
- access_token = self._get_google_drive_access_token()
- components = self._get_folder_name_components()
-
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- try:
- # Create folder structure in batch for better performance
- folder_structure = self._create_folder_structure_batch(headers, 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, headers, components):
- """Create folder structure in batch for better performance"""
- 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(
- headers, root_folder_id, components['primary_name']
- )
-
- # Step 2: Create or get year folder
- year_folder_id = self._create_or_get_folder(
- headers, primary_folder_id, components['year']
- )
-
- # Step 3: Create or get opportunity folder
- opportunity_folder_id = self._create_or_get_folder(
- headers, year_folder_id, components['opportunity_name']
- )
-
- # Step 4: Create Meets and Archivos cliente folders (parallel creation)
- meets_folder_id = self._create_or_get_folder(
- headers, opportunity_folder_id, 'Meets'
- )
-
- archivos_folder_id = self._create_or_get_folder(
- headers, 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(self, headers, parent_folder_id, folder_name):
- """Create a folder or get existing one by name"""
- # First, check if folder already exists
- existing_folder = self._find_folder_by_name(headers, parent_folder_id, folder_name)
- if existing_folder:
- return existing_folder['id']
-
- # Create new folder
- folder_metadata = {
- 'name': folder_name,
- 'mimeType': 'application/vnd.google-apps.folder',
- 'parents': [parent_folder_id]
- }
-
- response = requests.post(
- 'https://www.googleapis.com/drive/v3/files',
- headers=headers,
- json=folder_metadata,
- timeout=30
- )
-
- if response.status_code == 200:
- folder_data = response.json()
- return folder_data['id']
- else:
- raise UserError(_('Failed to create Google Drive folder "%s". Status: %s') % (folder_name, response.status_code))
- def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
- """Find a folder by name in the parent folder with caching"""
- # Check cache first
- cache_key = f'folder_{parent_folder_id}_{folder_name}'
- if cache_key in _GOOGLE_DRIVE_CACHE:
- cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
- if datetime.now() < cache_entry['expires']:
- return cache_entry['result']
-
- params = {
- 'q': f"'{parent_folder_id}' in parents and name='{folder_name}' and mimeType='application/vnd.google-apps.folder' and trashed=false",
- 'fields': 'files(id,name)',
- 'pageSize': 1
- }
-
- try:
- response = requests.get(
- 'https://www.googleapis.com/drive/v3/files',
- headers=headers,
- params=params,
- timeout=30
- )
-
- if response.status_code == 200:
- data = response.json()
- folders = data.get('files', [])
- result = folders[0] if folders else None
-
- # Cache the result for 2 minutes
- _GOOGLE_DRIVE_CACHE[cache_key] = {
- 'result': result,
- 'expires': datetime.now() + timedelta(seconds=120)
- }
-
- return result
- else:
- _logger.warning(f"Failed to find folder '{folder_name}' in parent {parent_folder_id}. Status: {response.status_code}")
- 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"""
- 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
-
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- try:
- folder_metadata = {
- 'name': sanitized_name
- }
-
- _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
-
- response = requests.patch(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
- headers=headers,
- json=folder_metadata,
- timeout=30
- )
-
- if response.status_code == 200:
- # 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. Status: {response.status_code}'
- if response.text:
- error_msg += f' - Response: {response.text}'
- _logger.error(error_msg)
- raise UserError(_(error_msg))
-
- except requests.exceptions.Timeout:
- raise UserError(_('Timeout while renaming Google Drive folder. Please try again.'))
- except requests.exceptions.ConnectionError:
- raise UserError(_('Connection error while renaming Google Drive folder. Please check your internet connection.'))
- 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'
- }
-
- # Get current folder's parent
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- raise UserError(_('Failed to get current folder information'))
-
- folder_data = response.json()
- current_parents = folder_data.get('parents', [])
-
- # Remove from current parent and add to new parent
- if current_parents:
- # Remove from current parent
- remove_response = requests.delete(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents/{current_parents[0]}',
- headers=headers,
- timeout=30
- )
-
- if remove_response.status_code != 204:
- raise UserError(_('Failed to remove folder from current parent'))
-
- # Add to new parent
- add_response = requests.post(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}/parents',
- headers=headers,
- json={'id': new_root_folder_id},
- timeout=30
- )
-
- if add_response.status_code != 200:
- raise UserError(_('Failed to move folder to new company'))
- def _delete_google_drive_folder_structure(self):
- """Delete the Google Drive folder structure when contact is removed"""
- if not self.google_drive_folder_id:
- return
-
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- }
-
- # Delete the folder (this will also delete subfolders)
- response = requests.delete(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}',
- headers=headers,
- timeout=30
- )
-
- if response.status_code == 204:
- # 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'))
- 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, new_components):
- """Rename the entire folder structure instead of recreating it"""
- if not self.google_drive_folder_id:
- return
-
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Get the current folder path to understand the structure
- current_folder_id = self.google_drive_folder_id
-
- # Get folder information to find parent folders
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=parents,name',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- raise UserError(_('Failed to get current folder information'))
-
- folder_data = response.json()
- parent_ids = folder_data.get('parents', [])
-
- if not parent_ids:
- raise UserError(_('Cannot rename folder structure: no parent folder found'))
-
- # Navigate up the hierarchy to find the primary folder (company/contact)
- primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
- if not primary_folder_id:
- raise UserError(_('Cannot find primary folder in the structure'))
-
- # Rename the primary folder (company/contact name)
- if old_components['primary_name'] != new_components['primary_name']:
- self._rename_folder(headers, primary_folder_id, new_components['primary_name'])
-
- # Find and rename year folder if needed
- if old_components['year'] != new_components['year']:
- year_folder_id = self._find_year_folder_id(headers, primary_folder_id, old_components['year'])
- if year_folder_id:
- self._rename_folder(headers, year_folder_id, new_components['year'])
-
- # 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']}'")
- self._rename_folder(headers, current_folder_id, new_components['opportunity_name'])
- # Update the folder name in Odoo (with context to prevent loop)
- 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']}'")
- def _find_primary_folder_id(self, headers, current_folder_id):
- """Find the primary folder (company/contact) by navigating up the hierarchy"""
- max_depth = 5 # Prevent infinite loops
- current_id = current_folder_id
-
- for depth in range(max_depth):
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=parents,name',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- return None
-
- folder_data = response.json()
- parent_ids = folder_data.get('parents', [])
-
- if not parent_ids:
- # This is the root folder, go back one level
- return current_id
-
- # Check if this folder is the primary folder (not a year or opportunity folder)
- # Primary folder is typically the company/contact name
- folder_name = folder_data.get('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 = self._find_folders_by_name(headers, current_id, r'^\d{4}$')
- if year_folders:
- # This is the primary folder
- return current_id
-
- current_id = parent_ids[0]
-
- return None
- def _find_year_folder_id(self, headers, primary_folder_id, year):
- """Find the year folder within the primary folder"""
- year_folders = self._find_folders_by_name(headers, primary_folder_id, f'^{year}$')
- return year_folders[0]['id'] if year_folders else None
- def _find_folders_by_name(self, headers, parent_id, name_pattern):
- """Find folders by name pattern in a parent folder"""
- import re
-
- params = {
- 'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
- 'fields': 'files(id,name)',
- 'pageSize': 100
- }
-
- response = requests.get(
- 'https://www.googleapis.com/drive/v3/files',
- headers=headers,
- params=params,
- timeout=30
- )
-
- if response.status_code != 200:
- return []
-
- data = response.json()
- folders = data.get('files', [])
-
- # Filter by name pattern
- pattern = re.compile(name_pattern)
- return [folder for folder in folders if pattern.match(folder.get('name', ''))]
- def _rename_folder(self, headers, folder_id, new_name):
- """Rename a specific folder"""
- _logger.info(f"Attempting to rename folder {folder_id} to '{new_name}'")
-
- folder_metadata = {
- 'name': new_name
- }
-
- try:
- response = requests.patch(
- f'https://www.googleapis.com/drive/v3/files/{folder_id}',
- headers=headers,
- json=folder_metadata,
- timeout=30
- )
-
- if response.status_code == 200:
- _logger.info(f"Successfully renamed folder {folder_id} to '{new_name}'")
- else:
- _logger.error(f"Failed to rename folder {folder_id}. Status: {response.status_code}, Response: {response.text}")
- raise UserError(_('Failed to rename folder "%s" to "%s". Status: %s') % (folder_id, new_name, response.status_code))
-
- except requests.exceptions.Timeout:
- _logger.error(f"Timeout while renaming folder {folder_id}")
- raise UserError(_('Timeout while renaming folder "%s"') % folder_id)
- except requests.exceptions.ConnectionError:
- _logger.error(f"Connection error while renaming folder {folder_id}")
- raise UserError(_('Connection error while renaming folder "%s"') % folder_id)
- except Exception as e:
- _logger.error(f"Error renaming folder {folder_id}: {str(e)}")
- raise UserError(_('Failed to rename folder "%s" to "%s": %s') % (folder_id, new_name, str(e)))
- 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_immediate(vals, current_values[record.id])
-
- return result
- def _process_google_drive_updates(self, vals):
- """Process Google Drive updates for a single record"""
- try:
- # Check if we need to create folder (stage-based creation)
- if 'stage_id' in vals and not self.google_drive_folder_id:
- if not self._validate_folder_creation_prerequisites():
- return
-
- # If we have a folder, verify and update structure if needed
- if self.google_drive_folder_id:
- self._verify_and_update_folder_structure(vals)
-
- 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 _process_google_drive_updates_immediate(self, vals, old_values):
- """Process Google Drive updates immediately after write"""
- try:
- _logger.info(f"=== INICIO _process_google_drive_updates_immediate para oportunidad {self.id} ===")
- _logger.info(f"Vals recibidos: {vals}")
- _logger.info(f"Google Drive Folder ID actual: {self.google_drive_folder_id}")
-
- # PASO 1: Verificar si necesitamos crear carpeta (stage-based creation)
- should_create_folder = False
-
- # Caso 1: Cambió stage_id y no tiene carpeta
- if 'stage_id' in vals and not self.google_drive_folder_id:
- should_create_folder = True
- _logger.info(f"CASO 1: Stage changed for opportunity {self.id}. Checking if should create folder.")
-
- # Caso 2: Ya está en el stage correcto, no tiene carpeta, y se actualizó cualquier campo
- elif not self.google_drive_folder_id:
- # Verificar si está en el stage correcto para crear automáticamente
- company = self.company_id
- _logger.info(f"CASO 2: Checking if opportunity {self.id} is in correct stage for auto-creation")
- _logger.info(f"Company Google Drive enabled: {company.google_drive_crm_enabled}")
- _logger.info(f"Company stage configured: {company.google_drive_crm_stage_id.name if company.google_drive_crm_stage_id else 'None'}")
- _logger.info(f"Opportunity stage: {self.stage_id.name}")
-
- if (company.google_drive_crm_enabled and
- company.google_drive_crm_stage_id and
- self.stage_id.id == company.google_drive_crm_stage_id.id):
- should_create_folder = True
- _logger.info(f"CASO 2: Opportunity {self.id} is in correct stage but has no folder. Will create.")
- else:
- _logger.info(f"CASO 2: Opportunity {self.id} is NOT in correct stage for auto-creation")
-
- _logger.info(f"¿Debería crear carpeta?: {should_create_folder}")
-
- # Crear carpeta si es necesario
- if should_create_folder:
- _logger.info(f"Intentando crear carpeta para oportunidad {self.id}")
-
- if self._validate_folder_creation_prerequisites():
- _logger.info(f"Prerrequisitos válidos. Creando Google Drive folder para opportunity {self.id}")
- try:
- self._create_google_drive_folder_structure()
- _logger.info(f"Carpeta creada exitosamente para oportunidad {self.id}")
-
- # Store the initial structure and update URL
- if self._store_initial_structure_and_update_url():
- self.message_post(
- body=_("✅ Google Drive folder created automatically"),
- message_type='comment'
- )
- except Exception as e:
- _logger.error(f"ERROR creando carpeta para oportunidad {self.id}: {str(e)}")
- raise
- else:
- _logger.info(f"Prerrequisitos no cumplidos para oportunidad {self.id}. Skipping folder creation.")
-
- # PASO 2: Si ya tiene carpeta, verificar y actualizar estructura si es necesario
- if self.google_drive_folder_id:
- _logger.info(f"Oportunidad {self.id} ya tiene carpeta. Verificando estructura.")
- self._verify_and_update_folder_structure_immediate(vals, old_values)
-
- _logger.info(f"=== FIN _process_google_drive_updates_immediate para oportunidad {self.id} ===")
-
- 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 _verify_and_update_folder_structure_immediate(self, vals, old_values):
- """Verificar y actualizar estructura de Google Drive - SECUENCIAL"""
- try:
- _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
-
- # PASO 1: Si cambia company_id y tiene folder_id → Mover
- if 'company_id' in vals and self.google_drive_folder_id:
- new_company_id = vals['company_id']
- if 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)
-
- # PASO 2: Si cambia otro campo → Verificar string de estructura
- relevant_fields = ['name', 'partner_id', 'create_date']
- if any(field in vals for field in relevant_fields):
- expected_structure = self._build_structure_string(self._get_folder_name_components())
- current_structure = old_values.get('google_drive_folder_name', '')
-
- _logger.info(f"Structure comparison: Current='{current_structure}' vs Expected='{expected_structure}'")
-
- if expected_structure != current_structure:
- _logger.info(f"Structure changed. Renaming folder structure.")
- self._rename_entire_folder_structure_from_components(self._get_folder_name_components())
-
- # Actualizar estructura local al final
- expected_structure = self._build_structure_string(self._get_folder_name_components())
- self.with_context(skip_google_drive_update=True).write({
- 'google_drive_folder_name': expected_structure
- })
-
- self.message_post(
- body=_("✅ Google Drive folder structure updated immediately"),
- message_type='comment'
- )
-
- except Exception as e:
- _logger.error(f"Error verifying folder structure: {str(e)}")
- raise
- def _verify_and_update_folder_structure(self, vals):
- """Verify current structure vs expected and update if needed"""
- try:
- # Get expected structure components
- expected_components = self._get_folder_name_components()
-
- # Build expected structure string
- expected_structure = self._build_structure_string(expected_components)
-
- # Get current structure from stored field
- current_structure = self.google_drive_folder_name or ''
-
- _logger.info(f"Structure comparison for opportunity {self.id}:")
- _logger.info(f"Current: '{current_structure}'")
- _logger.info(f"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:
- self._handle_company_change(vals['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 successfully"),
- 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:
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Get current structure from Google Drive
- current_structure = self._analyze_complete_folder_structure(headers)
-
- if not current_structure:
- raise UserError(_('Could not analyze current folder structure'))
-
- # Build current components from actual structure
- current_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', '')
- }
-
- # Rename the structure
- self._rename_entire_folder_structure(current_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"""
- try:
- # Get current folder info and navigate up to find the primary folder (company/contact)
- current_folder_id = self.google_drive_folder_id
-
- # Navigate up the hierarchy to find the primary folder
- primary_folder_id = self._find_primary_folder_id(headers, current_folder_id)
-
- if not primary_folder_id:
- raise UserError(_('Could not find primary folder in hierarchy'))
-
- # Get current parent of primary folder
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?fields=parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- raise UserError(_('Failed to get primary folder information'))
-
- current_data = response.json()
- current_parents = current_data.get('parents', [])
-
- if not current_parents:
- raise UserError(_('Primary folder has no parent'))
-
- current_parent_id = current_parents[0]
-
- # Move the entire structure by moving the primary folder
- move_response = requests.patch(
- f'https://www.googleapis.com/drive/v3/files/{primary_folder_id}?addParents={new_parent_id}&removeParents={current_parent_id}',
- headers=headers,
- timeout=30
- )
-
- if move_response.status_code != 200:
- raise UserError(_('Failed to move folder structure to new parent'))
-
- _logger.info(f"Successfully moved entire folder structure from {current_parent_id} 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"""
- try:
- current_id = start_folder_id
-
- # Navigate up to 3 levels to find the primary folder
- for _ in range(3):
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_id}?fields=name,parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- break
-
- folder_data = response.json()
- folder_name = folder_data.get('name', '')
- parent_ids = folder_data.get('parents', [])
-
- # Check if this is the primary folder (not year, not opportunity)
- # Primary folder is typically the company/contact name
- if not parent_ids:
- break
-
- # If this folder's name looks like a year (4 digits), continue up
- if folder_name.isdigit() and len(folder_name) == 4:
- current_id = parent_ids[0]
- continue
-
- # If this folder's name contains opportunity ID pattern, continue up
- if ' - ' in folder_name and folder_name.split(' - ')[0].isdigit():
- current_id = parent_ids[0]
- continue
-
- # This should be the primary folder
- return current_id
-
- return None
-
- except Exception as e:
- _logger.error(f"Error finding primary folder: {str(e)}")
- return None
- 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'
- }
-
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{self.google_drive_folder_id}?fields=name',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- raise UserError(_('Failed to get current folder information from Google Drive'))
-
- current_folder_data = response.json()
- current_folder_name = current_folder_data.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"""
- current_folder_id = self.google_drive_folder_id
-
- # Get current folder info
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- return None
-
- folder_data = response.json()
- current_name = folder_data.get('name', '')
- parent_ids = folder_data.get('parents', [])
-
- # Navigate up the hierarchy
- structure = {
- 'opportunity_folder': {
- 'id': current_folder_id,
- 'name': current_name
- }
- }
-
- if parent_ids:
- # Get year folder
- year_response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{parent_ids[0]}?fields=name,parents',
- headers=headers,
- timeout=30
- )
-
- if year_response.status_code == 200:
- year_data = year_response.json()
- structure['year_folder'] = {
- 'id': parent_ids[0],
- 'name': year_data.get('name', '')
- }
-
- year_parent_ids = year_data.get('parents', [])
- if year_parent_ids:
- # Get primary folder (company/contact)
- primary_response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{year_parent_ids[0]}?fields=name',
- headers=headers,
- timeout=30
- )
-
- if primary_response.status_code == 200:
- primary_data = primary_response.json()
- structure['primary_folder'] = {
- 'id': year_parent_ids[0],
- 'name': primary_data.get('name', '')
- }
-
- return structure
- def _analyze_complete_folder_structure(self, headers):
- """Analyze the complete folder structure from root to opportunity"""
- current_folder_id = self.google_drive_folder_id
-
- # Get current folder info
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{current_folder_id}?fields=name,parents',
- headers=headers,
- timeout=30
- )
-
- if response.status_code != 200:
- return None
-
- folder_data = response.json()
- current_name = folder_data.get('name', '')
- parent_ids = folder_data.get('parents', [])
-
- # Build complete structure
- complete_structure = {
- 'opportunity_folder': {
- 'id': current_folder_id,
- 'name': current_name,
- 'level': 3
- }
- }
-
- current_id = current_folder_id
- level = 3 # Opportunity level
-
- # Navigate up the hierarchy
- for _ in range(5): # Max 5 levels up
- if not parent_ids:
- break
-
- parent_id = parent_ids[0]
-
- # Get parent folder info
- parent_response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{parent_id}?fields=name,parents',
- headers=headers,
- timeout=30
- )
-
- if parent_response.status_code != 200:
- break
-
- parent_data = parent_response.json()
- parent_name = parent_data.get('name', '')
- parent_ids = parent_data.get('parents', [])
-
- level -= 1
-
- if level == 2: # Year level
- complete_structure['year_folder'] = {
- 'id': parent_id,
- 'name': parent_name,
- 'level': level
- }
- elif level == 1: # Primary level (company/contact)
- complete_structure['primary_folder'] = {
- 'id': parent_id,
- 'name': parent_name,
- 'level': level
- }
- elif level == 0: # Root level
- complete_structure['root_folder'] = {
- 'id': parent_id,
- 'name': parent_name,
- 'level': level
- }
-
- current_id = parent_id
-
- return complete_structure
- 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"""
- params = {
- 'q': f"'{parent_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
- 'fields': 'files(id,name)',
- 'pageSize': 100
- }
-
- try:
- response = requests.get(
- 'https://www.googleapis.com/drive/v3/files',
- headers=headers,
- params=params,
- timeout=30
- )
-
- if response.status_code == 200:
- data = response.json()
- return data.get('files', [])
- else:
- return []
- except:
- 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"""
- try:
- access_token = self._get_google_drive_access_token()
- headers = {
- 'Authorization': f'Bearer {access_token}',
- 'Content-Type': 'application/json'
- }
-
- # Test access to the specific folder - try both regular Drive and Shared Drive endpoints
- folder_found = False
- folder_name = 'Unknown'
-
- # First, try the regular Drive API endpoint
- response = requests.get(
- f'https://www.googleapis.com/drive/v3/files/{folder_id}?fields=id,name,mimeType',
- headers=headers,
- timeout=10
- )
-
- if response.status_code == 200:
- folder_data = response.json()
- if folder_data.get('mimeType') == 'application/vnd.google-apps.folder':
- _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
- folder_found = True
- folder_name = folder_data.get('name', 'Unknown')
- else:
- _logger.warning(f"❌ ID {folder_id} exists but is not a folder")
- return False, "Not a folder"
- elif response.status_code == 404:
- # If not found in regular Drive, try Shared Drive endpoint
- shared_drive_response = requests.get(
- f'https://www.googleapis.com/drive/v3/drives/{folder_id}?fields=id,name',
- headers=headers,
- timeout=10
- )
-
- if shared_drive_response.status_code == 200:
- shared_drive_data = shared_drive_response.json()
- folder_name = shared_drive_data.get('name', 'Unknown')
- folder_found = True
-
- # For shared drives, we also need to check if we can access files within it
- files_in_drive_url = f'https://www.googleapis.com/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true&corpora=drive&driveId={folder_id}&pageSize=1'
- files_response = requests.get(files_in_drive_url, headers=headers, timeout=10)
-
- if files_response.status_code != 200:
- _logger.warning(f"❌ Access denied to Shared Drive {folder_id}")
- return False, "Access denied to Shared Drive"
-
- _logger.info(f"✅ Shared Drive ID {folder_id} validated successfully")
- elif shared_drive_response.status_code == 403:
- _logger.warning(f"❌ Access denied to Shared Drive {folder_id}")
- return False, "Access denied to Shared Drive"
- elif shared_drive_response.status_code == 404:
- _logger.warning(f"❌ Folder ID {folder_id} not found in Google Drive or Shared Drives")
- return False, "Not found"
- else:
- _logger.warning(f"❌ Shared Drive API error: {shared_drive_response.status_code}")
- return False, f"Shared Drive API error: {shared_drive_response.status_code}"
- elif response.status_code == 403:
- _logger.warning(f"❌ Access denied to folder ID {folder_id}")
- return False, "Access denied"
- elif response.status_code == 401:
- _logger.error(f"❌ OAuth token expired or invalid")
- return False, "Authentication error"
- else:
- _logger.warning(f"❌ Google Drive API error: {response.status_code}")
- return False, f"API error: {response.status_code}"
-
- if folder_found:
- return True, folder_name
- else:
- return False, "Not found"
-
- except requests.exceptions.Timeout:
- _logger.error(f"❌ Timeout validating folder ID {folder_id}")
- return False, "Timeout"
- except requests.exceptions.ConnectionError:
- _logger.error(f"❌ Connection error validating folder ID {folder_id}")
- return False, "Connection error"
- except Exception as e:
- _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
- return False, str(e)
|