| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227 |
- # -*- 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
- 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)
|