crm_lead_backup.py 75 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import requests
  4. import json
  5. import logging
  6. import re
  7. from datetime import datetime, timedelta
  8. from odoo import fields, models, api, _
  9. from odoo.exceptions import UserError, ValidationError
  10. _logger = logging.getLogger(__name__)
  11. # Cache for Google Drive API responses (5 minutes)
  12. _GOOGLE_DRIVE_CACHE = {}
  13. _CACHE_TIMEOUT = 300 # 5 minutes
  14. def _clear_google_drive_cache():
  15. """Clear expired cache entries"""
  16. global _GOOGLE_DRIVE_CACHE
  17. current_time = datetime.now()
  18. expired_keys = [
  19. key for key, entry in _GOOGLE_DRIVE_CACHE.items()
  20. if current_time >= entry['expires']
  21. ]
  22. for key in expired_keys:
  23. del _GOOGLE_DRIVE_CACHE[key]
  24. class CrmLead(models.Model):
  25. _inherit = 'crm.lead'
  26. google_drive_documents_count = fields.Integer(
  27. string='Google Drive Documents',
  28. compute='_compute_google_drive_documents_count',
  29. help='Number of documents in Google Drive for this opportunity'
  30. )
  31. google_drive_folder_id = fields.Char(
  32. string='Google Drive Folder ID',
  33. help='ID del folder específico en Google Drive para esta oportunidad',
  34. readonly=True
  35. )
  36. google_drive_folder_name = fields.Char(
  37. string='Google Drive Folder Name',
  38. help='Nombre del folder en Google Drive para esta oportunidad',
  39. readonly=True
  40. )
  41. google_drive_url = fields.Char(
  42. string='URL Drive',
  43. help='URL de la carpeta de Google Drive de la oportunidad'
  44. )
  45. @api.depends('google_drive_folder_id')
  46. def _compute_google_drive_documents_count(self):
  47. """Compute the number of documents in Google Drive with caching"""
  48. for record in self:
  49. if record.google_drive_folder_id:
  50. # Check cache first
  51. cache_key = f'doc_count_{record.google_drive_folder_id}'
  52. if cache_key in _GOOGLE_DRIVE_CACHE:
  53. cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
  54. if datetime.now() < cache_entry['expires']:
  55. record.google_drive_documents_count = cache_entry['count']
  56. continue
  57. # TODO: Implement Google Drive API call to count documents
  58. # For now, return 0 but cache the result
  59. count = 0
  60. _GOOGLE_DRIVE_CACHE[cache_key] = {
  61. 'count': count,
  62. 'expires': datetime.now() + timedelta(seconds=300) # 5 minutes
  63. }
  64. record.google_drive_documents_count = count
  65. else:
  66. record.google_drive_documents_count = 0
  67. def _get_company_root_folder_id(self):
  68. """Get the company's root Google Drive folder ID"""
  69. if not self.company_id or not self.company_id.google_drive_crm_enabled:
  70. return None
  71. if not self.company_id.google_drive_crm_folder_id:
  72. return None
  73. return self.company_id.google_drive_crm_folder_id
  74. def _extract_folder_id_from_url(self, url):
  75. """Extract folder ID from Google Drive URL"""
  76. if not url:
  77. return None
  78. # Handle different Google Drive URL formats
  79. import re
  80. # Format: https://drive.google.com/drive/folders/FOLDER_ID
  81. folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
  82. match = re.search(folder_pattern, url)
  83. if match:
  84. return match.group(1)
  85. # Format: https://drive.google.com/open?id=FOLDER_ID
  86. open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
  87. match = re.search(open_pattern, url)
  88. if match:
  89. return match.group(1)
  90. return None
  91. def _get_google_drive_field_value(self):
  92. """Get value from the configured Google Drive field in company settings"""
  93. if not self.company_id.google_drive_crm_field_id:
  94. return None
  95. field_name = self.company_id.google_drive_crm_field_id.name
  96. if not field_name:
  97. return None
  98. # Get the field value from the record
  99. field_value = getattr(self, field_name, None)
  100. return field_value
  101. def _try_extract_folder_id_from_field(self):
  102. """Try to extract folder ID from the configured field value"""
  103. field_value = self._get_google_drive_field_value()
  104. if not field_value:
  105. return None
  106. # Try to extract folder ID from the field value (could be URL or direct ID)
  107. folder_id = self._extract_folder_id_from_url(field_value)
  108. if folder_id:
  109. return folder_id
  110. # If it's not a URL, check if it looks like a folder ID
  111. if isinstance(field_value, str) and len(field_value) >= 10 and len(field_value) <= 50:
  112. # Basic validation for Google Drive folder ID format
  113. import re
  114. if re.match(r'^[a-zA-Z0-9_-]+$', field_value):
  115. return field_value
  116. return None
  117. def _validate_folder_creation_prerequisites(self):
  118. """Validate prerequisites before creating Google Drive folder"""
  119. # Check if company has Google Drive folder configured
  120. root_folder_id = self._get_company_root_folder_id()
  121. if not root_folder_id:
  122. self.message_post(
  123. body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
  124. message_type='comment'
  125. )
  126. return False
  127. # Validate contact exists
  128. if not self.partner_id:
  129. self.message_post(
  130. body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
  131. message_type='comment'
  132. )
  133. return False
  134. return True
  135. def _try_get_existing_folder_id(self):
  136. """Try to get existing folder ID from various sources"""
  137. # First, check if we already have a folder ID
  138. if self.google_drive_folder_id:
  139. return self.google_drive_folder_id
  140. # Second, try to extract from the configured field
  141. field_folder_id = self._try_extract_folder_id_from_field()
  142. if field_folder_id:
  143. _logger.info(f"Found folder ID from configured field: {field_folder_id}")
  144. return field_folder_id
  145. # Third, try to extract from google_drive_url field
  146. if self.google_drive_url:
  147. url_folder_id = self._extract_folder_id_from_url(self.google_drive_url)
  148. if url_folder_id:
  149. _logger.info(f"Found folder ID from URL field: {url_folder_id}")
  150. return url_folder_id
  151. return None
  152. def _check_folder_company_mismatch(self):
  153. """Check if the current folder belongs to the correct company using the new service"""
  154. if not self.google_drive_folder_id or not self.company_id:
  155. return False
  156. try:
  157. # Get company root folder ID
  158. company_root_folder_id = self._get_company_root_folder_id()
  159. if not company_root_folder_id:
  160. return False
  161. # Use the new Google Drive service
  162. drive_service = self.env['google.drive.service']
  163. # Check if folder belongs to the company root
  164. belongs_to_company = drive_service.check_folder_belongs_to_parent(
  165. self.google_drive_folder_id,
  166. company_root_folder_id
  167. )
  168. # Return True if there's a mismatch (folder doesn't belong to company)
  169. return not belongs_to_company
  170. except Exception as e:
  171. _logger.error(f"Error checking folder company mismatch: {str(e)}")
  172. return False
  173. def _get_folder_name_components(self):
  174. """Get the components for folder naming with priority for partner_id"""
  175. primary_name = None
  176. if self.partner_id:
  177. _logger.info(f"Contact found: {self.partner_id.name} (ID: {self.partner_id.id})")
  178. # Prioridad 1: partner_id.company_id.name
  179. if self.partner_id.company_id and self.partner_id.company_id.name:
  180. primary_name = self.partner_id.company_id.name
  181. _logger.info(f"Using company_id.name: {primary_name}")
  182. # Prioridad 2: partner_id.company_name
  183. elif self.partner_id.company_name:
  184. primary_name = self.partner_id.company_name
  185. _logger.info(f"Using company_name: {primary_name}")
  186. # Prioridad 3: partner_id.name
  187. else:
  188. primary_name = self.partner_id.name
  189. _logger.info(f"Using partner name: {primary_name}")
  190. else:
  191. _logger.warning("No contact assigned to opportunity")
  192. primary_name = "Sin Contacto"
  193. if not primary_name:
  194. raise UserError(_('No company or contact name available. Please assign a contact with company information before creating Google Drive folders.'))
  195. # Validate and sanitize the primary name
  196. sanitized_primary_name = self._sanitize_folder_name(primary_name)
  197. sanitized_opportunity_name = self._sanitize_folder_name(self.name)
  198. year = str(self.create_date.year) if self.create_date else str(datetime.now().year)
  199. _logger.info(f"Folder components - Primary: '{sanitized_primary_name}', Opportunity: '{sanitized_opportunity_name}', Year: {year}")
  200. return {
  201. 'primary_name': sanitized_primary_name,
  202. 'opportunity_name': sanitized_opportunity_name,
  203. 'year': year
  204. }
  205. def _check_partner_name_changes(self, vals):
  206. """Check if partner or partner's company name has changed"""
  207. if 'partner_id' not in vals:
  208. return False
  209. old_partner = self.partner_id
  210. new_partner_id = vals['partner_id']
  211. if not new_partner_id:
  212. return False
  213. new_partner = self.env['res.partner'].browse(new_partner_id)
  214. # Check if partner changed
  215. if old_partner.id != new_partner.id:
  216. return True
  217. # Check if partner's company name changed
  218. old_company_name = old_partner.parent_id.name if old_partner.parent_id else old_partner.name
  219. new_company_name = new_partner.parent_id.name if new_partner.parent_id else new_partner.name
  220. return old_company_name != new_company_name
  221. def _sanitize_folder_name(self, name):
  222. """Sanitize folder name to be Google Drive compatible with optimization"""
  223. if not name:
  224. return 'Sin nombre'
  225. # Use regex for better performance
  226. sanitized_name = re.sub(r'[<>:"|?*/\\]', '_', name)
  227. # Remove leading/trailing spaces and dots
  228. sanitized_name = sanitized_name.strip(' .')
  229. # Ensure it's not empty after sanitization
  230. if not sanitized_name:
  231. return 'Sin nombre'
  232. # Limit length to 255 characters (Google Drive limit)
  233. if len(sanitized_name) > 255:
  234. sanitized_name = sanitized_name[:252] + '...'
  235. return sanitized_name
  236. def _validate_folder_id_with_google_drive(self, folder_id):
  237. """Validate if the folder ID exists and is accessible in Google Drive using the new service"""
  238. try:
  239. drive_service = self.env['google.drive.service']
  240. validation = drive_service.validate_folder_id(folder_id)
  241. if validation.get('valid'):
  242. folder_name = validation.get('name', 'Unknown')
  243. _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
  244. return True, folder_name
  245. else:
  246. error_message = validation.get('error', 'Unknown error')
  247. _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
  248. return False, error_message
  249. except Exception as e:
  250. _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
  251. return False, str(e)
  252. def _create_google_drive_folder_structure(self):
  253. """Create the complete Google Drive folder structure for this opportunity with optimization"""
  254. self.ensure_one()
  255. # Validate prerequisites
  256. if not self._validate_folder_creation_prerequisites():
  257. return None
  258. # Try to get existing folder ID first
  259. existing_folder_id = self._try_get_existing_folder_id()
  260. if existing_folder_id:
  261. _logger.info(f"Found existing folder ID: {existing_folder_id}")
  262. # Validate the folder ID against Google Drive
  263. is_valid, error_message = self._validate_folder_id_with_google_drive(existing_folder_id)
  264. if is_valid:
  265. _logger.info(f"✅ Folder ID {existing_folder_id} validated successfully")
  266. # Update the record with the existing folder ID
  267. self.with_context(skip_google_drive_update=True).write({
  268. 'google_drive_folder_id': existing_folder_id
  269. })
  270. # Get expected structure and store it
  271. expected_components = self._get_folder_name_components()
  272. expected_structure = self._build_structure_string(expected_components)
  273. self.with_context(skip_google_drive_update=True).write({
  274. 'google_drive_folder_name': expected_structure
  275. })
  276. self.message_post(
  277. body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
  278. message_type='comment'
  279. )
  280. return True
  281. else:
  282. _logger.warning(f"❌ Folder ID {existing_folder_id} validation failed: {error_message}")
  283. self.message_post(
  284. body=_("⚠️ Folder ID from configured field is not accessible: %s. Creating new folder structure.") % error_message,
  285. message_type='comment'
  286. )
  287. # Continue to create new folder structure
  288. components = self._get_folder_name_components()
  289. try:
  290. # Create folder structure in batch for better performance
  291. folder_structure = self._create_folder_structure_batch(components)
  292. # Update the opportunity with the main folder ID
  293. self.with_context(skip_google_drive_update=True).write({
  294. 'google_drive_folder_id': folder_structure['opportunity_folder_id'],
  295. 'google_drive_folder_name': self._build_structure_string(components)
  296. })
  297. return folder_structure
  298. except Exception as e:
  299. _logger.error(f"Failed to create folder structure for opportunity {self.id}: {str(e)}")
  300. raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
  301. def _create_folder_structure_batch(self, components):
  302. """Create folder structure in batch for better performance using the new service"""
  303. try:
  304. drive_service = self.env['google.drive.service']
  305. root_folder_id = self._get_company_root_folder_id()
  306. # Step 1: Create or get primary folder (Company/Contact)
  307. primary_folder_id = self._create_or_get_folder_crm(
  308. root_folder_id, components['primary_name']
  309. )
  310. # Step 2: Create or get year folder
  311. year_folder_id = self._create_or_get_folder_crm(
  312. primary_folder_id, components['year']
  313. )
  314. # Step 3: Create or get opportunity folder
  315. opportunity_folder_id = self._create_or_get_folder_crm(
  316. year_folder_id, components['opportunity_name']
  317. )
  318. # Step 4: Create Meets and Archivos cliente folders (parallel creation)
  319. meets_folder_id = self._create_or_get_folder_crm(
  320. opportunity_folder_id, 'Meets'
  321. )
  322. archivos_folder_id = self._create_or_get_folder_crm(
  323. opportunity_folder_id, 'Archivos cliente'
  324. )
  325. return {
  326. 'opportunity_folder_id': opportunity_folder_id,
  327. 'meets_folder_id': meets_folder_id,
  328. 'archivos_folder_id': archivos_folder_id
  329. }
  330. except Exception as e:
  331. _logger.error(f"Error creating folder structure batch: {str(e)}")
  332. raise
  333. def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
  334. """Create a folder or get existing one by name using the new service"""
  335. try:
  336. drive_service = self.env['google.drive.service']
  337. # First, check if folder already exists
  338. existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
  339. if existing_folders:
  340. return existing_folders[0]['id']
  341. # Create new folder
  342. result = drive_service.create_folder(folder_name, parent_folder_id)
  343. if result.get('success'):
  344. return result.get('folder_id')
  345. else:
  346. error_msg = result.get('error', 'Unknown error')
  347. raise UserError(_('Failed to create Google Drive folder "%s": %s') % (folder_name, error_msg))
  348. except Exception as e:
  349. _logger.error(f"Error creating or getting folder {folder_name}: {str(e)}")
  350. raise
  351. def _find_folder_by_name(self, headers, parent_folder_id, folder_name):
  352. """Find a folder by name in the parent folder with caching using the new service"""
  353. try:
  354. drive_service = self.env['google.drive.service']
  355. # Use the new service method
  356. folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
  357. if folders:
  358. result = folders[0]
  359. # Cache the result for 2 minutes
  360. cache_key = f'folder_{parent_folder_id}_{folder_name}'
  361. _GOOGLE_DRIVE_CACHE[cache_key] = {
  362. 'result': result,
  363. 'expires': datetime.now() + timedelta(seconds=120)
  364. }
  365. return result
  366. else:
  367. return None
  368. except Exception as e:
  369. _logger.error(f"Error finding folder '{folder_name}' in parent {parent_folder_id}: {str(e)}")
  370. return None
  371. def _rename_google_drive_folder(self, new_name):
  372. """Rename the Google Drive folder with optimization using the new service"""
  373. if not self.google_drive_folder_id:
  374. return
  375. # Sanitize the new name
  376. sanitized_name = self._sanitize_folder_name(new_name)
  377. # Check if the name is actually different
  378. if self.google_drive_folder_name == sanitized_name:
  379. return
  380. try:
  381. drive_service = self.env['google.drive.service']
  382. _logger.info(f"Renaming Google Drive folder {self.google_drive_folder_id} to '{sanitized_name}'")
  383. result = drive_service.rename_folder(self.google_drive_folder_id, sanitized_name)
  384. if result.get('success'):
  385. # Update the folder name in Odoo (with context to prevent loop)
  386. self.with_context(skip_google_drive_update=True).write({'google_drive_folder_name': sanitized_name})
  387. _logger.info(f"Successfully renamed Google Drive folder to '{sanitized_name}'")
  388. # Clear cache for this folder
  389. cache_key = f'folder_{self.google_drive_folder_id}'
  390. if cache_key in _GOOGLE_DRIVE_CACHE:
  391. del _GOOGLE_DRIVE_CACHE[cache_key]
  392. else:
  393. error_msg = f'Failed to rename Google Drive folder: {result.get("error", "Unknown error")}'
  394. _logger.error(error_msg)
  395. raise UserError(_(error_msg))
  396. except Exception as e:
  397. _logger.error(f"Error renaming folder {self.google_drive_folder_id}: {str(e)}")
  398. raise UserError(_('Failed to rename Google Drive folder: %s') % str(e))
  399. def _move_google_drive_folder(self, new_company_id):
  400. """Move the Google Drive folder to a new company's structure"""
  401. if not self.google_drive_folder_id:
  402. return
  403. # Get new company's root folder
  404. new_root_folder_id = new_company_id.google_drive_crm_folder_id
  405. if not new_root_folder_id:
  406. raise UserError(_('New company does not have Google Drive CRM folder configured'))
  407. access_token = self._get_google_drive_access_token()
  408. headers = {
  409. 'Authorization': f'Bearer {access_token}',
  410. 'Content-Type': 'application/json'
  411. }
  412. try:
  413. drive_service = self.env['google.drive.service']
  414. # Move the folder to the new company root
  415. result = drive_service.move_folder(self.google_drive_folder_id, new_root_folder_id)
  416. if not result.get('success'):
  417. raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
  418. except Exception as e:
  419. _logger.error(f"Error moving folder to new company: {str(e)}")
  420. raise UserError(_('Failed to move folder to new company: %s') % str(e))
  421. def _delete_google_drive_folder_structure(self):
  422. """Delete the Google Drive folder structure when contact is removed using the new service"""
  423. if not self.google_drive_folder_id:
  424. return
  425. try:
  426. drive_service = self.env['google.drive.service']
  427. # Delete the folder (this will also delete subfolders)
  428. result = drive_service.delete_folder(self.google_drive_folder_id)
  429. if result.get('success'):
  430. # Clear the folder ID from the record
  431. self.write({
  432. 'google_drive_folder_id': False,
  433. 'google_drive_folder_name': False
  434. })
  435. else:
  436. raise UserError(_('Failed to delete Google Drive folder structure: %s') % result.get('error', 'Unknown error'))
  437. except Exception as e:
  438. _logger.error(f"Error deleting folder structure: {str(e)}")
  439. raise UserError(_('Failed to delete Google Drive folder structure: %s') % str(e))
  440. def _recreate_google_drive_folder_structure(self):
  441. """Recreate the Google Drive folder structure when contact changes"""
  442. if not self.partner_id:
  443. raise UserError(_('No contact associated with this opportunity. Cannot recreate folder structure.'))
  444. # Store old folder information for reference
  445. old_folder_id = self.google_drive_folder_id
  446. old_folder_name = self.google_drive_folder_name
  447. # Clear the folder ID but don't delete the actual folder
  448. self.write({
  449. 'google_drive_folder_id': False,
  450. 'google_drive_folder_name': False
  451. })
  452. # Create new structure
  453. try:
  454. new_structure = self._create_google_drive_folder_structure()
  455. # Log the recreation for audit purposes
  456. if old_folder_id:
  457. _logger.info(f"Recreated Google Drive folder structure for opportunity {self.id}: "
  458. f"Old folder: {old_folder_id} ({old_folder_name}) -> "
  459. f"New folder: {self.google_drive_folder_id} ({self.google_drive_folder_name})")
  460. return new_structure
  461. except Exception as e:
  462. # Restore old values if recreation fails
  463. self.write({
  464. 'google_drive_folder_id': old_folder_id,
  465. 'google_drive_folder_name': old_folder_name
  466. })
  467. raise
  468. def _rename_entire_folder_structure(self, old_components=None, new_components=None):
  469. """Unified method to rename folder structure"""
  470. if not self.google_drive_folder_id:
  471. return
  472. try:
  473. # If only new_components provided, get current from Google Drive
  474. if old_components is None and new_components is not None:
  475. current_structure = self._analyze_complete_folder_structure()
  476. if not current_structure:
  477. raise UserError(_('Could not analyze current folder structure'))
  478. old_components = {
  479. 'primary_name': current_structure.get('primary_folder', {}).get('name', ''),
  480. 'year': current_structure.get('year_folder', {}).get('name', ''),
  481. 'opportunity_name': current_structure.get('opportunity_folder', {}).get('name', '')
  482. }
  483. # If only old_components provided, get new from current record
  484. if new_components is None and old_components is not None:
  485. new_components = self._get_folder_name_components()
  486. drive_service = self.env['google.drive.service']
  487. current_folder_id = self.google_drive_folder_id
  488. # Navigate up to find primary folder
  489. primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
  490. if not primary_folder_id:
  491. raise UserError(_('Cannot find primary folder in the structure'))
  492. # Rename primary folder if needed
  493. if old_components['primary_name'] != new_components['primary_name']:
  494. result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
  495. if not result.get('success'):
  496. raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
  497. # Rename year folder if needed
  498. if old_components['year'] != new_components['year']:
  499. year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
  500. if year_folder_id:
  501. result = drive_service.rename_folder(year_folder_id, new_components['year'])
  502. if not result.get('success'):
  503. raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
  504. # Rename opportunity folder if needed
  505. if old_components['opportunity_name'] != new_components['opportunity_name']:
  506. _logger.info(f"Renaming opportunity folder from '{old_components['opportunity_name']}' to '{new_components['opportunity_name']}'")
  507. result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
  508. if not result.get('success'):
  509. raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
  510. self.with_context(skip_google_drive_update=True).write({
  511. 'google_drive_folder_name': new_components['opportunity_name']
  512. })
  513. else:
  514. _logger.info(f"Opportunity folder name is already correct: '{new_components['opportunity_name']}'")
  515. except Exception as e:
  516. _logger.error(f"Error renaming folder structure: {str(e)}")
  517. raise
  518. def _update_google_drive_folder_structure(self, vals):
  519. """Update the Google Drive folder structure based on changes"""
  520. if not self.google_drive_folder_id:
  521. return
  522. # Check if company has Google Drive folder configured
  523. root_folder_id = self._get_company_root_folder_id()
  524. if not root_folder_id:
  525. # Company doesn't have Google Drive configured, do nothing
  526. return
  527. # Get current folder information
  528. access_token = self._get_google_drive_access_token()
  529. headers = {
  530. 'Authorization': f'Bearer {access_token}',
  531. 'Content-Type': 'application/json'
  532. }
  533. # Check if company changed - move folder to new company structure
  534. if 'company_id' in vals and vals['company_id'] != self.company_id.id:
  535. new_company = self.env['res.company'].browse(vals['company_id'])
  536. if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
  537. _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
  538. self._move_google_drive_folder(new_company)
  539. else:
  540. # If new company doesn't have Google Drive configured, keep the folder but log it
  541. self.message_post(
  542. body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
  543. message_type='comment'
  544. )
  545. return
  546. # Check if contact changed - this requires recreating the entire structure
  547. if 'partner_id' in vals:
  548. if not vals['partner_id']:
  549. # Contact was removed, but we don't delete - just log it
  550. self.message_post(
  551. body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
  552. message_type='comment'
  553. )
  554. return
  555. else:
  556. # Contact changed, recreate the entire structure
  557. _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
  558. self._recreate_google_drive_folder_structure()
  559. return
  560. # Check if name changed - rename the opportunity folder
  561. if 'name' in vals and vals['name'] != self.name:
  562. _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
  563. self._rename_google_drive_folder(vals['name'])
  564. # Validate and update entire folder structure if needed (only for non-name changes)
  565. # This will handle changes in company name, contact name, or year
  566. if 'partner_id' in vals or 'create_date' in vals:
  567. self._validate_and_update_folder_structure(vals)
  568. # Check if stage changed and we need to create folder
  569. if 'stage_id' in vals:
  570. if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
  571. if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
  572. # Check if company has Google Drive folder configured
  573. root_folder_id = self._get_company_root_folder_id()
  574. if not root_folder_id:
  575. # Company doesn't have Google Drive configured, do nothing
  576. return
  577. # Validate contact exists before attempting to create folder
  578. if not self.partner_id:
  579. self.message_post(
  580. body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity. Please assign a contact before creating Google Drive folders."),
  581. message_type='comment'
  582. )
  583. else:
  584. try:
  585. self._create_google_drive_folder_structure()
  586. except Exception as e:
  587. _logger.error(f"Failed to create Google Drive folder for opportunity {self.id}: {str(e)}")
  588. self.message_post(
  589. body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
  590. message_type='comment'
  591. )
  592. def _validate_and_update_folder_structure(self, vals):
  593. """Validate and update the entire folder structure if any component changed"""
  594. if not self.google_drive_folder_id:
  595. return
  596. # Get current and new components
  597. current_components = self._get_folder_name_components()
  598. # Create a temporary record with new values to get new components
  599. temp_vals = {}
  600. if 'name' in vals:
  601. temp_vals['name'] = vals['name']
  602. if 'partner_id' in vals:
  603. temp_vals['partner_id'] = vals['partner_id']
  604. if 'create_date' in vals:
  605. temp_vals['create_date'] = vals['create_date']
  606. # Create a temporary record to get new components
  607. temp_record = self.with_context(skip_google_drive_update=True)
  608. for field, value in temp_vals.items():
  609. setattr(temp_record, field, value)
  610. try:
  611. new_components = temp_record._get_folder_name_components()
  612. except:
  613. # If we can't get new components, skip validation
  614. return
  615. # Check if any component changed
  616. components_changed = (
  617. current_components['primary_name'] != new_components['primary_name'] or
  618. current_components['year'] != new_components['year']
  619. )
  620. # Also check if partner name changed (even if same partner, name might have changed)
  621. partner_name_changed = self._check_partner_name_changes(vals)
  622. if components_changed or partner_name_changed:
  623. _logger.info(f"Folder structure components changed. Renaming entire structure.")
  624. _logger.info(f"Old components: {current_components}")
  625. _logger.info(f"New components: {new_components}")
  626. # Store the old folder information for reference
  627. old_folder_id = self.google_drive_folder_id
  628. old_folder_name = self.google_drive_folder_name
  629. old_structure = f"{current_components['primary_name']}/{current_components['year']}/{current_components['opportunity_name']}"
  630. new_structure = f"{new_components['primary_name']}/{new_components['year']}/{new_components['opportunity_name']}"
  631. # Rename the entire folder structure instead of recreating
  632. try:
  633. self._rename_entire_folder_structure(current_components, new_components)
  634. # Log the change for audit
  635. self.message_post(
  636. body=_("🔄 Google Drive folder structure renamed due to changes:<br/>"
  637. "• Old structure: %s<br/>"
  638. "• New structure: %s<br/>"
  639. "• Folder ID: %s (same folder, renamed)") % (
  640. old_structure,
  641. new_structure,
  642. self.google_drive_folder_id
  643. ),
  644. message_type='comment'
  645. )
  646. except Exception as e:
  647. _logger.error(f"Failed to rename folder structure: {str(e)}")
  648. self.message_post(
  649. body=_("❌ Failed to rename Google Drive folder structure: %s") % str(e),
  650. message_type='comment'
  651. )
  652. raise
  653. return
  654. # Check if company has Google Drive folder configured
  655. root_folder_id = self._get_company_root_folder_id()
  656. if not root_folder_id:
  657. # Company doesn't have Google Drive configured, do nothing
  658. return
  659. # Get current folder information
  660. access_token = self._get_google_drive_access_token()
  661. headers = {
  662. 'Authorization': f'Bearer {access_token}',
  663. 'Content-Type': 'application/json'
  664. }
  665. # Check if company changed - move folder to new company structure
  666. if 'company_id' in vals and vals['company_id'] != self.company_id.id:
  667. new_company = self.env['res.company'].browse(vals['company_id'])
  668. if new_company.google_drive_crm_enabled and new_company.google_drive_crm_folder_id:
  669. _logger.info(f"Company changed from {self.company_id.name} to {new_company.name}. Moving Google Drive folder structure.")
  670. self._move_google_drive_folder(new_company)
  671. else:
  672. # If new company doesn't have Google Drive configured, keep the folder but log it
  673. self.message_post(
  674. body=_("⚠️ Company changed to one without Google Drive configuration. Existing folder structure remains unchanged."),
  675. message_type='comment'
  676. )
  677. return
  678. # Check if contact changed - this requires recreating the entire structure
  679. if 'partner_id' in vals:
  680. if not vals['partner_id']:
  681. # Contact was removed, but we don't delete - just log it
  682. self.message_post(
  683. body=_("⚠️ Contact was removed from opportunity. Google Drive folder structure remains unchanged."),
  684. message_type='comment'
  685. )
  686. return
  687. else:
  688. # Contact changed, recreate the entire structure
  689. _logger.info(f"Contact changed. Recreating entire Google Drive folder structure.")
  690. self._recreate_google_drive_folder_structure()
  691. return
  692. # Check if name changed - rename the opportunity folder
  693. if 'name' in vals and vals['name'] != self.name:
  694. _logger.info(f"Name changed from '{self.name}' to '{vals['name']}'. Renaming Google Drive folder.")
  695. self._rename_google_drive_folder(vals['name'])
  696. # Validate and update entire folder structure if needed
  697. self._validate_and_update_folder_structure(vals)
  698. # Check if stage changed and we need to create folder
  699. if 'stage_id' in vals:
  700. if self.company_id.google_drive_crm_enabled and self.company_id.google_drive_crm_stage_id:
  701. if vals['stage_id'] == self.company_id.google_drive_crm_stage_id.id and not self.google_drive_folder_id:
  702. if self.partner_id:
  703. self._create_google_drive_folder_structure()
  704. else:
  705. self.message_post(
  706. body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
  707. message_type='comment'
  708. )
  709. @api.model
  710. def create(self, vals):
  711. """Override create to handle Google Drive folder creation with optimization"""
  712. record = super().create(vals)
  713. # Check if we should create Google Drive folder (optimized conditions)
  714. if (record.company_id.google_drive_crm_enabled and
  715. record.company_id.google_drive_crm_stage_id and
  716. record.stage_id.id == record.company_id.google_drive_crm_stage_id.id):
  717. # Validate prerequisites before creating folder
  718. if record._validate_folder_creation_prerequisites():
  719. try:
  720. record._create_google_drive_folder_structure()
  721. # Store the initial structure and update URL
  722. if record._store_initial_structure_and_update_url():
  723. record.message_post(
  724. body=_("✅ Google Drive folder created automatically"),
  725. message_type='comment'
  726. )
  727. except Exception as e:
  728. # Log error but don't fail record creation
  729. _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
  730. record.message_post(
  731. body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
  732. message_type='comment'
  733. )
  734. return record
  735. def write(self, vals):
  736. """Override write method to handle Google Drive folder updates"""
  737. # Skip Google Drive updates if this is an internal update (to prevent loops)
  738. if self.env.context.get('skip_google_drive_update'):
  739. return super().write(vals)
  740. # Clear cache before processing
  741. _clear_google_drive_cache()
  742. # Check if any relevant fields are being updated
  743. relevant_fields = ['name', 'partner_id', 'create_date', 'stage_id', 'company_id']
  744. needs_update = any(field in vals for field in relevant_fields)
  745. if not needs_update:
  746. return super().write(vals)
  747. # Store current values for comparison - PROCESAR TODAS LAS OPORTUNIDADES
  748. current_values = {}
  749. for record in self:
  750. current_values[record.id] = {
  751. 'name': record.name,
  752. 'partner_id': record.partner_id.id if record.partner_id else None,
  753. 'create_date': record.create_date,
  754. 'company_id': record.company_id.id if record.company_id else None,
  755. 'google_drive_folder_name': record.google_drive_folder_name or ''
  756. }
  757. # Execute the write first
  758. result = super().write(vals)
  759. # Now process Google Drive updates with updated values - PROCESAR TODAS
  760. for record in self:
  761. record._process_google_drive_updates(vals, current_values[record.id])
  762. return result
  763. def _process_google_drive_updates(self, vals, old_values=None):
  764. """Unified method to process Google Drive updates"""
  765. try:
  766. _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
  767. # Check if we need to create folder
  768. should_create = self._should_create_folder(vals)
  769. if should_create and self._validate_folder_creation_prerequisites():
  770. _logger.info(f"Creating Google Drive folder for opportunity {self.id}")
  771. self._create_google_drive_folder_structure()
  772. if self._store_initial_structure_and_update_url():
  773. self.message_post(
  774. body=_("✅ Google Drive folder created automatically"),
  775. message_type='comment'
  776. )
  777. # If we have a folder, verify and update structure
  778. if self.google_drive_folder_id:
  779. self._verify_and_update_folder_structure(vals, old_values)
  780. except Exception as e:
  781. _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
  782. self.message_post(
  783. body=_("❌ Error updating Google Drive: %s") % str(e),
  784. message_type='comment'
  785. )
  786. def _should_create_folder(self, vals):
  787. """Helper method to determine if folder should be created"""
  788. # Stage-based creation logic
  789. if 'stage_id' in vals and not self.google_drive_folder_id:
  790. return True
  791. # Auto-creation logic
  792. if not self.google_drive_folder_id:
  793. company = self.company_id
  794. return (company.google_drive_crm_enabled and
  795. company.google_drive_crm_stage_id and
  796. self.stage_id.id == company.google_drive_crm_stage_id.id)
  797. return False
  798. def _verify_and_update_folder_structure(self, vals, old_values=None):
  799. """Unified method to verify and update folder structure"""
  800. try:
  801. _logger.info(f"Processing Google Drive updates for opportunity {self.id}")
  802. # Get expected structure components
  803. expected_components = self._get_folder_name_components()
  804. expected_structure = self._build_structure_string(expected_components)
  805. # Get current structure from old_values or stored field
  806. if old_values:
  807. current_structure = old_values.get('google_drive_folder_name', '')
  808. else:
  809. current_structure = self.google_drive_folder_name or ''
  810. _logger.info(f"Structure comparison: Current='{current_structure}' vs Expected='{expected_structure}'")
  811. # Compare structures
  812. if current_structure != expected_structure:
  813. _logger.info(f"Structure mismatch detected. Updating Google Drive...")
  814. # Determine what type of change occurred
  815. if 'company_id' in vals and self.google_drive_folder_id:
  816. new_company_id = vals['company_id']
  817. if old_values and new_company_id != old_values.get('company_id'):
  818. _logger.info(f"Company changed from {old_values.get('company_id')} to {new_company_id}. Moving folder.")
  819. self._move_folder_to_new_company(new_company_id)
  820. else:
  821. # For other changes, rename the structure
  822. self._rename_entire_folder_structure_from_components(expected_components)
  823. # Update the stored structure
  824. self.with_context(skip_google_drive_update=True).write({
  825. 'google_drive_folder_name': expected_structure
  826. })
  827. self.message_post(
  828. body=_("✅ Google Drive folder structure updated"),
  829. message_type='comment'
  830. )
  831. else:
  832. _logger.info(f"Structure is up to date. No changes needed.")
  833. except Exception as e:
  834. _logger.error(f"Error verifying folder structure: {str(e)}")
  835. raise
  836. def _build_structure_string(self, components):
  837. """Build a string representation of the folder structure"""
  838. return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
  839. def _rename_entire_folder_structure_from_components(self, expected_components):
  840. """Rename entire folder structure based on expected components"""
  841. try:
  842. # Use the unified method with only new_components
  843. self._rename_entire_folder_structure(new_components=expected_components)
  844. except Exception as e:
  845. _logger.error(f"Error renaming folder structure: {str(e)}")
  846. raise
  847. def _move_folder_to_new_company(self, new_company_id):
  848. """Mover folder a nueva empresa - SIMPLE"""
  849. if not self.google_drive_folder_id:
  850. return
  851. try:
  852. # Obtener nueva empresa
  853. new_company = self.env['res.company'].browse(new_company_id)
  854. new_root_folder_id = new_company.google_drive_crm_folder_id
  855. if not new_root_folder_id:
  856. self.message_post(
  857. body=_("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado."),
  858. message_type='comment'
  859. )
  860. return
  861. _logger.info(f"Moviendo folder de {self.company_id.name} a {new_company.name}")
  862. # Obtener access token
  863. access_token = self._get_google_drive_access_token()
  864. headers = {
  865. 'Authorization': f'Bearer {access_token}',
  866. 'Content-Type': 'application/json'
  867. }
  868. # Mover el folder primario al nuevo root
  869. self._move_folder_to_new_parent(headers, new_root_folder_id)
  870. self.message_post(
  871. body=_("✅ Folder movido a nueva empresa: %s") % new_company.name,
  872. message_type='comment'
  873. )
  874. except Exception as e:
  875. _logger.error(f"Error moviendo folder: {str(e)}")
  876. raise
  877. def _handle_structural_changes(self, vals):
  878. """Handle structural changes (partner_id, create_date) - rename entire structure"""
  879. if not self.google_drive_folder_id:
  880. return
  881. try:
  882. # Get current and new components
  883. current_components = self._get_folder_name_components()
  884. # Temporarily update the record to get new components
  885. temp_record = self.with_context(skip_google_drive_update=True)
  886. temp_vals = {}
  887. if 'partner_id' in vals:
  888. temp_vals['partner_id'] = vals['partner_id']
  889. if 'create_date' in vals:
  890. temp_vals['create_date'] = vals['create_date']
  891. if temp_vals:
  892. temp_record.write(temp_vals)
  893. new_components = temp_record._get_folder_name_components()
  894. # Check if any component changed
  895. components_changed = (
  896. current_components['primary_name'] != new_components['primary_name'] or
  897. current_components['year'] != new_components['year']
  898. )
  899. if components_changed:
  900. _logger.info(f"Structural changes detected. Renaming folder structure.")
  901. self._rename_entire_folder_structure(current_components, new_components)
  902. else:
  903. _logger.info(f"No structural changes detected. Skipping rename.")
  904. except Exception as e:
  905. _logger.error(f"Error handling structural changes: {str(e)}")
  906. raise
  907. def _handle_name_change(self, new_name):
  908. """Handle simple name change - only rename opportunity folder"""
  909. if not self.google_drive_folder_id:
  910. return
  911. try:
  912. sanitized_new_name = self._sanitize_folder_name(new_name)
  913. self._rename_google_drive_folder(sanitized_new_name)
  914. except Exception as e:
  915. _logger.error(f"Error handling name change: {str(e)}")
  916. raise
  917. def _move_folder_to_new_parent(self, headers, new_parent_id):
  918. """Move entire folder structure to new parent in Google Drive using the new service"""
  919. try:
  920. # Use the new Google Drive service
  921. drive_service = self.env['google.drive.service']
  922. # Get current folder info and navigate up to find the primary folder (company/contact)
  923. current_folder_id = self.google_drive_folder_id
  924. # Validate current folder using the service
  925. validation = drive_service.validate_folder_id(current_folder_id)
  926. if not validation.get('valid'):
  927. raise UserError(_('Failed to get current folder information'))
  928. # Navigate up the hierarchy to find the primary folder
  929. primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
  930. if not primary_folder_id:
  931. raise UserError(_('Could not find primary folder in hierarchy'))
  932. # Move the primary folder to the new parent
  933. result = drive_service.move_folder(primary_folder_id, new_parent_id)
  934. if not result.get('success'):
  935. raise UserError(_('Failed to move folder structure to new parent: %s') % result.get('error', 'Unknown error'))
  936. _logger.info(f"Successfully moved entire folder structure to {new_parent_id}")
  937. except Exception as e:
  938. _logger.error(f"Error moving folder structure: {str(e)}")
  939. raise
  940. def _find_primary_folder_id(self, headers, start_folder_id):
  941. """Find the primary folder (company/contact level) in the hierarchy using the new service"""
  942. return self._find_primary_folder_id_crm(start_folder_id)
  943. def action_open_google_drive_folder(self):
  944. """Open Google Drive folder for this opportunity"""
  945. self.ensure_one()
  946. if not self.google_drive_folder_id:
  947. raise UserError(_('No Google Drive folder configured for this opportunity'))
  948. folder_url = f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
  949. return {
  950. 'type': 'ir.actions.act_url',
  951. 'url': folder_url,
  952. 'target': 'new',
  953. }
  954. def action_create_google_drive_folder(self):
  955. """Create Google Drive folder structure for this opportunity"""
  956. self.ensure_one()
  957. if self.google_drive_folder_id:
  958. raise UserError(_('Google Drive folder already exists for this opportunity'))
  959. # Check if company has Google Drive folder configured
  960. root_folder_id = self._get_company_root_folder_id()
  961. if not root_folder_id:
  962. raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
  963. try:
  964. folder_structure = self._create_google_drive_folder_structure()
  965. # Store the initial structure and update URL
  966. self._store_initial_structure_and_update_url()
  967. return {
  968. 'type': 'ir.actions.client',
  969. 'tag': 'display_notification',
  970. 'params': {
  971. 'title': _('Success'),
  972. 'message': _('Google Drive folder structure created successfully!'),
  973. 'type': 'success',
  974. 'sticky': False,
  975. }
  976. }
  977. except Exception as e:
  978. raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
  979. def action_upload_to_google_drive(self):
  980. """Upload documents to Google Drive"""
  981. self.ensure_one()
  982. if not self.google_drive_folder_id:
  983. raise UserError(_('Please create a Google Drive folder for this opportunity first'))
  984. try:
  985. # TODO: Implement Google Drive API call to upload documents
  986. # For now, just show a message
  987. return {
  988. 'type': 'ir.actions.client',
  989. 'tag': 'display_notification',
  990. 'params': {
  991. 'title': _('Info'),
  992. 'message': _('Document upload to Google Drive will be implemented soon.'),
  993. 'type': 'info',
  994. 'sticky': False,
  995. }
  996. }
  997. except Exception as e:
  998. raise UserError(_('Failed to upload to Google Drive: %s') % str(e))
  999. def action_recreate_google_drive_structure(self):
  1000. """Manually rename the Google Drive folder structure"""
  1001. self.ensure_one()
  1002. if not self.google_drive_folder_id:
  1003. raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
  1004. # Check if company has Google Drive folder configured
  1005. root_folder_id = self._get_company_root_folder_id()
  1006. if not root_folder_id:
  1007. raise UserError(_('Google Drive CRM folder is not configured for this company. Please configure it in company settings.'))
  1008. try:
  1009. # Get expected components
  1010. expected_components = self._get_folder_name_components()
  1011. # Get current folder name from Google Drive
  1012. access_token = self._get_google_drive_access_token()
  1013. headers = {
  1014. 'Authorization': f'Bearer {access_token}',
  1015. 'Content-Type': 'application/json'
  1016. }
  1017. # Use the new Google Drive service to get folder information
  1018. drive_service = self.env['google.drive.service']
  1019. validation = drive_service.validate_folder_id(self.google_drive_folder_id)
  1020. if not validation.get('valid'):
  1021. raise UserError(_('Failed to get current folder information from Google Drive'))
  1022. current_folder_name = validation.get('name', '')
  1023. _logger.info(f"Current folder name in Google Drive: '{current_folder_name}'")
  1024. _logger.info(f"Expected folder name: '{expected_components['opportunity_name']}'")
  1025. # Create old components with current Google Drive name
  1026. old_components = expected_components.copy()
  1027. old_components['opportunity_name'] = current_folder_name
  1028. # Rename the structure
  1029. self._rename_entire_folder_structure(old_components, expected_components)
  1030. # Update the stored structure
  1031. expected_structure = self._build_structure_string(expected_components)
  1032. self.with_context(skip_google_drive_update=True).write({
  1033. 'google_drive_folder_name': expected_structure
  1034. })
  1035. return {
  1036. 'type': 'ir.actions.client',
  1037. 'tag': 'display_notification',
  1038. 'params': {
  1039. 'title': _('Success'),
  1040. 'message': _('Google Drive folder structure renamed successfully!<br/>'
  1041. 'Folder ID: %s<br/>'
  1042. 'Old name: %s<br/>'
  1043. 'New name: %s') % (self.google_drive_folder_id, current_folder_name, expected_components['opportunity_name']),
  1044. 'type': 'success',
  1045. 'sticky': False,
  1046. }
  1047. }
  1048. except Exception as e:
  1049. _logger.error(f"Failed to rename Google Drive folder structure: {str(e)}")
  1050. raise UserError(_('Failed to rename Google Drive folder structure: %s') % str(e))
  1051. def action_analyze_folder_structure(self):
  1052. """Analyze current vs expected folder structure"""
  1053. self.ensure_one()
  1054. if not self.google_drive_folder_id:
  1055. raise UserError(_('No Google Drive folder exists for this opportunity. Please create one first.'))
  1056. try:
  1057. # Get expected components
  1058. expected_components = self._get_folder_name_components()
  1059. # Get current folder information
  1060. access_token = self._get_google_drive_access_token()
  1061. headers = {
  1062. 'Authorization': f'Bearer {access_token}',
  1063. 'Content-Type': 'application/json'
  1064. }
  1065. # Analyze current structure
  1066. current_structure = self._analyze_complete_folder_structure(headers)
  1067. # Compare structures
  1068. analysis = self._compare_folder_structures(expected_components, current_structure, headers)
  1069. return {
  1070. 'type': 'ir.actions.client',
  1071. 'tag': 'display_notification',
  1072. 'params': {
  1073. 'title': _('Folder Structure Analysis'),
  1074. 'message': analysis,
  1075. 'type': 'info',
  1076. 'sticky': True,
  1077. }
  1078. }
  1079. except Exception as e:
  1080. raise UserError(_('Failed to analyze folder structure: %s') % str(e))
  1081. def _analyze_current_folder_structure(self, headers):
  1082. """Analyze the current folder structure in Google Drive using the new service"""
  1083. current_folder_id = self.google_drive_folder_id
  1084. try:
  1085. # Use the new CRM-specific method
  1086. return self._analyze_crm_folder_structure(current_folder_id)
  1087. except Exception as e:
  1088. _logger.error(f"Error analyzing current folder structure: {str(e)}")
  1089. return None
  1090. def _analyze_complete_folder_structure(self, headers):
  1091. """Analyze the complete folder structure from root to opportunity using the new CRM-specific method"""
  1092. current_folder_id = self.google_drive_folder_id
  1093. try:
  1094. # Use the new CRM-specific method
  1095. return self._analyze_crm_folder_structure(current_folder_id)
  1096. except Exception as e:
  1097. _logger.error(f"Error in _analyze_complete_folder_structure: {str(e)}")
  1098. return None
  1099. def _compare_folder_structures(self, expected_components, current_structure, headers):
  1100. """Compare expected vs current folder structure"""
  1101. if not current_structure:
  1102. return _('❌ Could not analyze current folder structure')
  1103. analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
  1104. # Expected structure
  1105. analysis += f"<strong>Expected Structure:</strong><br/>"
  1106. analysis += f"📁 [Root Folder] (MC Team)<br/>"
  1107. analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
  1108. analysis += f" └── 📁 {expected_components['year']} (Year)<br/>"
  1109. analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
  1110. analysis += f" ├── 📁 Meets<br/>"
  1111. analysis += f" └── 📁 Archivos cliente<br/><br/>"
  1112. # Current structure
  1113. analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
  1114. # Root folder
  1115. if 'root_folder' in current_structure:
  1116. root_name = current_structure['root_folder']['name']
  1117. analysis += f"📁 {root_name} (Root)<br/>"
  1118. else:
  1119. analysis += f"📁 [Unknown Root]<br/>"
  1120. # Primary folder
  1121. if 'primary_folder' in current_structure:
  1122. primary_name = current_structure['primary_folder']['name']
  1123. analysis += f"└── 📁 {primary_name}"
  1124. if primary_name != expected_components['primary_name']:
  1125. analysis += f" ❌ (Expected: {expected_components['primary_name']})"
  1126. else:
  1127. analysis += " ✅"
  1128. analysis += "<br/>"
  1129. else:
  1130. analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
  1131. # Year folder
  1132. if 'year_folder' in current_structure:
  1133. year_name = current_structure['year_folder']['name']
  1134. analysis += f" └── 📁 {year_name}"
  1135. if year_name != expected_components['year']:
  1136. analysis += f" ❌ (Expected: {expected_components['year']})"
  1137. else:
  1138. analysis += " ✅"
  1139. analysis += "<br/>"
  1140. else:
  1141. analysis += f" └── 📁 [Missing Year Folder] ❌<br/>"
  1142. # Opportunity folder
  1143. if 'opportunity_folder' in current_structure:
  1144. opp_name = current_structure['opportunity_folder']['name']
  1145. analysis += f" └── 📁 {opp_name}"
  1146. if opp_name != expected_components['opportunity_name']:
  1147. analysis += f" ❌ (Expected: {expected_components['opportunity_name']})"
  1148. else:
  1149. analysis += " ✅"
  1150. analysis += "<br/>"
  1151. else:
  1152. analysis += f" └── 📁 [Missing Opportunity Folder] ❌<br/>"
  1153. # Check subfolders
  1154. if 'opportunity_folder' in current_structure:
  1155. opp_id = current_structure['opportunity_folder']['id']
  1156. subfolders = self._get_subfolders(headers, opp_id)
  1157. if subfolders:
  1158. analysis += f" ├── 📁 Meets ✅<br/>"
  1159. analysis += f" └── 📁 Archivos cliente ✅<br/>"
  1160. else:
  1161. analysis += f" ├── 📁 Meets ❌ (Missing)<br/>"
  1162. analysis += f" └── 📁 Archivos cliente ❌ (Missing)<br/>"
  1163. # Summary
  1164. analysis += f"<br/><strong>Summary:</strong><br/>"
  1165. correct_count = 0
  1166. total_count = 0
  1167. if 'primary_folder' in current_structure:
  1168. total_count += 1
  1169. if current_structure['primary_folder']['name'] == expected_components['primary_name']:
  1170. correct_count += 1
  1171. if 'year_folder' in current_structure:
  1172. total_count += 1
  1173. if current_structure['year_folder']['name'] == expected_components['year']:
  1174. correct_count += 1
  1175. if 'opportunity_folder' in current_structure:
  1176. total_count += 1
  1177. if current_structure['opportunity_folder']['name'] == expected_components['opportunity_name']:
  1178. correct_count += 1
  1179. if total_count == 3 and correct_count == 3:
  1180. analysis += "✅ Complete structure is correct"
  1181. else:
  1182. analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct). Use 'Rename Folder Structure' button to fix."
  1183. return analysis
  1184. def _get_subfolders(self, headers, parent_id):
  1185. """Get subfolders of a parent folder using the new service"""
  1186. try:
  1187. drive_service = self.env['google.drive.service']
  1188. return drive_service.find_folders_by_name(parent_id, r'.*')
  1189. except Exception as e:
  1190. _logger.error(f"Error getting subfolders: {str(e)}")
  1191. return []
  1192. def _store_initial_structure_and_update_url(self):
  1193. """Centralized method to store initial structure and update URL"""
  1194. if self.google_drive_folder_id:
  1195. expected_components = self._get_folder_name_components()
  1196. expected_structure = self._build_structure_string(expected_components)
  1197. self.with_context(skip_google_drive_update=True).write({
  1198. 'google_drive_folder_name': expected_structure
  1199. })
  1200. # Update Google Drive URL if empty
  1201. self._update_google_drive_url()
  1202. # Copy URL to configured field if it's empty
  1203. self._copy_google_drive_url_to_configured_field()
  1204. _logger.info(f"Estructura inicial almacenada para oportunidad {self.id}")
  1205. return True
  1206. else:
  1207. _logger.error(f"ERROR: _create_google_drive_folder_structure no asignó google_drive_folder_id para oportunidad {self.id}")
  1208. return False
  1209. def _generate_google_drive_url(self):
  1210. """Generate Google Drive URL for the opportunity folder"""
  1211. if self.google_drive_folder_id:
  1212. return f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}"
  1213. return False
  1214. def _update_google_drive_url(self):
  1215. """Update the google_drive_url field if it's empty and we have a folder ID"""
  1216. if self.google_drive_folder_id and not self.google_drive_url:
  1217. url = self._generate_google_drive_url()
  1218. if url:
  1219. self.with_context(skip_google_drive_update=True).write({
  1220. 'google_drive_url': url
  1221. })
  1222. _logger.info(f"Updated Google Drive URL for opportunity {self.id}: {url}")
  1223. def _extract_folder_id_from_url(self, url):
  1224. """Extract folder ID from Google Drive URL"""
  1225. if not url:
  1226. return None
  1227. # Handle different Google Drive URL formats
  1228. import re
  1229. # Format: https://drive.google.com/drive/folders/FOLDER_ID
  1230. folder_pattern = r'drive\.google\.com/drive/folders/([a-zA-Z0-9_-]+)'
  1231. match = re.search(folder_pattern, url)
  1232. if match:
  1233. return match.group(1)
  1234. # Format: https://drive.google.com/open?id=FOLDER_ID
  1235. open_pattern = r'drive\.google\.com/open\?id=([a-zA-Z0-9_-]+)'
  1236. match = re.search(open_pattern, url)
  1237. if match:
  1238. return match.group(1)
  1239. return None
  1240. def _get_configured_field_name(self):
  1241. """Get the field name configured in company settings"""
  1242. if not self.company_id or not self.company_id.google_drive_crm_field_id:
  1243. return None
  1244. return self.company_id.google_drive_crm_field_id.name
  1245. def _get_configured_field_value(self):
  1246. """Get the value of the configured field"""
  1247. field_name = self._get_configured_field_name()
  1248. if not field_name:
  1249. return None
  1250. return getattr(self, field_name, None)
  1251. def _set_configured_field_value(self, value):
  1252. """Set the value of the configured field"""
  1253. field_name = self._get_configured_field_name()
  1254. if not field_name:
  1255. return False
  1256. self.with_context(skip_google_drive_update=True).write({field_name: value})
  1257. return True
  1258. def _copy_google_drive_url_to_configured_field(self):
  1259. """Copy google_drive_url to the configured field if it's empty"""
  1260. if not self.google_drive_url:
  1261. return False
  1262. field_name = self._get_configured_field_name()
  1263. if not field_name:
  1264. return False
  1265. current_value = getattr(self, field_name, None)
  1266. if not current_value: # Solo si está vacío
  1267. self._set_configured_field_value(self.google_drive_url)
  1268. _logger.info(f"Copied google_drive_url to {field_name} for opportunity {self.id}")
  1269. return True
  1270. return False
  1271. def _validate_folder_id_with_google_drive(self, folder_id):
  1272. """Validate if the folder ID exists and is accessible in Google Drive using the new service"""
  1273. try:
  1274. drive_service = self.env['google.drive.service']
  1275. validation = drive_service.validate_folder_id(folder_id)
  1276. if validation.get('valid'):
  1277. folder_name = validation.get('name', 'Unknown')
  1278. _logger.info(f"✅ Folder ID {folder_id} validated successfully in Google Drive")
  1279. return True, folder_name
  1280. else:
  1281. error_message = validation.get('error', 'Unknown error')
  1282. _logger.warning(f"❌ Folder ID {folder_id} validation failed: {error_message}")
  1283. return False, error_message
  1284. except Exception as e:
  1285. _logger.error(f"❌ Error validating folder ID {folder_id}: {str(e)}")
  1286. return False, str(e)
  1287. # ============================================================================
  1288. # MÉTODOS ESPECÍFICOS DE CRM QUE USAN SERVICIOS GENÉRICOS
  1289. # ============================================================================
  1290. def _find_primary_folder_id_crm(self, start_folder_id):
  1291. """Find the primary folder (company/contact level) in the hierarchy - CRM specific logic"""
  1292. try:
  1293. # Use the generic service to navigate the hierarchy
  1294. drive_service = self.env['google.drive.service']
  1295. hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
  1296. # Apply CRM-specific logic to identify the primary folder
  1297. for folder_info in hierarchy:
  1298. folder_name = folder_info.get('name', '')
  1299. level = folder_info.get('level', 0)
  1300. # Skip root level folders
  1301. if level == 0:
  1302. continue
  1303. # Check if this folder is the primary folder (not a year or opportunity folder)
  1304. # Primary folder is typically the company/contact name
  1305. if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
  1306. # This could be the primary folder, but let's check if it has a year folder as child
  1307. year_folders = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
  1308. if year_folders:
  1309. # This is the primary folder
  1310. return folder_info['id']
  1311. return None
  1312. except Exception as e:
  1313. _logger.error(f"Error finding primary folder (CRM): {str(e)}")
  1314. return None
  1315. def _find_year_folder_id_crm(self, primary_folder_id, year):
  1316. """Find the year folder within the primary folder - CRM specific"""
  1317. try:
  1318. drive_service = self.env['google.drive.service']
  1319. year_folders = drive_service.find_folders_by_name(primary_folder_id, f'^{year}$')
  1320. return year_folders[0]['id'] if year_folders else None
  1321. except Exception as e:
  1322. _logger.error(f"Error finding year folder (CRM): {str(e)}")
  1323. return None
  1324. def _analyze_crm_folder_structure(self, folder_id):
  1325. """Analyze the complete folder structure from root to opportunity - CRM specific"""
  1326. try:
  1327. drive_service = self.env['google.drive.service']
  1328. # Get current folder info
  1329. validation = drive_service.validate_folder_id(folder_id)
  1330. if not validation.get('valid'):
  1331. return None
  1332. current_name = validation.get('name', '')
  1333. # Build complete structure
  1334. complete_structure = {
  1335. 'opportunity_folder': {
  1336. 'id': folder_id,
  1337. 'name': current_name,
  1338. 'level': 3
  1339. }
  1340. }
  1341. # Navigate up the hierarchy using the generic service
  1342. hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
  1343. # Apply CRM-specific logic to categorize folders
  1344. for folder_info in hierarchy:
  1345. folder_name = folder_info.get('name', '')
  1346. level = folder_info.get('level', 0)
  1347. if level == 2: # Year level
  1348. if folder_name.isdigit() and len(folder_name) == 4:
  1349. complete_structure['year_folder'] = {
  1350. 'id': folder_info['id'],
  1351. 'name': folder_name,
  1352. 'level': level
  1353. }
  1354. elif level == 1: # Primary level (company/contact)
  1355. if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
  1356. complete_structure['primary_folder'] = {
  1357. 'id': folder_info['id'],
  1358. 'name': folder_name,
  1359. 'level': level
  1360. }
  1361. elif level == 0: # Root level
  1362. complete_structure['root_folder'] = {
  1363. 'id': folder_info['id'],
  1364. 'name': folder_name,
  1365. 'level': level
  1366. }
  1367. return complete_structure
  1368. except Exception as e:
  1369. _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}")
  1370. return None
  1371. def _check_crm_folder_company_mismatch(self, folder_id, company_root_id):
  1372. """Check if folder belongs to the correct company root - CRM specific"""
  1373. try:
  1374. drive_service = self.env['google.drive.service']
  1375. # Use the generic service to check folder parent relationship
  1376. belongs_to_company = drive_service.check_folder_belongs_to_parent(
  1377. folder_id,
  1378. company_root_id
  1379. )
  1380. # Return True if there's a mismatch (folder doesn't belong to company)
  1381. return not belongs_to_company
  1382. except Exception as e:
  1383. _logger.error(f"Error checking CRM folder company mismatch: {str(e)}")
  1384. return False