crm_lead.py 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import logging
  4. import re
  5. from datetime import datetime, timedelta
  6. from odoo import fields, models, api, _
  7. from odoo.exceptions import UserError
  8. _logger = logging.getLogger(__name__)
  9. # Cache for Google Drive API responses (5 minutes)
  10. _GOOGLE_DRIVE_CACHE = {}
  11. def _clear_google_drive_cache():
  12. """Clear expired cache entries"""
  13. global _GOOGLE_DRIVE_CACHE
  14. current_time = datetime.now()
  15. expired_keys = [key for key, entry in _GOOGLE_DRIVE_CACHE.items() if current_time >= entry['expires']]
  16. for key in expired_keys:
  17. del _GOOGLE_DRIVE_CACHE[key]
  18. class CrmLead(models.Model):
  19. _inherit = 'crm.lead'
  20. google_drive_documents_count = fields.Integer(
  21. string='Google Drive Documents',
  22. compute='_compute_google_drive_documents_count',
  23. help='Number of documents in Google Drive for this opportunity'
  24. )
  25. google_drive_folder_id = fields.Char(
  26. string='Google Drive Folder ID',
  27. help='ID del folder específico en Google Drive para esta oportunidad',
  28. readonly=True
  29. )
  30. google_drive_folder_name = fields.Char(
  31. string='Google Drive Folder Name',
  32. help='Nombre del folder en Google Drive para esta oportunidad',
  33. readonly=True
  34. )
  35. google_drive_url = fields.Char(
  36. string='URL Drive',
  37. help='URL de la carpeta de Google Drive de la oportunidad'
  38. )
  39. @api.depends('google_drive_folder_id')
  40. def _compute_google_drive_documents_count(self):
  41. """Compute the number of documents in Google Drive with caching"""
  42. for record in self:
  43. if record.google_drive_folder_id:
  44. cache_key = f'doc_count_{record.google_drive_folder_id}'
  45. if cache_key in _GOOGLE_DRIVE_CACHE:
  46. cache_entry = _GOOGLE_DRIVE_CACHE[cache_key]
  47. if datetime.now() < cache_entry['expires']:
  48. record.google_drive_documents_count = cache_entry['count']
  49. continue
  50. # TODO: Implement Google Drive API call to count documents
  51. count = 0
  52. _GOOGLE_DRIVE_CACHE[cache_key] = {
  53. 'count': count,
  54. 'expires': datetime.now() + timedelta(seconds=300)
  55. }
  56. record.google_drive_documents_count = count
  57. else:
  58. record.google_drive_documents_count = 0
  59. # ============================================================================
  60. # MÉTODOS DE CONFIGURACIÓN Y UTILIDADES
  61. # ============================================================================
  62. def _get_company_root_folder_id(self):
  63. """Get the company's root Google Drive folder ID"""
  64. company = self.company_id
  65. return (company.google_drive_crm_folder_id
  66. if company and company.google_drive_crm_enabled else None)
  67. def _extract_folder_id_from_url(self, url):
  68. """Extract folder ID from Google Drive URL - Now uses generic service"""
  69. drive_service = self.env['google.drive.service']
  70. return drive_service.extract_folder_id_from_url(url)
  71. def _get_configured_field_value(self):
  72. """Get value from the configured Google Drive field in company settings"""
  73. field_id = self.company_id.google_drive_crm_field_id
  74. return getattr(self, field_id.name, None) if field_id else None
  75. def _set_configured_field_value(self, value):
  76. """Set the value of the configured field"""
  77. field_id = self.company_id.google_drive_crm_field_id
  78. if field_id:
  79. self.with_context(skip_google_drive_update=True).write({field_id.name: value})
  80. return True
  81. return False
  82. def _sanitize_folder_name(self, name):
  83. """Sanitize folder name to be Google Drive compatible - Now uses generic service"""
  84. drive_service = self.env['google.drive.service']
  85. return drive_service.sanitize_folder_name(name)
  86. def _build_structure_string(self, components):
  87. """Build a string representation of the folder structure"""
  88. return f"{components['primary_name']}/{components['year']}/{components['opportunity_name']}"
  89. # ============================================================================
  90. # MÉTODOS DE VALIDACIÓN Y PRERREQUISITOS
  91. # ============================================================================
  92. def _validate_folder_creation_prerequisites(self):
  93. """Validate prerequisites before creating Google Drive folder"""
  94. if not self._get_company_root_folder_id():
  95. self.message_post(
  96. body=_("⚠️ Google Drive folder creation skipped: Company doesn't have Google Drive configured."),
  97. message_type='comment'
  98. )
  99. return False
  100. if not self.partner_id:
  101. self.message_post(
  102. body=_("⚠️ Google Drive folder creation skipped: No contact associated with this opportunity."),
  103. message_type='comment'
  104. )
  105. return False
  106. return True
  107. def _try_get_existing_folder_id(self):
  108. """Try to get existing folder ID from various sources"""
  109. # Check if we already have a folder ID
  110. if self.google_drive_folder_id:
  111. return self.google_drive_folder_id
  112. # Try to extract from configured field
  113. field_value = self._get_configured_field_value()
  114. if field_value:
  115. folder_id = self._extract_folder_id_from_url(field_value)
  116. if folder_id:
  117. _logger.info(f"Found folder ID from configured field: {folder_id}")
  118. return folder_id
  119. # Try to extract from google_drive_url field
  120. if self.google_drive_url:
  121. folder_id = self._extract_folder_id_from_url(self.google_drive_url)
  122. if folder_id:
  123. _logger.info(f"Found folder ID from URL field: {folder_id}")
  124. return folder_id
  125. return None
  126. def _validate_folder_id_with_google_drive(self, folder_id):
  127. """Validate if the folder ID exists and is accessible in Google Drive - Now uses generic service"""
  128. drive_service = self.env['google.drive.service']
  129. return drive_service.validate_folder_id_with_google_drive(folder_id)
  130. # ============================================================================
  131. # MÉTODOS DE COMPONENTES DE CARPETA
  132. # ============================================================================
  133. def _get_folder_name_components(self):
  134. """Get the components for folder naming based on partner/contact information"""
  135. # Priority 1: partner_id.company_id.name (empresa del cliente)
  136. if self.partner_id and self.partner_id.company_id and self.partner_id.company_id.name:
  137. primary_name = self.partner_id.company_id.name
  138. # Priority 2: partner_id.company_name
  139. elif self.partner_id and self.partner_id.company_name:
  140. primary_name = self.partner_id.company_name
  141. # Priority 3: partner_id.name
  142. elif self.partner_id:
  143. primary_name = self.partner_id.name
  144. else:
  145. primary_name = "Sin Contacto"
  146. if not primary_name:
  147. raise UserError(_('No company or contact name available. Please assign a contact with company information.'))
  148. return {
  149. 'primary_name': self._sanitize_folder_name(primary_name),
  150. 'opportunity_name': self._sanitize_folder_name(self.name),
  151. 'year': str(self.create_date.year) if self.create_date else str(datetime.now().year)
  152. }
  153. def _check_partner_name_changes(self, vals):
  154. """Check if partner or partner's company name has changed"""
  155. if 'partner_id' not in vals:
  156. return False
  157. old_partner = self.partner_id
  158. new_partner_id = vals['partner_id']
  159. if not new_partner_id or old_partner.id == new_partner_id:
  160. return False
  161. new_partner = self.env['res.partner'].browse(new_partner_id)
  162. # Check if partner's company name changed
  163. old_company_name = old_partner.parent_id.name if old_partner.parent_id else old_partner.name
  164. new_company_name = new_partner.parent_id.name if new_partner.parent_id else new_partner.name
  165. return old_company_name != new_company_name
  166. # ============================================================================
  167. # MÉTODOS DE CREACIÓN DE CARPETAS
  168. # ============================================================================
  169. def _create_google_drive_folder_structure(self):
  170. """Create the complete Google Drive folder structure for this opportunity"""
  171. self.ensure_one()
  172. if not self._validate_folder_creation_prerequisites():
  173. return None
  174. # Try to get existing folder ID first
  175. existing_folder_id = self._try_get_existing_folder_id()
  176. if existing_folder_id:
  177. is_valid, _ = self._validate_folder_id_with_google_drive(existing_folder_id)
  178. if is_valid:
  179. self._store_folder_info(existing_folder_id)
  180. self.message_post(
  181. body=_("✅ Using existing Google Drive folder: %s") % existing_folder_id,
  182. message_type='comment'
  183. )
  184. return True
  185. # Check if structure already exists before creating
  186. existing_structure = self._find_existing_folder_structure()
  187. if existing_structure:
  188. self._store_folder_info(existing_structure['opportunity_folder_id'])
  189. self.message_post(
  190. body=_("✅ Using existing folder structure: %s") % existing_structure['opportunity_folder_id'],
  191. message_type='comment'
  192. )
  193. return existing_structure
  194. # Create new folder structure
  195. components = self._get_folder_name_components()
  196. try:
  197. folder_structure = self._create_folder_structure_batch(components)
  198. self._store_folder_info(folder_structure['opportunity_folder_id'])
  199. return folder_structure
  200. except Exception as e:
  201. _logger.error(f"Failed to create folder structure for opportunity {self.id}: {str(e)}")
  202. raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
  203. def _create_folder_structure_batch(self, components):
  204. """Create folder structure in batch, reusing existing levels to avoid duplicates"""
  205. drive_service = self.env['google.drive.service']
  206. root_folder_id = self._get_company_root_folder_id()
  207. # Level 1: Primary folder (empresa cliente) - reutilizar si existe
  208. primary_folder_id, was_reused = self._create_or_get_folder_crm(root_folder_id, components['primary_name'])
  209. _logger.info(f"✅ Primary folder '{components['primary_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {primary_folder_id})")
  210. # Level 2: Year folder - reutilizar si existe
  211. year_folder_id, was_reused = self._create_or_get_folder_crm(primary_folder_id, components['year'])
  212. _logger.info(f"✅ Year folder '{components['year']}': {'REUSED' if was_reused else 'CREATED'} (ID: {year_folder_id})")
  213. # Level 3: Opportunity folder - siempre crear (único por oportunidad)
  214. opportunity_folder_id, was_reused = self._create_or_get_folder_crm(year_folder_id, components['opportunity_name'])
  215. _logger.info(f"✅ Opportunity folder '{components['opportunity_name']}': {'REUSED' if was_reused else 'CREATED'} (ID: {opportunity_folder_id})")
  216. # Subfolders - reutilizar si existen
  217. meets_folder_id, _ = self._create_or_get_folder_crm(opportunity_folder_id, 'Meets')
  218. archivos_folder_id, _ = self._create_or_get_folder_crm(opportunity_folder_id, 'Archivos cliente')
  219. return {
  220. 'opportunity_folder_id': opportunity_folder_id,
  221. 'meets_folder_id': meets_folder_id,
  222. 'archivos_folder_id': archivos_folder_id
  223. }
  224. def _create_or_get_folder_crm(self, parent_folder_id, folder_name):
  225. """Create a folder or get existing one by name, avoiding duplicates - Now uses generic service"""
  226. drive_service = self.env['google.drive.service']
  227. _logger.info(f"🔍 Checking for existing folder '{folder_name}' in parent {parent_folder_id}")
  228. folder_id = drive_service.create_or_get_folder(parent_folder_id, folder_name)
  229. # Check if it was reused or created
  230. existing_folders = drive_service.find_folders_by_name(parent_folder_id, f'^{folder_name}$')
  231. was_reused = len(existing_folders) > 0
  232. if was_reused:
  233. _logger.info(f"🔄 REUSING existing folder '{folder_name}' (ID: {folder_id})")
  234. else:
  235. _logger.info(f"📁 CREATED new folder '{folder_name}' (ID: {folder_id})")
  236. return folder_id, was_reused
  237. def _find_existing_folder_structure(self):
  238. """Find existing folder structure to avoid creating duplicates"""
  239. components = self._get_folder_name_components()
  240. drive_service = self.env['google.drive.service']
  241. root_folder_id = self._get_company_root_folder_id()
  242. try:
  243. # Level 1: Find primary folder (empresa cliente)
  244. primary_folders = drive_service.find_folders_by_name(root_folder_id, f"^{components['primary_name']}$")
  245. if not primary_folders:
  246. return None
  247. primary_folder_id = primary_folders[0]['id']
  248. # Level 2: Find year folder
  249. year_folders = drive_service.find_folders_by_name(primary_folder_id, f"^{components['year']}$")
  250. if not year_folders:
  251. return None
  252. year_folder_id = year_folders[0]['id']
  253. # Level 3: Find opportunity folder
  254. opportunity_folders = drive_service.find_folders_by_name(year_folder_id, f"^{components['opportunity_name']}$")
  255. if not opportunity_folders:
  256. return None
  257. opportunity_folder_id = opportunity_folders[0]['id']
  258. # Check if subfolders exist
  259. meets_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Meets$')
  260. archivos_folders = drive_service.find_folders_by_name(opportunity_folder_id, r'^Archivos cliente$')
  261. _logger.info(f"🔄 Found existing complete folder structure for opportunity '{components['opportunity_name']}'")
  262. return {
  263. 'opportunity_folder_id': opportunity_folder_id,
  264. 'meets_folder_id': meets_folders[0]['id'] if meets_folders else None,
  265. 'archivos_folder_id': archivos_folders[0]['id'] if archivos_folders else None
  266. }
  267. except Exception as e:
  268. _logger.warning(f"Error finding existing folder structure: {str(e)}")
  269. return None
  270. def _store_folder_info(self, folder_id):
  271. """Store folder information and update URL"""
  272. expected_components = self._get_folder_name_components()
  273. expected_structure = self._build_structure_string(expected_components)
  274. self.with_context(skip_google_drive_update=True).write({
  275. 'google_drive_folder_id': folder_id,
  276. 'google_drive_folder_name': expected_structure,
  277. 'google_drive_url': f"https://drive.google.com/drive/folders/{folder_id}"
  278. })
  279. # Copy URL to configured field if empty
  280. if not self._get_configured_field_value():
  281. self._set_configured_field_value(self.google_drive_url)
  282. # ============================================================================
  283. # MÉTODOS DE ACTUALIZACIÓN Y RENOMBRADO
  284. # ============================================================================
  285. def _process_google_drive_updates(self, vals, old_values=None):
  286. """Unified method to process Google Drive updates"""
  287. try:
  288. # Check if we need to create folder
  289. if self._should_create_folder(vals) and self._validate_folder_creation_prerequisites():
  290. self._create_google_drive_folder_structure()
  291. self.message_post(
  292. body=_("✅ Google Drive folder created automatically"),
  293. message_type='comment'
  294. )
  295. # If we have a folder, verify and update structure
  296. if self.google_drive_folder_id:
  297. self._verify_and_update_folder_structure(vals, old_values)
  298. except Exception as e:
  299. _logger.error(f"Error processing Google Drive updates for record {self.id}: {str(e)}")
  300. self.message_post(
  301. body=_("❌ Error updating Google Drive: %s") % str(e),
  302. message_type='comment'
  303. )
  304. def _should_create_folder(self, vals):
  305. """Helper method to determine if folder should be created"""
  306. if 'stage_id' in vals and not self.google_drive_folder_id:
  307. return True
  308. if not self.google_drive_folder_id:
  309. company = self.company_id
  310. return (company.google_drive_crm_enabled and
  311. company.google_drive_crm_stage_id and
  312. self.stage_id.id == company.google_drive_crm_stage_id.id)
  313. return False
  314. def _verify_and_update_folder_structure(self, vals, old_values=None):
  315. """Unified method to verify and update folder structure"""
  316. # Handle company change (move folder to new company, don't rename)
  317. if 'company_id' in vals and self.google_drive_folder_id:
  318. new_company_id = vals['company_id']
  319. if old_values and new_company_id != old_values.get('company_id'):
  320. self._move_folder_to_new_company(new_company_id)
  321. # Don't update folder name - just move it
  322. return # Exit early after moving folder
  323. # Handle partner changes (this should trigger folder rename)
  324. if 'partner_id' in vals and self.google_drive_folder_id:
  325. old_partner_id = old_values.get('partner_id') if old_values else self.partner_id.id
  326. new_partner_id = vals['partner_id']
  327. if old_partner_id != new_partner_id:
  328. # Partner changed - rename folder structure
  329. self._rename_entire_folder_structure()
  330. # Update stored structure
  331. expected_components = self._get_folder_name_components()
  332. expected_structure = self._build_structure_string(expected_components)
  333. self.with_context(skip_google_drive_update=True).write({
  334. 'google_drive_folder_name': expected_structure
  335. })
  336. self.message_post(
  337. body=_("✅ Google Drive folder renamed due to partner change"),
  338. message_type='comment'
  339. )
  340. return
  341. # Handle other structure changes (name changes, etc.)
  342. expected_components = self._get_folder_name_components()
  343. expected_structure = self._build_structure_string(expected_components)
  344. current_structure = (old_values.get('google_drive_folder_name', '') if old_values
  345. else self.google_drive_folder_name or '')
  346. if current_structure != expected_structure:
  347. # Rename structure
  348. self._rename_entire_folder_structure(new_components=expected_components)
  349. # Update stored structure
  350. self.with_context(skip_google_drive_update=True).write({
  351. 'google_drive_folder_name': expected_structure
  352. })
  353. self.message_post(
  354. body=_("✅ Google Drive folder structure updated"),
  355. message_type='comment'
  356. )
  357. def _rename_entire_folder_structure(self, old_components=None, new_components=None):
  358. """Unified method to rename folder structure"""
  359. if not self.google_drive_folder_id:
  360. return
  361. # Get current structure if needed
  362. if old_components is None and new_components is not None:
  363. current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id)
  364. if not current_structure:
  365. raise UserError(_('Could not analyze current folder structure'))
  366. old_components = {
  367. 'primary_name': current_structure.get('primary_folder', {}).get('name', ''),
  368. 'year': current_structure.get('year_folder', {}).get('name', ''),
  369. 'opportunity_name': current_structure.get('opportunity_folder', {}).get('name', '')
  370. }
  371. # Get new components if needed
  372. if new_components is None and old_components is not None:
  373. new_components = self._get_folder_name_components()
  374. drive_service = self.env['google.drive.service']
  375. current_folder_id = self.google_drive_folder_id
  376. # Navigate up to find primary folder
  377. primary_folder_id = self._find_primary_folder_id_crm(current_folder_id)
  378. if not primary_folder_id:
  379. raise UserError(_('Cannot find primary folder in the structure'))
  380. # Rename folders if needed
  381. if old_components['primary_name'] != new_components['primary_name']:
  382. result = drive_service.rename_folder(primary_folder_id, new_components['primary_name'])
  383. if not result.get('success'):
  384. raise UserError(_('Failed to rename primary folder: %s') % result.get('error', 'Unknown error'))
  385. if old_components['year'] != new_components['year']:
  386. year_folder_id = self._find_year_folder_id_crm(primary_folder_id, old_components['year'])
  387. if year_folder_id:
  388. result = drive_service.rename_folder(year_folder_id, new_components['year'])
  389. if not result.get('success'):
  390. raise UserError(_('Failed to rename year folder: %s') % result.get('error', 'Unknown error'))
  391. if old_components['opportunity_name'] != new_components['opportunity_name']:
  392. result = drive_service.rename_folder(current_folder_id, new_components['opportunity_name'])
  393. if not result.get('success'):
  394. raise UserError(_('Failed to rename opportunity folder: %s') % result.get('error', 'Unknown error'))
  395. self.with_context(skip_google_drive_update=True).write({
  396. 'google_drive_folder_name': new_components['opportunity_name']
  397. })
  398. def _move_folder_to_new_company(self, new_company_id):
  399. """Move only this opportunity's folder to new company"""
  400. if not self.google_drive_folder_id:
  401. return
  402. new_company = self.env['res.company'].browse(new_company_id)
  403. new_root_folder_id = new_company.google_drive_crm_folder_id
  404. if not new_root_folder_id:
  405. self.message_post(
  406. body=_("⚠️ No se puede mover: Nueva empresa no tiene Google Drive configurado."),
  407. message_type='comment'
  408. )
  409. return
  410. try:
  411. drive_service = self.env['google.drive.service']
  412. # Get current opportunity folder components
  413. current_components = self._get_folder_name_components()
  414. # Create new structure in the new company
  415. new_primary_folder_id, _ = self._create_or_get_folder_crm(new_root_folder_id, current_components['primary_name'])
  416. new_year_folder_id, _ = self._create_or_get_folder_crm(new_primary_folder_id, current_components['year'])
  417. # Move only the opportunity folder (not the entire primary folder)
  418. result = drive_service.move_folder(self.google_drive_folder_id, new_year_folder_id)
  419. if not result.get('success'):
  420. raise UserError(_('Failed to move folder to new company: %s') % result.get('error', 'Unknown error'))
  421. # Update the stored folder ID to the new location
  422. self.with_context(skip_google_drive_update=True).write({
  423. 'google_drive_folder_id': self.google_drive_folder_id, # Same ID, new location
  424. 'google_drive_folder_name': self._build_structure_string(current_components)
  425. })
  426. self.message_post(
  427. body=_("✅ Oportunidad movida a nueva empresa: %s") % new_company.name,
  428. message_type='comment'
  429. )
  430. except Exception as e:
  431. _logger.error(f"Error moviendo folder: {str(e)}")
  432. raise
  433. # ============================================================================
  434. # MÉTODOS ESPECÍFICOS DE CRM
  435. # ============================================================================
  436. def _find_primary_folder_id_crm(self, start_folder_id):
  437. """Find the primary folder (company/contact level) in the hierarchy"""
  438. try:
  439. drive_service = self.env['google.drive.service']
  440. hierarchy = drive_service.navigate_folder_hierarchy(start_folder_id, max_levels=5)
  441. for folder_info in hierarchy:
  442. folder_name = folder_info.get('name', '')
  443. level = folder_info.get('level', 0)
  444. if level == 0:
  445. continue
  446. if not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
  447. year_folders = drive_service.find_folders_by_name(folder_info['id'], r'^\d{4}$')
  448. if year_folders:
  449. return folder_info['id']
  450. return None
  451. except Exception as e:
  452. _logger.error(f"Error finding primary folder (CRM): {str(e)}")
  453. return None
  454. def _find_year_folder_id_crm(self, primary_folder_id, year):
  455. """Find the year folder within the primary folder"""
  456. try:
  457. drive_service = self.env['google.drive.service']
  458. year_folders = drive_service.find_folders_by_name(primary_folder_id, f'^{year}$')
  459. return year_folders[0]['id'] if year_folders else None
  460. except Exception as e:
  461. _logger.error(f"Error finding year folder (CRM): {str(e)}")
  462. return None
  463. def _analyze_crm_folder_structure(self, folder_id):
  464. """Analyze the complete folder structure from root to opportunity"""
  465. try:
  466. drive_service = self.env['google.drive.service']
  467. validation = drive_service.validate_folder_id(folder_id)
  468. if not validation.get('valid'):
  469. return None
  470. current_name = validation.get('name', '')
  471. complete_structure = {
  472. 'opportunity_folder': {
  473. 'id': folder_id,
  474. 'name': current_name,
  475. 'level': 3
  476. }
  477. }
  478. hierarchy = drive_service.navigate_folder_hierarchy(folder_id, max_levels=5)
  479. for folder_info in hierarchy:
  480. folder_name = folder_info.get('name', '')
  481. level = folder_info.get('level', 0)
  482. if level == 2 and folder_name.isdigit() and len(folder_name) == 4:
  483. complete_structure['year_folder'] = {
  484. 'id': folder_info['id'],
  485. 'name': folder_name,
  486. 'level': level
  487. }
  488. elif level == 1 and not folder_name.isdigit() and folder_name not in ['Meets', 'Archivos cliente']:
  489. complete_structure['primary_folder'] = {
  490. 'id': folder_info['id'],
  491. 'name': folder_name,
  492. 'level': level
  493. }
  494. elif level == 0:
  495. complete_structure['root_folder'] = {
  496. 'id': folder_info['id'],
  497. 'name': folder_name,
  498. 'level': level
  499. }
  500. return complete_structure
  501. except Exception as e:
  502. _logger.error(f"Error in _analyze_crm_folder_structure: {str(e)}")
  503. return None
  504. # ============================================================================
  505. # MÉTODOS DE ACCIÓN
  506. # ============================================================================
  507. @api.model
  508. def create(self, vals):
  509. """Override create to handle Google Drive folder creation"""
  510. record = super().create(vals)
  511. if (record.company_id.google_drive_crm_enabled and
  512. record.company_id.google_drive_crm_stage_id and
  513. record.stage_id.id == record.company_id.google_drive_crm_stage_id.id):
  514. if record._validate_folder_creation_prerequisites():
  515. try:
  516. record._create_google_drive_folder_structure()
  517. record.message_post(
  518. body=_("✅ Google Drive folder created automatically"),
  519. message_type='comment'
  520. )
  521. except Exception as e:
  522. _logger.error(f"Failed to create Google Drive folder for opportunity {record.id}: {str(e)}")
  523. record.message_post(
  524. body=_("⚠️ Google Drive folder creation failed: %s") % str(e),
  525. message_type='comment'
  526. )
  527. return record
  528. def write(self, vals):
  529. """Override write method to handle Google Drive folder updates"""
  530. if self.env.context.get('skip_google_drive_update'):
  531. return super().write(vals)
  532. _clear_google_drive_cache()
  533. relevant_fields = ['name', 'partner_id', 'create_date', 'stage_id', 'company_id']
  534. needs_update = any(field in vals for field in relevant_fields)
  535. if not needs_update:
  536. return super().write(vals)
  537. # Store current values for comparison
  538. current_values = {}
  539. for record in self:
  540. current_values[record.id] = {
  541. 'name': record.name,
  542. 'partner_id': record.partner_id.id if record.partner_id else None,
  543. 'create_date': record.create_date,
  544. 'company_id': record.company_id.id if record.company_id else None,
  545. 'google_drive_folder_name': record.google_drive_folder_name or ''
  546. }
  547. result = super().write(vals)
  548. # Process Google Drive updates
  549. for record in self:
  550. record._process_google_drive_updates(vals, current_values[record.id])
  551. return result
  552. def action_open_google_drive_folder(self):
  553. """Open Google Drive folder for this opportunity"""
  554. self.ensure_one()
  555. if not self.google_drive_folder_id:
  556. raise UserError(_('No Google Drive folder configured for this opportunity'))
  557. return {
  558. 'type': 'ir.actions.act_url',
  559. 'url': f"https://drive.google.com/drive/folders/{self.google_drive_folder_id}",
  560. 'target': 'new',
  561. }
  562. def action_create_google_drive_folder(self):
  563. """Create Google Drive folder structure for this opportunity"""
  564. self.ensure_one()
  565. if self.google_drive_folder_id:
  566. raise UserError(_('Google Drive folder already exists for this opportunity'))
  567. if not self._get_company_root_folder_id():
  568. raise UserError(_('Google Drive CRM folder is not configured for this company.'))
  569. try:
  570. self._create_google_drive_folder_structure()
  571. return {
  572. 'type': 'ir.actions.client',
  573. 'tag': 'display_notification',
  574. 'params': {
  575. 'title': _('Success'),
  576. 'message': _('Google Drive folder structure created successfully!'),
  577. 'type': 'success',
  578. 'sticky': False,
  579. }
  580. }
  581. except Exception as e:
  582. raise UserError(_('Failed to create Google Drive folder structure: %s') % str(e))
  583. def action_recreate_google_drive_structure(self):
  584. """Manually rename the Google Drive folder structure"""
  585. self.ensure_one()
  586. if not self.google_drive_folder_id:
  587. raise UserError(_('No Google Drive folder exists for this opportunity.'))
  588. if not self._get_company_root_folder_id():
  589. raise UserError(_('Google Drive CRM folder is not configured for this company.'))
  590. try:
  591. expected_components = self._get_folder_name_components()
  592. self._rename_entire_folder_structure(new_components=expected_components)
  593. expected_structure = self._build_structure_string(expected_components)
  594. self.with_context(skip_google_drive_update=True).write({
  595. 'google_drive_folder_name': expected_structure
  596. })
  597. return {
  598. 'type': 'ir.actions.client',
  599. 'tag': 'display_notification',
  600. 'params': {
  601. 'title': _('Success'),
  602. 'message': _('Google Drive folder structure renamed successfully!'),
  603. 'type': 'success',
  604. 'sticky': False,
  605. }
  606. }
  607. except Exception as e:
  608. _logger.error(f"Failed to rename Google Drive folder structure: {str(e)}")
  609. raise UserError(_('Failed to rename Google Drive folder structure: %s') % str(e))
  610. def action_analyze_folder_structure(self):
  611. """Analyze current vs expected folder structure"""
  612. self.ensure_one()
  613. if not self.google_drive_folder_id:
  614. raise UserError(_('No Google Drive folder exists for this opportunity.'))
  615. try:
  616. expected_components = self._get_folder_name_components()
  617. current_structure = self._analyze_crm_folder_structure(self.google_drive_folder_id)
  618. if not current_structure:
  619. raise UserError(_('Could not analyze current folder structure'))
  620. analysis = self._compare_folder_structures(expected_components, current_structure)
  621. return {
  622. 'type': 'ir.actions.client',
  623. 'tag': 'display_notification',
  624. 'params': {
  625. 'title': _('Folder Structure Analysis'),
  626. 'message': analysis,
  627. 'type': 'info',
  628. 'sticky': True,
  629. }
  630. }
  631. except Exception as e:
  632. raise UserError(_('Failed to analyze folder structure: %s') % str(e))
  633. def _compare_folder_structures(self, expected_components, current_structure):
  634. """Compare expected vs current folder structure"""
  635. analysis = f"<strong>📁 Complete Folder Structure Analysis</strong><br/><br/>"
  636. # Expected structure
  637. analysis += f"<strong>Expected Structure:</strong><br/>"
  638. analysis += f"📁 [Root Folder] (MC Team)<br/>"
  639. analysis += f"└── 📁 {expected_components['primary_name']} (Company/Contact)<br/>"
  640. analysis += f" └── 📁 {expected_components['year']} (Year)<br/>"
  641. analysis += f" └── 📁 {expected_components['opportunity_name']} (Opportunity)<br/>"
  642. analysis += f" ├── 📁 Meets<br/>"
  643. analysis += f" └── 📁 Archivos cliente<br/><br/>"
  644. # Current structure
  645. analysis += f"<strong>Current Structure in Google Drive:</strong><br/>"
  646. if 'root_folder' in current_structure:
  647. analysis += f"📁 {current_structure['root_folder']['name']} (Root)<br/>"
  648. else:
  649. analysis += f"📁 [Unknown Root]<br/>"
  650. if 'primary_folder' in current_structure:
  651. primary_name = current_structure['primary_folder']['name']
  652. analysis += f"└── 📁 {primary_name}"
  653. analysis += " ✅" if primary_name == expected_components['primary_name'] else f" ❌ (Expected: {expected_components['primary_name']})"
  654. analysis += "<br/>"
  655. else:
  656. analysis += f"└── 📁 [Missing Primary Folder] ❌<br/>"
  657. if 'year_folder' in current_structure:
  658. year_name = current_structure['year_folder']['name']
  659. analysis += f" └── 📁 {year_name}"
  660. analysis += " ✅" if year_name == expected_components['year'] else f" ❌ (Expected: {expected_components['year']})"
  661. analysis += "<br/>"
  662. else:
  663. analysis += f" └── 📁 [Missing Year Folder] ❌<br/>"
  664. if 'opportunity_folder' in current_structure:
  665. opp_name = current_structure['opportunity_folder']['name']
  666. analysis += f" └── 📁 {opp_name}"
  667. analysis += " ✅" if opp_name == expected_components['opportunity_name'] else f" ❌ (Expected: {expected_components['opportunity_name']})"
  668. analysis += "<br/>"
  669. else:
  670. analysis += f" └── 📁 [Missing Opportunity Folder] ❌<br/>"
  671. # Summary
  672. correct_count = 0
  673. total_count = 0
  674. for folder_type in ['primary_folder', 'year_folder', 'opportunity_folder']:
  675. if folder_type in current_structure:
  676. total_count += 1
  677. current_name = current_structure[folder_type]['name']
  678. expected_name = expected_components[folder_type.replace('_folder', '_name')]
  679. if current_name == expected_name:
  680. correct_count += 1
  681. analysis += f"<br/><strong>Summary:</strong><br/>"
  682. if total_count == 3 and correct_count == 3:
  683. analysis += "✅ Complete structure is correct"
  684. else:
  685. analysis += f"❌ Structure has issues ({correct_count}/{total_count} correct). Use 'Rename Folder Structure' button to fix."
  686. return analysis
  687. # ============================================================================
  688. # MÉTODOS DE SINCRONIZACIÓN DE MEETS
  689. # ============================================================================
  690. @api.model
  691. def _sync_meetings_with_opportunities(self, time_filter=15):
  692. """Sync Google Meet recordings with CRM opportunities
  693. Args:
  694. time_filter: Can be:
  695. - int: Number of days back (e.g., 15 for last 15 days)
  696. - str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day)
  697. """
  698. # Parse time_filter parameter
  699. if isinstance(time_filter, str):
  700. # Try to parse as date
  701. try:
  702. from datetime import datetime
  703. target_date = datetime.strptime(time_filter, '%Y-%m-%d').date()
  704. _logger.info(f"Starting CRM meetings synchronization for specific date: {time_filter}")
  705. days_back = None
  706. specific_date = target_date
  707. except ValueError:
  708. # If not a valid date, try to convert to int
  709. try:
  710. days_back = int(time_filter)
  711. specific_date = None
  712. _logger.info(f"Starting CRM meetings synchronization for last {days_back} days...")
  713. except ValueError:
  714. raise UserError(_('Invalid time_filter parameter. Use integer (days back) or date string (YYYY-MM-DD)'))
  715. else:
  716. # Assume it's an integer
  717. days_back = int(time_filter)
  718. specific_date = None
  719. _logger.info(f"Starting CRM meetings synchronization for last {days_back} days...")
  720. try:
  721. # Get Google Drive service
  722. drive_service = self.env['google.drive.service']
  723. # Get meetings based on filter type
  724. if specific_date:
  725. meetings = self._get_meetings_with_recordings_for_date(specific_date)
  726. else:
  727. meetings = self._get_meetings_with_recordings(days_back=days_back)
  728. _logger.info(f"Found {len(meetings)} meetings with recordings")
  729. # Limit to first 10 meetings for performance
  730. meetings = meetings[:10]
  731. _logger.info(f"Processing first {len(meetings)} meetings for performance")
  732. sync_results = {
  733. 'total_meetings': len(meetings),
  734. 'opportunities_found': 0,
  735. 'folders_created': 0,
  736. 'files_moved': 0,
  737. 'errors': []
  738. }
  739. for meeting in meetings:
  740. try:
  741. result = self._process_meeting_for_opportunities(meeting, drive_service)
  742. sync_results['opportunities_found'] += result.get('opportunities_found', 0)
  743. sync_results['folders_created'] += result.get('folders_created', 0)
  744. sync_results['files_moved'] += result.get('files_moved', 0)
  745. except Exception as e:
  746. error_msg = f"Error processing meeting {meeting.get('id', 'unknown')}: {str(e)}"
  747. _logger.error(error_msg)
  748. sync_results['errors'].append(error_msg)
  749. # Generate summary message
  750. summary = self._generate_sync_summary(sync_results)
  751. _logger.info(f"CRM sync completed: {summary}")
  752. return summary
  753. except Exception as e:
  754. _logger.error(f"Failed to sync meetings with opportunities: {str(e)}")
  755. raise UserError(_('Failed to sync meetings: %s') % str(e))
  756. @api.model
  757. def _sync_meetings_with_opportunities_cron(self, time_filter=15):
  758. """Cron job method for automatic CRM calendar sync - handles errors gracefully
  759. Args:
  760. time_filter: Can be:
  761. - int: Number of days back (e.g., 15 for last 15 days)
  762. - str: Specific date in YYYY-MM-DD format (e.g., '2024-01-15' for that specific day)
  763. """
  764. # Parse time_filter parameter for logging
  765. if isinstance(time_filter, str):
  766. try:
  767. from datetime import datetime
  768. target_date = datetime.strptime(time_filter, '%Y-%m-%d').date()
  769. log_message = f"🔄 Starting automatic CRM calendar sync (cron) for specific date: {time_filter}"
  770. except ValueError:
  771. try:
  772. days_back = int(time_filter)
  773. log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..."
  774. except ValueError:
  775. log_message = f"🔄 Starting automatic CRM calendar sync (cron) with invalid time_filter: {time_filter}"
  776. else:
  777. days_back = int(time_filter)
  778. log_message = f"🔄 Starting automatic CRM calendar sync (cron) for last {days_back} days..."
  779. _logger.info(log_message)
  780. try:
  781. # Check if any company has Google Drive CRM enabled
  782. companies_with_google = self.env['res.company'].search([
  783. ('google_drive_crm_enabled', '=', True),
  784. ('google_drive_crm_folder_id', '!=', False)
  785. ])
  786. if not companies_with_google:
  787. _logger.info("🔄 No companies configured for Google Drive CRM. Skipping sync.")
  788. return
  789. # Check if there are any opportunities to process
  790. opportunities_count = self.env['crm.lead'].search_count([
  791. ('type', '=', 'opportunity'),
  792. ('stage_id.is_won', '=', False)
  793. ])
  794. if opportunities_count == 0:
  795. _logger.info("🔄 No active opportunities found. Skipping sync.")
  796. return
  797. # Find users with Google authentication
  798. authenticated_users = self.env['res.users'].search([
  799. ('res_users_settings_id.google_rtoken', '!=', False),
  800. ('res_users_settings_id.google_token', '!=', False)
  801. ])
  802. if not authenticated_users:
  803. _logger.warning("🔄 No users with Google authentication found. Skipping sync.")
  804. return
  805. _logger.info(f"🔄 Found {len(authenticated_users)} users with Google authentication")
  806. # Execute sync for each authenticated user
  807. total_results = {
  808. 'total_meetings': 0,
  809. 'opportunities_found': 0,
  810. 'folders_created': 0,
  811. 'files_moved': 0,
  812. 'errors': []
  813. }
  814. for user in authenticated_users:
  815. try:
  816. _logger.info(f"🔄 Executing sync for user: {user.name} ({user.login})")
  817. # Execute sync with user context
  818. result = self.with_user(user.id)._sync_meetings_with_opportunities(time_filter=time_filter)
  819. # Parse result if it's a string (summary)
  820. if isinstance(result, str):
  821. # Extract numbers from summary (basic parsing)
  822. import re
  823. meetings_match = re.search(r'Reuniones procesadas: (\d+)', result)
  824. opportunities_match = re.search(r'Oportunidades encontradas: (\d+)', result)
  825. folders_match = re.search(r'Carpetas creadas: (\d+)', result)
  826. files_match = re.search(r'Archivos movidos: (\d+)', result)
  827. if meetings_match:
  828. total_results['total_meetings'] += int(meetings_match.group(1))
  829. if opportunities_match:
  830. total_results['opportunities_found'] += int(opportunities_match.group(1))
  831. if folders_match:
  832. total_results['folders_created'] += int(folders_match.group(1))
  833. if files_match:
  834. total_results['files_moved'] += int(files_match.group(1))
  835. _logger.info(f"✅ Sync completed for user {user.name}")
  836. except Exception as e:
  837. error_msg = f"Error syncing for user {user.name}: {str(e)}"
  838. _logger.error(error_msg)
  839. total_results['errors'].append(error_msg)
  840. continue
  841. # Generate final summary
  842. final_summary = self._generate_sync_summary(total_results)
  843. _logger.info(f"✅ Automatic CRM calendar sync completed for all users: {final_summary}")
  844. return final_summary
  845. except Exception as e:
  846. # Log error but don't fail the cron job
  847. _logger.error(f"❌ Automatic CRM calendar sync failed: {str(e)}")
  848. # Create a system message for administrators
  849. try:
  850. admin_users = self.env['res.users'].search([('groups_id', 'in', self.env.ref('base.group_system').id)])
  851. for admin in admin_users:
  852. admin.message_post(
  853. body=f"❌ <strong>CRM Calendar Sync Error</strong><br/>"
  854. f"Error: {str(e)}<br/>"
  855. f"Time: {fields.Datetime.now()}<br/>"
  856. f"Time Filter: {time_filter}",
  857. message_type='comment',
  858. subtype_xmlid='mail.mt_comment'
  859. )
  860. except:
  861. pass # Don't fail if we can't create messages
  862. return False
  863. def _get_meetings_with_recordings(self, days_back=15):
  864. """Get Google Meet meetings from last N days that have recordings"""
  865. try:
  866. # Use Google Calendar service to get real meetings
  867. calendar_service = self.env['google.calendar.service']
  868. meetings = calendar_service.get_meetings_with_recordings(days_back=days_back)
  869. _logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar")
  870. return meetings
  871. except Exception as e:
  872. _logger.error(f"Failed to get meetings from Google Calendar: {str(e)}")
  873. # Fallback to empty list if API fails
  874. return []
  875. def _get_meetings_with_recordings_for_date(self, target_date):
  876. """Get Google Meet meetings from a specific date that have recordings"""
  877. try:
  878. # Use Google Calendar service to get meetings for specific date
  879. calendar_service = self.env['google.calendar.service']
  880. meetings = calendar_service.get_meetings_with_recordings_for_date(target_date)
  881. _logger.info(f"Retrieved {len(meetings)} meetings with recordings from Google Calendar for date: {target_date}")
  882. return meetings
  883. except Exception as e:
  884. _logger.error(f"Failed to get meetings from Google Calendar for date {target_date}: {str(e)}")
  885. # Fallback to empty list if API fails
  886. return []
  887. def _process_meeting_for_opportunities(self, meeting, drive_service):
  888. """Process a single meeting for opportunities"""
  889. result = {
  890. 'opportunities_found': 0,
  891. 'folders_created': 0,
  892. 'files_moved': 0
  893. }
  894. # Find opportunities by participant emails
  895. opportunities = self._find_opportunities_by_participants(meeting['participants'])
  896. result['opportunities_found'] = len(opportunities)
  897. if not opportunities:
  898. _logger.info(f"❌ No opportunities found for meeting '{meeting['title']}' with participants: {meeting['participants']}")
  899. return result
  900. for opportunity in opportunities:
  901. try:
  902. _logger.info(f"🎯 Processing meeting '{meeting['title']}' for opportunity '{opportunity.name}' (ID: {opportunity.id})")
  903. # Ensure opportunity has Google Drive folder structure
  904. if not opportunity.google_drive_folder_id:
  905. _logger.info(f"📁 Creating Google Drive folder for opportunity {opportunity.name}")
  906. opportunity._create_google_drive_folder_structure()
  907. result['folders_created'] += 1
  908. # Get or create Meets folder
  909. meets_folder_id = self._get_or_create_meets_folder(opportunity, drive_service)
  910. # Move recording files to Meets folder
  911. files_moved = self._move_recording_files_to_meets_folder(
  912. meeting['recording_files'],
  913. meets_folder_id,
  914. drive_service,
  915. meeting['title']
  916. )
  917. result['files_moved'] += files_moved
  918. # Search for additional files in the user's configured CRM meets folder
  919. additional_files_moved = self._search_and_move_crm_meets_files(meeting['title'], meets_folder_id, drive_service)
  920. result['files_moved'] += additional_files_moved
  921. _logger.info(f"✅ Successfully processed meeting '{meeting['title']}' for opportunity '{opportunity.name}': {files_moved + additional_files_moved} files moved")
  922. except Exception as e:
  923. _logger.error(f"❌ Error processing opportunity {opportunity.name}: {str(e)}")
  924. continue
  925. return result
  926. def _find_opportunities_by_participants(self, participant_emails):
  927. """Find opportunities where participants are partners - returns the most recent one"""
  928. opportunities = self.env['crm.lead'].search([
  929. ('partner_id.email', 'in', participant_emails),
  930. ('type', '=', 'opportunity'),
  931. ('stage_id.is_won', '=', False) # Only active opportunities
  932. ], order='create_date desc', limit=1)
  933. if opportunities:
  934. opportunity = opportunities[0]
  935. _logger.info(f"Found most recent opportunity '{opportunity.name}' (ID: {opportunity.id}, created: {opportunity.create_date}) for participants: {participant_emails}")
  936. else:
  937. _logger.info(f"No opportunities found for participants: {participant_emails}")
  938. return opportunities
  939. def _get_or_create_meets_folder(self, opportunity, drive_service):
  940. """Get or create Meets folder within opportunity folder"""
  941. try:
  942. # List folders in opportunity folder
  943. folders = drive_service._do_request(
  944. '/drive/v3/files',
  945. params={
  946. 'q': f"'{opportunity.google_drive_folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and trashed=false",
  947. 'fields': 'files(id,name)',
  948. 'supportsAllDrives': 'true',
  949. 'includeItemsFromAllDrives': 'true'
  950. }
  951. )
  952. # Look for existing Meets folder
  953. meets_folder_id = None
  954. for folder in folders.get('files', []):
  955. if folder['name'] == 'Meets':
  956. meets_folder_id = folder['id']
  957. _logger.info(f"Found existing Meets folder: {meets_folder_id}")
  958. break
  959. # Create Meets folder if it doesn't exist
  960. if not meets_folder_id:
  961. _logger.info(f"Creating Meets folder for opportunity {opportunity.name}")
  962. meets_folder = drive_service._do_request(
  963. '/drive/v3/files',
  964. method='POST',
  965. json_data={
  966. 'name': 'Meets',
  967. 'mimeType': 'application/vnd.google-apps.folder',
  968. 'parents': [opportunity.google_drive_folder_id]
  969. }
  970. )
  971. meets_folder_id = meets_folder['id']
  972. _logger.info(f"Created Meets folder: {meets_folder_id}")
  973. return meets_folder_id
  974. except Exception as e:
  975. _logger.error(f"Error getting/creating Meets folder: {str(e)}")
  976. raise
  977. def _move_recording_files_to_meets_folder(self, file_ids, meets_folder_id, drive_service, meeting_title):
  978. """Move recording files to Meets folder"""
  979. files_moved = 0
  980. _logger.info(f"🔍 Starting to move {len(file_ids)} files to Meets folder: {meets_folder_id}")
  981. _logger.info(f"📁 Target Meets folder ID: {meets_folder_id}")
  982. for file_id in file_ids:
  983. try:
  984. _logger.info(f"📄 Processing file ID: {file_id}")
  985. # Check if file is already in the Meets folder
  986. file_info = drive_service._do_request(
  987. f'/drive/v3/files/{file_id}',
  988. params={
  989. 'fields': 'id,name,parents',
  990. 'supportsAllDrives': 'true',
  991. 'includeItemsFromAllDrives': 'true'
  992. }
  993. )
  994. file_name = file_info.get('name', 'Unknown')
  995. current_parents = file_info.get('parents', [])
  996. _logger.info(f"📄 File: {file_name} (ID: {file_id})")
  997. _logger.info(f"📍 Current parents: {current_parents}")
  998. _logger.info(f"🎯 Target folder: {meets_folder_id}")
  999. # Check if file is already in the target folder
  1000. if meets_folder_id in current_parents:
  1001. _logger.info(f"✅ File {file_name} already in Meets folder")
  1002. continue
  1003. # Move file to Meets folder
  1004. _logger.info(f"🚚 Moving file {file_name} to Meets folder...")
  1005. # Prepare parameters for Shared Drive support
  1006. move_params = {
  1007. 'supportsAllDrives': 'true',
  1008. 'includeItemsFromAllDrives': 'true'
  1009. }
  1010. # Try alternative approach: use addParents and removeParents as URL parameters
  1011. move_params['addParents'] = meets_folder_id
  1012. move_params['removeParents'] = ','.join(current_parents)
  1013. _logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}")
  1014. drive_service._do_request(
  1015. f'/drive/v3/files/{file_id}',
  1016. method='PATCH',
  1017. params=move_params
  1018. )
  1019. # Verify the move was successful
  1020. updated_file_info = drive_service._do_request(
  1021. f'/drive/v3/files/{file_id}',
  1022. params={
  1023. 'fields': 'id,name,parents',
  1024. 'supportsAllDrives': 'true',
  1025. 'includeItemsFromAllDrives': 'true'
  1026. }
  1027. )
  1028. updated_parents = updated_file_info.get('parents', [])
  1029. _logger.info(f"✅ File moved! New parents: {updated_parents}")
  1030. files_moved += 1
  1031. _logger.info(f"✅ Successfully moved file {file_name} to Meets folder for meeting: {meeting_title}")
  1032. except Exception as e:
  1033. _logger.error(f"❌ Error moving file {file_id}: {str(e)}")
  1034. continue
  1035. _logger.info(f"📊 Total files moved: {files_moved} out of {len(file_ids)}")
  1036. return files_moved
  1037. def _search_and_move_crm_meets_files(self, meeting_title, meets_folder_id, drive_service):
  1038. """Search for files in the user's configured CRM meets folder and move them to the opportunity's Meets folder"""
  1039. files_moved = 0
  1040. try:
  1041. # Get the current user's CRM meets folder configuration
  1042. current_user = self.env.user
  1043. user_settings = current_user.res_users_settings_id
  1044. if not user_settings or not user_settings.google_crm_meets_folder_id:
  1045. _logger.info(f"🔍 No CRM meets folder configured for user {current_user.name}. Skipping additional file search.")
  1046. return 0
  1047. crm_meets_folder_id = user_settings.google_crm_meets_folder_id
  1048. _logger.info(f"🔍 Searching for files in user's CRM meets folder ({crm_meets_folder_id}) containing meeting title: '{meeting_title}'")
  1049. # Search for files in the user's CRM meets folder that contain the meeting title
  1050. files_in_crm_meets = drive_service._do_request(
  1051. '/drive/v3/files',
  1052. params={
  1053. 'q': f"'{crm_meets_folder_id}' in parents and trashed=false and name contains '{meeting_title}'",
  1054. 'fields': 'files(id,name,parents)',
  1055. 'supportsAllDrives': 'true',
  1056. 'includeItemsFromAllDrives': 'true'
  1057. }
  1058. )
  1059. found_files = files_in_crm_meets.get('files', [])
  1060. _logger.info(f"🔍 Found {len(found_files)} files in CRM meets folder containing meeting title: '{meeting_title}'")
  1061. for file_info in found_files:
  1062. file_id = file_info['id']
  1063. file_name = file_info['name']
  1064. current_parents = file_info.get('parents', [])
  1065. _logger.info(f"📄 Processing file '{file_name}' (ID: {file_id}) from CRM meets folder")
  1066. # Check if file is already in the target folder
  1067. if meets_folder_id in current_parents:
  1068. _logger.info(f"✅ File '{file_name}' already in opportunity's Meets folder")
  1069. continue
  1070. # Move file to the opportunity's Meets folder
  1071. _logger.info(f"🚚 Moving file '{file_name}' to opportunity's Meets folder...")
  1072. # Prepare parameters for Shared Drive support
  1073. move_params = {
  1074. 'supportsAllDrives': 'true',
  1075. 'includeItemsFromAllDrives': 'true'
  1076. }
  1077. # Try alternative approach: use addParents and removeParents as URL parameters
  1078. move_params['addParents'] = meets_folder_id
  1079. move_params['removeParents'] = ','.join(current_parents)
  1080. _logger.info(f"🔧 Using URL parameters: addParents={meets_folder_id}, removeParents={','.join(current_parents)}")
  1081. drive_service._do_request(
  1082. f'/drive/v3/files/{file_id}',
  1083. method='PATCH',
  1084. params=move_params
  1085. )
  1086. # Verify the move was successful
  1087. updated_file_info = drive_service._do_request(
  1088. f'/drive/v3/files/{file_id}',
  1089. params={
  1090. 'fields': 'id,name,parents',
  1091. 'supportsAllDrives': 'true',
  1092. 'includeItemsFromAllDrives': 'true'
  1093. }
  1094. )
  1095. updated_parents = updated_file_info.get('parents', [])
  1096. _logger.info(f"✅ File moved! New parents: {updated_parents}")
  1097. files_moved += 1
  1098. _logger.info(f"✅ Successfully moved file '{file_name}' to opportunity's Meets folder for meeting: {meeting_title}")
  1099. _logger.info(f"📊 Total additional files moved from CRM meets folder: {files_moved}")
  1100. return files_moved
  1101. except Exception as e:
  1102. _logger.error(f"❌ Error searching or moving files from CRM meets folder: {str(e)}")
  1103. return 0
  1104. def _generate_sync_summary(self, sync_results):
  1105. """Generate summary message for sync results"""
  1106. summary = f"<strong>🔄 Sincronización CRM Completada</strong><br/><br/>"
  1107. summary += f"📊 <strong>Resumen:</strong><br/>"
  1108. summary += f"• Reuniones procesadas: {sync_results['total_meetings']}<br/>"
  1109. summary += f"• Oportunidades encontradas: {sync_results['opportunities_found']}<br/>"
  1110. summary += f"• Carpetas creadas: {sync_results['folders_created']}<br/>"
  1111. summary += f"• Archivos movidos: {sync_results['files_moved']}<br/>"
  1112. if sync_results['errors']:
  1113. summary += f"<br/>❌ <strong>Errores ({len(sync_results['errors'])}):</strong><br/>"
  1114. for error in sync_results['errors'][:5]: # Show first 5 errors
  1115. summary += f"• {error}<br/>"
  1116. if len(sync_results['errors']) > 5:
  1117. summary += f"• ... y {len(sync_results['errors']) - 5} errores más<br/>"
  1118. return summary