Просмотр исходного кода

Restore stash before subtree add whatsapp_web_groups; add whatsapp_web_mail; helpdesk_extras portal approval flow; theme_m22tc updates

odoo 1 месяц назад
Родитель
Сommit
f8510d7087
77 измененных файлов с 4789 добавлено и 4176 удалено
  1. 1 1
      README.md
  2. 2 0
      helpdesk_extras/__manifest__.py
  3. 50 0
      helpdesk_extras/controllers/helpdesk_portal.py
  4. 2 2
      helpdesk_extras/controllers/website_helpdesk_hours.py
  5. 1724 0
      helpdesk_extras/i18n/es.po
  6. 1724 0
      helpdesk_extras/i18n/es_ES.po
  7. 279 0
      helpdesk_extras/i18n/es_MX.po
  8. 1 0
      helpdesk_extras/models/__init__.py
  9. 14 0
      helpdesk_extras/models/helpdesk_stage.py
  10. 3 0
      helpdesk_extras/models/helpdesk_team.py
  11. 47 1
      helpdesk_extras/models/helpdesk_ticket.py
  12. 6 1
      helpdesk_extras/models/helpdesk_workflow_template_stage.py
  13. 14 0
      helpdesk_extras/static/src/js/m2o_urlrelation_guard_patch.js
  14. 208 34
      helpdesk_extras/views/helpdesk_portal_templates.xml
  15. 16 0
      helpdesk_extras/views/helpdesk_stage_views.xml
  16. 10 1
      helpdesk_extras/views/helpdesk_ticket_views.xml
  17. 1 0
      helpdesk_extras/views/helpdesk_workflow_template_views.xml
  18. 24 1
      theme_m22tc/README.md
  19. 2 1
      theme_m22tc/__manifest__.py
  20. 36 14
      theme_m22tc/controllers/helpdesk_portal.py
  21. 323 0
      theme_m22tc/static/src/scss/snippets/s_popup_m22.scss
  22. 4 4
      theme_m22tc/views/login_custom.xml
  23. 24 0
      theme_m22tc/views/snippets_popup.xml
  24. 0 53
      whatsapp_web_groups/.gitignore
  25. 0 583
      whatsapp_web_groups/API_REFERENCE.md
  26. 0 386
      whatsapp_web_groups/README.md
  27. 0 1
      whatsapp_web_groups/__init__.py
  28. 0 31
      whatsapp_web_groups/__manifest__.py
  29. 0 14
      whatsapp_web_groups/data/ir_cron.xml
  30. 0 7
      whatsapp_web_groups/models/__init__.py
  31. 0 99
      whatsapp_web_groups/models/marketing_activity.py
  32. 0 115
      whatsapp_web_groups/models/whatsapp_composer.py
  33. 0 64
      whatsapp_web_groups/models/whatsapp_message.py
  34. 0 37
      whatsapp_web_groups/models/ww_contact.py
  35. 0 331
      whatsapp_web_groups/models/ww_group.py
  36. 0 22
      whatsapp_web_groups/models/ww_group_contact_rel.py
  37. 0 8
      whatsapp_web_groups/models/ww_role.py
  38. 0 7
      whatsapp_web_groups/security/ir.model.access.csv
  39. 0 19
      whatsapp_web_groups/views/marketing_activity_views.xml
  40. 0 22
      whatsapp_web_groups/views/whatsapp_composer_views.xml
  41. 0 34
      whatsapp_web_groups/views/whatsapp_message_views.xml
  42. 0 63
      whatsapp_web_groups/views/ww_contact_views.xml
  43. 0 48
      whatsapp_web_groups/views/ww_group_contact_rel_views.xml
  44. 0 70
      whatsapp_web_groups/views/ww_group_views.xml
  45. 0 44
      whatsapp_web_groups/views/ww_role_views.xml
  46. 0 53
      whatsapp_web_groups_local_backup_20251214231432/.gitignore
  47. 0 583
      whatsapp_web_groups_local_backup_20251214231432/API_REFERENCE.md
  48. 0 386
      whatsapp_web_groups_local_backup_20251214231432/README.md
  49. 0 1
      whatsapp_web_groups_local_backup_20251214231432/__init__.py
  50. 0 31
      whatsapp_web_groups_local_backup_20251214231432/__manifest__.py
  51. 0 14
      whatsapp_web_groups_local_backup_20251214231432/data/ir_cron.xml
  52. 0 7
      whatsapp_web_groups_local_backup_20251214231432/models/__init__.py
  53. 0 99
      whatsapp_web_groups_local_backup_20251214231432/models/marketing_activity.py
  54. 0 115
      whatsapp_web_groups_local_backup_20251214231432/models/whatsapp_composer.py
  55. 0 64
      whatsapp_web_groups_local_backup_20251214231432/models/whatsapp_message.py
  56. 0 37
      whatsapp_web_groups_local_backup_20251214231432/models/ww_contact.py
  57. 0 331
      whatsapp_web_groups_local_backup_20251214231432/models/ww_group.py
  58. 0 22
      whatsapp_web_groups_local_backup_20251214231432/models/ww_group_contact_rel.py
  59. 0 8
      whatsapp_web_groups_local_backup_20251214231432/models/ww_role.py
  60. 0 7
      whatsapp_web_groups_local_backup_20251214231432/security/ir.model.access.csv
  61. 0 19
      whatsapp_web_groups_local_backup_20251214231432/views/marketing_activity_views.xml
  62. 0 22
      whatsapp_web_groups_local_backup_20251214231432/views/whatsapp_composer_views.xml
  63. 0 34
      whatsapp_web_groups_local_backup_20251214231432/views/whatsapp_message_views.xml
  64. 0 63
      whatsapp_web_groups_local_backup_20251214231432/views/ww_contact_views.xml
  65. 0 48
      whatsapp_web_groups_local_backup_20251214231432/views/ww_group_contact_rel_views.xml
  66. 0 70
      whatsapp_web_groups_local_backup_20251214231432/views/ww_group_views.xml
  67. 0 44
      whatsapp_web_groups_local_backup_20251214231432/views/ww_role_views.xml
  68. 1 0
      whatsapp_web_mail/__init__.py
  69. 14 0
      whatsapp_web_mail/__manifest__.py
  70. 3 0
      whatsapp_web_mail/models/__init__.py
  71. 6 0
      whatsapp_web_mail/models/ir_model.py
  72. 90 0
      whatsapp_web_mail/models/mail_template.py
  73. 18 0
      whatsapp_web_mail/models/res_partner.py
  74. 3 0
      whatsapp_web_mail/security/ir.model.access.csv
  75. 37 0
      whatsapp_web_mail/views/mail_template_views.xml
  76. 17 0
      whatsapp_web_mail/views/res_partner_views.xml
  77. 85 0
      whatsapp_web_mail/views/whatsapp_notifications_views.xml

+ 1 - 1
README.md

@@ -41,4 +41,4 @@ git subtree push --prefix=helpdesk_extras helpdesk_extras-remote develop
 
 
 # Para subir cambios a whatsapp_web
 # Para subir cambios a whatsapp_web
 git subtree push --prefix=whatsapp_web whatsapp_web-remote develop
 git subtree push --prefix=whatsapp_web whatsapp_web-remote develop
-```
+```

+ 2 - 0
helpdesk_extras/__manifest__.py

@@ -34,6 +34,7 @@ Funcionalidades adicionales para el módulo de Helpdesk:
         "views/helpdesk_workflow_template_views.xml",
         "views/helpdesk_workflow_template_views.xml",
         "views/helpdesk_team_views.xml",
         "views/helpdesk_team_views.xml",
         "views/helpdesk_ticket_views.xml",
         "views/helpdesk_ticket_views.xml",
+        "views/helpdesk_stage_views.xml",
         "views/helpdesk_affected_module_views.xml",
         "views/helpdesk_affected_module_views.xml",
         "views/helpdesk_portal_templates.xml",
         "views/helpdesk_portal_templates.xml",
         "views/website_helpdesk_form.xml",
         "views/website_helpdesk_form.xml",
@@ -44,6 +45,7 @@ Funcionalidades adicionales para el módulo de Helpdesk:
         "web.assets_backend": [
         "web.assets_backend": [
             "helpdesk_extras/static/src/js/helpdesk_template_field_list.js",
             "helpdesk_extras/static/src/js/helpdesk_template_field_list.js",
             "helpdesk_extras/static/src/js/helpdesk_template_field_m2o_widget.js",
             "helpdesk_extras/static/src/js/helpdesk_template_field_m2o_widget.js",
+            "helpdesk_extras/static/src/js/m2o_urlrelation_guard_patch.js",
             "helpdesk_extras/static/src/xml/helpdesk_template_field_list.xml",
             "helpdesk_extras/static/src/xml/helpdesk_template_field_list.xml",
             "helpdesk_extras/static/src/xml/helpdesk_template_field_m2o_widget.xml",
             "helpdesk_extras/static/src/xml/helpdesk_template_field_m2o_widget.xml",
         ],
         ],

+ 50 - 0
helpdesk_extras/controllers/helpdesk_portal.py

@@ -633,3 +633,53 @@ class CustomerPortal(HelpdeskCustomerPortal):
             _logger = logging.getLogger(__name__)
             _logger = logging.getLogger(__name__)
             _logger.error(f"Error creating partner: {str(e)}", exc_info=True)
             _logger.error(f"Error creating partner: {str(e)}", exc_info=True)
             return {'error': _("An error occurred while creating the contact. Please try again.")}
             return {'error': _("An error occurred while creating the contact. Please try again.")}
+    
+    @http.route(['/my/ticket/<int:ticket_id>/approve'], type='http', auth='public', methods=['POST'], website=True, csrf=True)
+    def ticket_approve(self, ticket_id, access_token=None, **kwargs):
+        """Approve ticket by customer"""
+        from odoo import _, fields
+        
+        try:
+            ticket_sudo = self._document_check_access('helpdesk.ticket', ticket_id, access_token)
+        except (AccessError, MissingError):
+            return request.redirect('/my')
+        
+        # Approve
+        ticket_sudo.write({
+            'customer_approval_status': 'approved',
+        })
+        
+        # Message in chatter
+        ticket_sudo.message_post(
+            body=_("Ticket approved by customer"),
+            message_type='notification',
+            subtype_xmlid='mail.mt_comment',
+        )
+        
+        return request.redirect(ticket_sudo.get_portal_url() + '?message=approved')
+    
+    @http.route(['/my/ticket/<int:ticket_id>/reject'], type='http', auth='public', methods=['POST'], website=True, csrf=True)
+    def ticket_reject(self, ticket_id, access_token=None, reason=None, **kwargs):
+        """Reject ticket by customer"""
+        from odoo import _, fields
+        
+        try:
+            ticket_sudo = self._document_check_access('helpdesk.ticket', ticket_id, access_token)
+        except (AccessError, MissingError):
+            return request.redirect('/my')
+        
+        # Reject
+        ticket_sudo.write({
+            'customer_approval_status': 'rejected',
+            'customer_rejection_reason': reason or _('No reason provided'),
+        })
+        
+        # Message in chatter
+        rejection_msg = reason or _('No reason provided')
+        ticket_sudo.message_post(
+            body=_("Ticket rejected by customer: %s") % rejection_msg,
+            message_type='comment',
+            subtype_xmlid='mail.mt_comment',
+        )
+        
+        return request.redirect(ticket_sudo.get_portal_url() + '?message=rejected')

+ 2 - 2
helpdesk_extras/controllers/website_helpdesk_hours.py

@@ -63,8 +63,8 @@ class WebsiteHelpdeskHours(http.Controller):
                     "packages_url": packages_url,
                     "packages_url": packages_url,
                 }
                 }
 
 
-            partner = request.env.user.partner_id.commercial_partner_id
-            user_partner = request.env.user.partner_id
+            partner = request.env.user.partner_id.sudo().commercial_partner_id
+            user_partner = request.env.user.partner_id.sudo()
 
 
             # Get UoM hour reference (use sudo to access uom.uom)
             # Get UoM hour reference (use sudo to access uom.uom)
             try:
             try:

+ 1724 - 0
helpdesk_extras/i18n/es.po

@@ -0,0 +1,1724 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * helpdesk_extras
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-12-07 01:00:00+0000\n"
+"PO-Revision-Date: 2025-12-07 01:00:00+0000\n"
+"Last-Translator: M22 Tech\n"
+"Language-Team: Spanish (Mexico)\n"
+"Language: es_MX\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_basic_support
+msgid "Basic Support"
+msgstr "Soporte Básico"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_premium_support
+msgid "Premium Support"
+msgstr "Soporte Premium"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_development
+msgid "Development"
+msgstr "Desarrollo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_basic_support
+msgid "Simple workflow with 3 stages: New, In Progress, and Solved. Includes basic SLA policies for response and resolution times."
+msgstr "Flujo de trabajo simple con 3 etapas: Nuevo, En Progreso y Resuelto. Incluye políticas SLA básicas para tiempos de respuesta y resolución."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_premium_support
+msgid "Enhanced workflow with 4 stages including On Hold. Faster SLA policies for premium customers."
+msgstr "Flujo de trabajo mejorado con 4 etapas incluyendo En Espera. Políticas SLA más rápidas para clientes premium."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_development
+msgid "Workflow for development teams with stages: New, Analysis, In Progress, Testing, and Done. Includes SLA policies by priority."
+msgstr "Flujo de trabajo para equipos de desarrollo con etapas: Nuevo, Análisis, En Progreso, Pruebas y Completado. Incluye políticas SLA por prioridad."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new_dev
+msgid "New"
+msgstr "Nuevo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress_dev
+msgid "In Progress"
+msgstr "En Progreso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_solved_premium
+msgid "Solved"
+msgstr "Resuelto"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_on_hold_premium
+msgid "On Hold"
+msgstr "En Espera"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_analysis
+msgid "Analysis"
+msgstr "Análisis"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_testing
+msgid "Testing"
+msgstr "Pruebas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_done
+msgid "Done"
+msgstr "Completado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_done
+msgid "Blocked"
+msgstr "Bloqueado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_done
+msgid "Ready"
+msgstr "Listo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_done
+msgid "In Progress"
+msgstr "En Progreso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_response
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_response_normal
+msgid "Response Time - Normal Priority"
+msgstr "Tiempo de Respuesta - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_resolution
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_high
+msgid "Resolution Time - Normal Priority"
+msgstr "Tiempo de Resolución - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_response_high
+msgid "Response Time - High Priority"
+msgstr "Tiempo de Respuesta - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_resolution
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_normal
+msgid "Resolution Time - Normal Priority"
+msgstr "Tiempo de Resolución - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_high
+msgid "Resolution Time - High Priority"
+msgstr "Tiempo de Resolución - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_high
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "Analysis Time - Normal Priority"
+msgstr "Tiempo de Análisis - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_high
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "Completion Time - Normal Priority"
+msgstr "Tiempo de Finalización - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_high
+msgid "Analysis Time - High Priority"
+msgstr "Tiempo de Análisis - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_high
+msgid "Completion Time - High Priority"
+msgstr "Tiempo de Finalización - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "Analysis Time - Urgent Priority"
+msgstr "Tiempo de Análisis - Prioridad Urgente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "Completion Time - Urgent Priority"
+msgstr "Tiempo de Finalización - Prioridad Urgente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_basic_response
+msgid "<p>Tickets should be responded to within 4 working hours</p>"
+msgstr "<p>Los tickets deben ser respondidos dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_basic_resolution
+msgid "<p>Tickets should be resolved within 24 working hours</p>"
+msgstr "<p>Los tickets deben ser resueltos dentro de 24 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_response_normal
+msgid "<p>Premium tickets should be responded to within 2 working hours</p>"
+msgstr "<p>Los tickets premium deben ser respondidos dentro de 2 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_resolution_normal
+msgid "<p>Premium tickets should be resolved within 8 working hours</p>"
+msgstr "<p>Los tickets premium deben ser resueltos dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_response_high
+msgid "<p>High priority premium tickets should be responded to within 1 working hour</p>"
+msgstr "<p>Los tickets premium de alta prioridad deben ser respondidos dentro de 1 hora laborable</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_resolution_high
+msgid "<p>High priority premium tickets should be resolved within 4 working hours</p>"
+msgstr "<p>Los tickets premium de alta prioridad deben ser resueltos dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_normal
+msgid "<p>Normal priority tickets should be analyzed within 8 working hours</p>"
+msgstr "<p>Los tickets de prioridad normal deben ser analizados dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_normal
+msgid "<p>Normal priority tickets should be completed within 40 working hours</p>"
+msgstr "<p>Los tickets de prioridad normal deben ser completados dentro de 40 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_high
+msgid "<p>High priority tickets should be analyzed within 4 working hours</p>"
+msgstr "<p>Los tickets de alta prioridad deben ser analizados dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_high
+msgid "<p>High priority tickets should be completed within 16 working hours</p>"
+msgstr "<p>Los tickets de alta prioridad deben ser completados dentro de 16 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "<p>Urgent tickets should be analyzed within 2 working hours</p>"
+msgstr "<p>Los tickets urgentes deben ser analizados dentro de 2 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "<p>Urgent tickets should be completed within 8 working hours</p>"
+msgstr "<p>Los tickets urgentes deben ser completados dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Stages"
+msgstr "Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "SLA Policies"
+msgstr "Políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Teams Using"
+msgstr "Equipos Usando"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Total Stages"
+msgstr "Total de Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Total SLAs"
+msgstr "Total de SLAs"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Teams Using This Template"
+msgstr "Equipos Usando Esta Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Excluded Stages"
+msgstr "Etapas Excluidas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "SLA Policy"
+msgstr "Política SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Stages where time spent will NOT count towards the SLA deadline. Useful for 'On Hold' or 'Waiting for Customer' stages."
+msgstr "Etapas donde el tiempo transcurrido NO contará hacia el plazo del SLA. Útil para etapas como 'En Espera' o 'Esperando Cliente'."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Template Name"
+msgstr "Nombre de Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Archived"
+msgstr "Archivado"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Has Stages"
+msgstr "Tiene Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Has SLAs"
+msgstr "Tiene SLAs"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Group By"
+msgstr "Agrupar Por"
+
+#. module: helpdesk_extras
+#: model:ir.ui.menu,name:helpdesk_extras.menu_helpdesk_workflow_template
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,help:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Create your first workflow template!"
+msgstr "¡Crea tu primera plantilla de flujo de trabajo!"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,help:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Workflow templates allow you to quickly set up stages and SLA policies for helpdesk teams. Create a template with predefined stages and SLAs, then apply it to any team with one click."
+msgstr "Las plantillas de flujo de trabajo te permiten configurar rápidamente etapas y políticas SLA para equipos de helpdesk. Crea una plantilla con etapas y SLAs predefinidos, luego aplícala a cualquier equipo con un clic."
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.action_helpdesk_workflow_template_apply_wizard
+msgid "Apply Workflow Template"
+msgstr "Aplicar Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__team_id
+msgid "Team"
+msgstr "Equipo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__workflow_template_id
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__replace_existing
+msgid "Replace Existing Stages and SLAs"
+msgstr "Reemplazar Etapas y SLAs Existentes"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__replace_existing
+msgid "If checked, existing stages and SLAs will be removed before applying the template"
+msgstr "Si está marcado, las etapas y SLAs existentes se eliminarán antes de aplicar la plantilla"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__stage_count
+msgid "Stages to Create"
+msgstr "Etapas a Crear"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__sla_count
+msgid "SLA Policies to Create"
+msgstr "Políticas SLA a Crear"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__existing_stage_count
+msgid "Existing Stages"
+msgstr "Etapas Existentes"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__existing_sla_count
+msgid "Existing SLA Policies"
+msgstr "Políticas SLA Existentes"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_apply_wizard_form
+msgid "Apply Template"
+msgstr "Aplicar Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_apply_wizard_form
+msgid "Summary"
+msgstr "Resumen"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_team_view_form_inherit_helpdesk_extras
+msgid "Select a workflow template to quickly set up stages and SLA policies"
+msgstr "Selecciona una plantilla de flujo de trabajo para configurar rápidamente etapas y políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "Stages"
+msgstr "Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "SLA Policies"
+msgstr "Políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "team(s)"
+msgstr "equipo(s)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Teams"
+msgstr "Equipos"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "e.g., Basic Support, Premium Support"
+msgstr "ej., Soporte Básico, Soporte Premium"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Describe this workflow template..."
+msgstr "Describe esta plantilla de flujo de trabajo..."
+
+
+#. module: helpdesk_extras
+#: model:helpdesk.request.type,name:helpdesk_extras.type_incident
+msgid "Incident"
+msgstr "Incidente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.request.type,name:helpdesk_extras.type_improvement
+msgid "Improvement"
+msgstr "Mejora"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:0
+msgid "Critical"
+msgstr "Crítico"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:1
+msgid "High"
+msgstr "Alto"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:2
+msgid "Normal"
+msgstr "Normal"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__request_type_id
+msgid "Request Type"
+msgstr "Tipo de Solicitud"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__request_type_code
+msgid "Request Type Code"
+msgstr "Código de Tipo de Solicitud"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__affected_module_id
+msgid "Affected Module"
+msgstr "Módulo Afectado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__affected_user_email
+msgid "Affected User Email"
+msgstr "Email del Usuario Afectado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__business_impact
+msgid "Business Impact"
+msgstr "Impacto de Negocio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__reproduce_steps
+msgid "Steps to Reproduce"
+msgstr "Pasos para Reproducir"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__business_goal
+msgid "Business Goal"
+msgstr "Objetivo de Negocio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__attachment_ids
+msgid "Attachments"
+msgstr "Adjuntos"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__estimated_hours
+msgid "Estimated Hours"
+msgstr "Horas Estimadas"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__approval_status
+msgid "Approval Status"
+msgstr "Estado de Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "N/A"
+msgstr "N/A"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Waiting for Approval"
+msgstr "Esperando Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Rejected"
+msgstr "Rechazado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__client_authorization
+msgid "Client Authorization"
+msgstr "Autorización del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__has_template
+msgid "Has Template"
+msgstr "Tiene Plantilla"
+
+#. module: helpdesk_extras
+#. odoo-python
+#: code:addons/helpdesk_extras/models/helpdesk_team.py:896
+#: code:addons/helpdesk_extras/models/helpdesk_team.py:1083
+msgid "-- Select --"
+msgstr "-- Seleccionar --"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Request Information"
+msgstr "Información de Solicitud"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Details"
+msgstr "Detalles"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Approval & Billing"
+msgstr "Aprobación y Facturación"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__name
+msgid "Name"
+msgstr "Nombre"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__code
+msgid "Code"
+msgstr "Código"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__active
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__is_main_application
+msgid "Main Application"
+msgstr "Aplicación Principal"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__description
+msgid "Description"
+msgstr "Descripción"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Inactive"
+msgstr "Inactivo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Main Applications"
+msgstr "Aplicaciones Principales"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.helpdesk_affected_module_action
+msgid "Affected Modules"
+msgstr "Módulos Afectados"
+
+#. module: helpdesk_extras
+#: model:ir.ui.menu,name:helpdesk_extras.helpdesk_affected_module_menu
+msgid "Affected Modules"
+msgstr "Módulos Afectados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_accountant
+msgid "Accounting"
+msgstr "Contabilidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_ai
+msgid "AI Base"
+msgstr "Base de IA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_ai_app
+msgid "AI"
+msgstr "IA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_analytic
+msgid "Analytic Accounting"
+msgstr "Contabilidad Analítica"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_analytic_enterprise
+msgid "Analytic Accounting Enterprise"
+msgstr "Contabilidad Analítica"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_appointment
+msgid "Appointments"
+msgstr "Citas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_approvals
+msgid "Approvals"
+msgstr "Aprobaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_barcodes
+msgid "Barcode"
+msgstr "Código de Barras"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_base
+msgid "Base"
+msgstr "Base"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_calendar
+msgid "Calendar"
+msgstr "Calendario"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_certificate
+msgid "Certificate"
+msgstr "Certificado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_cloud_storage
+msgid "Cloud Storage"
+msgstr "Almacenamiento en la Nube"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_contacts
+msgid "Contacts"
+msgstr "Contactos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_crm
+msgid "CRM"
+msgstr "CRM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_loyalty
+msgid "Coupons & Loyalty"
+msgstr "Cupones y Fidelidad"
+
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_attendance
+msgid "Attendances"
+msgstr "Asistencias"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_cohort
+msgid "Cohort View"
+msgstr "Vista de cohorte"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_portal
+msgid "Customer Portal"
+msgstr "Portal del cliente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_rating
+msgid "Customer Rating"
+msgstr "Valoración del cliente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_board
+msgid "Dashboards"
+msgstr "Tableros"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_databases
+msgid "Databases"
+msgstr "Bases de datos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_delivery
+msgid "Delivery Costs"
+msgstr "Gastos de envío"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mail
+msgid "Discuss"
+msgstr "Conversaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_documents
+msgid "Documents"
+msgstr "Documentos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_esg
+msgid "ESG"
+msgstr "ASG"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mass_mailing
+msgid "Email Marketing"
+msgstr "Marketing por correo electrónico"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_contract
+msgid "Employee Contracts"
+msgstr "Contratos de los empleados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr
+msgid "Employees"
+msgstr "Empleados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_equity
+msgid "Equity"
+msgstr "Patrimonio"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_event
+msgid "Events Organization"
+msgstr "Organización de eventos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_expense
+msgid "Expenses"
+msgstr "Gastos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_industry_fsm
+msgid "Field Service"
+msgstr "Servicio de campo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_fleet
+msgid "Fleet"
+msgstr "Flota"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_frontdesk
+msgid "Frontdesk"
+msgstr "Recepción"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_gamification
+msgid "Gamification"
+msgstr "Ludificación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_grid
+msgid "Grid View"
+msgstr "Vista de cuadrícula"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_helpdesk
+msgid "Helpdesk"
+msgstr "Servicio de asistencia"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_bus
+msgid "IM Bus"
+msgstr "Bus IM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_iot
+msgid "Internet of Things"
+msgstr "Internet de las cosas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_stock
+msgid "Inventory"
+msgstr "Inventarios"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_account
+msgid "Invoicing"
+msgstr "Facturación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_digest
+msgid "KPI Digests"
+msgstr "Resúmenes de KPI"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_knowledge
+msgid "Knowledge"
+msgstr "Información"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_link_tracker
+msgid "Link Tracker"
+msgstr "Rastreador de enlaces"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_im_livechat
+msgid "Live Chat"
+msgstr "Chat en directo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_lunch
+msgid "Lunch"
+msgstr "Comida"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_maintenance
+msgid "Maintenance"
+msgstr "Mantenimiento"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mrp
+msgid "Manufacturing"
+msgstr "Fabricación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_map
+msgid "Map View"
+msgstr "Vista del mapa"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_marketing_automation
+msgid "Marketing Automation"
+msgstr "Automatización de marketing"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_room
+msgid "Meeting Rooms"
+msgstr "Sala de reuniones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_membership
+msgid "Members"
+msgstr "Miembros"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_mobile
+msgid "Mobile"
+msgstr "Móvil"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_onboarding
+msgid "Onboarding Toolbox"
+msgstr "Caja de herramientas para la incorporación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_partnership
+msgid "Partnership / Membership"
+msgstr "Asociación / Afiliación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_payment
+msgid "Payment Engine"
+msgstr "Motor de pago"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_payroll
+msgid "Payroll"
+msgstr "Nómina"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_phone_validation
+msgid "Phone Numbers Validation"
+msgstr "Validación de números de teléfono"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_planning
+msgid "Planning"
+msgstr "Planificación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_point_of_sale
+msgid "Point of Sale"
+msgstr "Punto de venta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_privacy_lookup
+msgid "Privacy"
+msgstr "Privacidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_product
+msgid "Products & Pricelists"
+msgstr "Productos y listas de precios"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_project
+msgid "Project"
+msgstr "Proyecto"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_purchase
+msgid "Purchase"
+msgstr "Compra"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_quality_control
+msgid "Quality"
+msgstr "Calidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_quality
+msgid "Quality Base"
+msgstr "Base de calidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_rpc
+msgid "RPC endpoints"
+msgstr "Puntos de conexión RPC"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_recruitment
+msgid "Recruitment"
+msgstr "Reclutamiento"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_repair
+msgid "Repairs"
+msgstr "Reparaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_resource
+msgid "Resource"
+msgstr "Recurso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sms
+msgid "SMS gateway"
+msgstr "Puerta de enlace SMS"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sale
+msgid "Sales"
+msgstr "Ventas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sales_team
+msgid "Sales Teams"
+msgstr "Equipos de ventas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sign
+msgid "Sign"
+msgstr "Firmar"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_snailmail
+msgid "Snail Mail"
+msgstr "Correo postal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_social
+msgid "Social Marketing"
+msgstr "Marketing social"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_spreadsheet
+msgid "Spreadsheet"
+msgstr "Hoja de cálculo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_studio
+msgid "Studio"
+msgstr "Studio"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_survey
+msgid "Surveys"
+msgstr "Encuestas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_timesheet
+msgid "Task Logs"
+msgstr "Registros de tareas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_holidays
+msgid "Time Off"
+msgstr "Ausencia"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_timer
+msgid "Timer"
+msgstr "Temporizador"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_timesheet_grid
+msgid "Timesheets"
+msgstr "Partes de horas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_tour
+msgid "Tours"
+msgstr "Recorridos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_transifex
+msgid "Transifex integration"
+msgstr "Integración en Transifex"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_utm
+msgid "UTM Trackers"
+msgstr "Rastreadores UTM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_uom
+msgid "Units of measure"
+msgstr "Unidades de medida"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_unsplash
+msgid "Unsplash Image Library"
+msgstr "Biblioteca de imágenes de Unsplash"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_voip
+msgid "VoIP"
+msgstr "VoIP"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_stock_account
+msgid "WMS Accounting"
+msgstr "Contabilidad del SGA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web
+msgid "Web"
+msgstr "Web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_editor
+msgid "Web Editor"
+msgstr "Editor web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_enterprise
+msgid "Web Enterprise"
+msgstr "Web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_gantt
+msgid "Web Gantt"
+msgstr "Diagrama Gantt web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_hierarchy
+msgid "Web Hierarchy"
+msgstr "Jerarquía web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_website
+msgid "Website"
+msgstr "Sitio web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_whatsapp
+msgid "WhatsApp Messaging"
+msgstr "Mensajes de WhatsApp"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_worksheet
+msgid "Worksheet"
+msgstr "Hoja de trabajo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_account_accountant
+msgid "Account Accountant"
+msgstr "Contabilidad de Cuentas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_preventa
+msgid "Preventa"
+msgstr "Preventa"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sale_management
+msgid "Sales Management"
+msgstr "Gestión de Ventas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_ids
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_count
+msgid "Form Fields Count"
+msgstr "Cantidad de Campos"
+
+#. module: helpdesk_extras
+#: model:ir.model,name:helpdesk_extras.model_helpdesk_workflow_template_field
+msgid "Workflow Template Form Field"
+msgstr "Campo de Formulario de Plantilla de Flujo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__workflow_template_id
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_id
+msgid "Field"
+msgstr "Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_name
+msgid "Field Name"
+msgstr "Nombre del Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_type
+msgid "Field Type"
+msgstr "Tipo de Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__label_custom
+msgid "Custom Label"
+msgstr "Etiqueta Personalizada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__placeholder
+msgid "Placeholder"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__default_value
+msgid "Default Value"
+msgstr "Valor por Defecto"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__help_text
+msgid "Help Text"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__widget
+msgid "Widget"
+msgstr "Widget"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_type
+msgid "Selection Type"
+msgstr "Tipo de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_options
+msgid "Selection Options"
+msgstr "Opciones de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__rows
+msgid "Height (Rows)"
+msgstr "Altura (Filas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__input_type
+msgid "Input Type"
+msgstr "Tipo de Entrada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__sequence
+msgid "Sequence"
+msgstr "Secuencia"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__required
+msgid "Required"
+msgstr "Obligatorio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__model_required
+msgid "Model Required"
+msgstr "Obligatorio del Modelo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_dependency
+msgid "Visibility Dependency"
+msgstr "Dependencia de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_condition
+msgid "Visibility Condition Value"
+msgstr "Valor de Condición de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_comparator
+msgid "Visibility Comparator"
+msgstr "Comparador de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_between
+msgid "Visibility Between (End Value)"
+msgstr "Visibilidad Entre (Valor Final)"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Dropdown List"
+msgstr "Lista Desplegable"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Radio"
+msgstr "Botones de Radio"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Text"
+msgstr "Texto"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Email"
+msgstr "Correo Electrónico"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Telephone"
+msgstr "Teléfono"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Url"
+msgstr "URL"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is equal to"
+msgstr "Es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not equal to"
+msgstr "No es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Contains"
+msgstr "Contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Doesn't contain"
+msgstr "No contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is set"
+msgstr "Está definido"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not set"
+msgstr "No está definido"
+
+#. module: helpdesk_extras
+#. odoo-python
+#: code:addons/helpdesk_extras/models/helpdesk_workflow_template_field.py:0
+msgid "Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. Try hiding it with the 'Visibility' option instead and add it a default value."
+msgstr "No se puede eliminar el/los campo(s) obligatorio(s) del modelo: %s. Este campo es obligatorio para el modelo y no puede ser eliminado. Intenta ocultarlo con la opción 'Visibilidad' y agrega un valor por defecto."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Field Configuration"
+msgstr "Configuración del Campo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Display Options"
+msgstr "Opciones de Visualización"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Visibility Conditions"
+msgstr "Condiciones de Visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Field"
+msgstr "Campo de Formulario"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Custom label (optional)"
+msgstr "Etiqueta personalizada (opcional)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Placeholder text"
+msgstr "Texto de ejemplo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Default value"
+msgstr "Valor por defecto"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Help text (HTML)"
+msgstr "Texto de ayuda (HTML)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select field for visibility condition"
+msgstr "Seleccionar campo para condición de visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select comparator"
+msgstr "Seleccionar comparador"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Enter value to compare"
+msgstr "Ingresa el valor para comparar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select value"
+msgstr "Seleccionar valor"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "End value for range (date/datetime)"
+msgstr "Valor final para rango (fecha/hora)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "e.g., Basic Support, Premium Support"
+msgstr "ej., Soporte Básico, Soporte Premium"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Describe this workflow template..."
+msgstr "Describe esta plantilla de flujo de trabajo..."
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__name
+msgid "Stage Name"
+msgstr "Nombre de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__fold
+msgid "Folded"
+msgstr "Plegada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__description
+msgid "Stage Description"
+msgstr "Descripción de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__name
+msgid "SLA Name"
+msgstr "Nombre del SLA"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__stage_template_id
+msgid "Target Stage"
+msgstr "Etapa Objetivo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__time
+msgid "Time (Hours)"
+msgstr "Tiempo (Horas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__priority
+msgid "Priority"
+msgstr "Prioridad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__exclude_stage_template_ids
+msgid "Excluded Stages"
+msgstr "Etapas Excluidas"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__tag_ids
+msgid "Tags"
+msgstr "Etiquetas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Documentation"
+msgstr "Documentación"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__documentation_html
+msgid "Documentation"
+msgstr "Documentación"
+
+#. module: helpdesk_extras
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "Requires Customer Approval"
+msgstr "Requiere Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "If checked, tickets in this stage will require customer approval via portal before advancing"
+msgstr "Si está marcado, los tickets en esta etapa requerirán aprobación del cliente vía portal antes de avanzar"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer Approval"
+msgstr "Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer approval status for stages that require it"
+msgstr "Estado de aprobación del cliente para etapas que lo requieren"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Customer Rejection Reason"
+msgstr "Razón de Rechazo del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Reason provided by customer for rejecting the ticket"
+msgstr "Razón proporcionada por el cliente para rechazar el ticket"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:0
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:1
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:2
+msgid "Rejected"
+msgstr "Rechazado"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket approved by customer"
+msgstr "Ticket aprobado por el cliente"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "No reason provided"
+msgstr "Sin razón proporcionada"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket rejected by customer: %s"
+msgstr "Ticket rechazado por el cliente: %s"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Cannot move ticket to next stage. Customer approval required."
+msgstr "No se puede mover el ticket a la siguiente etapa. Se requiere aprobación del cliente."
+
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Current approval status: %s"
+msgstr "Estado de aprobación actual: %s"
+
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."
+

+ 1724 - 0
helpdesk_extras/i18n/es_ES.po

@@ -0,0 +1,1724 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * helpdesk_extras
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-12-07 01:00:00+0000\n"
+"PO-Revision-Date: 2025-12-07 01:00:00+0000\n"
+"Last-Translator: M22 Tech\n"
+"Language-Team: Spanish (Mexico)\n"
+"Language: es_MX\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_basic_support
+msgid "Basic Support"
+msgstr "Soporte Básico"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_premium_support
+msgid "Premium Support"
+msgstr "Soporte Premium"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,name:helpdesk_extras.workflow_template_development
+msgid "Development"
+msgstr "Desarrollo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_basic_support
+msgid "Simple workflow with 3 stages: New, In Progress, and Solved. Includes basic SLA policies for response and resolution times."
+msgstr "Flujo de trabajo simple con 3 etapas: Nuevo, En Progreso y Resuelto. Incluye políticas SLA básicas para tiempos de respuesta y resolución."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_premium_support
+msgid "Enhanced workflow with 4 stages including On Hold. Faster SLA policies for premium customers."
+msgstr "Flujo de trabajo mejorado con 4 etapas incluyendo En Espera. Políticas SLA más rápidas para clientes premium."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template,description:helpdesk_extras.workflow_template_development
+msgid "Workflow for development teams with stages: New, Analysis, In Progress, Testing, and Done. Includes SLA policies by priority."
+msgstr "Flujo de trabajo para equipos de desarrollo con etapas: Nuevo, Análisis, En Progreso, Pruebas y Completado. Incluye políticas SLA por prioridad."
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_new_dev
+msgid "New"
+msgstr "Nuevo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_in_progress_dev
+msgid "In Progress"
+msgstr "En Progreso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_solved_premium
+msgid "Solved"
+msgstr "Resuelto"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_on_hold_premium
+msgid "On Hold"
+msgstr "En Espera"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_analysis
+msgid "Analysis"
+msgstr "Análisis"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_testing
+msgid "Testing"
+msgstr "Pruebas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,name:helpdesk_extras.stage_template_done
+msgid "Done"
+msgstr "Completado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_blocked:helpdesk_extras.stage_template_done
+msgid "Blocked"
+msgstr "Bloqueado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_done:helpdesk_extras.stage_template_done
+msgid "Ready"
+msgstr "Listo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_solved
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_on_hold_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_solved_premium
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_new_dev
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_analysis
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_in_progress_dev
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_testing
+#: model:helpdesk.workflow.template.stage,legend_normal:helpdesk_extras.stage_template_done
+msgid "In Progress"
+msgstr "En Progreso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_response
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_response_normal
+msgid "Response Time - Normal Priority"
+msgstr "Tiempo de Respuesta - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_resolution
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_high
+msgid "Resolution Time - Normal Priority"
+msgstr "Tiempo de Resolución - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_response_high
+msgid "Response Time - High Priority"
+msgstr "Tiempo de Respuesta - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_basic_resolution
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_normal
+msgid "Resolution Time - Normal Priority"
+msgstr "Tiempo de Resolución - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_premium_resolution_high
+msgid "Resolution Time - High Priority"
+msgstr "Tiempo de Resolución - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_high
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "Analysis Time - Normal Priority"
+msgstr "Tiempo de Análisis - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_normal
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_high
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "Completion Time - Normal Priority"
+msgstr "Tiempo de Finalización - Prioridad Normal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_high
+msgid "Analysis Time - High Priority"
+msgstr "Tiempo de Análisis - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_high
+msgid "Completion Time - High Priority"
+msgstr "Tiempo de Finalización - Prioridad Alta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "Analysis Time - Urgent Priority"
+msgstr "Tiempo de Análisis - Prioridad Urgente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,name:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "Completion Time - Urgent Priority"
+msgstr "Tiempo de Finalización - Prioridad Urgente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_basic_response
+msgid "<p>Tickets should be responded to within 4 working hours</p>"
+msgstr "<p>Los tickets deben ser respondidos dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_basic_resolution
+msgid "<p>Tickets should be resolved within 24 working hours</p>"
+msgstr "<p>Los tickets deben ser resueltos dentro de 24 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_response_normal
+msgid "<p>Premium tickets should be responded to within 2 working hours</p>"
+msgstr "<p>Los tickets premium deben ser respondidos dentro de 2 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_resolution_normal
+msgid "<p>Premium tickets should be resolved within 8 working hours</p>"
+msgstr "<p>Los tickets premium deben ser resueltos dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_response_high
+msgid "<p>High priority premium tickets should be responded to within 1 working hour</p>"
+msgstr "<p>Los tickets premium de alta prioridad deben ser respondidos dentro de 1 hora laborable</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_premium_resolution_high
+msgid "<p>High priority premium tickets should be resolved within 4 working hours</p>"
+msgstr "<p>Los tickets premium de alta prioridad deben ser resueltos dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_normal
+msgid "<p>Normal priority tickets should be analyzed within 8 working hours</p>"
+msgstr "<p>Los tickets de prioridad normal deben ser analizados dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_normal
+msgid "<p>Normal priority tickets should be completed within 40 working hours</p>"
+msgstr "<p>Los tickets de prioridad normal deben ser completados dentro de 40 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_high
+msgid "<p>High priority tickets should be analyzed within 4 working hours</p>"
+msgstr "<p>Los tickets de alta prioridad deben ser analizados dentro de 4 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_high
+msgid "<p>High priority tickets should be completed within 16 working hours</p>"
+msgstr "<p>Los tickets de alta prioridad deben ser completados dentro de 16 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_analysis_urgent
+msgid "<p>Urgent tickets should be analyzed within 2 working hours</p>"
+msgstr "<p>Los tickets urgentes deben ser analizados dentro de 2 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model:helpdesk.workflow.template.sla,description:helpdesk_extras.sla_template_dev_completion_urgent
+msgid "<p>Urgent tickets should be completed within 8 working hours</p>"
+msgstr "<p>Los tickets urgentes deben ser completados dentro de 8 horas laborables</p>"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Stages"
+msgstr "Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "SLA Policies"
+msgstr "Políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Teams Using"
+msgstr "Equipos Usando"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Total Stages"
+msgstr "Total de Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_tree
+msgid "Total SLAs"
+msgstr "Total de SLAs"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Teams Using This Template"
+msgstr "Equipos Usando Esta Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Excluded Stages"
+msgstr "Etapas Excluidas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "SLA Policy"
+msgstr "Política SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Stages where time spent will NOT count towards the SLA deadline. Useful for 'On Hold' or 'Waiting for Customer' stages."
+msgstr "Etapas donde el tiempo transcurrido NO contará hacia el plazo del SLA. Útil para etapas como 'En Espera' o 'Esperando Cliente'."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Template Name"
+msgstr "Nombre de Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Archived"
+msgstr "Archivado"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Has Stages"
+msgstr "Tiene Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Has SLAs"
+msgstr "Tiene SLAs"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_search
+msgid "Group By"
+msgstr "Agrupar Por"
+
+#. module: helpdesk_extras
+#: model:ir.ui.menu,name:helpdesk_extras.menu_helpdesk_workflow_template
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Workflow Templates"
+msgstr "Plantillas de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,help:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Create your first workflow template!"
+msgstr "¡Crea tu primera plantilla de flujo de trabajo!"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,help:helpdesk_extras.helpdesk_workflow_template_action
+msgid "Workflow templates allow you to quickly set up stages and SLA policies for helpdesk teams. Create a template with predefined stages and SLAs, then apply it to any team with one click."
+msgstr "Las plantillas de flujo de trabajo te permiten configurar rápidamente etapas y políticas SLA para equipos de helpdesk. Crea una plantilla con etapas y SLAs predefinidos, luego aplícala a cualquier equipo con un clic."
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.action_helpdesk_workflow_template_apply_wizard
+msgid "Apply Workflow Template"
+msgstr "Aplicar Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__team_id
+msgid "Team"
+msgstr "Equipo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__workflow_template_id
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__replace_existing
+msgid "Replace Existing Stages and SLAs"
+msgstr "Reemplazar Etapas y SLAs Existentes"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__replace_existing
+msgid "If checked, existing stages and SLAs will be removed before applying the template"
+msgstr "Si está marcado, las etapas y SLAs existentes se eliminarán antes de aplicar la plantilla"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__stage_count
+msgid "Stages to Create"
+msgstr "Etapas a Crear"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__sla_count
+msgid "SLA Policies to Create"
+msgstr "Políticas SLA a Crear"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__existing_stage_count
+msgid "Existing Stages"
+msgstr "Etapas Existentes"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_apply_wizard__existing_sla_count
+msgid "Existing SLA Policies"
+msgstr "Políticas SLA Existentes"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_apply_wizard_form
+msgid "Apply Template"
+msgstr "Aplicar Plantilla"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_apply_wizard_form
+msgid "Summary"
+msgstr "Resumen"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_team_view_form_inherit_helpdesk_extras
+msgid "Select a workflow template to quickly set up stages and SLA policies"
+msgstr "Selecciona una plantilla de flujo de trabajo para configurar rápidamente etapas y políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "Stages"
+msgstr "Etapas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "SLA Policies"
+msgstr "Políticas SLA"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_kanban
+msgid "team(s)"
+msgstr "equipo(s)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Teams"
+msgstr "Equipos"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "e.g., Basic Support, Premium Support"
+msgstr "ej., Soporte Básico, Soporte Premium"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Describe this workflow template..."
+msgstr "Describe esta plantilla de flujo de trabajo..."
+
+
+#. module: helpdesk_extras
+#: model:helpdesk.request.type,name:helpdesk_extras.type_incident
+msgid "Incident"
+msgstr "Incidente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.request.type,name:helpdesk_extras.type_improvement
+msgid "Improvement"
+msgstr "Mejora"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:0
+msgid "Critical"
+msgstr "Crítico"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:1
+msgid "High"
+msgstr "Alto"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,business_impact:2
+msgid "Normal"
+msgstr "Normal"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__request_type_id
+msgid "Request Type"
+msgstr "Tipo de Solicitud"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__request_type_code
+msgid "Request Type Code"
+msgstr "Código de Tipo de Solicitud"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__affected_module_id
+msgid "Affected Module"
+msgstr "Módulo Afectado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__affected_user_email
+msgid "Affected User Email"
+msgstr "Email del Usuario Afectado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__business_impact
+msgid "Business Impact"
+msgstr "Impacto de Negocio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__reproduce_steps
+msgid "Steps to Reproduce"
+msgstr "Pasos para Reproducir"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__business_goal
+msgid "Business Goal"
+msgstr "Objetivo de Negocio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__attachment_ids
+msgid "Attachments"
+msgstr "Adjuntos"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__estimated_hours
+msgid "Estimated Hours"
+msgstr "Horas Estimadas"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__approval_status
+msgid "Approval Status"
+msgstr "Estado de Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "N/A"
+msgstr "N/A"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Waiting for Approval"
+msgstr "Esperando Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,approval_status:helpdesk_extras
+msgid "Rejected"
+msgstr "Rechazado"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__client_authorization
+msgid "Client Authorization"
+msgstr "Autorización del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__has_template
+msgid "Has Template"
+msgstr "Tiene Plantilla"
+
+#. module: helpdesk_extras
+#. odoo-python
+#: code:addons/helpdesk_extras/models/helpdesk_team.py:896
+#: code:addons/helpdesk_extras/models/helpdesk_team.py:1083
+msgid "-- Select --"
+msgstr "-- Seleccionar --"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Request Information"
+msgstr "Información de Solicitud"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Details"
+msgstr "Detalles"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_ticket_view_form_inherit_helpdesk_extras
+msgid "Approval & Billing"
+msgstr "Aprobación y Facturación"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__name
+msgid "Name"
+msgstr "Nombre"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__code
+msgid "Code"
+msgstr "Código"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__active
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__is_main_application
+msgid "Main Application"
+msgstr "Aplicación Principal"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_affected_module__description
+msgid "Description"
+msgstr "Descripción"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Active"
+msgstr "Activo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Inactive"
+msgstr "Inactivo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_affected_module_view_search
+msgid "Main Applications"
+msgstr "Aplicaciones Principales"
+
+#. module: helpdesk_extras
+#: model:ir.actions.act_window,name:helpdesk_extras.helpdesk_affected_module_action
+msgid "Affected Modules"
+msgstr "Módulos Afectados"
+
+#. module: helpdesk_extras
+#: model:ir.ui.menu,name:helpdesk_extras.helpdesk_affected_module_menu
+msgid "Affected Modules"
+msgstr "Módulos Afectados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_accountant
+msgid "Accounting"
+msgstr "Contabilidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_ai
+msgid "AI Base"
+msgstr "Base de IA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_ai_app
+msgid "AI"
+msgstr "IA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_analytic
+msgid "Analytic Accounting"
+msgstr "Contabilidad Analítica"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_analytic_enterprise
+msgid "Analytic Accounting Enterprise"
+msgstr "Contabilidad Analítica"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_appointment
+msgid "Appointments"
+msgstr "Citas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_approvals
+msgid "Approvals"
+msgstr "Aprobaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_barcodes
+msgid "Barcode"
+msgstr "Código de Barras"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_base
+msgid "Base"
+msgstr "Base"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_calendar
+msgid "Calendar"
+msgstr "Calendario"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_certificate
+msgid "Certificate"
+msgstr "Certificado"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_cloud_storage
+msgid "Cloud Storage"
+msgstr "Almacenamiento en la Nube"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_contacts
+msgid "Contacts"
+msgstr "Contactos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_crm
+msgid "CRM"
+msgstr "CRM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_loyalty
+msgid "Coupons & Loyalty"
+msgstr "Cupones y Fidelidad"
+
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_attendance
+msgid "Attendances"
+msgstr "Asistencias"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_cohort
+msgid "Cohort View"
+msgstr "Vista de cohorte"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_portal
+msgid "Customer Portal"
+msgstr "Portal del cliente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_rating
+msgid "Customer Rating"
+msgstr "Valoración del cliente"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_board
+msgid "Dashboards"
+msgstr "Tableros"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_databases
+msgid "Databases"
+msgstr "Bases de datos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_delivery
+msgid "Delivery Costs"
+msgstr "Gastos de envío"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mail
+msgid "Discuss"
+msgstr "Conversaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_documents
+msgid "Documents"
+msgstr "Documentos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_esg
+msgid "ESG"
+msgstr "ASG"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mass_mailing
+msgid "Email Marketing"
+msgstr "Marketing por correo electrónico"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_contract
+msgid "Employee Contracts"
+msgstr "Contratos de los empleados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr
+msgid "Employees"
+msgstr "Empleados"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_equity
+msgid "Equity"
+msgstr "Patrimonio"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_event
+msgid "Events Organization"
+msgstr "Organización de eventos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_expense
+msgid "Expenses"
+msgstr "Gastos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_industry_fsm
+msgid "Field Service"
+msgstr "Servicio de campo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_fleet
+msgid "Fleet"
+msgstr "Flota"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_frontdesk
+msgid "Frontdesk"
+msgstr "Recepción"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_gamification
+msgid "Gamification"
+msgstr "Ludificación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_grid
+msgid "Grid View"
+msgstr "Vista de cuadrícula"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_helpdesk
+msgid "Helpdesk"
+msgstr "Servicio de asistencia"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_bus
+msgid "IM Bus"
+msgstr "Bus IM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_iot
+msgid "Internet of Things"
+msgstr "Internet de las cosas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_stock
+msgid "Inventory"
+msgstr "Inventarios"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_account
+msgid "Invoicing"
+msgstr "Facturación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_digest
+msgid "KPI Digests"
+msgstr "Resúmenes de KPI"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_knowledge
+msgid "Knowledge"
+msgstr "Información"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_link_tracker
+msgid "Link Tracker"
+msgstr "Rastreador de enlaces"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_im_livechat
+msgid "Live Chat"
+msgstr "Chat en directo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_lunch
+msgid "Lunch"
+msgstr "Comida"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_maintenance
+msgid "Maintenance"
+msgstr "Mantenimiento"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_mrp
+msgid "Manufacturing"
+msgstr "Fabricación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_map
+msgid "Map View"
+msgstr "Vista del mapa"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_marketing_automation
+msgid "Marketing Automation"
+msgstr "Automatización de marketing"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_room
+msgid "Meeting Rooms"
+msgstr "Sala de reuniones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_membership
+msgid "Members"
+msgstr "Miembros"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_mobile
+msgid "Mobile"
+msgstr "Móvil"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_onboarding
+msgid "Onboarding Toolbox"
+msgstr "Caja de herramientas para la incorporación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_partnership
+msgid "Partnership / Membership"
+msgstr "Asociación / Afiliación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_payment
+msgid "Payment Engine"
+msgstr "Motor de pago"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_payroll
+msgid "Payroll"
+msgstr "Nómina"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_phone_validation
+msgid "Phone Numbers Validation"
+msgstr "Validación de números de teléfono"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_planning
+msgid "Planning"
+msgstr "Planificación"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_point_of_sale
+msgid "Point of Sale"
+msgstr "Punto de venta"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_privacy_lookup
+msgid "Privacy"
+msgstr "Privacidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_product
+msgid "Products & Pricelists"
+msgstr "Productos y listas de precios"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_project
+msgid "Project"
+msgstr "Proyecto"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_purchase
+msgid "Purchase"
+msgstr "Compra"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_quality_control
+msgid "Quality"
+msgstr "Calidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_quality
+msgid "Quality Base"
+msgstr "Base de calidad"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_rpc
+msgid "RPC endpoints"
+msgstr "Puntos de conexión RPC"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_recruitment
+msgid "Recruitment"
+msgstr "Reclutamiento"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_repair
+msgid "Repairs"
+msgstr "Reparaciones"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_resource
+msgid "Resource"
+msgstr "Recurso"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sms
+msgid "SMS gateway"
+msgstr "Puerta de enlace SMS"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sale
+msgid "Sales"
+msgstr "Ventas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sales_team
+msgid "Sales Teams"
+msgstr "Equipos de ventas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sign
+msgid "Sign"
+msgstr "Firmar"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_snailmail
+msgid "Snail Mail"
+msgstr "Correo postal"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_social
+msgid "Social Marketing"
+msgstr "Marketing social"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_spreadsheet
+msgid "Spreadsheet"
+msgstr "Hoja de cálculo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_studio
+msgid "Studio"
+msgstr "Studio"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_survey
+msgid "Surveys"
+msgstr "Encuestas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_timesheet
+msgid "Task Logs"
+msgstr "Registros de tareas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_hr_holidays
+msgid "Time Off"
+msgstr "Ausencia"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_timer
+msgid "Timer"
+msgstr "Temporizador"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_timesheet_grid
+msgid "Timesheets"
+msgstr "Partes de horas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_tour
+msgid "Tours"
+msgstr "Recorridos"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_transifex
+msgid "Transifex integration"
+msgstr "Integración en Transifex"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_utm
+msgid "UTM Trackers"
+msgstr "Rastreadores UTM"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_uom
+msgid "Units of measure"
+msgstr "Unidades de medida"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_unsplash
+msgid "Unsplash Image Library"
+msgstr "Biblioteca de imágenes de Unsplash"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_voip
+msgid "VoIP"
+msgstr "VoIP"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_stock_account
+msgid "WMS Accounting"
+msgstr "Contabilidad del SGA"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web
+msgid "Web"
+msgstr "Web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_editor
+msgid "Web Editor"
+msgstr "Editor web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_enterprise
+msgid "Web Enterprise"
+msgstr "Web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_gantt
+msgid "Web Gantt"
+msgstr "Diagrama Gantt web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_web_hierarchy
+msgid "Web Hierarchy"
+msgstr "Jerarquía web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_website
+msgid "Website"
+msgstr "Sitio web"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_whatsapp
+msgid "WhatsApp Messaging"
+msgstr "Mensajes de WhatsApp"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_worksheet
+msgid "Worksheet"
+msgstr "Hoja de trabajo"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_account_accountant
+msgid "Account Accountant"
+msgstr "Contabilidad de Cuentas"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_preventa
+msgid "Preventa"
+msgstr "Preventa"
+
+#. module: helpdesk_extras
+#: model:helpdesk.affected.module,name:helpdesk_extras.module_sale_management
+msgid "Sales Management"
+msgstr "Gestión de Ventas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_ids
+msgid "Form Fields"
+msgstr "Campos del Formulario"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__field_count
+msgid "Form Fields Count"
+msgstr "Cantidad de Campos"
+
+#. module: helpdesk_extras
+#: model:ir.model,name:helpdesk_extras.model_helpdesk_workflow_template_field
+msgid "Workflow Template Form Field"
+msgstr "Campo de Formulario de Plantilla de Flujo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__workflow_template_id
+msgid "Workflow Template"
+msgstr "Plantilla de Flujo de Trabajo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_id
+msgid "Field"
+msgstr "Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_name
+msgid "Field Name"
+msgstr "Nombre del Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__field_type
+msgid "Field Type"
+msgstr "Tipo de Campo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__label_custom
+msgid "Custom Label"
+msgstr "Etiqueta Personalizada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__placeholder
+msgid "Placeholder"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__default_value
+msgid "Default Value"
+msgstr "Valor por Defecto"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__help_text
+msgid "Help Text"
+msgstr "Texto de Ayuda"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__widget
+msgid "Widget"
+msgstr "Widget"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_type
+msgid "Selection Type"
+msgstr "Tipo de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__selection_options
+msgid "Selection Options"
+msgstr "Opciones de Selección"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__rows
+msgid "Height (Rows)"
+msgstr "Altura (Filas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__input_type
+msgid "Input Type"
+msgstr "Tipo de Entrada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__sequence
+msgid "Sequence"
+msgstr "Secuencia"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__required
+msgid "Required"
+msgstr "Obligatorio"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__model_required
+msgid "Model Required"
+msgstr "Obligatorio del Modelo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_dependency
+msgid "Visibility Dependency"
+msgstr "Dependencia de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_condition
+msgid "Visibility Condition Value"
+msgstr "Valor de Condición de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_comparator
+msgid "Visibility Comparator"
+msgstr "Comparador de Visibilidad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_field__visibility_between
+msgid "Visibility Between (End Value)"
+msgstr "Visibilidad Entre (Valor Final)"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Dropdown List"
+msgstr "Lista Desplegable"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,selection_type
+msgid "Radio"
+msgstr "Botones de Radio"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Text"
+msgstr "Texto"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Email"
+msgstr "Correo Electrónico"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Telephone"
+msgstr "Teléfono"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,input_type
+msgid "Url"
+msgstr "URL"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is equal to"
+msgstr "Es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not equal to"
+msgstr "No es igual a"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Contains"
+msgstr "Contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Doesn't contain"
+msgstr "No contiene"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is set"
+msgstr "Está definido"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.workflow.template.field,visibility_comparator
+msgid "Is not set"
+msgstr "No está definido"
+
+#. module: helpdesk_extras
+#. odoo-python
+#: code:addons/helpdesk_extras/models/helpdesk_workflow_template_field.py:0
+msgid "Cannot delete model required field(s): %s. This field is mandatory for the model and cannot be removed. Try hiding it with the 'Visibility' option instead and add it a default value."
+msgstr "No se puede eliminar el/los campo(s) obligatorio(s) del modelo: %s. Este campo es obligatorio para el modelo y no puede ser eliminado. Intenta ocultarlo con la opción 'Visibilidad' y agrega un valor por defecto."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Field Configuration"
+msgstr "Configuración del Campo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Display Options"
+msgstr "Opciones de Visualización"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Visibility Conditions"
+msgstr "Condiciones de Visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Form Field"
+msgstr "Campo de Formulario"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Custom label (optional)"
+msgstr "Etiqueta personalizada (opcional)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Placeholder text"
+msgstr "Texto de ejemplo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Default value"
+msgstr "Valor por defecto"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Help text (HTML)"
+msgstr "Texto de ayuda (HTML)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select field for visibility condition"
+msgstr "Seleccionar campo para condición de visibilidad"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select comparator"
+msgstr "Seleccionar comparador"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Enter value to compare"
+msgstr "Ingresa el valor para comparar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Select value"
+msgstr "Seleccionar valor"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "End value for range (date/datetime)"
+msgstr "Valor final para rango (fecha/hora)"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "e.g., Basic Support, Premium Support"
+msgstr "ej., Soporte Básico, Soporte Premium"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Describe this workflow template..."
+msgstr "Describe esta plantilla de flujo de trabajo..."
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__name
+msgid "Stage Name"
+msgstr "Nombre de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__fold
+msgid "Folded"
+msgstr "Plegada"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__description
+msgid "Stage Description"
+msgstr "Descripción de Etapa"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__name
+msgid "SLA Name"
+msgstr "Nombre del SLA"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__stage_template_id
+msgid "Target Stage"
+msgstr "Etapa Objetivo"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__time
+msgid "Time (Hours)"
+msgstr "Tiempo (Horas)"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__priority
+msgid "Priority"
+msgstr "Prioridad"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__exclude_stage_template_ids
+msgid "Excluded Stages"
+msgstr "Etapas Excluidas"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_sla__tag_ids
+msgid "Tags"
+msgstr "Etiquetas"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.helpdesk_workflow_template_view_form
+msgid "Documentation"
+msgstr "Documentación"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template__documentation_html
+msgid "Documentation"
+msgstr "Documentación"
+
+#. module: helpdesk_extras
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "Requires Customer Approval"
+msgstr "Requiere Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "If checked, tickets in this stage will require customer approval via portal before advancing"
+msgstr "Si está marcado, los tickets en esta etapa requerirán aprobación del cliente vía portal antes de avanzar"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer Approval"
+msgstr "Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer approval status for stages that require it"
+msgstr "Estado de aprobación del cliente para etapas que lo requieren"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Customer Rejection Reason"
+msgstr "Razón de Rechazo del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Reason provided by customer for rejecting the ticket"
+msgstr "Razón proporcionada por el cliente para rechazar el ticket"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:0
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:1
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:2
+msgid "Rejected"
+msgstr "Rechazado"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket approved by customer"
+msgstr "Ticket aprobado por el cliente"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "No reason provided"
+msgstr "Sin razón proporcionada"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket rejected by customer: %s"
+msgstr "Ticket rechazado por el cliente: %s"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Cannot move ticket to next stage. Customer approval required."
+msgstr "No se puede mover el ticket a la siguiente etapa. Se requiere aprobación del cliente."
+
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Current approval status: %s"
+msgstr "Estado de aprobación actual: %s"
+
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."
+

+ 279 - 0
helpdesk_extras/i18n/es_MX.po

@@ -1452,3 +1452,282 @@ msgstr "Documentación"
 msgid "Documentation"
 msgid "Documentation"
 msgstr "Documentación"
 msgstr "Documentación"
 
 
+#. module: helpdesk_extras
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "Requires Customer Approval"
+msgstr "Requiere Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_stage__requires_customer_approval
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_workflow_template_stage__requires_customer_approval
+msgid "If checked, tickets in this stage will require customer approval via portal before advancing"
+msgstr "Si está marcado, los tickets en esta etapa requerirán aprobación del cliente vía portal antes de avanzar"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer Approval"
+msgstr "Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_approval_status
+msgid "Customer approval status for stages that require it"
+msgstr "Estado de aprobación del cliente para etapas que lo requieren"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,field_description:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Customer Rejection Reason"
+msgstr "Razón de Rechazo del Cliente"
+
+#. module: helpdesk_extras
+#: model:ir.model.fields,help:helpdesk_extras.field_helpdesk_ticket__customer_rejection_reason
+msgid "Reason provided by customer for rejecting the ticket"
+msgstr "Razón proporcionada por el cliente para rechazar el ticket"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:0
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:1
+msgid "Approved"
+msgstr "Aprobado"
+
+#. module: helpdesk_extras
+#: selection:helpdesk.ticket,customer_approval_status:2
+msgid "Rejected"
+msgstr "Rechazado"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status:"
+msgstr "Estado de Aprobación del Cliente:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason:"
+msgstr "Razón de Rechazo:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket approved by customer"
+msgstr "Ticket aprobado por el cliente"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "No reason provided"
+msgstr "Sin razón proporcionada"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/controllers/helpdesk_portal.py:0
+msgid "Ticket rejected by customer: %s"
+msgstr "Ticket rechazado por el cliente: %s"
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Cannot move ticket to next stage. Customer approval required."
+msgstr "No se puede mover el ticket a la siguiente etapa. Se requiere aprobación del cliente."
+
+
+#. module: helpdesk_extras
+#: code:addons/helpdesk_extras/models/helpdesk_ticket.py:0
+msgid "Current approval status: %s"
+msgstr "Estado de aprobación actual: %s"
+
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approval Required"
+msgstr "Aprobación Requerida"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This ticket requires your approval to proceed. Please review and approve or reject."
+msgstr "Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve"
+msgstr "Aprobar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject"
+msgstr "Rechazar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Customer Approval Status"
+msgstr "Estado de Aprobación del Cliente"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Pending Approval"
+msgstr "Pendiente de Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Rejection Reason"
+msgstr "Razón de Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Approve Ticket"
+msgstr "Aprobar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Are you sure you want to approve this ticket?"
+msgstr "¿Está seguro de que desea aprobar este ticket?"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "This action will mark the ticket as approved and allow it to proceed."
+msgstr "Esta acción marcará el ticket como aprobado y le permitirá continuar."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Cancel"
+msgstr "Cancelar"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Approval"
+msgstr "Confirmar Aprobación"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Reject Ticket"
+msgstr "Rechazar Ticket"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Please provide a reason for rejecting this ticket:"
+msgstr "Por favor proporcione una razón para rechazar este ticket:"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Enter your reason..."
+msgstr "Ingrese su razón..."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Confirm Rejection"
+msgstr "Confirmar Rechazo"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Thank you!"
+msgstr "¡Gracias!"
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have approved this ticket."
+msgstr "Ha aprobado este ticket."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "Noted."
+msgstr "Tomado en cuenta."
+
+#. module: helpdesk_extras
+#: model_terms:ir.ui.view,arch_db:helpdesk_extras.portal_helpdesk_ticket_detail_extras
+msgid "You have rejected this ticket."
+msgstr "Ha rechazado este ticket."

+ 1 - 0
helpdesk_extras/models/__init__.py

@@ -4,6 +4,7 @@ from . import helpdesk_team_collaborator
 from . import helpdesk_team
 from . import helpdesk_team
 from . import helpdesk_request_type
 from . import helpdesk_request_type
 from . import helpdesk_ticket
 from . import helpdesk_ticket
+from . import helpdesk_stage
 from . import helpdesk_workflow_template
 from . import helpdesk_workflow_template
 from . import helpdesk_workflow_template_stage
 from . import helpdesk_workflow_template_stage
 from . import helpdesk_workflow_template_sla
 from . import helpdesk_workflow_template_sla

+ 14 - 0
helpdesk_extras/models/helpdesk_stage.py

@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, fields, models
+
+
+class HelpdeskStage(models.Model):
+    _inherit = 'helpdesk.stage'
+
+    requires_customer_approval = fields.Boolean(
+        string=_('Requires Customer Approval'),
+        default=False,
+        help=_('If checked, tickets in this stage will require customer approval via portal before advancing')
+    )

+ 3 - 0
helpdesk_extras/models/helpdesk_team.py

@@ -1320,6 +1320,8 @@ class HelpdeskTeamExtras(models.Model):
                     update_vals['legend_done'] = stage_template.legend_done
                     update_vals['legend_done'] = stage_template.legend_done
                 if stage_template.legend_normal != existing_stage.legend_normal:
                 if stage_template.legend_normal != existing_stage.legend_normal:
                     update_vals['legend_normal'] = stage_template.legend_normal
                     update_vals['legend_normal'] = stage_template.legend_normal
+                if stage_template.requires_customer_approval != existing_stage.requires_customer_approval:
+                    update_vals['requires_customer_approval'] = stage_template.requires_customer_approval
                 
                 
                 if update_vals:
                 if update_vals:
                     existing_stage.write(update_vals)
                     existing_stage.write(update_vals)
@@ -1338,6 +1340,7 @@ class HelpdeskTeamExtras(models.Model):
                     'legend_blocked': stage_template.legend_blocked,
                     'legend_blocked': stage_template.legend_blocked,
                     'legend_done': stage_template.legend_done,
                     'legend_done': stage_template.legend_done,
                     'legend_normal': stage_template.legend_normal,
                     'legend_normal': stage_template.legend_normal,
+                    'requires_customer_approval': stage_template.requires_customer_approval,
                     'team_ids': [(4, self.id)],
                     'team_ids': [(4, self.id)],
                 }
                 }
                 real_stage = self.env['helpdesk.stage'].create(stage_vals)
                 real_stage = self.env['helpdesk.stage'].create(stage_vals)

+ 47 - 1
helpdesk_extras/models/helpdesk_ticket.py

@@ -2,6 +2,7 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 
 from odoo import _, api, fields, models
 from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
 
 
 
 
 class HelpdeskTicket(models.Model):
 class HelpdeskTicket(models.Model):
@@ -74,6 +75,21 @@ class HelpdeskTicket(models.Model):
         tracking=True,
         tracking=True,
         help=_("Status of the approval workflow")
         help=_("Status of the approval workflow")
     )
     )
+    customer_approval_status = fields.Selection(
+        [
+            ('pending', _('Pending Approval')),
+            ('approved', _('Approved')),
+            ('rejected', _('Rejected')),
+        ],
+        string=_('Customer Approval'),
+        default='pending',
+        tracking=True,
+        help=_("Customer approval status for stages that require it")
+    )
+    customer_rejection_reason = fields.Text(
+        string=_('Customer Rejection Reason'),
+        help=_("Reason provided by customer for rejecting the ticket")
+    )
     has_template = fields.Boolean(
     has_template = fields.Boolean(
         string=_('Has Template'),
         string=_('Has Template'),
         compute='_compute_has_template',
         compute='_compute_has_template',
@@ -185,8 +201,38 @@ class HelpdeskTicket(models.Model):
         return super().create(vals_list)
         return super().create(vals_list)
 
 
     def write(self, vals):
     def write(self, vals):
-        """Override write to recalculate priority when request_type or business_impact changes."""
+        """Override write to recalculate priority and validate customer approval."""
+        # Validar aprobación del cliente antes de cambiar de etapa
+        if 'stage_id' in vals:
+            for ticket in self:
+                # Si la etapa actual requiere aprobación del cliente
+                if ticket.stage_id and ticket.stage_id.requires_customer_approval:
+                    # Verificar que el ticket esté aprobado antes de permitir el cambio
+                    if ticket.customer_approval_status != 'approved':
+                        # Validation message
+                        raise ValidationError(_(
+                            "Cannot move ticket to next stage. Customer approval required.\n"
+                            "Current approval status: %s"
+                        ) % dict(ticket._fields['customer_approval_status'].selection).get(ticket.customer_approval_status))
+        
         result = super().write(vals)
         result = super().write(vals)
+        
+        # Resetear estado de aprobación al entrar a una nueva etapa que requiere aprobación
+        if 'stage_id' in vals:
+            for ticket in self:
+                if ticket.stage_id and ticket.stage_id.requires_customer_approval:
+                    # Si el ticket acaba de entrar a una etapa que requiere aprobación,
+                    # resetear el estado a 'pending' (a menos que ya esté aprobado desde antes)
+                    if ticket.customer_approval_status == 'approved':
+                        # Mantener aprobado si ya lo estaba
+                        pass
+                    else:
+                        # Resetear a pending para la nueva etapa
+                        super(HelpdeskTicket, ticket).write({
+                            'customer_approval_status': 'pending',
+                            'customer_rejection_reason': False,
+                        })
+        
         # If either field was updated, recalculate priority for affected records
         # If either field was updated, recalculate priority for affected records
         if 'request_type_id' in vals or 'business_impact' in vals:
         if 'request_type_id' in vals or 'business_impact' in vals:
             for record in self:
             for record in self:

+ 6 - 1
helpdesk_extras/models/helpdesk_workflow_template_stage.py

@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 
 
-from odoo import fields, models
+from odoo import _, fields, models
 
 
 
 
 class HelpdeskWorkflowTemplateStage(models.Model):
 class HelpdeskWorkflowTemplateStage(models.Model):
@@ -60,4 +60,9 @@ class HelpdeskWorkflowTemplateStage(models.Model):
         translate=True,
         translate=True,
         required=True
         required=True
     )
     )
+    requires_customer_approval = fields.Boolean(
+        string=_('Requires Customer Approval'),
+        default=False,
+        help=_('If checked, tickets in this stage will require customer approval via portal before advancing')
+    )
 
 

+ 14 - 0
helpdesk_extras/static/src/js/m2o_urlrelation_guard_patch.js

@@ -0,0 +1,14 @@
+/** @odoo-module **/
+
+import { patch } from "@web/core/utils/patch";
+import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
+
+patch(Many2OneField.prototype, {
+    get urlRelation() {
+        const rel = this.relation;
+        if (!rel || typeof rel !== "string") {
+            return "m-unknown";
+        }
+        return rel.includes(".") ? rel : `m-${rel}`;
+    },
+});

+ 208 - 34
helpdesk_extras/views/helpdesk_portal_templates.xml

@@ -21,44 +21,218 @@
     
     
     <!-- Extend ticket detail view to show new fields -->
     <!-- Extend ticket detail view to show new fields -->
     <template id="portal_helpdesk_ticket_detail_extras" name="Portal Helpdesk Ticket Detail Extras" inherit_id="helpdesk.tickets_followup" priority="20">
     <template id="portal_helpdesk_ticket_detail_extras" name="Portal Helpdesk Ticket Detail Extras" inherit_id="helpdesk.tickets_followup" priority="20">
-        <!-- Add new fields after "Reported on" -->
-        <xpath expr="//div[@name='description']" position="before">
-            <div t-if="ticket.request_type_id" class="row mb-4">
-                <strong class="col-lg-3">Tipo de Solicitud</strong>
-                <span class="col-lg-9" t-field="ticket.request_type_id.name"/>
-            </div>
-            <div t-if="ticket.affected_module_id" class="row mb-4">
-                <strong class="col-lg-3">Módulo Afectado</strong>
-                <span class="col-lg-9" t-field="ticket.affected_module_id.name"/>
+        <!-- Remove hard-coded fields, render everything dynamically based on workflow template -->
+        <xpath expr="//div[@name='description']" position="replace">
+            <!-- Render all workflow template fields in order, considering visibility -->
+            <t t-if="ticket.team_id.workflow_template_id and ticket.team_id.workflow_template_id.field_ids">
+                <t t-foreach="ticket.team_id.workflow_template_id.field_ids.sorted(lambda f: f.sequence)" t-as="template_field">
+                    <!-- Check visibility condition -->
+                    <t t-set="is_visible" t-value="True"/>
+                    <t t-if="template_field.visibility_dependency">
+                        <t t-set="dependency_field_name" t-value="template_field.visibility_dependency.name"/>
+                        <t t-set="field_value" t-value="ticket[dependency_field_name] if dependency_field_name in ticket._fields else False"/>
+                        
+                        <!-- Evaluate visibility based on comparator -->
+                        <t t-if="template_field.visibility_comparator == 'equal'">
+                            <t t-if="template_field.visibility_dependency.ttype == 'many2one'">
+                                <t t-set="is_visible" t-value="field_value.id == template_field.visibility_condition_m2o_id if field_value else False"/>
+                            </t>
+                            <t t-elif="template_field.visibility_dependency.ttype == 'selection'">
+                                <t t-set="is_visible" t-value="field_value == template_field.visibility_condition_selection"/>
+                            </t>
+                            <t t-else="">
+                                <t t-set="is_visible" t-value="str(field_value) == template_field.visibility_condition"/>
+                            </t>
+                        </t>
+                        <t t-elif="template_field.visibility_comparator == '!equal'">
+                            <t t-if="template_field.visibility_dependency.ttype == 'many2one'">
+                                <t t-set="is_visible" t-value="field_value.id != template_field.visibility_condition_m2o_id if field_value else True"/>
+                            </t>
+                            <t t-elif="template_field.visibility_dependency.ttype == 'selection'">
+                                <t t-set="is_visible" t-value="field_value != template_field.visibility_condition_selection"/>
+                            </t>
+                            <t t-else="">
+                                <t t-set="is_visible" t-value="str(field_value) != template_field.visibility_condition"/>
+                            </t>
+                        </t>
+                        <t t-elif="template_field.visibility_comparator == 'set'">
+                            <t t-set="is_visible" t-value="bool(field_value)"/>
+                        </t>
+                        <t t-elif="template_field.visibility_comparator == '!set'">
+                            <t t-set="is_visible" t-value="not bool(field_value)"/>
+                        </t>
+                        <t t-elif="template_field.visibility_comparator == 'contains'">
+                            <t t-set="is_visible" t-value="template_field.visibility_condition in str(field_value)"/>
+                        </t>
+                        <t t-elif="template_field.visibility_comparator == '!contains'">
+                            <t t-set="is_visible" t-value="template_field.visibility_condition not in str(field_value)"/>
+                        </t>
+                    </t>
+                    
+                    <!-- Display field if visible and has value (or is description which is special) -->
+                    <t t-set="field_name" t-value="template_field.field_name"/>
+                    <t t-set="field_val" t-value="ticket[field_name] if field_name in ticket._fields else False"/>
+                    
+                    <div t-if="is_visible and (field_val or field_name == 'description')" class="row mb-4" t-att-name="field_name">
+                        <strong class="col-lg-3" t-esc="template_field.label_custom or template_field.field_id.field_description"/>
+                        <div class="col-lg-9">
+                            <t t-if="template_field.field_type in ['char', 'text']">
+                                <span t-out="field_val"/>
+                            </t>
+                            <t t-elif="template_field.field_type == 'html'">
+                                <div t-out="field_val"/>
+                            </t>
+                            <t t-elif="template_field.field_type in ['integer', 'float', 'monetary']">
+                                <span t-out="field_val"/>
+                            </t>
+                            <t t-elif="template_field.field_type in ['date', 'datetime']">
+                                <span t-out="field_val"/>
+                            </t>
+                            <t t-elif="template_field.field_type == 'boolean'">
+                                <span t-if="field_val">Yes</span>
+                                <span t-else="">No</span>
+                            </t>
+                            <t t-elif="template_field.field_type == 'selection'">
+                                <span t-out="field_val"/>
+                            </t>
+                            <t t-elif="template_field.field_type == 'many2one'">
+                                <span t-out="field_val.display_name if field_val else ''"/>
+                            </t>
+                            <t t-elif="template_field.field_type in ['many2many', 'one2many']">
+                                <t t-foreach="field_val" t-as="item">
+                                    <span class="badge bg-primary me-1" t-esc="item.display_name"/>
+                                </t>
+                            </t>
+                            <t t-else="">
+                                <span t-out="field_val"/>
+                            </t>
+                        </div>
+                    </div>
+                </t>
+            </t>
+            
+            <!-- Fallback: If no workflow template, show description at least -->
+            <div t-else="" class="row mb-4" name="description">
+                <strong class="col-lg-3">Description</strong>
+                <div class="col-lg-9" t-field="ticket.description"/>
             </div>
             </div>
-            <div t-if="ticket.business_impact" class="row mb-4">
-                <strong class="col-lg-3">Impacto de Negocio</strong>
-                <span class="col-lg-9">
-                    <t t-if="ticket.business_impact == '0'">Crítico</t>
-                    <t t-elif="ticket.business_impact == '1'">Alto</t>
-                    <t t-elif="ticket.business_impact == '2'">Normal</t>
-                </span>
+            
+            <!-- Customer Approval Section - ALWAYS AT THE END -->
+            <div t-if="ticket.stage_id.requires_customer_approval" class="row mb-4">
+                <div class="col-12">
+                    <div t-if="ticket.customer_approval_status == 'pending'" class="alert alert-warning mt-3" role="alert">
+                        <h5><i class="fa fa-exclamation-triangle"/> <span>Aprobación Requerida</span></h5>
+                        <p>Este ticket requiere su aprobación para continuar. Por favor revise y apruebe o rechace.</p>
+                        
+                        <div class="d-flex gap-2 mt-3">
+                            <!-- Approve Button (with confirmation modal) -->
+                            <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#approveModal">
+                                <i class="fa fa-check"/> <span>Aprobar</span>
+                            </button>
+                            
+                            <!-- Reject Button (with modal) -->
+                            <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#rejectModal">
+                                <i class="fa fa-times"/> <span>Rechazar</span>
+                            </button>
+                        </div>
+                    </div>
+                    
+                    <!-- Approval status badge -->
+                    <div class="mt-2">
+                        <strong><span>Estado de Aprobación del Cliente</span>:</strong>
+                        <span t-if="ticket.customer_approval_status == 'approved'" class="badge bg-success ms-2">
+                            <i class="fa fa-check"/> <span>Aprobado</span>
+                        </span>
+                        <span t-elif="ticket.customer_approval_status == 'rejected'" class="badge bg-danger ms-2">
+                            <i class="fa fa-times"/> <span>Rechazado</span>
+                        </span>
+                        <span t-else="" class="badge bg-warning ms-2">
+                            <i class="fa fa-clock-o"/> <span>Pendiente de Aprobación</span>
+                        </span>
+                    </div>
+                    
+                    <!-- Show rejection reason if exists -->
+                    <div t-if="ticket.customer_rejection_reason" class="mt-2">
+                        <strong><span>Razón de Rechazo</span>:</strong>
+                        <p class="text-muted" t-esc="ticket.customer_rejection_reason"/>
+                    </div>
+                </div>
             </div>
             </div>
-            <div t-if="ticket.reproduce_steps and ticket.request_type_code == 'incident'" class="row mb-4">
-                <strong class="col-lg-3">Pasos para Reproducir</strong>
-                <div class="col-lg-9" t-field="ticket.reproduce_steps"/>
+            
+            <!-- Modal for Approve -->
+            <div class="modal fade" id="approveModal" tabindex="-1" aria-hidden="true">
+                <div class="modal-dialog">
+                    <div class="modal-content">
+                        <form method="POST" t-attf-action="/my/ticket/#{ticket.id}/approve?access_token=#{ticket.access_token}">
+                            <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+                            
+                            <div class="modal-header bg-success text-white">
+                                <h5 class="modal-title">Aprobar Ticket</h5>
+                                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"/>
+                            </div>
+                            
+                            <div class="modal-body">
+                                <p>¿Está seguro de que desea aprobar este ticket?</p>
+                                <div class="alert alert-info">
+                                    <i class="fa fa-info-circle"/> <span>Esta acción marcará el ticket como aprobado y le permitirá continuar.</span>
+                                </div>
+                            </div>
+                            
+                            <div class="modal-footer">
+                                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
+                                    Cancelar
+                                </button>
+                                <button type="submit" class="btn btn-success">
+                                    <i class="fa fa-check"/> <span>Confirmar Aprobación</span>
+                                </button>
+                            </div>
+                        </form>
+                    </div>
+                </div>
             </div>
             </div>
-            <div t-if="ticket.business_goal and ticket.request_type_code == 'improvement'" class="row mb-4">
-                <strong class="col-lg-3">Objetivo de Negocio</strong>
-                <div class="col-lg-9" t-field="ticket.business_goal"/>
+            
+            <!-- Modal for Reject -->
+            <div class="modal fade" id="rejectModal" tabindex="-1" aria-hidden="true">
+                <div class="modal-dialog">
+                    <div class="modal-content">
+                        <form method="POST" t-attf-action="/my/ticket/#{ticket.id}/reject?access_token=#{ticket.access_token}">
+                            <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+                            
+                            <div class="modal-header bg-danger text-white">
+                                <h5 class="modal-title">Rechazar Ticket</h5>
+                                <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"/>
+                            </div>
+                            
+                            <div class="modal-body">
+                                <p>Por favor proporcione una razón para rechazar este ticket:</p>
+                                <textarea name="reason" 
+                                          class="form-control" 
+                                          rows="3" 
+                                          placeholder="Ingrese su razón..."
+                                          required=""/>
+                            </div>
+                            
+                            <div class="modal-footer">
+                                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
+                                    Cancelar
+                                </button>
+                                <button type="submit" class="btn btn-danger">
+                                    <i class="fa fa-times"/> <span>Confirmar Rechazo</span>
+                                </button>
+                            </div>
+                        </form>
+                    </div>
+                </div>
             </div>
             </div>
-            <div t-if="ticket.estimated_hours" class="row mb-4">
-                <strong class="col-lg-3">Horas Estimadas</strong>
-                <span class="col-lg-9" t-field="ticket.estimated_hours" t-options='{"widget": "float"}'/>
+            
+            <!-- Success/Error messages -->
+            <div t-if="message == 'approved'" class="alert alert-success mt-3" role="alert">
+                <strong><span>¡Gracias!</span></strong><br/>
+                <span>Ha aprobado este ticket.</span>
             </div>
             </div>
-            <div t-if="ticket.approval_status" class="row mb-4">
-                <strong class="col-lg-3">Estado de Aprobación</strong>
-                <span class="col-lg-9">
-                    <t t-if="ticket.approval_status == 'draft'">N/A</t>
-                    <t t-elif="ticket.approval_status == 'waiting'">Esperando Aprobación</t>
-                    <t t-elif="ticket.approval_status == 'approved'">Aprobado</t>
-                    <t t-elif="ticket.approval_status == 'rejected'">Rechazado</t>
-                </span>
+            <div t-if="message == 'rejected'" class="alert alert-info mt-3" role="alert">
+                <strong><span>Tomado en cuenta.</span></strong><br/>
+                <span>Ha rechazado este ticket.</span>
             </div>
             </div>
         </xpath>
         </xpath>
     </template>
     </template>
@@ -769,4 +943,4 @@
             </script>
             </script>
         </t>
         </t>
     </template>
     </template>
-</odoo>
+</odoo>

+ 16 - 0
helpdesk_extras/views/helpdesk_stage_views.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <!-- Extend helpdesk.stage form view -->
+    <record id="helpdesk_stage_view_form_inherit_approval" model="ir.ui.view">
+        <field name="name">helpdesk.stage.form.inherit.approval</field>
+        <field name="inherit_id" ref="helpdesk.helpdesk_stage_view_form"/>
+        <field name="model">helpdesk.stage</field>
+        <field name="arch" type="xml">
+            <!-- Add requires_customer_approval after fold field -->
+            <xpath expr="//field[@name='fold']" position="after">
+                <field name="requires_customer_approval"/>
+            </xpath>
+        </field>
+    </record>
+
+</odoo>

+ 10 - 1
helpdesk_extras/views/helpdesk_ticket_views.xml

@@ -13,6 +13,8 @@
                 <page string="Extras" name="extras">
                 <page string="Extras" name="extras">
                     <group>
                     <group>
                         <group string="Request Information">
                         <group string="Request Information">
+                            <field name="request_type_code" invisible="1"/>
+                            <field name="stage_id" invisible="1"/>
                             <field name="request_type_id" required="1"/>
                             <field name="request_type_id" required="1"/>
                             <field name="affected_module_id"/>
                             <field name="affected_module_id"/>
                             <field name="affected_user_email" widget="email" placeholder="email@example.com"/>
                             <field name="affected_user_email" widget="email" placeholder="email@example.com"/>
@@ -31,9 +33,15 @@
                         <field name="client_authorization"/>
                         <field name="client_authorization"/>
                         <field name="estimated_hours"/>
                         <field name="estimated_hours"/>
                         <field name="approval_status"/>
                         <field name="approval_status"/>
+                        <field name="customer_approval_status" 
+                               invisible="not stage_id or not stage_id.requires_customer_approval"
+                               readonly="1"/>
+                        <field name="customer_rejection_reason" 
+                               invisible="customer_approval_status != 'rejected'"
+                               readonly="1"/>
                     </group>
                     </group>
                     <group>
                     <group>
-                        <field name="attachment_ids"/>
+                        <field name="attachment_ids" widget="many2many_binary" readonly="1"/>
                     </group>
                     </group>
                     <group string="Template Information" invisible="not has_template">
                     <group string="Template Information" invisible="not has_template">
                         <field name="team_id" invisible="1"/>
                         <field name="team_id" invisible="1"/>
@@ -60,6 +68,7 @@
                 <field name="request_type_id" optional="show"/>
                 <field name="request_type_id" optional="show"/>
                 <field name="affected_module_id" optional="show"/>
                 <field name="affected_module_id" optional="show"/>
                 <field name="approval_status" optional="show"/>
                 <field name="approval_status" optional="show"/>
+                <field name="customer_approval_status" optional="hide"/>
             </xpath>
             </xpath>
         </field>
         </field>
     </record>
     </record>

+ 1 - 0
helpdesk_extras/views/helpdesk_workflow_template_views.xml

@@ -89,6 +89,7 @@
                                     <field name="sequence" widget="handle"/>
                                     <field name="sequence" widget="handle"/>
                                     <field name="name" required="1"/>
                                     <field name="name" required="1"/>
                                     <field name="fold" widget="boolean_toggle"/>
                                     <field name="fold" widget="boolean_toggle"/>
+                                    <field name="requires_customer_approval" widget="boolean_toggle" string="Requires Approval"/>
                                     <field name="description"/>
                                     <field name="description"/>
                                 </list>
                                 </list>
                             </field>
                             </field>

+ 24 - 1
theme_m22tc/README.md

@@ -98,7 +98,9 @@ theme_m22tc/
 │       └── scss/
 │       └── scss/
 │           ├── primary_variables.scss  # Variables de diseño (colores, fuentes)
 │           ├── primary_variables.scss  # Variables de diseño (colores, fuentes)
 │           ├── bootstrap_overridden.scss # Overrides de Bootstrap
 │           ├── bootstrap_overridden.scss # Overrides de Bootstrap
-│           └── m22tc_styles.scss       # Estilos personalizados
+│           ├── m22tc_styles.scss       # Estilos personalizados
+│           └── snippets/
+│               └── s_popup_m22.scss    # Estilos del popup M22
 └── views/
 └── views/
     ├── customizations.xml             # Personalizaciones generales (Tailwind, login enforcement)
     ├── customizations.xml             # Personalizaciones generales (Tailwind, login enforcement)
@@ -106,6 +108,7 @@ theme_m22tc/
     ├── login_custom.xml               # Login, Signup, Reset Password
     ├── login_custom.xml               # Login, Signup, Reset Password
     ├── portal_sidebar.xml             # Cleanup de vistas legacy (sin overrides)
     ├── portal_sidebar.xml             # Cleanup de vistas legacy (sin overrides)
     ├── snippets.xml                   # Snippets personalizados (Bento Grid)
     ├── snippets.xml                   # Snippets personalizados (Bento Grid)
+    ├── snippets_popup.xml             # Popup M22 personalizado
     ├── website_menu_view.xml          # Vistas de menú
     ├── website_menu_view.xml          # Vistas de menú
     ├── helpdesk_dashboard.xml         # Template del dashboard de tickets
     ├── helpdesk_dashboard.xml         # Template del dashboard de tickets
     └── helpdesk_portal_approval.xml    # Template de botones de aprobación/rechazo
     └── helpdesk_portal_approval.xml    # Template de botones de aprobación/rechazo
@@ -333,6 +336,7 @@ El tema sigue una **filosofía de no-interferencia** con los templates nativos d
 
 
 El archivo `portal_sidebar.xml` solo contiene una función de cleanup de vistas legacy. Todos los ajustes visuales se manejan en `m22tc_styles.scss`.
 El archivo `portal_sidebar.xml` solo contiene una función de cleanup de vistas legacy. Todos los ajustes visuales se manejan en `m22tc_styles.scss`.
 
 
+
 ## 🧩 Snippets Disponibles
 ## 🧩 Snippets Disponibles
 
 
 ### Bento Grid (`s_m22_bento_grid`)
 ### Bento Grid (`s_m22_bento_grid`)
@@ -342,6 +346,25 @@ Grid asimétrico estilo "Bento Box" para mostrar servicios/features:
 - Iconos con gradiente
 - Iconos con gradiente
 - Compatible con el editor de Odoo
 - Compatible con el editor de Odoo
 
 
+### M22 Popup (`s_popup` variante M22)
+Popup personalizado con estilos glassmorphism del tema M22:
+- **Dark Glassmorphism**: Fondo oscuro con blur y transparencia
+- **Gradiente M22**: Botón cerrar con gradiente naranja-magenta característico
+- **Botones estilizados**: Primarios con gradiente, secundarios con glassmorphism
+- **Forms personalizados**: Inputs con fondo translúcido y focus naranja
+- **Responsive**: Adaptado para móvil y desktop
+- **Opciones configurables**:
+  - Posición: Top, Middle, Bottom
+  - Tamaño: Small, Medium, Large, XL, Full
+  - Display: Delay, Exit, Click
+  - Backdrop con blur
+
+**Cómo usar:**
+1. En el Website Builder, arrastra el snippet "Popup" a tu página
+2. El popup usará automáticamente los estilos M22 (variante `data-vcss="m22"`)
+3. Personaliza posición, tamaño y comportamiento desde el panel de opciones
+4. Edita el contenido del popup haciendo clic dentro de él
+
 ## ⚙️ Configuración del Website Editor
 ## ⚙️ Configuración del Website Editor
 
 
 El tema configura automáticamente:
 El tema configura automáticamente:

+ 2 - 1
theme_m22tc/__manifest__.py

@@ -13,7 +13,7 @@
     "category": "Theme/Corporate",
     "category": "Theme/Corporate",
     "summary": "Tech, Consulting, Dark Mode, Glassmorphism, Tailwind Compatible",
     "summary": "Tech, Consulting, Dark Mode, Glassmorphism, Tailwind Compatible",
     "sequence": 120,
     "sequence": 120,
-    "version": "1.0.3",
+    "version": "1.0.4",
     "depends": ["website", "auth_signup", "helpdesk", "helpdesk_extras"],
     "depends": ["website", "auth_signup", "helpdesk", "helpdesk_extras"],
     "data": [
     "data": [
         "data/generate_primary_template.xml",
         "data/generate_primary_template.xml",
@@ -21,6 +21,7 @@
         "data/menu_data.xml",
         "data/menu_data.xml",
         "views/website_menu_view.xml",
         "views/website_menu_view.xml",
         "views/snippets.xml",
         "views/snippets.xml",
+        "views/snippets_popup.xml",
         "views/customizations.xml",
         "views/customizations.xml",
         "views/frontend_layout.xml",
         "views/frontend_layout.xml",
         "views/portal_sidebar.xml",
         "views/portal_sidebar.xml",

+ 36 - 14
theme_m22tc/controllers/helpdesk_portal.py

@@ -24,13 +24,25 @@ class CustomerPortal(HelpdeskCustomerPortal):
     - Set default grouping by stage for list view
     - Set default grouping by stage for list view
     - Calculate dashboard metrics
     - Calculate dashboard metrics
     """
     """
+    def _prepare_portal_layout_values(self):
+        values = super()._prepare_portal_layout_values()
+        sales_user = request.env['res.users']
+        partner = request.env.user.partner_id.sudo()
+        if partner.user_id and not partner.user_id._is_public():
+            sales_user = partner.user_id
+        else:
+            fallback_sales_user = partner.commercial_partner_id.sudo().user_id
+            if fallback_sales_user and not fallback_sales_user._is_public():
+                sales_user = fallback_sales_user
+        values.update({'sales_user': sales_user.sudo()})
+        return values
 
 
     def _prepare_tickets_dashboard_values(self):
     def _prepare_tickets_dashboard_values(self):
         """
         """
         Calculate dashboard metrics for tickets.
         Calculate dashboard metrics for tickets.
         Returns dict with all metrics needed for the dashboard.
         Returns dict with all metrics needed for the dashboard.
         """
         """
-        partner = request.env.user.partner_id.commercial_partner_id
+        partner = request.env.user.partner_id.sudo().commercial_partner_id
         HelpdeskTicket = request.env['helpdesk.ticket']
         HelpdeskTicket = request.env['helpdesk.ticket']
         
         
         # Base domain for partner's tickets
         # Base domain for partner's tickets
@@ -175,19 +187,29 @@ class CustomerPortal(HelpdeskCustomerPortal):
     def _calculate_ticket_summary(self, tickets):
     def _calculate_ticket_summary(self, tickets):
         """
         """
         Calculate ticket summary: total, open, closed, by stage, by priority.
         Calculate ticket summary: total, open, closed, by stage, by priority.
-        Returns by_stage as list of tuples for QWeb template compatibility.
+        Returns by_stage as list of tuples for QWeb template compatibility, ordered by stage sequence.
         """
         """
         open_tickets = tickets.filtered(lambda t: not t.stage_id.fold)
         open_tickets = tickets.filtered(lambda t: not t.stage_id.fold)
         closed_tickets = tickets.filtered(lambda t: t.stage_id.fold)
         closed_tickets = tickets.filtered(lambda t: t.stage_id.fold)
         
         
-        # By stage - convert to list of tuples for QWeb
-        by_stage_dict = {}
+        # By stage with ordering by stage sequence
+        by_stage_counts = {}
+        by_stage_order = {}
         for ticket in open_tickets:
         for ticket in open_tickets:
-            stage_name = ticket.stage_id.name or 'Sin etapa'
-            by_stage_dict[stage_name] = by_stage_dict.get(stage_name, 0) + 1
+            stage = ticket.stage_id
+            stage_name = stage.name or 'Sin etapa'
+            by_stage_counts[stage_name] = by_stage_counts.get(stage_name, 0) + 1
+            seq = stage.sequence or 0
+            if stage_name not in by_stage_order:
+                by_stage_order[stage_name] = seq
+            else:
+                by_stage_order[stage_name] = min(by_stage_order[stage_name], seq)
         
         
-        # Convert to list of tuples for QWeb iteration
-        by_stage_list = [(name, count) for name, count in by_stage_dict.items()]
+        # Build ordered list by min sequence, then by name for stability
+        by_stage_list = sorted(
+            [(name, count) for name, count in by_stage_counts.items()],
+            key=lambda item: (by_stage_order.get(item[0], 0), item[0].lower())
+        )
         
         
         # By priority
         # By priority
         priority_labels = {
         priority_labels = {
@@ -282,11 +304,11 @@ class CustomerPortal(HelpdeskCustomerPortal):
         
         
         # Get all messages for open tickets in one efficient query
         # Get all messages for open tickets in one efficient query
         # Exclude system messages (author_id = False or OdooBot)
         # Exclude system messages (author_id = False or OdooBot)
-        comment_subtype = request.env.ref('mail.mt_comment')
-        odoobot = request.env.ref('base.partner_root', raise_if_not_found=False)
+        comment_subtype = request.env.ref('mail.mt_comment').sudo()
+        odoobot = request.env.ref('base.partner_root', raise_if_not_found=False).sudo()
         odoobot_id = odoobot.id if odoobot else False
         odoobot_id = odoobot.id if odoobot else False
         
         
-        all_messages = request.env['mail.message'].search([
+        all_messages = request.env['mail.message'].sudo().search([
             ('model', '=', 'helpdesk.ticket'),
             ('model', '=', 'helpdesk.ticket'),
             ('res_id', 'in', open_tickets.ids),
             ('res_id', 'in', open_tickets.ids),
             ('subtype_id', '=', comment_subtype.id),
             ('subtype_id', '=', comment_subtype.id),
@@ -334,9 +356,10 @@ class CustomerPortal(HelpdeskCustomerPortal):
             if last_msg:
             if last_msg:
                 # Check if last message is from helpdesk team (internal user)
                 # Check if last message is from helpdesk team (internal user)
                 is_helpdesk_msg = False
                 is_helpdesk_msg = False
-                if last_msg.author_id:
+                last_msg_sudo = last_msg.sudo()
+                if last_msg_sudo.author_id:
                     # Check if author has internal users (not share/portal)
                     # Check if author has internal users (not share/portal)
-                    author_users = last_msg.author_id.user_ids
+                    author_users = last_msg_sudo.author_id.sudo().user_ids
                     if author_users:
                     if author_users:
                         # Message is from helpdesk if any user is internal (not share/portal)
                         # Message is from helpdesk if any user is internal (not share/portal)
                         is_helpdesk_msg = any(not user.share for user in author_users)
                         is_helpdesk_msg = any(not user.share for user in author_users)
@@ -518,4 +541,3 @@ class CustomerPortal(HelpdeskCustomerPortal):
         )
         )
 
 
         return request.redirect('/my/ticket/%s/%s?ticket_rejected=1' % (ticket_id, access_token or ''))
         return request.redirect('/my/ticket/%s/%s?ticket_rejected=1' % (ticket_id, access_token or ''))
-

+ 323 - 0
theme_m22tc/static/src/scss/snippets/s_popup_m22.scss

@@ -0,0 +1,323 @@
+/**
+ * M22 Popup Styles - Variante con Glassmorphism
+ * 
+ * Personalización del snippet s_popup de Odoo con el estilo característico
+ * del tema M22TC: dark glassmorphism, gradientes naranja-magenta, y
+ * efectos de blur.
+ */
+
+.s_popup[data-vcss='m22'] {
+    
+    // ===================================
+    // Modal Container
+    // ===================================
+    .modal {
+        // Backdrop personalizado con blur
+        &::before {
+            content: '';
+            position: fixed;
+            inset: 0;
+            background: rgba(0, 0, 0, 0.6);
+            backdrop-filter: blur(8px);
+            z-index: -1;
+        }
+    }
+
+    // ===================================
+    // Modal Content - Dark Glassmorphism
+    // ===================================
+    .modal-content {
+        // Glassmorphism dark background
+        background: rgba(15, 17, 26, 0.95) !important;
+        backdrop-filter: blur(20px);
+        border: 1px solid rgba(255, 255, 255, 0.08) !important;
+        border-radius: 1.5rem !important;
+        box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5) !important;
+        
+        // Asegurar que el contenido sea visible
+        min-height: inherit;
+        
+        // Colores de texto
+        color: rgba(255, 255, 255, 0.9);
+        
+        // Headings en blanco puro
+        h1, h2, h3, h4, h5, h6,
+        .h1, .h2, .h3, .h4, .h5, .h6,
+        .display-1, .display-2, .display-3, .display-4,
+        .display-1-fs, .display-2-fs, .display-3-fs, .display-4-fs {
+            color: white !important;
+            text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+        }
+        
+        // Párrafos y leads en gris claro
+        p, .lead, .text-muted {
+            color: rgba(255, 255, 255, 0.8) !important;
+        }
+        
+        // Links con color naranja M22
+        a:not(.btn) {
+            color: #FF6B00;
+            
+            &:hover {
+                color: #E1467C;
+            }
+        }
+    }
+
+    // ===================================
+    // Close Button - Gradiente M22
+    // ===================================
+    .s_popup_close {
+        // Tamaño y posición
+        z-index: $zindex-modal + 10;
+        width: 48px !important;
+        height: 48px !important;
+        line-height: 48px !important;
+        
+        // Gradiente característico M22
+        background: linear-gradient(90deg, #FF6B00, #E1467C) !important;
+        color: white !important;
+        
+        // Bordes redondeados
+        border-radius: 0 1.5rem 0 1rem !important;
+        
+        // Sombra
+        box-shadow: 0 4px 12px rgba(255, 107, 0, 0.4) !important;
+        
+        // Transiciones suaves
+        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+        
+        // Hover effect
+        &:hover {
+            opacity: 0.9;
+            transform: scale(1.1) rotate(90deg);
+            box-shadow: 0 6px 16px rgba(255, 107, 0, 0.6) !important;
+        }
+        
+        // Asegurar que el símbolo × sea visible
+        font-size: 1.5rem !important;
+        font-weight: 300;
+    }
+
+    // ===================================
+    // Buttons - Estilos M22
+    // ===================================
+    
+    // Botón primario con gradiente
+    .btn-primary {
+        background: linear-gradient(90deg, #FF6B00, #E1467C) !important;
+        border: none !important;
+        color: white !important;
+        font-weight: 600;
+        padding: 0.75rem 2rem;
+        border-radius: 0.5rem;
+        transition: all 0.3s ease;
+        box-shadow: 0 4px 12px rgba(255, 107, 0, 0.3);
+        
+        &:hover,
+        &:focus,
+        &:active {
+            opacity: 0.9;
+            transform: translateY(-2px);
+            box-shadow: 0 6px 20px rgba(255, 107, 0, 0.5) !important;
+            color: white !important;
+        }
+    }
+    
+    // Botón secundario con glassmorphism
+    .btn-secondary,
+    .btn-light {
+        background: rgba(255, 255, 255, 0.1) !important;
+        border: 1px solid rgba(255, 255, 255, 0.2) !important;
+        color: white !important;
+        backdrop-filter: blur(10px);
+        
+        &:hover,
+        &:focus {
+            background: rgba(255, 255, 255, 0.15) !important;
+            border-color: rgba(255, 255, 255, 0.3) !important;
+            color: white !important;
+        }
+    }
+    
+    // Botón outline
+    .btn-outline-primary {
+        border: 2px solid #FF6B00 !important;
+        color: #FF6B00 !important;
+        background: transparent !important;
+        
+        &:hover {
+            background: linear-gradient(90deg, #FF6B00, #E1467C) !important;
+            color: white !important;
+            border-color: transparent !important;
+        }
+    }
+
+    // ===================================
+    // Form Controls
+    // ===================================
+    
+    .form-control,
+    .form-select,
+    input[type="text"],
+    input[type="email"],
+    input[type="tel"],
+    textarea {
+        background: rgba(255, 255, 255, 0.08) !important;
+        border: 1px solid rgba(255, 255, 255, 0.15) !important;
+        color: white !important;
+        border-radius: 0.5rem;
+        
+        &::placeholder {
+            color: rgba(255, 255, 255, 0.4);
+        }
+        
+        &:focus {
+            background: rgba(255, 255, 255, 0.12) !important;
+            border-color: #FF6B00 !important;
+            box-shadow: 0 0 0 3px rgba(255, 107, 0, 0.2) !important;
+            color: white !important;
+        }
+    }
+    
+    // Input group con botón
+    .input-group {
+        .form-control {
+            border-right: none;
+        }
+        
+        .btn {
+            border-top-left-radius: 0;
+            border-bottom-left-radius: 0;
+        }
+    }
+
+    // ===================================
+    // Badges
+    // ===================================
+    
+    .badge {
+        &.badge-primary,
+        &.bg-primary {
+            background: linear-gradient(90deg, #FF6B00, #E1467C) !important;
+            color: white !important;
+        }
+    }
+
+    // ===================================
+    // Tamaño Full - Ajustes especiales
+    // ===================================
+    
+    .s_popup_size_full {
+        padding: 0 !important;
+        max-width: 100%;
+
+        > .modal-content {
+            // Mantener glassmorphism en modo full
+            background: rgba(15, 17, 26, 0.95) !important;
+            backdrop-filter: blur(20px);
+            border-radius: 0 !important;
+            box-shadow: none !important;
+        }
+    }
+
+    // ===================================
+    // Posiciones - Refinamientos
+    // ===================================
+    
+    .modal-dialog {
+        margin: 0 0 0 auto;
+        min-height: 100%;
+        
+        &:not(.s_popup_size_full) {
+            padding: 1.5rem !important;
+        }
+    }
+    
+    .s_popup_top .modal-dialog {
+        align-items: flex-start;
+    }
+    
+    .s_popup_middle .modal-dialog {
+        align-items: center;
+        margin-right: auto;
+    }
+    
+    .s_popup_bottom .modal-dialog {
+        align-items: flex-end;
+    }
+
+    // ===================================
+    // Sin Backdrop - Ajustes
+    // ===================================
+    
+    .s_popup_no_backdrop {
+        // Permitir scroll detrás
+        pointer-events: none;
+        
+        // Ocultar el backdrop custom
+        &::before {
+            display: none;
+        }
+
+        .modal-dialog {
+            height: 100%;
+            pointer-events: auto;
+
+            .modal-content {
+                max-height: 100%;
+                overflow-y: auto;
+                overflow-x: hidden;
+            }
+        }
+    }
+
+    // ===================================
+    // Animaciones adicionales
+    // ===================================
+    
+    .modal.fade .modal-dialog {
+        transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    }
+    
+    .modal.show .modal-dialog {
+        transform: none;
+    }
+
+    // ===================================
+    // Responsive adjustments
+    // ===================================
+    
+    @media (max-width: 576px) {
+        .modal-dialog {
+            &:not(.s_popup_size_full) {
+                padding: 1rem !important;
+            }
+        }
+        
+        .modal-content {
+            border-radius: 1rem !important;
+        }
+        
+        .s_popup_close {
+            width: 40px !important;
+            height: 40px !important;
+            line-height: 40px !important;
+            font-size: 1.25rem !important;
+        }
+    }
+
+    // ===================================
+    // Website Builder adjustments
+    // ===================================
+    
+    // Asegurar que el contenido sea editable en el builder
+    .oe_structure {
+        color: inherit;
+    }
+    
+    // Ajustes para snippets internos
+    section[data-snippet] {
+        color: inherit;
+    }
+}

+ 4 - 4
theme_m22tc/views/login_custom.xml

@@ -123,18 +123,18 @@
                                     <label class="block text-sm font-medium text-gray-300 mb-2" for="login">Correo Electrónico</label>
                                     <label class="block text-sm font-medium text-gray-300 mb-2" for="login">Correo Electrónico</label>
                                     <input class="w-full bg-gray-800/50 border border-gray-700 text-white placeholder-gray-500 px-4 py-3 rounded-md focus:outline-none focus:border-orange-500 transition-all duration-300" 
                                     <input class="w-full bg-gray-800/50 border border-gray-700 text-white placeholder-gray-500 px-4 py-3 rounded-md focus:outline-none focus:border-orange-500 transition-all duration-300" 
                                            id="login" name="login" t-att-value="login" 
                                            id="login" name="login" t-att-value="login" 
-                                           placeholder="tu@email.com" required="required" type="text" autofocus="autofocus" autocapitalize="off"/>
+                                           placeholder="tu@email.com" required="required" type="text" autofocus="autofocus" autocapitalize="off" tabindex="1"/>
                                 </div>
                                 </div>
 
 
                                 <!-- Password Input -->
                                 <!-- Password Input -->
                                 <div>
                                 <div>
                                     <div class="flex items-center justify-between mb-2">
                                     <div class="flex items-center justify-between mb-2">
                                         <label class="block text-sm font-medium text-gray-300" for="password">Contraseña</label>
                                         <label class="block text-sm font-medium text-gray-300" for="password">Contraseña</label>
-                                        <a t-if="reset_password_enabled" class="text-sm font-medium text-orange-500 hover:text-orange-400 transition-colors" t-attf-href="/web/reset_password?{{ keep_query() }}">¿Olvidaste tu contraseña?</a>
+                                        <a t-if="reset_password_enabled" class="text-sm font-medium text-orange-500 hover:text-orange-400 transition-colors" t-attf-href="/web/reset_password?{{ keep_query() }}" tabindex="4">¿Olvidaste tu contraseña?</a>
                                     </div>
                                     </div>
                                     <input class="w-full bg-gray-800/50 border border-gray-700 text-white placeholder-gray-500 px-4 py-3 rounded-md focus:outline-none focus:border-orange-500 transition-all duration-300" 
                                     <input class="w-full bg-gray-800/50 border border-gray-700 text-white placeholder-gray-500 px-4 py-3 rounded-md focus:outline-none focus:border-orange-500 transition-all duration-300" 
                                            id="password" name="password" 
                                            id="password" name="password" 
-                                           placeholder="••••••••" required="required" type="password" autocomplete="current-password" t-att-autofocus="'autofocus' if login else None"/>
+                                           placeholder="••••••••" required="required" type="password" autocomplete="current-password" t-att-autofocus="'autofocus' if login else None" tabindex="2"/>
                                 </div>
                                 </div>
 
 
                                 <!-- Error Messages -->
                                 <!-- Error Messages -->
@@ -147,7 +147,7 @@
 
 
                                 <!-- Submit Button -->
                                 <!-- Submit Button -->
                                 <div>
                                 <div>
-                                    <button style="background: linear-gradient(90deg, #FF6B00, #E1467C) !important; border: 0 !important; outline: 0 !important; box-shadow: none !important;" class="w-full text-white font-bold py-3 px-4 rounded-md hover:opacity-90 transition-all transform hover:scale-105 focus:outline-none border-0" type="submit">
+                                    <button style="background: linear-gradient(90deg, #FF6B00, #E1467C) !important; border: 0 !important; outline: 0 !important; box-shadow: none !important;" class="w-full text-white font-bold py-3 px-4 rounded-md hover:opacity-90 transition-all transform hover:scale-105 focus:outline-none border-0" type="submit" tabindex="3">
                                         Iniciar Sesión
                                         Iniciar Sesión
                                     </button>
                                     </button>
                                 </div>
                                 </div>

+ 24 - 0
theme_m22tc/views/snippets_popup.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+    <!--
+        M22 Popup Customization
+        Hereda del snippet s_popup estándar de Odoo y agrega una variante M22
+        con estilos glassmorphism y gradientes característicos del tema.
+    -->
+
+    <!-- Registrar variante M22 del popup -->
+    <template id="m22_popup_variant" inherit_id="website.s_popup" name="M22 Popup Variant">
+        <xpath expr="//div[@class='s_popup o_snippet_invisible']" position="attributes">
+            <!-- Agregar variante M22 como opción -->
+            <attribute name="data-vcss">m22</attribute>
+        </xpath>
+    </template>
+
+    <!-- SCSS para el popup M22 -->
+    <record id="s_popup_m22_scss" model="theme.ir.asset">
+        <field name="name">M22 Popup SCSS</field>
+        <field name="bundle">web.assets_frontend</field>
+        <field name="path">theme_m22tc/static/src/scss/snippets/s_popup_m22.scss</field>
+    </record>
+
+</odoo>

+ 0 - 53
whatsapp_web_groups/.gitignore

@@ -1,53 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Odoo
-*.pot
-*.mo
-*.log
-
-# IDE
-.idea/
-.vscode/
-*.swp
-*.swo
-*~
-.DS_Store
-
-# Environment
-.env
-.venv
-env/
-venv/
-ENV/
-
-# Testing
-.pytest_cache/
-.coverage
-htmlcov/
-
-# Temporary files
-*.tmp
-*.bak
-*.orig
-
-

+ 0 - 583
whatsapp_web_groups/API_REFERENCE.md

@@ -1,583 +0,0 @@
-# API Reference - Gestor de Grupos WhatsApp Web
-
-## Índice
-- [Modelos](#modelos)
-- [Métodos](#métodos)
-- [Campos](#campos)
-- [Ejemplos de Uso](#ejemplos-de-uso)
-- [Respuestas de API](#respuestas-de-api)
-
-## Modelos
-
-### ww.group
-
-Modelo principal para gestión de grupos de WhatsApp Web.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `name` | Char | Nombre del grupo (requerido) |
-| `whatsapp_web_id` | Char | ID único del grupo en WhatsApp Web |
-| `whatsapp_account_id` | Many2one | Cuenta de WhatsApp asociada |
-| `channel_id` | Many2one | Canal de discusión creado |
-| `contact_ids` | Many2many | Contactos miembros del grupo |
-
-#### Métodos
-
-##### `_process_messages(messages_data)`
-Procesa mensajes de WhatsApp y los crea en el canal de discusión.
-
-**Parámetros:**
-- `messages_data` (list): Lista de mensajes de WhatsApp
-
-**Retorna:** `bool` - True si se procesó correctamente
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-messages = [
-    {
-        'id': {'_serialized': '3EB0C767D26A3D1B7B4A'},
-        'body': 'Hola grupo!',
-        'author': '5215551234567@c.us',
-        'timestamp': 1640995200,
-        'hasQuotedMsg': False
-    }
-]
-result = group._process_messages(messages)
-```
-
-##### `_create_discussion_channel()`
-Crea un canal de discusión para el grupo.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `discuss.channel|False` - Canal creado o False si falla
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._create_discussion_channel()
-if channel:
-    print(f"Canal creado: {channel.name}")
-```
-
-##### `_update_discussion_channel()`
-Actualiza los miembros del canal de discusión.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `discuss.channel|False` - Canal actualizado o False si falla
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._update_discussion_channel()
-```
-
-##### `sync_ww_contacts_groups()`
-Sincroniza todos los grupos y contactos desde WhatsApp Web.
-
-**Parámetros:** Ninguno (método de modelo)
-
-**Retorna:** `bool` - True si se sincronizó correctamente
-
-**Ejemplo:**
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Sincronización desde un grupo específico
-group = self.env['ww.group'].browse(1)
-# (Este método es estático, no se llama desde instancia)
-```
-
-##### `send_whatsapp_message(body, attachment=None, wa_template_id=None)`
-Envía un mensaje WhatsApp al grupo.
-
-**Parámetros:**
-- `body` (str): Contenido del mensaje
-- `attachment` (ir.attachment, opcional): Archivo adjunto
-- `wa_template_id` (whatsapp.template, opcional): Plantilla WhatsApp
-
-**Retorna:** `whatsapp.message` - Mensaje creado y enviado
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-attachment = self.env['ir.attachment'].browse(1)
-template = self.env['whatsapp.template'].browse(1)
-
-message = group.send_whatsapp_message(
-    body="Mensaje importante para el grupo",
-    attachment=attachment,
-    wa_template_id=template
-)
-```
-
-##### `action_send_whatsapp_message()`
-Abre el composer de WhatsApp para enviar mensaje al grupo.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `dict` - Acción de ventana para abrir composer
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-action = group.action_send_whatsapp_message()
-# Retorna acción para abrir ventana del composer
-```
-
----
-
-### ww.contact (Extiende res.partner)
-
-Extensión del modelo de contactos para WhatsApp Web.
-
-#### Campos Adicionales
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `whatsapp_web_id` | Char | ID único del contacto en WhatsApp Web |
-| `group_ids` | Many2many | Grupos donde participa el contacto |
-
-#### Relaciones
-
-- **group_ids**: Relación many2many con `ww.group`
-- **channel_ids**: Relación con canales de discusión
-- **meeting_ids**: Relación con eventos de calendario
-- **sla_ids**: Relación con SLAs de helpdesk
-
----
-
-### ww.role
-
-Modelo para roles de miembros en grupos.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `name` | Char | Nombre del rol (requerido) |
-| `description` | Text | Descripción del rol |
-
----
-
-### ww.group_contact_rel
-
-Modelo de relación entre grupos y contactos con información adicional.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `group_id` | Many2one | Referencia al grupo |
-| `contact_id` | Many2one | Referencia al contacto |
-| `is_admin` | Boolean | Si es administrador del grupo |
-| `is_super_admin` | Boolean | Si es super administrador |
-| `role_id` | Many2one | Rol asignado en el grupo |
-
-#### Constraints
-
-- **group_contact_uniq**: Constraint único para evitar duplicados (group_id, contact_id)
-
----
-
-## Ejemplos de Uso
-
-### Sincronización de Grupos
-
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Verificar grupos sincronizados
-groups = self.env['ww.group'].search([])
-for group in groups:
-    print(f"Grupo: {group.name}")
-    print(f"Miembros: {len(group.contact_ids)}")
-    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
-```
-
-### Creación Manual de Grupo
-
-```python
-# Crear grupo manualmente
-group = self.env['ww.group'].create({
-    'name': 'Mi Grupo Personalizado',
-    'whatsapp_web_id': '120363158956331133@g.us',
-    'whatsapp_account_id': account.id
-})
-
-# Agregar contactos al grupo
-contacts = self.env['res.partner'].search([('whatsapp_web_id', '!=', False)])
-group.contact_ids = [(6, 0, contacts.ids)]
-
-# Crear canal de discusión
-channel = group._create_discussion_channel()
-```
-
-### Gestión de Contactos
-
-```python
-# Buscar contactos de WhatsApp Web
-contacts = self.env['res.partner'].search([
-    ('whatsapp_web_id', '!=', False)
-])
-
-# Agregar contacto a grupo
-group = self.env['ww.group'].browse(1)
-contact = self.env['res.partner'].browse(1)
-
-# Crear relación con rol
-rel = self.env['ww.group_contact_rel'].create({
-    'group_id': group.id,
-    'contact_id': contact.id,
-    'is_admin': True,
-    'role_id': admin_role.id
-})
-```
-
-### Envío de Mensajes a Grupos
-
-```python
-# Envío directo
-group = self.env['ww.group'].browse(1)
-message = group.send_whatsapp_message("Mensaje importante!")
-
-# Envío con adjunto
-attachment = self.env['ir.attachment'].create({
-    'name': 'documento.pdf',
-    'type': 'binary',
-    'datas': base64.b64encode(pdf_content),
-    'mimetype': 'application/pdf'
-})
-
-message = group.send_whatsapp_message(
-    body="Adjunto documento importante",
-    attachment=attachment
-)
-
-# Envío usando composer
-action = group.action_send_whatsapp_message()
-```
-
-### Procesamiento de Mensajes
-
-```python
-# Procesar mensajes de un grupo
-group = self.env['ww.group'].browse(1)
-messages_data = [
-    {
-        'id': {'_serialized': 'msg_id_123'},
-        'body': 'Mensaje de prueba',
-        'author': '5215551234567@c.us',
-        'timestamp': 1640995200,
-        'hasQuotedMsg': True,
-        'quotedMsg': {
-            'body': 'Mensaje citado',
-            'author': '5215557654321@c.us'
-        }
-    }
-]
-
-result = group._process_messages(messages_data)
-```
-
-### Gestión de Roles
-
-```python
-# Crear roles
-admin_role = self.env['ww.role'].create({
-    'name': 'Administrador',
-    'description': 'Administrador del grupo'
-})
-
-member_role = self.env['ww.role'].create({
-    'name': 'Miembro',
-    'description': 'Miembro regular del grupo'
-})
-
-# Asignar roles a contactos en grupos
-group = self.env['ww.group'].browse(1)
-for contact in group.contact_ids:
-    rel = self.env['ww.group_contact_rel'].search([
-        ('group_id', '=', group.id),
-        ('contact_id', '=', contact.id)
-    ])
-    
-    if contact.is_admin:
-        rel.role_id = admin_role.id
-    else:
-        rel.role_id = member_role.id
-```
-
-### Consultas Avanzadas
-
-```python
-# Grupos con más de 10 miembros
-large_groups = self.env['ww.group'].search([
-    ('contact_ids', '!=', False)
-])
-large_groups = [g for g in large_groups if len(g.contact_ids) > 10]
-
-# Contactos administradores
-admin_contacts = self.env['res.partner'].search([
-    ('group_ids', '!=', False)
-])
-admin_contacts = [c for c in admin_contacts 
-                  if any(rel.is_admin for rel in c.group_ids)]
-
-# Grupos sin canal
-groups_without_channel = self.env['ww.group'].search([
-    ('channel_id', '=', False)
-])
-```
-
-## Respuestas de API
-
-### Respuesta de getGroups()
-
-```json
-[
-    {
-        "id": {
-            "_serialized": "120363158956331133@g.us"
-        },
-        "name": "Mi Grupo de Trabajo",
-        "description": "Grupo para coordinación de proyectos",
-        "members": [
-            {
-                "id": {
-                    "_serialized": "5215551234567@c.us"
-                },
-                "number": "5551234567",
-                "name": "Juan Pérez",
-                "pushname": "Juan",
-                "isAdmin": true,
-                "isSuperAdmin": false
-            },
-            {
-                "id": {
-                    "_serialized": "5215557654321@c.us"
-                },
-                "number": "5557654321",
-                "name": "María García",
-                "pushname": "María",
-                "isAdmin": false,
-                "isSuperAdmin": false
-            }
-        ],
-        "messages": [
-            {
-                "id": {
-                    "_serialized": "3EB0C767D26A3D1B7B4A"
-                },
-                "body": "Hola grupo!",
-                "author": "5215551234567@c.us",
-                "timestamp": 1640995200,
-                "hasQuotedMsg": false,
-                "quotedParticipant": null,
-                "quotedMsg": null
-            }
-        ]
-    }
-]
-```
-
-### Respuesta de Creación de Canal
-
-```python
-# Respuesta exitosa
-{
-    'id': 123,
-    'name': '📱 Mi Grupo de Trabajo',
-    'channel_type': 'channel',
-    'member_count': 5
-}
-
-# Respuesta de error (False)
-False
-```
-
-### Respuesta de Envío de Mensaje
-
-```python
-# Mensaje enviado exitosamente
-{
-    'id': 456,
-    'state': 'sent',
-    'msg_uid': '3EB0C767D26A3D1B7B4A_120363158956331133@g.us',
-    'body': 'Mensaje importante!',
-    'recipient_type': 'group'
-}
-```
-
-## Manejo de Errores
-
-### Excepciones Comunes
-
-#### ValueError - Grupo sin cuenta
-```python
-try:
-    group.send_whatsapp_message("Mensaje")
-except ValueError as e:
-    print(f"Error: {e}")  # "Group must have a WhatsApp account configured"
-```
-
-#### ValidationError - Datos inválidos
-```python
-from odoo.exceptions import ValidationError
-
-try:
-    group = self.env['ww.group'].create({
-        'name': '',  # Nombre vacío
-        'whatsapp_web_id': 'invalid_id'
-    })
-except ValidationError as e:
-    print(f"Error de validación: {e}")
-```
-
-### Logs de Debugging
-
-```python
-import logging
-_logger = logging.getLogger(__name__)
-
-# Log de sincronización
-_logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
-
-# Log de error
-_logger.error("Error en la sincronización de grupos para la cuenta %s: %s", 
-              account.name, str(e))
-
-# Log de advertencia
-_logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
-```
-
-## Configuración de Cron Jobs
-
-### Configuración Básica
-
-```xml
-<record id="ir_cron_sync_ww_contacts_groups" model="ir.cron">
-    <field name="name">Sincronizar Contactos y Grupos WhatsApp Web</field>
-    <field name="model_id" ref="model_ww_group"/>
-    <field name="state">code</field>
-    <field name="code">model.sync_ww_contacts_groups()</field>
-    <field name="interval_number">1</field>
-    <field name="interval_type">hours</field>
-    <field name="active" eval="False"/>
-</record>
-```
-
-### Configuración Personalizada
-
-```python
-# Crear cron job personalizado
-cron = self.env['ir.cron'].create({
-    'name': 'Sincronización Personalizada',
-    'model_id': self.env.ref('whatsapp_web_groups.model_ww_group').id,
-    'state': 'code',
-    'code': 'model.sync_ww_contacts_groups()',
-    'interval_number': 2,
-    'interval_type': 'hours',
-    'active': True,
-    'user_id': self.env.user.id
-})
-```
-
-## Optimización de Performance
-
-### Sincronización en Lotes
-
-```python
-# Procesar grupos en lotes para evitar timeouts
-def sync_groups_in_batches(self, batch_size=10):
-    accounts = self.env['whatsapp.account'].search([])
-    
-    for account in accounts:
-        groups_data = account.get_groups()
-        
-        # Procesar en lotes
-        for i in range(0, len(groups_data), batch_size):
-            batch = groups_data[i:i + batch_size]
-            self._process_groups_batch(batch, account)
-            
-            # Commit después de cada lote
-            self._cr.commit()
-```
-
-### Creación Bulk de Mensajes
-
-```python
-def _process_messages_bulk(self, messages_data):
-    """Procesar mensajes en bulk para mejor performance"""
-    if not messages_data:
-        return True
-    
-    # Preparar valores para creación bulk
-    message_vals_list = []
-    for msg_data in messages_data:
-        message_vals = self._prepare_message_vals(msg_data)
-        message_vals_list.append(message_vals)
-    
-    # Crear todos los mensajes de una vez
-    if message_vals_list:
-        self.env['mail.message'].create(message_vals_list)
-    
-    return True
-```
-
-## Limpieza y Mantenimiento
-
-### Limpiar Grupos Vacíos
-
-```python
-def cleanup_empty_groups(self):
-    """Eliminar grupos sin contactos"""
-    empty_groups = self.env['ww.group'].search([
-        ('contact_ids', '=', False)
-    ])
-    
-    if empty_groups:
-        _logger.info(f"Eliminando {len(empty_groups)} grupos vacíos")
-        empty_groups.unlink()
-```
-
-### Limpiar Contactos Huérfanos
-
-```python
-def cleanup_orphan_contacts(self):
-    """Limpiar contactos sin grupos"""
-    orphan_contacts = self.env['res.partner'].search([
-        ('whatsapp_web_id', '!=', False),
-        ('group_ids', '=', False)
-    ])
-    
-    if orphan_contacts:
-        _logger.info(f"Limpiando {len(orphan_contacts)} contactos huérfanos")
-        orphan_contacts.write({'whatsapp_web_id': False})
-```
-
-### Regenerar Canales Perdidos
-
-```python
-def regenerate_missing_channels(self):
-    """Regenerar canales que se perdieron"""
-    groups_without_channel = self.env['ww.group'].search([
-        ('channel_id', '=', False),
-        ('contact_ids', '!=', False)
-    ])
-    
-    for group in groups_without_channel:
-        try:
-            channel = group._create_discussion_channel()
-            if channel:
-                _logger.info(f"Canal regenerado para grupo {group.name}")
-        except Exception as e:
-            _logger.error(f"Error regenerando canal para {group.name}: {e}")
-```
-

+ 0 - 386
whatsapp_web_groups/README.md

@@ -1,386 +0,0 @@
-# Gestor de Grupos de WhatsApp Web para Odoo 18
-
-## Descripción
-
-Este módulo permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando automáticamente con la API de whatsapp-web.js. Proporciona una interfaz completa para administrar grupos de WhatsApp y sus miembros dentro de Odoo.
-
-## Características Principales
-
-- ✅ Sincronización automática de grupos de WhatsApp Web
-- ✅ Gestión de contactos y miembros de grupos
-- ✅ Creación automática de canales de discusión para cada grupo
-- ✅ Sistema de roles y permisos para miembros
-- ✅ Integración con el sistema de contactos de Odoo
-- ✅ Procesamiento automático de mensajes de grupos
-- ✅ Sincronización programada con cron jobs
-- ✅ Envío directo de mensajes a grupos
-
-## Requisitos
-
-- Odoo 18.0
-- Módulo `whatsapp_web` (dependencia obligatoria)
-- Módulos: `base`, `contacts`, `mail`, `calendar`, `helpdesk`
-- Servidor whatsapp-web.js configurado y funcionando
-
-## Instalación
-
-1. **Instalar dependencias:**
-   ```bash
-   cd /var/odoo/mcteam.run
-   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web
-   ```
-
-2. **Instalar el módulo:**
-   ```bash
-   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web_groups
-   ```
-
-3. **Reiniciar el servidor Odoo:**
-   ```bash
-   ./restart_odoo.sh
-   ```
-
-## Configuración Inicial
-
-### 1. Configurar Cuenta WhatsApp Web
-
-1. Ir a **WhatsApp > Configuración > Cuentas WhatsApp**
-2. Crear o editar una cuenta WhatsApp
-3. Configurar la **WhatsApp Web URL** (ej: `https://web.whatsapp.com/api`)
-4. Guardar la configuración
-
-### 2. Sincronización Inicial
-
-1. Ir a **WhatsApp Web > Grupos**
-2. Hacer clic en "Sincronizar" o ejecutar manualmente:
-   ```python
-   self.env['ww.group'].sync_ww_contacts_groups()
-   ```
-
-### 3. Configurar Cron Job (Opcional)
-
-Para sincronización automática cada hora:
-
-1. Ir a **Configuración > Técnico > Automatización > Acciones Programadas**
-2. Buscar "Sincronizar Contactos y Grupos WhatsApp Web"
-3. Activar el cron job
-4. Configurar intervalo deseado
-
-## Uso
-
-### Gestión de Grupos
-
-#### Ver Grupos Sincronizados
-1. Ir a **WhatsApp Web > Grupos**
-2. Ver lista de todos los grupos sincronizados
-3. Hacer clic en un grupo para ver detalles
-
-#### Enviar Mensaje a Grupo
-1. Seleccionar un grupo de la lista
-2. Hacer clic en "Send WhatsApp Message"
-3. Completar el composer de WhatsApp
-4. Enviar el mensaje
-
-#### Sincronización Manual
-```python
-# Desde shell de Odoo
-groups = self.env['ww.group']
-groups.sync_ww_contacts_groups()
-```
-
-### Gestión de Contactos
-
-#### Ver Contactos WhatsApp
-1. Ir a **Contactos > Contactos WhatsApp Web**
-2. Ver todos los contactos sincronizados de WhatsApp
-3. Editar información de contactos si es necesario
-
-#### Contactos en Grupos
-- Los contactos se sincronizan automáticamente cuando están en grupos
-- Se actualizan los números de teléfono y nombres
-- Se evitan duplicados usando los últimos 10 dígitos del móvil
-
-### Canales de Discusión
-
-#### Creación Automática
-- Cada grupo crea automáticamente un canal de discusión
-- El canal se nombra con prefijo "📱" + nombre del grupo
-- Todos los miembros del grupo se agregan al canal
-
-#### Gestión de Canales
-1. Ir al canal desde el menú de discusión
-2. Los mensajes de WhatsApp se procesan automáticamente
-3. Se mantiene historial de conversaciones
-
-## Estructura de Datos
-
-### Modelos Principales
-
-#### `ww.group`
-Representa un grupo de WhatsApp Web.
-
-**Campos:**
-- `name`: Nombre del grupo
-- `whatsapp_web_id`: ID único en WhatsApp Web
-- `whatsapp_account_id`: Cuenta WhatsApp asociada
-- `channel_id`: Canal de discusión creado
-- `contact_ids`: Contactos miembros del grupo
-
-#### `ww.contact` (Extiende `res.partner`)
-Contactos de WhatsApp Web.
-
-**Campos adicionales:**
-- `whatsapp_web_id`: ID único en WhatsApp Web
-- `group_ids`: Grupos donde participa el contacto
-
-#### `ww.role`
-Roles que pueden tener los miembros en grupos.
-
-**Campos:**
-- `name`: Nombre del rol
-- `description`: Descripción del rol
-
-#### `ww.group_contact_rel`
-Relación entre grupos y contactos con roles.
-
-**Campos:**
-- `group_id`: Referencia al grupo
-- `contact_id`: Referencia al contacto
-- `is_admin`: Si es administrador
-- `is_super_admin`: Si es super administrador
-- `role_id`: Rol asignado
-
-## API del Módulo
-
-### Métodos Principales
-
-#### `ww.group.sync_ww_contacts_groups()`
-Sincroniza todos los grupos y contactos desde WhatsApp Web.
-
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Sincronización por cuenta específica
-account = self.env['whatsapp.account'].browse(1)
-groups_data = account.get_groups()
-```
-
-#### `ww.group._create_discussion_channel()`
-Crea un canal de discusión para el grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._create_discussion_channel()
-```
-
-#### `ww.group._process_messages(messages_data)`
-Procesa mensajes de WhatsApp y los crea en el canal.
-
-```python
-group = self.env['ww.group'].browse(1)
-messages = [
-    {
-        'id': {'_serialized': 'message_id'},
-        'body': 'Contenido del mensaje',
-        'author': 'contacto_id',
-        'timestamp': 1234567890
-    }
-]
-group._process_messages(messages)
-```
-
-#### `ww.group.send_whatsapp_message(body, attachment=None, wa_template_id=None)`
-Envía un mensaje WhatsApp al grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-message = group.send_whatsapp_message(
-    body="Hola grupo!",
-    attachment=attachment_record,
-    wa_template_id=template_record
-)
-```
-
-#### `ww.group.action_send_whatsapp_message()`
-Abre el composer de WhatsApp para enviar mensaje al grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-action = group.action_send_whatsapp_message()
-```
-
-### Métodos de Sincronización
-
-#### `whatsapp.account.get_groups()`
-Obtiene grupos desde el servidor whatsapp-web.js.
-
-**Respuesta esperada:**
-```json
-[
-    {
-        "id": {"_serialized": "120363158956331133@g.us"},
-        "name": "Mi Grupo",
-        "members": [
-            {
-                "id": {"_serialized": "5215551234567@c.us"},
-                "number": "5551234567",
-                "name": "Juan Pérez",
-                "isAdmin": false,
-                "isSuperAdmin": false
-            }
-        ],
-        "messages": [...]
-    }
-]
-```
-
-## Configuración Avanzada
-
-### Personalización de Sincronización
-
-#### Modificar Intervalo de Sincronización
-```xml
-<!-- En data/ir_cron.xml -->
-<field name="interval_number">2</field>
-<field name="interval_type">hours</field>
-```
-
-#### Filtros de Sincronización
-```python
-# Sincronizar solo grupos específicos
-accounts = self.env['whatsapp.account'].search([
-    ('name', 'ilike', 'cuenta_especifica')
-])
-for account in accounts:
-    groups_data = account.get_groups()
-    # Procesar solo grupos deseados
-```
-
-### Gestión de Permisos
-
-#### Configurar Accesos
-```csv
-# En security/ir.model.access.csv
-access_ww_group_user,ww.group.user,model_ww_group,base.group_user,1,1,1,0
-access_ww_group_manager,ww.group.manager,model_ww_group,base.group_system,1,1,1,1
-```
-
-## Solución de Problemas
-
-### Error: "No hay contactos para crear el canal"
-- **Causa**: El grupo no tiene miembros o la sincronización falló
-- **Solución**: Ejecutar sincronización manual y verificar conectividad
-
-### Error: "Error al crear el canal para el grupo"
-- **Causa**: Problemas de permisos o configuración de grupos de usuario
-- **Solución**: Verificar que el usuario tenga permisos para crear canales
-
-### Grupos no se sincronizan
-- **Causa**: Servidor whatsapp-web.js no responde o método `getGroups` no implementado
-- **Solución**: 
-  1. Verificar conectividad con el servidor
-  2. Confirmar implementación del método `getGroups`
-  3. Revisar logs del servidor whatsapp-web.js
-
-### Contactos duplicados
-- **Causa**: Múltiples números similares en diferentes formatos
-- **Solución**: El módulo usa los últimos 10 dígitos para evitar duplicados automáticamente
-
-### Mensajes no se procesan en canales
-- **Causa**: Formato incorrecto de datos de mensajes o canal no creado
-- **Solución**:
-  1. Verificar que el grupo tenga canal asociado
-  2. Confirmar formato de datos de mensajes
-  3. Revisar permisos de escritura en canales
-
-## Logs y Debugging
-
-### Logs de Sincronización
-```bash
-# Ver logs de sincronización
-tail -f /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "sincroniz\|grupo\|contacto"
-```
-
-### Debug de Grupos
-```python
-# Verificar estado de grupos
-groups = self.env['ww.group'].search([])
-for group in groups:
-    print(f"Grupo: {group.name}")
-    print(f"Contactos: {len(group.contact_ids)}")
-    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
-```
-
-### Debug de Sincronización
-```python
-# Probar sincronización paso a paso
-account = self.env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
-groups_data = account.get_groups()
-print(f"Grupos obtenidos: {len(groups_data)}")
-```
-
-## Mantenimiento
-
-### Limpieza de Datos
-```python
-# Limpiar grupos sin contactos
-empty_groups = self.env['ww.group'].search([
-    ('contact_ids', '=', False)
-])
-empty_groups.unlink()
-
-# Limpiar contactos sin grupos
-orphan_contacts = self.env['res.partner'].search([
-    ('whatsapp_web_id', '!=', False),
-    ('group_ids', '=', False)
-])
-orphan_contacts.write({'whatsapp_web_id': False})
-```
-
-### Optimización de Performance
-- La sincronización procesa grupos en lotes para evitar timeouts
-- Los mensajes se crean en bulk para mejorar performance
-- Se evitan duplicados usando constraints de base de datos
-
-## Actualizaciones
-
-Para actualizar el módulo:
-
-```bash
-cd /var/odoo/mcteam.run
-sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -u whatsapp_web_groups
-./restart_odoo.sh
-```
-
-## Integración con Otros Módulos
-
-### Marketing
-- Los grupos se pueden usar como destinatarios en campañas de marketing
-- Soporte para plantillas personalizadas por grupo
-
-### Helpdesk
-- Crear tickets automáticamente desde mensajes de grupos
-- Asignar SLAs basados en roles de grupo
-
-### Calendar
-- Programar reuniones con miembros de grupos
-- Invitaciones automáticas a eventos
-
-## Soporte
-
-Para soporte técnico o reportar bugs:
-- Revisar logs de Odoo y del servidor whatsapp-web.js
-- Verificar configuración de cuentas WhatsApp
-- Confirmar conectividad de red
-
-## Changelog
-
-### Versión 1.0
-- Implementación inicial del gestor de grupos
-- Sincronización automática de grupos y contactos
-- Creación automática de canales de discusión
-- Sistema de roles y permisos
-- Procesamiento de mensajes de grupos
-- Integración con composer de WhatsApp
-

+ 0 - 1
whatsapp_web_groups/__init__.py

@@ -1 +0,0 @@
-from . import models 

+ 0 - 31
whatsapp_web_groups/__manifest__.py

@@ -1,31 +0,0 @@
-{
-    'name': 'Gestor de Grupos de WhatsApp',
-    'version': '1.0',
-    'summary': 'Gestión de grupos y contactos de WhatsApp Web',
-    'description': 'Permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando con la API de whatsapp_web.js',
-    'author': 'MC Team',
-    'category': 'Tools',
-    'depends': [
-        'base',
-        'contacts',
-        'whatsapp_web',
-        'marketing_automation_whatsapp',
-        'mail',
-        'calendar',
-        'helpdesk',
-    ],
-    'data': [
-        'security/ir.model.access.csv',
-        'views/ww_contact_views.xml',
-        'views/ww_group_views.xml',
-        'views/ww_role_views.xml',
-        'views/ww_group_contact_rel_views.xml',
-        'views/marketing_activity_views.xml',
-        'views/whatsapp_message_views.xml',
-        'views/whatsapp_composer_views.xml',
-        'data/ir_cron.xml',
-    ],
-    'installable': True,
-    'application': True,
-    'auto_install': False,
-} 

+ 0 - 14
whatsapp_web_groups/data/ir_cron.xml

@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo noupdate="1">
-
-    <record id="ir_cron_sync_ww_contacts_groups" model="ir.cron">
-        <field name="name">Sincronizar Contactos y Grupos WhatsApp Web</field>
-        <field name="model_id" ref="model_ww_group"/>
-        <field name="state">code</field>
-        <field name="code">model.sync_ww_contacts_groups()</field>
-        <field name="interval_number">1</field>
-        <field name="interval_type">hours</field>
-        <field name="active" eval="False"/>
-    </record>
-    
-</odoo> 

+ 0 - 7
whatsapp_web_groups/models/__init__.py

@@ -1,7 +0,0 @@
-from . import ww_contact
-from . import ww_group
-from . import ww_role
-from . import ww_group_contact_rel
-from . import marketing_activity
-from . import whatsapp_message
-from . import whatsapp_composer 

+ 0 - 99
whatsapp_web_groups/models/marketing_activity.py

@@ -1,99 +0,0 @@
-from odoo import models, fields
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class MarketingActivity(models.Model):
-    _inherit = 'marketing.activity'
-
-    # Campo para grupo de WhatsApp en actividades de marketing
-    whatsapp_group_id = fields.Many2one(
-        'ww.group', 
-        string='Grupo de WhatsApp',
-        help="Grupo de WhatsApp para enviar mensajes (opcional). Si está vacío, se envían a destinatarios individuales según la plantilla."
-    )
-
-    def _execute_whatsapp(self, traces):
-        """Override para soportar envío a grupos de WhatsApp usando campos nativos"""
-        _logger.info(f"Ejecutando WhatsApp para actividad {self.id}: {self.name}")
-        
-        # Si hay grupo configurado, usar la lógica simple que funcionaba antes
-        if self.whatsapp_group_id:
-            _logger.info(f"Enviando mensaje WhatsApp al grupo NATIVO: {self.whatsapp_group_id.name}")
-            
-            try:
-                # Usar el método original pero con mobile_number del grupo
-                res_ids = [res_id for res_id in set(traces.mapped('res_id')) if res_id]
-                now = self.env.cr.now()
-
-                composer_vals = {
-                    'res_model': self.model_name, 
-                    'res_ids': res_ids,
-                    'wa_template_id': self.whatsapp_template_id.id,
-                    'batch_mode': True,
-                    'phone': self.whatsapp_group_id.whatsapp_web_id,  # ✅ Usar ID del grupo como "teléfono"
-                }
-                
-                _logger.info(f"Usando método original con grupo ID: {self.whatsapp_group_id.whatsapp_web_id}")
-                composer = self.env['whatsapp.composer'].with_context(active_model=self.model_name).create(composer_vals)
-                messages = composer._create_whatsapp_messages(force_create=True)
-                message_by_res_id = {r.mail_message_id.res_id: r for r in messages}
-                
-                # Asignar mensajes a traces (como el original)
-                for trace in self.trace_ids:
-                    res_id = trace.res_id
-                    message = message_by_res_id.get(res_id, self.env['whatsapp.message'])
-                    if message:
-                        trace.whatsapp_message_id = message.id
-                        # Marcar como grupo
-                        message.write({
-                            'recipient_type': 'group',
-                            'whatsapp_group_id': self.whatsapp_group_id.id,
-                        })
-                    if not message.mobile_number:
-                        message.state = 'error'
-                        message.failure_type = 'phone_invalid'
-
-                # Enviar mensajes (como el original)
-                messages._send()
-                    
-                _logger.info(f"Mensaje enviado exitosamente al grupo {self.whatsapp_group_id.name}")
-                    
-            except Exception as e:
-                _logger.warning('Marketing Automation: actividad <%s> error al enviar a grupo WhatsApp %s', self.id, str(e))
-                traces.write({
-                    'state': 'error',
-                    'schedule_date': now,
-                    'state_msg': f'Error al enviar a grupo WhatsApp: {e}',
-                })
-            else:
-                # LÓGICA ORIGINAL: Marcar traces como procesados
-                cancelled_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'cancel')
-                error_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'error')
-
-                if cancelled_traces:
-                    cancelled_traces.write({
-                        'state': 'canceled',
-                        'schedule_date': now,
-                        'state_msg': 'WhatsApp canceled'
-                    })
-                if error_traces:
-                    error_traces.write({
-                        'state': 'error',
-                        'schedule_date': now,
-                        'state_msg': 'WhatsApp failed'
-                    })
-
-                processed_traces = traces - (cancelled_traces | error_traces)
-                if processed_traces:
-                    processed_traces.write({
-                        'state': 'processed',
-                        'schedule_date': now,
-                    })
-        else:
-            _logger.info(f"Campo whatsapp_group_id VACÍO - Enviando a destinatarios individuales")
-            # Usar lógica original para envío individual
-            super()._execute_whatsapp(traces)
-        
-        return True
-

+ 0 - 115
whatsapp_web_groups/models/whatsapp_composer.py

@@ -1,115 +0,0 @@
-from odoo import models, fields, api
-from odoo.exceptions import ValidationError
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class WhatsAppComposer(models.TransientModel):
-    _inherit = 'whatsapp.composer'
-
-    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
-    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
-                                        help="Select WhatsApp group to send message to",
-                                        ondelete='set null')
-
-    @api.onchange('whatsapp_group_id')
-    def _onchange_whatsapp_group_id(self):
-        """Actualizar campos cuando se selecciona un grupo"""
-        if self.whatsapp_group_id:
-            self.whatsapp_group_id_char = self.whatsapp_group_id.whatsapp_web_id
-            self.recipient_type = 'group'
-            self.phone = False
-
-    @api.onchange('recipient_type')
-    def _onchange_recipient_type(self):
-        """Limpiar campos al cambiar tipo de destinatario"""
-        super()._onchange_recipient_type()
-        if self.recipient_type != 'group':
-            self.whatsapp_group_id = False
-
-    @api.constrains('recipient_type', 'phone', 'whatsapp_group_id', 'whatsapp_group_id_char', 'wa_template_id', 'body')
-    def _check_recipient_configuration(self):
-        """Extender validación para incluir whatsapp_group_id"""
-        super()._check_recipient_configuration()
-        
-        for record in self:
-            if record.recipient_type == 'group':
-                if not record.whatsapp_group_id and not record.whatsapp_group_id_char:
-                    raise ValidationError("Please select a WhatsApp group or enter a Group ID when sending to groups")
-
-    def _send_whatsapp_web_message(self):
-        """Extender método para usar whatsapp_group_id si está disponible"""
-        records = self._get_active_records()
-        
-        for record in records:
-            # Determinar destinatario - priorizar whatsapp_group_id sobre whatsapp_group_id_char
-            if self.recipient_type == 'group':
-                if self.whatsapp_group_id:
-                    mobile_number = self.whatsapp_group_id.whatsapp_web_id
-                elif self.whatsapp_group_id_char:
-                    mobile_number = self.whatsapp_group_id_char
-                else:
-                    raise ValidationError("Please specify a group")
-            else:
-                mobile_number = self.phone
-                if not mobile_number:
-                    raise ValidationError("Please provide a phone number")
-            
-            # Crear mail.message con adjuntos si existen
-            post_values = {
-                'attachment_ids': [self.attachment_id.id] if self.attachment_id else [],
-                'body': self.body,
-                'message_type': 'whatsapp_message',
-                'partner_ids': hasattr(record, '_mail_get_partners') and record._mail_get_partners()[record.id].ids or record._whatsapp_get_responsible().partner_id.ids,
-            }
-            
-            if hasattr(records, '_message_log'):
-                message = record._message_log(**post_values)
-            else:
-                message = self.env['mail.message'].create(
-                    dict(post_values, res_id=record.id, model=self.res_model,
-                         subtype_id=self.env['ir.model.data']._xmlid_to_res_id("mail.mt_note"))
-                )
-            
-            # Crear mensaje WhatsApp
-            message_vals = {
-                'mail_message_id': message.id,
-                'mobile_number': mobile_number,
-                'mobile_number_formatted': mobile_number,
-                'recipient_type': self.recipient_type,
-                'wa_template_id': False,
-                'wa_account_id': self._get_whatsapp_web_account().id,
-                'state': 'outgoing',
-            }
-            
-            # Agregar whatsapp_group_id si está disponible
-            if self.whatsapp_group_id:
-                message_vals['whatsapp_group_id'] = self.whatsapp_group_id.id
-            
-            whatsapp_message = self.env['whatsapp.message'].create(message_vals)
-            
-            # Enviar mensaje
-            whatsapp_message._send_message()
-        
-        return {'type': 'ir.actions.act_window_close'}
-
-    def _prepare_whatsapp_message_values(self, record):
-        """Extender método para agregar información de grupo"""
-        values = super()._prepare_whatsapp_message_values(record)
-        
-        # Agregar información de grupo si está disponible
-        if (hasattr(self, 'recipient_type') and self.recipient_type == 'group'):
-            if self.whatsapp_group_id:
-                values.update({
-                    'whatsapp_group_id': self.whatsapp_group_id.id,
-                    'mobile_number': self.whatsapp_group_id.whatsapp_web_id,
-                    'mobile_number_formatted': self.whatsapp_group_id.whatsapp_web_id,
-                })
-            elif self.whatsapp_group_id_char:
-                values.update({
-                    'mobile_number': self.whatsapp_group_id_char,
-                    'mobile_number_formatted': self.whatsapp_group_id_char,
-                })
-        
-        return values
-

+ 0 - 64
whatsapp_web_groups/models/whatsapp_message.py

@@ -1,64 +0,0 @@
-from odoo import models, fields, api
-from odoo.exceptions import ValidationError
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class WhatsAppMessage(models.Model):
-    _inherit = 'whatsapp.message'
-
-    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
-    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
-                                        help="WhatsApp group to send message to (if recipient_type is group)",
-                                        ondelete='set null')
-
-    @api.depends('recipient_type', 'mobile_number', 'whatsapp_group_id')
-    def _compute_final_recipient(self):
-        """Compute the final recipient based on type - extiende la lógica base"""
-        # Primero ejecutar la lógica base de whatsapp_web
-        super()._compute_final_recipient()
-        
-        # Si hay grupo seleccionado, usar su ID (sobrescribe la lógica base)
-        for record in self:
-            if record.recipient_type == 'group' and record.whatsapp_group_id:
-                record.final_recipient = record.whatsapp_group_id.whatsapp_web_id
-
-    @api.onchange('whatsapp_group_id')
-    def _onchange_whatsapp_group_id(self):
-        """Actualizar mobile_number cuando se selecciona un grupo"""
-        if self.whatsapp_group_id:
-            self.mobile_number = self.whatsapp_group_id.whatsapp_web_id
-            self.recipient_type = 'group'
-
-    @api.constrains('recipient_type', 'mobile_number', 'whatsapp_group_id')
-    def _check_recipient_configuration(self):
-        """Extender validación para incluir whatsapp_group_id"""
-        super()._check_recipient_configuration()
-        
-        for record in self:
-            if record.recipient_type == 'group':
-                if not record.whatsapp_group_id and not (record.mobile_number and record.mobile_number.endswith('@g.us')):
-                    raise ValidationError("Para mensajes a grupos, debe seleccionar un grupo o proporcionar un ID de grupo válido (@g.us)")
-
-    def _get_final_destination(self):
-        """Método mejorado para obtener destino final - extiende la lógica base"""
-        self.ensure_one()
-        
-        # Si hay grupo seleccionado, usar su ID
-        if self.recipient_type == 'group' and self.whatsapp_group_id:
-            return self.whatsapp_group_id.whatsapp_web_id
-        
-        # De lo contrario, usar la lógica base (incluye verificación de mobile_number @g.us)
-        result = super()._get_final_destination()
-        if result:
-            return result
-        
-        # Fallback adicional si no hay resultado
-        return False
-
-    def _send_message(self, with_commit=False):
-        """Extender método _send_message para manejar whatsapp_group_id"""
-        # El método _get_final_destination ya maneja whatsapp_group_id,
-        # así que la lógica base funcionará correctamente
-        return super()._send_message(with_commit)
-

+ 0 - 37
whatsapp_web_groups/models/ww_contact.py

@@ -1,37 +0,0 @@
-from odoo import models, fields
-
-class WWContact(models.Model):
-    _inherit = 'res.partner'
-    _description = 'Contacto de WhatsApp Web'
-
-    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del contacto en WhatsApp Web')
-    group_ids = fields.Many2many(
-        comodel_name='ww.group',
-        relation='ww_group_contact_rel',
-        column1='contact_id',
-        column2='group_id',
-        string='Grupos',
-        readonly=True,
-    )
-    channel_ids = fields.Many2many(
-        comodel_name='discuss.channel',
-        relation='discuss_channel_member',
-        column1='partner_id',
-        column2='channel_id',
-        string='Canales',
-        readonly=True,
-    )
-    meeting_ids = fields.One2many(
-        comodel_name='calendar.event',
-        inverse_name='partner_id',
-        string='Reuniones',
-        readonly=True,
-    )
-    sla_ids = fields.Many2many(
-        comodel_name='helpdesk.sla',
-        relation='helpdesk_sla_partner',
-        column1='partner_id',
-        column2='sla_id',
-        string='SLAs',
-        readonly=True,
-    ) 

+ 0 - 331
whatsapp_web_groups/models/ww_group.py

@@ -1,331 +0,0 @@
-from odoo import models, fields, api
-import logging
-from datetime import datetime
-
-_logger = logging.getLogger(__name__)
-
-class WWGroup(models.Model):
-    _name = 'ww.group'
-    _description = 'Grupo de WhatsApp Web'
-
-    name = fields.Char(string='Nombre del Grupo', required=True)
-    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del grupo en WhatsApp Web')
-    whatsapp_account_id = fields.Many2one('whatsapp.account', string='Cuenta de WhatsApp', required=True)
-    channel_id = fields.Many2one('discuss.channel', string='Canal de Discusión', readonly=True)
-    contact_ids = fields.Many2many(
-        comodel_name='res.partner',
-        relation='ww_group_contact_rel',
-        column1='group_id',
-        column2='contact_id',
-        string='Contactos',
-        readonly=True,
-    )
-
-    def _process_messages(self, messages_data):
-        """Process WhatsApp messages and create them in the channel"""
-        self.ensure_one()
-        
-        if not messages_data or not self.channel_id:
-            return True
-
-        # Get existing message IDs to avoid duplicates
-        existing_ids = set(self.channel_id.message_ids.mapped('message_id'))
-        
-        # Prepare bulk create values
-        message_vals_list = []
-        for msg_data in messages_data:
-            msg_id = msg_data.get('id', {}).get('_serialized')
-            
-            # Skip if message already exists
-            if msg_id in existing_ids:
-                continue
-
-            # Get author partner
-            author_whatsapp_id = msg_data.get('author')
-            author = self.env['res.partner'].search([
-                ('whatsapp_web_id', '=', author_whatsapp_id)
-            ], limit=1) if author_whatsapp_id else False
-
-            # Get quoted message author if exists
-            quoted_author = False
-            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedParticipant'):
-                quoted_author = self.env['res.partner'].search([
-                    ('whatsapp_web_id', '=', msg_data['quotedParticipant'])
-                ], limit=1)
-
-            # Convert timestamp to datetime
-            timestamp = datetime.fromtimestamp(msg_data.get('timestamp', 0))
-
-            # Prepare message body with author and content
-            author_name = author.name if author else "Desconocido"
-            message_body = f"{msg_data.get('body', '')}"
-
-            # Add quoted message if exists
-            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedMsg', {}).get('body'):
-                quoted_author_name = quoted_author.name if quoted_author else "Desconocido"
-                message_body += f"\n\n<blockquote><strong>{quoted_author_name}:</strong> {msg_data['quotedMsg']['body']}</blockquote>"
-
-            message_vals = {
-                'model': 'discuss.channel',
-                'res_id': self.channel_id.id,
-                'message_type': 'comment',
-                'subtype_id': self.env.ref('mail.mt_comment').id,
-                'body': message_body,
-                'date': timestamp,
-                'author_id': author.id if author else self.env.user.partner_id.id,
-                'message_id': msg_id,
-            }
-            message_vals_list.append(message_vals)
-
-        # Bulk create messages
-        if message_vals_list:
-            self.env['mail.message'].create(message_vals_list)
-
-        return True
-
-    def _create_discussion_channel(self):
-        """Create a discussion channel for the WhatsApp group"""
-        self.ensure_one()
-        
-        try:
-            # Verificar si ya existe un canal para este grupo
-            if self.channel_id:
-                return self.channel_id
-
-            # Create channel name with WhatsApp prefix
-            channel_name = f"📱 {self.name}"
-            
-            # Verificar que hay contactos
-            if not self.contact_ids:
-                _logger.warning(f"No hay contactos para crear el canal del grupo {self.name} - saltando creación de canal")
-                # No crear canal pero no fallar, permitir que el grupo exista
-                return False
-
-            # Obtener los IDs de los contactos de forma segura
-            partner_ids = []
-            for contact in self.contact_ids:
-                if contact and contact.id:
-                    partner_ids.append(contact.id)
-
-            if not partner_ids:
-                _logger.warning(f"No se encontraron IDs válidos de contactos para el grupo {self.name}")
-                return False
-
-            # Create the channel using channel_create
-            channel = self.env['discuss.channel'].channel_create(
-                name=channel_name,
-                group_id=self.env.user.groups_id[0].id,  # Usar el primer grupo del usuario actual
-            )
-            
-            # Add members to the channel
-            channel.add_members(partner_ids=partner_ids)
-            
-            # Link the channel to the group
-            self.write({'channel_id': channel.id})
-            return channel
-            
-        except Exception as e:
-            _logger.error(f"Error al crear el canal para el grupo {self.name}: {str(e)}")
-            return False
-
-    def _update_discussion_channel(self):
-        """Update the discussion channel members"""
-        self.ensure_one()
-        
-        try:
-            # Si no existe el canal, intentar crearlo
-            if not self.channel_id:
-                return self._create_discussion_channel()
-            
-            # Verificar que el canal aún existe
-            channel = self.env['discuss.channel'].browse(self.channel_id.id)
-            if not channel.exists():
-                _logger.warning(f"El canal para el grupo {self.name} ya no existe, creando uno nuevo")
-                self.write({'channel_id': False})
-                return self._create_discussion_channel()
-                
-            # Obtener los IDs de los contactos de forma segura
-            partner_ids = []
-            for contact in self.contact_ids:
-                if contact and contact.id:
-                    partner_ids.append(contact.id)
-
-            if not partner_ids:
-                _logger.warning(f"No hay contactos válidos para actualizar el canal del grupo {self.name} - saltando actualización")
-                # Si no hay contactos, no actualizar pero no fallar
-                return channel
-
-            # Update channel members using add_members
-            channel.add_members(partner_ids=partner_ids)
-            return channel
-            
-        except Exception as e:
-            _logger.error(f"Error al actualizar el canal para el grupo {self.name}: {str(e)}")
-            return False
-
-    @api.model
-    def sync_ww_contacts_groups(self):
-        """
-        Sincroniza los contactos y grupos de WhatsApp Web.
-        Solo sincroniza contactos que están dentro de grupos y valida que no se dupliquen,
-        verificando los últimos 10 dígitos del campo mobile.
-        """
-        accounts = self.env['whatsapp.account'].search([])
-        
-        for account in accounts:
-            try:
-                # Obtener grupos usando el método de la cuenta
-                groups_data = account.get_groups()
-                if not groups_data:
-                    continue
-                
-                # Procesar cada grupo
-                for group_data in groups_data:
-                    group_id = group_data.get('id').get('_serialized')
-                    group_name = group_data.get('name', 'Sin nombre')
-                    
-                    # Buscar o crear grupo
-                    group = self.search([
-                        ('whatsapp_web_id', '=', group_id),
-                        ('whatsapp_account_id', '=', account.id)
-                    ], limit=1)
-                    
-                    if not group:
-                        group = self.create({
-                            'name': group_name,
-                            'whatsapp_web_id': group_id,
-                            'whatsapp_account_id': account.id
-                        })
-                        # Crear canal solo después de procesar participantes
-                        # Se hará más abajo si hay contact_ids
-                    else:
-                        # Actualizar nombre del grupo si cambió
-                        if group.name != group_name:
-                            group.write({'name': group_name})
-                            # Actualizar nombre del canal si existe
-                            if group.channel_id:
-                                group.channel_id.write({'name': f"📱 {group_name}"})
-
-                    # Procesar participantes del grupo
-                    participants = group_data.get('members', [])
-                    contact_ids = []
-                    
-                    # Log para debug
-                    _logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
-                    if not participants:
-                        _logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
-
-                    for participant in participants:
-                        whatsapp_web_id = participant.get('id', {}).get('_serialized')
-                        mobile = participant.get('number', '')
-                        is_admin = participant.get('isAdmin', False)
-                        is_super_admin = participant.get('isSuperAdmin', False)
-
-                        # Derive participant name
-                        participant_name = participant.get('name') or participant.get('pushname') or mobile
-
-                        # Search for existing contact
-                        contact = self.env['res.partner'].search([
-                            ('whatsapp_web_id', '=', whatsapp_web_id)
-                        ], limit=1)
-
-                        if not contact and mobile and len(mobile) >= 10:
-                            last_10_digits = mobile[-10:]
-                            contact = self.env['res.partner'].search([
-                                ('mobile', 'like', '%' + last_10_digits)
-                            ], limit=1)
-
-                        partner_vals = {
-                            'name': participant_name,
-                            'mobile': mobile,
-                            'whatsapp_web_id': whatsapp_web_id,
-                        }
-
-                        if contact:
-                            # Update existing contact
-                            contact.write(partner_vals)
-                        else:
-                            # Create new contact
-                            contact = self.env['res.partner'].create(partner_vals)
-                        
-                        if contact:
-                            contact_ids.append(contact.id)
-                    
-                    # Actualizar contactos del grupo
-                    group.write({'contact_ids': [(6, 0, contact_ids)]})
-                    
-                    # Update discussion channel members solo si hay contactos
-                    if contact_ids:
-                        # Si es un grupo nuevo sin canal, crear uno
-                        if not group.channel_id:
-                            group._create_discussion_channel()
-                        else:
-                            group._update_discussion_channel()
-                    else:
-                        _logger.info(f"Grupo {group_name} sincronizado sin contactos - no se creará canal de discusión")
-
-                    # Process messages if available
-                    messages = group_data.get('messages', [])
-                    if messages:
-                        group._process_messages(messages)
-
-            except Exception as e:
-                _logger.error("Error en la sincronización de grupos para la cuenta %s: %s", account.name, str(e))
-                continue
-
-        return True
-    
-    def send_whatsapp_message(self, body, attachment=None, wa_template_id=None):
-        """Enviar mensaje WhatsApp al grupo"""
-        self.ensure_one()
-        
-        if not self.whatsapp_account_id:
-            raise ValueError("Group must have a WhatsApp account configured")
-        
-        # Crear mail.message si hay adjunto
-        mail_message = None
-        if attachment:
-            mail_message = self.env['mail.message'].create({
-                'body': body,
-                'attachment_ids': [(4, attachment.id)]
-            })
-        
-        # Crear mensaje WhatsApp
-        message_vals = {
-            'body': body,
-            'recipient_type': 'group',
-            'whatsapp_group_id': self.id,
-            'mobile_number': self.whatsapp_web_id,
-            'wa_account_id': self.whatsapp_account_id.id,
-            'state': 'outgoing',
-        }
-        
-        if mail_message:
-            message_vals['mail_message_id'] = mail_message.id
-            
-        if wa_template_id:
-            message_vals['wa_template_id'] = wa_template_id
-        
-        whatsapp_message = self.env['whatsapp.message'].create(message_vals)
-        
-        # Enviar mensaje
-        whatsapp_message._send_message()
-        
-        return whatsapp_message
-    
-    def action_send_whatsapp_message(self):
-        """Acción para abrir el composer de WhatsApp para el grupo"""
-        self.ensure_one()
-        
-        return {
-            'type': 'ir.actions.act_window',
-            'name': f'Send Message to {self.name}',
-            'res_model': 'whatsapp.composer',
-            'view_mode': 'form',
-            'target': 'new',
-            'context': {
-                'default_recipient_type': 'group',
-                'default_whatsapp_group_id': self.id,
-                'default_wa_template_id': False,
-            }
-        } 

+ 0 - 22
whatsapp_web_groups/models/ww_group_contact_rel.py

@@ -1,22 +0,0 @@
-from odoo import models, fields
-
-class WWGroupContactRel(models.Model):
-    _name = 'ww.group_contact_rel'
-    _description = 'Relación Contacto-Grupo de WhatsApp Web'
-    _table = 'ww_group_contact_rel'
-
-    # Explicitly define 'id' field. models.Model does this automatically,
-    # but being explicit can sometimes help clarify intent or resolve
-    # obscure schema generation issues if the table had a troubled history.
-    # fields.Id() is the standard way to define Odoo's automatic ID.
-    # id = fields.Id(string='ID')
-
-    group_id = fields.Many2one('ww.group', string='Grupo', required=True, ondelete='cascade', index=True)
-    contact_id = fields.Many2one('res.partner', string='Contacto', required=True, ondelete='cascade', index=True)
-    is_admin = fields.Boolean(string='Administrador del Grupo', default=False)
-    is_super_admin = fields.Boolean(string='Super Administrador del Grupo', default=False)
-    role_id = fields.Many2one('ww.role', string='Rol en el Grupo') 
-
-    _sql_constraints = [
-        ('group_contact_uniq', 'unique(group_id, contact_id)', 'El contacto debe ser único por grupo.')
-    ]

+ 0 - 8
whatsapp_web_groups/models/ww_role.py

@@ -1,8 +0,0 @@
-from odoo import models, fields
-
-class WWRole(models.Model):
-    _name = 'ww.role'
-    _description = 'Rol de Grupo de WhatsApp Web'
-
-    name = fields.Char(string='Nombre del Rol', required=True)
-    description = fields.Text(string='Descripción') 

+ 0 - 7
whatsapp_web_groups/security/ir.model.access.csv

@@ -1,7 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_ww_group_user,ww.group.user,model_ww_group,base.group_user,1,1,1,0
-access_ww_group_manager,ww.group.manager,model_ww_group,base.group_system,1,1,1,1
-access_ww_role_user,ww.role.user,model_ww_role,base.group_user,1,1,1,0
-access_ww_role_manager,ww.role.manager,model_ww_role,base.group_system,1,1,1,1
-access_ww_group_contact_rel_user,ww.group.contact.rel.user,model_ww_group_contact_rel,base.group_user,1,1,1,0
-access_ww_group_contact_rel_manager,ww.group.contact.rel.manager,model_ww_group_contact_rel,base.group_system,1,1,1,1

+ 0 - 19
whatsapp_web_groups/views/marketing_activity_views.xml

@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <!-- Vista heredada que agrega el campo de grupo de WhatsApp después del template -->
-    <record id="marketing_activity_view_form_inherit_group" model="ir.ui.view">
-        <field name="name">marketing.activity.form.inherit.group</field>
-        <field name="model">marketing.activity</field>
-        <field name="inherit_id" ref="marketing_automation_whatsapp.marketing_activity_view_form"/>
-        <field name="priority">5</field>
-        <field name="arch" type="xml">
-            <!-- Agregar el campo de grupo de WhatsApp después del campo whatsapp_template_id -->
-            <xpath expr="//field[@name='whatsapp_template_id']" position="after">
-                <field name="whatsapp_group_id" 
-                       invisible="activity_type != 'whatsapp'" 
-                       placeholder="Seleccionar grupo de WhatsApp (opcional)..."
-                       help="Si se selecciona un grupo, todos los mensajes se enviarán al grupo. Si está vacío, se envían a destinatarios individuales según la plantilla."/>
-            </xpath>
-        </field>
-    </record>
-</odoo>

+ 0 - 22
whatsapp_web_groups/views/whatsapp_composer_views.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Extender vista de formulario del composer de WhatsApp para agregar campo Many2one -->
-        <record id="whatsapp_composer_view_form_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.composer.view.form.groups.m2o</field>
-            <field name="model">whatsapp.composer</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_composer_view_form_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar campo Many2one whatsapp_group_id antes de whatsapp_group_id_char -->
-                <xpath expr="//field[@name='whatsapp_group_id_char']" position="before">
-                    <field name="whatsapp_group_id" 
-                           invisible="recipient_type != 'group'"
-                           string="WhatsApp Group"
-                           placeholder="Select a WhatsApp group..."
-                           options="{'no_create': True}"/>
-                </xpath>
-            </field>
-        </record>
-    </data>
-</odoo>
-

+ 0 - 34
whatsapp_web_groups/views/whatsapp_message_views.xml

@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Extender vista de formulario de WhatsApp Message para agregar campo Many2one -->
-        <record id="whatsapp_message_view_form_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.message.view.form.groups.m2o</field>
-            <field name="model">whatsapp.message</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_form_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar campo Many2one whatsapp_group_id después de recipient_type -->
-                <xpath expr="//field[@name='recipient_type']" position="after">
-                    <field name="whatsapp_group_id" 
-                           invisible="recipient_type != 'group'"
-                           required="recipient_type == 'group'"
-                           options="{'no_create': True}"/>
-                </xpath>
-            </field>
-        </record>
-
-        <!-- Extender vista de lista de WhatsApp Message -->
-        <record id="whatsapp_message_view_tree_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.message.view.tree.groups.m2o</field>
-            <field name="model">whatsapp.message</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_tree_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar columna whatsapp_group_id -->
-                <xpath expr="//field[@name='recipient_type']" position="after">
-                    <field name="whatsapp_group_id" optional="hide"/>
-                </xpath>
-            </field>
-        </record>
-    </data>
-</odoo>
-

+ 0 - 63
whatsapp_web_groups/views/ww_contact_views.xml

@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <record id="view_res_partner_whatsapp_tree" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.tree</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_partner_tree"/>
-            <field name="arch" type="xml">
-                <xpath expr="//field[@name='complete_name']" position="after">
-                    <field name="whatsapp_web_id"/>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="view_res_partner_whatsapp_form" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.form</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_partner_form"/>
-            <field name="arch" type="xml">
-                <xpath expr="//notebook" position="inside">
-                    <page string="WhatsApp" name="whatsapp">
-                        <group>
-                            <field name="whatsapp_web_id"/>
-                        </group>
-                        <notebook>
-                            <page string="Grupos">
-                                <field name="group_ids" widget="many2many_tags">
-                                    <list string="Grupos">
-                                        <field name="name"/>
-                                    </list>
-                                </field>
-                            </page>
-                        </notebook>
-                    </page>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="view_res_partner_whatsapp_search" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.search</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_res_partner_filter"/>
-            <field name="arch" type="xml">
-                <xpath expr="//filter[@name='type_company']" position="after">
-                    <filter string="WhatsApp" name="whatsapp" domain="[('whatsapp_web_id', '!=', False)]"/>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="action_res_partner_whatsapp" model="ir.actions.act_window">
-            <field name="name">Contactos WhatsApp Web</field>
-            <field name="res_model">res.partner</field>
-            <field name="view_mode">list,form</field>
-            <field name="domain">[('whatsapp_web_id', '!=', False)]</field>
-            <field name="context">{'search_default_whatsapp': 1}</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay contactos de WhatsApp
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 0 - 48
whatsapp_web_groups/views/ww_group_contact_rel_views.xml

@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_group_contact_rel_list" model="ir.ui.view">
-            <field name="name">ww.group.contact.rel.list</field>
-            <field name="model">ww.group_contact_rel</field>
-            <field name="arch" type="xml">
-                <list string="Contactos de Grupo" editable="bottom">
-                    <field name="group_id"/>
-                    <field name="contact_id"/>
-                    <field name="is_admin"/>
-                    <field name="role_id"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_group_contact_rel_form" model="ir.ui.view">
-            <field name="name">ww.group.contact.rel.form</field>
-            <field name="model">ww.group_contact_rel</field>
-            <field name="arch" type="xml">
-                <form string="Contacto de Grupo">
-                    <sheet>
-                        <group>
-                            <field name="group_id"/>
-                            <field name="contact_id"/>
-                            <field name="is_admin"/>
-                            <field name="role_id"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción de ventana -->
-        <record id="action_ww_group_contact_rel" model="ir.actions.act_window">
-            <field name="name">Contactos de Grupo</field>
-            <field name="res_model">ww.group_contact_rel</field>
-            <field name="view_mode">list,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay contactos en grupos
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 0 - 70
whatsapp_web_groups/views/ww_group_views.xml

@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_group_list" model="ir.ui.view">
-            <field name="name">ww.group.list</field>
-            <field name="model">ww.group</field>
-            <field name="arch" type="xml">
-                <list string="Grupos WhatsApp" editable="bottom">
-                    <field name="name"/>
-                    <field name="whatsapp_web_id"/>
-                    <field name="whatsapp_account_id"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_group_form" model="ir.ui.view">
-            <field name="name">ww.group.form</field>
-            <field name="model">ww.group</field>
-            <field name="arch" type="xml">
-                <form string="Grupo WhatsApp">
-                    <header>
-                        <button name="action_send_whatsapp_message" 
-                                string="Send WhatsApp Message" 
-                                type="object" 
-                                class="btn-primary"
-                                invisible="not whatsapp_account_id"/>
-                    </header>
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                            <field name="whatsapp_web_id"/>
-                            <field name="whatsapp_account_id"/>
-                            <field name="channel_id" readonly="1"/>
-                        </group>
-                        <notebook>
-                            <page string="Contactos">
-                                <field name="contact_ids" widget="many2many_tags"/>
-                            </page>
-                        </notebook>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción -->
-        <record id="action_ww_group" model="ir.actions.act_window">
-            <field name="name">Grupos de WhatsApp</field>
-            <field name="res_model">ww.group</field>
-            <field name="view_mode">list,form</field>
-            <field name="view_id" ref="view_ww_group_list"/>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    Crear un nuevo grupo de WhatsApp
-                </p>
-                <p>
-                    Los grupos de WhatsApp te permiten enviar mensajes a múltiples contactos de forma simultánea.
-                </p>
-            </field>
-        </record>
-
-        <!-- Menú Grupos dentro del menú nativo de WhatsApp -->
-        <menuitem id="menu_whatsapp_web_groups"
-                  name="Grupos"
-                  parent="whatsapp.whatsapp_menu_main"
-                  action="action_ww_group"
-                  sequence="3"/>
-    </data>
-</odoo>

+ 0 - 44
whatsapp_web_groups/views/ww_role_views.xml

@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_role_list" model="ir.ui.view">
-            <field name="name">ww.role.list</field>
-            <field name="model">ww.role</field>
-            <field name="arch" type="xml">
-                <list string="Roles de WhatsApp" editable="bottom">
-                    <field name="name"/>
-                    <field name="description"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_role_form" model="ir.ui.view">
-            <field name="name">ww.role.form</field>
-            <field name="model">ww.role</field>
-            <field name="arch" type="xml">
-                <form string="Rol de WhatsApp">
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                            <field name="description"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción de ventana -->
-        <record id="action_ww_role" model="ir.actions.act_window">
-            <field name="name">Roles de WhatsApp</field>
-            <field name="res_model">ww.role</field>
-            <field name="view_mode">list,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay roles de WhatsApp
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 0 - 53
whatsapp_web_groups_local_backup_20251214231432/.gitignore

@@ -1,53 +0,0 @@
-# Python
-__pycache__/
-*.py[cod]
-*$py.class
-*.so
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# Odoo
-*.pot
-*.mo
-*.log
-
-# IDE
-.idea/
-.vscode/
-*.swp
-*.swo
-*~
-.DS_Store
-
-# Environment
-.env
-.venv
-env/
-venv/
-ENV/
-
-# Testing
-.pytest_cache/
-.coverage
-htmlcov/
-
-# Temporary files
-*.tmp
-*.bak
-*.orig
-
-

+ 0 - 583
whatsapp_web_groups_local_backup_20251214231432/API_REFERENCE.md

@@ -1,583 +0,0 @@
-# API Reference - Gestor de Grupos WhatsApp Web
-
-## Índice
-- [Modelos](#modelos)
-- [Métodos](#métodos)
-- [Campos](#campos)
-- [Ejemplos de Uso](#ejemplos-de-uso)
-- [Respuestas de API](#respuestas-de-api)
-
-## Modelos
-
-### ww.group
-
-Modelo principal para gestión de grupos de WhatsApp Web.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `name` | Char | Nombre del grupo (requerido) |
-| `whatsapp_web_id` | Char | ID único del grupo en WhatsApp Web |
-| `whatsapp_account_id` | Many2one | Cuenta de WhatsApp asociada |
-| `channel_id` | Many2one | Canal de discusión creado |
-| `contact_ids` | Many2many | Contactos miembros del grupo |
-
-#### Métodos
-
-##### `_process_messages(messages_data)`
-Procesa mensajes de WhatsApp y los crea en el canal de discusión.
-
-**Parámetros:**
-- `messages_data` (list): Lista de mensajes de WhatsApp
-
-**Retorna:** `bool` - True si se procesó correctamente
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-messages = [
-    {
-        'id': {'_serialized': '3EB0C767D26A3D1B7B4A'},
-        'body': 'Hola grupo!',
-        'author': '5215551234567@c.us',
-        'timestamp': 1640995200,
-        'hasQuotedMsg': False
-    }
-]
-result = group._process_messages(messages)
-```
-
-##### `_create_discussion_channel()`
-Crea un canal de discusión para el grupo.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `discuss.channel|False` - Canal creado o False si falla
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._create_discussion_channel()
-if channel:
-    print(f"Canal creado: {channel.name}")
-```
-
-##### `_update_discussion_channel()`
-Actualiza los miembros del canal de discusión.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `discuss.channel|False` - Canal actualizado o False si falla
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._update_discussion_channel()
-```
-
-##### `sync_ww_contacts_groups()`
-Sincroniza todos los grupos y contactos desde WhatsApp Web.
-
-**Parámetros:** Ninguno (método de modelo)
-
-**Retorna:** `bool` - True si se sincronizó correctamente
-
-**Ejemplo:**
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Sincronización desde un grupo específico
-group = self.env['ww.group'].browse(1)
-# (Este método es estático, no se llama desde instancia)
-```
-
-##### `send_whatsapp_message(body, attachment=None, wa_template_id=None)`
-Envía un mensaje WhatsApp al grupo.
-
-**Parámetros:**
-- `body` (str): Contenido del mensaje
-- `attachment` (ir.attachment, opcional): Archivo adjunto
-- `wa_template_id` (whatsapp.template, opcional): Plantilla WhatsApp
-
-**Retorna:** `whatsapp.message` - Mensaje creado y enviado
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-attachment = self.env['ir.attachment'].browse(1)
-template = self.env['whatsapp.template'].browse(1)
-
-message = group.send_whatsapp_message(
-    body="Mensaje importante para el grupo",
-    attachment=attachment,
-    wa_template_id=template
-)
-```
-
-##### `action_send_whatsapp_message()`
-Abre el composer de WhatsApp para enviar mensaje al grupo.
-
-**Parámetros:** Ninguno
-
-**Retorna:** `dict` - Acción de ventana para abrir composer
-
-**Ejemplo:**
-```python
-group = self.env['ww.group'].browse(1)
-action = group.action_send_whatsapp_message()
-# Retorna acción para abrir ventana del composer
-```
-
----
-
-### ww.contact (Extiende res.partner)
-
-Extensión del modelo de contactos para WhatsApp Web.
-
-#### Campos Adicionales
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `whatsapp_web_id` | Char | ID único del contacto en WhatsApp Web |
-| `group_ids` | Many2many | Grupos donde participa el contacto |
-
-#### Relaciones
-
-- **group_ids**: Relación many2many con `ww.group`
-- **channel_ids**: Relación con canales de discusión
-- **meeting_ids**: Relación con eventos de calendario
-- **sla_ids**: Relación con SLAs de helpdesk
-
----
-
-### ww.role
-
-Modelo para roles de miembros en grupos.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `name` | Char | Nombre del rol (requerido) |
-| `description` | Text | Descripción del rol |
-
----
-
-### ww.group_contact_rel
-
-Modelo de relación entre grupos y contactos con información adicional.
-
-#### Campos
-
-| Campo | Tipo | Descripción |
-|-------|------|-------------|
-| `group_id` | Many2one | Referencia al grupo |
-| `contact_id` | Many2one | Referencia al contacto |
-| `is_admin` | Boolean | Si es administrador del grupo |
-| `is_super_admin` | Boolean | Si es super administrador |
-| `role_id` | Many2one | Rol asignado en el grupo |
-
-#### Constraints
-
-- **group_contact_uniq**: Constraint único para evitar duplicados (group_id, contact_id)
-
----
-
-## Ejemplos de Uso
-
-### Sincronización de Grupos
-
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Verificar grupos sincronizados
-groups = self.env['ww.group'].search([])
-for group in groups:
-    print(f"Grupo: {group.name}")
-    print(f"Miembros: {len(group.contact_ids)}")
-    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
-```
-
-### Creación Manual de Grupo
-
-```python
-# Crear grupo manualmente
-group = self.env['ww.group'].create({
-    'name': 'Mi Grupo Personalizado',
-    'whatsapp_web_id': '120363158956331133@g.us',
-    'whatsapp_account_id': account.id
-})
-
-# Agregar contactos al grupo
-contacts = self.env['res.partner'].search([('whatsapp_web_id', '!=', False)])
-group.contact_ids = [(6, 0, contacts.ids)]
-
-# Crear canal de discusión
-channel = group._create_discussion_channel()
-```
-
-### Gestión de Contactos
-
-```python
-# Buscar contactos de WhatsApp Web
-contacts = self.env['res.partner'].search([
-    ('whatsapp_web_id', '!=', False)
-])
-
-# Agregar contacto a grupo
-group = self.env['ww.group'].browse(1)
-contact = self.env['res.partner'].browse(1)
-
-# Crear relación con rol
-rel = self.env['ww.group_contact_rel'].create({
-    'group_id': group.id,
-    'contact_id': contact.id,
-    'is_admin': True,
-    'role_id': admin_role.id
-})
-```
-
-### Envío de Mensajes a Grupos
-
-```python
-# Envío directo
-group = self.env['ww.group'].browse(1)
-message = group.send_whatsapp_message("Mensaje importante!")
-
-# Envío con adjunto
-attachment = self.env['ir.attachment'].create({
-    'name': 'documento.pdf',
-    'type': 'binary',
-    'datas': base64.b64encode(pdf_content),
-    'mimetype': 'application/pdf'
-})
-
-message = group.send_whatsapp_message(
-    body="Adjunto documento importante",
-    attachment=attachment
-)
-
-# Envío usando composer
-action = group.action_send_whatsapp_message()
-```
-
-### Procesamiento de Mensajes
-
-```python
-# Procesar mensajes de un grupo
-group = self.env['ww.group'].browse(1)
-messages_data = [
-    {
-        'id': {'_serialized': 'msg_id_123'},
-        'body': 'Mensaje de prueba',
-        'author': '5215551234567@c.us',
-        'timestamp': 1640995200,
-        'hasQuotedMsg': True,
-        'quotedMsg': {
-            'body': 'Mensaje citado',
-            'author': '5215557654321@c.us'
-        }
-    }
-]
-
-result = group._process_messages(messages_data)
-```
-
-### Gestión de Roles
-
-```python
-# Crear roles
-admin_role = self.env['ww.role'].create({
-    'name': 'Administrador',
-    'description': 'Administrador del grupo'
-})
-
-member_role = self.env['ww.role'].create({
-    'name': 'Miembro',
-    'description': 'Miembro regular del grupo'
-})
-
-# Asignar roles a contactos en grupos
-group = self.env['ww.group'].browse(1)
-for contact in group.contact_ids:
-    rel = self.env['ww.group_contact_rel'].search([
-        ('group_id', '=', group.id),
-        ('contact_id', '=', contact.id)
-    ])
-    
-    if contact.is_admin:
-        rel.role_id = admin_role.id
-    else:
-        rel.role_id = member_role.id
-```
-
-### Consultas Avanzadas
-
-```python
-# Grupos con más de 10 miembros
-large_groups = self.env['ww.group'].search([
-    ('contact_ids', '!=', False)
-])
-large_groups = [g for g in large_groups if len(g.contact_ids) > 10]
-
-# Contactos administradores
-admin_contacts = self.env['res.partner'].search([
-    ('group_ids', '!=', False)
-])
-admin_contacts = [c for c in admin_contacts 
-                  if any(rel.is_admin for rel in c.group_ids)]
-
-# Grupos sin canal
-groups_without_channel = self.env['ww.group'].search([
-    ('channel_id', '=', False)
-])
-```
-
-## Respuestas de API
-
-### Respuesta de getGroups()
-
-```json
-[
-    {
-        "id": {
-            "_serialized": "120363158956331133@g.us"
-        },
-        "name": "Mi Grupo de Trabajo",
-        "description": "Grupo para coordinación de proyectos",
-        "members": [
-            {
-                "id": {
-                    "_serialized": "5215551234567@c.us"
-                },
-                "number": "5551234567",
-                "name": "Juan Pérez",
-                "pushname": "Juan",
-                "isAdmin": true,
-                "isSuperAdmin": false
-            },
-            {
-                "id": {
-                    "_serialized": "5215557654321@c.us"
-                },
-                "number": "5557654321",
-                "name": "María García",
-                "pushname": "María",
-                "isAdmin": false,
-                "isSuperAdmin": false
-            }
-        ],
-        "messages": [
-            {
-                "id": {
-                    "_serialized": "3EB0C767D26A3D1B7B4A"
-                },
-                "body": "Hola grupo!",
-                "author": "5215551234567@c.us",
-                "timestamp": 1640995200,
-                "hasQuotedMsg": false,
-                "quotedParticipant": null,
-                "quotedMsg": null
-            }
-        ]
-    }
-]
-```
-
-### Respuesta de Creación de Canal
-
-```python
-# Respuesta exitosa
-{
-    'id': 123,
-    'name': '📱 Mi Grupo de Trabajo',
-    'channel_type': 'channel',
-    'member_count': 5
-}
-
-# Respuesta de error (False)
-False
-```
-
-### Respuesta de Envío de Mensaje
-
-```python
-# Mensaje enviado exitosamente
-{
-    'id': 456,
-    'state': 'sent',
-    'msg_uid': '3EB0C767D26A3D1B7B4A_120363158956331133@g.us',
-    'body': 'Mensaje importante!',
-    'recipient_type': 'group'
-}
-```
-
-## Manejo de Errores
-
-### Excepciones Comunes
-
-#### ValueError - Grupo sin cuenta
-```python
-try:
-    group.send_whatsapp_message("Mensaje")
-except ValueError as e:
-    print(f"Error: {e}")  # "Group must have a WhatsApp account configured"
-```
-
-#### ValidationError - Datos inválidos
-```python
-from odoo.exceptions import ValidationError
-
-try:
-    group = self.env['ww.group'].create({
-        'name': '',  # Nombre vacío
-        'whatsapp_web_id': 'invalid_id'
-    })
-except ValidationError as e:
-    print(f"Error de validación: {e}")
-```
-
-### Logs de Debugging
-
-```python
-import logging
-_logger = logging.getLogger(__name__)
-
-# Log de sincronización
-_logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
-
-# Log de error
-_logger.error("Error en la sincronización de grupos para la cuenta %s: %s", 
-              account.name, str(e))
-
-# Log de advertencia
-_logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
-```
-
-## Configuración de Cron Jobs
-
-### Configuración Básica
-
-```xml
-<record id="ir_cron_sync_ww_contacts_groups" model="ir.cron">
-    <field name="name">Sincronizar Contactos y Grupos WhatsApp Web</field>
-    <field name="model_id" ref="model_ww_group"/>
-    <field name="state">code</field>
-    <field name="code">model.sync_ww_contacts_groups()</field>
-    <field name="interval_number">1</field>
-    <field name="interval_type">hours</field>
-    <field name="active" eval="False"/>
-</record>
-```
-
-### Configuración Personalizada
-
-```python
-# Crear cron job personalizado
-cron = self.env['ir.cron'].create({
-    'name': 'Sincronización Personalizada',
-    'model_id': self.env.ref('whatsapp_web_groups.model_ww_group').id,
-    'state': 'code',
-    'code': 'model.sync_ww_contacts_groups()',
-    'interval_number': 2,
-    'interval_type': 'hours',
-    'active': True,
-    'user_id': self.env.user.id
-})
-```
-
-## Optimización de Performance
-
-### Sincronización en Lotes
-
-```python
-# Procesar grupos en lotes para evitar timeouts
-def sync_groups_in_batches(self, batch_size=10):
-    accounts = self.env['whatsapp.account'].search([])
-    
-    for account in accounts:
-        groups_data = account.get_groups()
-        
-        # Procesar en lotes
-        for i in range(0, len(groups_data), batch_size):
-            batch = groups_data[i:i + batch_size]
-            self._process_groups_batch(batch, account)
-            
-            # Commit después de cada lote
-            self._cr.commit()
-```
-
-### Creación Bulk de Mensajes
-
-```python
-def _process_messages_bulk(self, messages_data):
-    """Procesar mensajes en bulk para mejor performance"""
-    if not messages_data:
-        return True
-    
-    # Preparar valores para creación bulk
-    message_vals_list = []
-    for msg_data in messages_data:
-        message_vals = self._prepare_message_vals(msg_data)
-        message_vals_list.append(message_vals)
-    
-    # Crear todos los mensajes de una vez
-    if message_vals_list:
-        self.env['mail.message'].create(message_vals_list)
-    
-    return True
-```
-
-## Limpieza y Mantenimiento
-
-### Limpiar Grupos Vacíos
-
-```python
-def cleanup_empty_groups(self):
-    """Eliminar grupos sin contactos"""
-    empty_groups = self.env['ww.group'].search([
-        ('contact_ids', '=', False)
-    ])
-    
-    if empty_groups:
-        _logger.info(f"Eliminando {len(empty_groups)} grupos vacíos")
-        empty_groups.unlink()
-```
-
-### Limpiar Contactos Huérfanos
-
-```python
-def cleanup_orphan_contacts(self):
-    """Limpiar contactos sin grupos"""
-    orphan_contacts = self.env['res.partner'].search([
-        ('whatsapp_web_id', '!=', False),
-        ('group_ids', '=', False)
-    ])
-    
-    if orphan_contacts:
-        _logger.info(f"Limpiando {len(orphan_contacts)} contactos huérfanos")
-        orphan_contacts.write({'whatsapp_web_id': False})
-```
-
-### Regenerar Canales Perdidos
-
-```python
-def regenerate_missing_channels(self):
-    """Regenerar canales que se perdieron"""
-    groups_without_channel = self.env['ww.group'].search([
-        ('channel_id', '=', False),
-        ('contact_ids', '!=', False)
-    ])
-    
-    for group in groups_without_channel:
-        try:
-            channel = group._create_discussion_channel()
-            if channel:
-                _logger.info(f"Canal regenerado para grupo {group.name}")
-        except Exception as e:
-            _logger.error(f"Error regenerando canal para {group.name}: {e}")
-```
-

+ 0 - 386
whatsapp_web_groups_local_backup_20251214231432/README.md

@@ -1,386 +0,0 @@
-# Gestor de Grupos de WhatsApp Web para Odoo 18
-
-## Descripción
-
-Este módulo permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando automáticamente con la API de whatsapp-web.js. Proporciona una interfaz completa para administrar grupos de WhatsApp y sus miembros dentro de Odoo.
-
-## Características Principales
-
-- ✅ Sincronización automática de grupos de WhatsApp Web
-- ✅ Gestión de contactos y miembros de grupos
-- ✅ Creación automática de canales de discusión para cada grupo
-- ✅ Sistema de roles y permisos para miembros
-- ✅ Integración con el sistema de contactos de Odoo
-- ✅ Procesamiento automático de mensajes de grupos
-- ✅ Sincronización programada con cron jobs
-- ✅ Envío directo de mensajes a grupos
-
-## Requisitos
-
-- Odoo 18.0
-- Módulo `whatsapp_web` (dependencia obligatoria)
-- Módulos: `base`, `contacts`, `mail`, `calendar`, `helpdesk`
-- Servidor whatsapp-web.js configurado y funcionando
-
-## Instalación
-
-1. **Instalar dependencias:**
-   ```bash
-   cd /var/odoo/mcteam.run
-   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web
-   ```
-
-2. **Instalar el módulo:**
-   ```bash
-   sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -i whatsapp_web_groups
-   ```
-
-3. **Reiniciar el servidor Odoo:**
-   ```bash
-   ./restart_odoo.sh
-   ```
-
-## Configuración Inicial
-
-### 1. Configurar Cuenta WhatsApp Web
-
-1. Ir a **WhatsApp > Configuración > Cuentas WhatsApp**
-2. Crear o editar una cuenta WhatsApp
-3. Configurar la **WhatsApp Web URL** (ej: `https://web.whatsapp.com/api`)
-4. Guardar la configuración
-
-### 2. Sincronización Inicial
-
-1. Ir a **WhatsApp Web > Grupos**
-2. Hacer clic en "Sincronizar" o ejecutar manualmente:
-   ```python
-   self.env['ww.group'].sync_ww_contacts_groups()
-   ```
-
-### 3. Configurar Cron Job (Opcional)
-
-Para sincronización automática cada hora:
-
-1. Ir a **Configuración > Técnico > Automatización > Acciones Programadas**
-2. Buscar "Sincronizar Contactos y Grupos WhatsApp Web"
-3. Activar el cron job
-4. Configurar intervalo deseado
-
-## Uso
-
-### Gestión de Grupos
-
-#### Ver Grupos Sincronizados
-1. Ir a **WhatsApp Web > Grupos**
-2. Ver lista de todos los grupos sincronizados
-3. Hacer clic en un grupo para ver detalles
-
-#### Enviar Mensaje a Grupo
-1. Seleccionar un grupo de la lista
-2. Hacer clic en "Send WhatsApp Message"
-3. Completar el composer de WhatsApp
-4. Enviar el mensaje
-
-#### Sincronización Manual
-```python
-# Desde shell de Odoo
-groups = self.env['ww.group']
-groups.sync_ww_contacts_groups()
-```
-
-### Gestión de Contactos
-
-#### Ver Contactos WhatsApp
-1. Ir a **Contactos > Contactos WhatsApp Web**
-2. Ver todos los contactos sincronizados de WhatsApp
-3. Editar información de contactos si es necesario
-
-#### Contactos en Grupos
-- Los contactos se sincronizan automáticamente cuando están en grupos
-- Se actualizan los números de teléfono y nombres
-- Se evitan duplicados usando los últimos 10 dígitos del móvil
-
-### Canales de Discusión
-
-#### Creación Automática
-- Cada grupo crea automáticamente un canal de discusión
-- El canal se nombra con prefijo "📱" + nombre del grupo
-- Todos los miembros del grupo se agregan al canal
-
-#### Gestión de Canales
-1. Ir al canal desde el menú de discusión
-2. Los mensajes de WhatsApp se procesan automáticamente
-3. Se mantiene historial de conversaciones
-
-## Estructura de Datos
-
-### Modelos Principales
-
-#### `ww.group`
-Representa un grupo de WhatsApp Web.
-
-**Campos:**
-- `name`: Nombre del grupo
-- `whatsapp_web_id`: ID único en WhatsApp Web
-- `whatsapp_account_id`: Cuenta WhatsApp asociada
-- `channel_id`: Canal de discusión creado
-- `contact_ids`: Contactos miembros del grupo
-
-#### `ww.contact` (Extiende `res.partner`)
-Contactos de WhatsApp Web.
-
-**Campos adicionales:**
-- `whatsapp_web_id`: ID único en WhatsApp Web
-- `group_ids`: Grupos donde participa el contacto
-
-#### `ww.role`
-Roles que pueden tener los miembros en grupos.
-
-**Campos:**
-- `name`: Nombre del rol
-- `description`: Descripción del rol
-
-#### `ww.group_contact_rel`
-Relación entre grupos y contactos con roles.
-
-**Campos:**
-- `group_id`: Referencia al grupo
-- `contact_id`: Referencia al contacto
-- `is_admin`: Si es administrador
-- `is_super_admin`: Si es super administrador
-- `role_id`: Rol asignado
-
-## API del Módulo
-
-### Métodos Principales
-
-#### `ww.group.sync_ww_contacts_groups()`
-Sincroniza todos los grupos y contactos desde WhatsApp Web.
-
-```python
-# Sincronización completa
-self.env['ww.group'].sync_ww_contacts_groups()
-
-# Sincronización por cuenta específica
-account = self.env['whatsapp.account'].browse(1)
-groups_data = account.get_groups()
-```
-
-#### `ww.group._create_discussion_channel()`
-Crea un canal de discusión para el grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-channel = group._create_discussion_channel()
-```
-
-#### `ww.group._process_messages(messages_data)`
-Procesa mensajes de WhatsApp y los crea en el canal.
-
-```python
-group = self.env['ww.group'].browse(1)
-messages = [
-    {
-        'id': {'_serialized': 'message_id'},
-        'body': 'Contenido del mensaje',
-        'author': 'contacto_id',
-        'timestamp': 1234567890
-    }
-]
-group._process_messages(messages)
-```
-
-#### `ww.group.send_whatsapp_message(body, attachment=None, wa_template_id=None)`
-Envía un mensaje WhatsApp al grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-message = group.send_whatsapp_message(
-    body="Hola grupo!",
-    attachment=attachment_record,
-    wa_template_id=template_record
-)
-```
-
-#### `ww.group.action_send_whatsapp_message()`
-Abre el composer de WhatsApp para enviar mensaje al grupo.
-
-```python
-group = self.env['ww.group'].browse(1)
-action = group.action_send_whatsapp_message()
-```
-
-### Métodos de Sincronización
-
-#### `whatsapp.account.get_groups()`
-Obtiene grupos desde el servidor whatsapp-web.js.
-
-**Respuesta esperada:**
-```json
-[
-    {
-        "id": {"_serialized": "120363158956331133@g.us"},
-        "name": "Mi Grupo",
-        "members": [
-            {
-                "id": {"_serialized": "5215551234567@c.us"},
-                "number": "5551234567",
-                "name": "Juan Pérez",
-                "isAdmin": false,
-                "isSuperAdmin": false
-            }
-        ],
-        "messages": [...]
-    }
-]
-```
-
-## Configuración Avanzada
-
-### Personalización de Sincronización
-
-#### Modificar Intervalo de Sincronización
-```xml
-<!-- En data/ir_cron.xml -->
-<field name="interval_number">2</field>
-<field name="interval_type">hours</field>
-```
-
-#### Filtros de Sincronización
-```python
-# Sincronizar solo grupos específicos
-accounts = self.env['whatsapp.account'].search([
-    ('name', 'ilike', 'cuenta_especifica')
-])
-for account in accounts:
-    groups_data = account.get_groups()
-    # Procesar solo grupos deseados
-```
-
-### Gestión de Permisos
-
-#### Configurar Accesos
-```csv
-# En security/ir.model.access.csv
-access_ww_group_user,ww.group.user,model_ww_group,base.group_user,1,1,1,0
-access_ww_group_manager,ww.group.manager,model_ww_group,base.group_system,1,1,1,1
-```
-
-## Solución de Problemas
-
-### Error: "No hay contactos para crear el canal"
-- **Causa**: El grupo no tiene miembros o la sincronización falló
-- **Solución**: Ejecutar sincronización manual y verificar conectividad
-
-### Error: "Error al crear el canal para el grupo"
-- **Causa**: Problemas de permisos o configuración de grupos de usuario
-- **Solución**: Verificar que el usuario tenga permisos para crear canales
-
-### Grupos no se sincronizan
-- **Causa**: Servidor whatsapp-web.js no responde o método `getGroups` no implementado
-- **Solución**: 
-  1. Verificar conectividad con el servidor
-  2. Confirmar implementación del método `getGroups`
-  3. Revisar logs del servidor whatsapp-web.js
-
-### Contactos duplicados
-- **Causa**: Múltiples números similares en diferentes formatos
-- **Solución**: El módulo usa los últimos 10 dígitos para evitar duplicados automáticamente
-
-### Mensajes no se procesan en canales
-- **Causa**: Formato incorrecto de datos de mensajes o canal no creado
-- **Solución**:
-  1. Verificar que el grupo tenga canal asociado
-  2. Confirmar formato de datos de mensajes
-  3. Revisar permisos de escritura en canales
-
-## Logs y Debugging
-
-### Logs de Sincronización
-```bash
-# Ver logs de sincronización
-tail -f /var/odoo/stg2.mcteam.run/logs/odoo-server.log | grep -i "sincroniz\|grupo\|contacto"
-```
-
-### Debug de Grupos
-```python
-# Verificar estado de grupos
-groups = self.env['ww.group'].search([])
-for group in groups:
-    print(f"Grupo: {group.name}")
-    print(f"Contactos: {len(group.contact_ids)}")
-    print(f"Canal: {group.channel_id.name if group.channel_id else 'Sin canal'}")
-```
-
-### Debug de Sincronización
-```python
-# Probar sincronización paso a paso
-account = self.env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
-groups_data = account.get_groups()
-print(f"Grupos obtenidos: {len(groups_data)}")
-```
-
-## Mantenimiento
-
-### Limpieza de Datos
-```python
-# Limpiar grupos sin contactos
-empty_groups = self.env['ww.group'].search([
-    ('contact_ids', '=', False)
-])
-empty_groups.unlink()
-
-# Limpiar contactos sin grupos
-orphan_contacts = self.env['res.partner'].search([
-    ('whatsapp_web_id', '!=', False),
-    ('group_ids', '=', False)
-])
-orphan_contacts.write({'whatsapp_web_id': False})
-```
-
-### Optimización de Performance
-- La sincronización procesa grupos en lotes para evitar timeouts
-- Los mensajes se crean en bulk para mejorar performance
-- Se evitan duplicados usando constraints de base de datos
-
-## Actualizaciones
-
-Para actualizar el módulo:
-
-```bash
-cd /var/odoo/mcteam.run
-sudo -u odoo venv/bin/python3 src/odoo-bin -c odoo.conf -u whatsapp_web_groups
-./restart_odoo.sh
-```
-
-## Integración con Otros Módulos
-
-### Marketing
-- Los grupos se pueden usar como destinatarios en campañas de marketing
-- Soporte para plantillas personalizadas por grupo
-
-### Helpdesk
-- Crear tickets automáticamente desde mensajes de grupos
-- Asignar SLAs basados en roles de grupo
-
-### Calendar
-- Programar reuniones con miembros de grupos
-- Invitaciones automáticas a eventos
-
-## Soporte
-
-Para soporte técnico o reportar bugs:
-- Revisar logs de Odoo y del servidor whatsapp-web.js
-- Verificar configuración de cuentas WhatsApp
-- Confirmar conectividad de red
-
-## Changelog
-
-### Versión 1.0
-- Implementación inicial del gestor de grupos
-- Sincronización automática de grupos y contactos
-- Creación automática de canales de discusión
-- Sistema de roles y permisos
-- Procesamiento de mensajes de grupos
-- Integración con composer de WhatsApp
-

+ 0 - 1
whatsapp_web_groups_local_backup_20251214231432/__init__.py

@@ -1 +0,0 @@
-from . import models 

+ 0 - 31
whatsapp_web_groups_local_backup_20251214231432/__manifest__.py

@@ -1,31 +0,0 @@
-{
-    'name': 'Gestor de Grupos de WhatsApp',
-    'version': '1.0',
-    'summary': 'Gestión de grupos y contactos de WhatsApp Web',
-    'description': 'Permite gestionar grupos, contactos y roles de WhatsApp Web, sincronizando con la API de whatsapp_web.js',
-    'author': 'MC Team',
-    'category': 'Tools',
-    'depends': [
-        'base',
-        'contacts',
-        'whatsapp_web',
-        'marketing_automation_whatsapp',
-        'mail',
-        'calendar',
-        'helpdesk',
-    ],
-    'data': [
-        'security/ir.model.access.csv',
-        'views/ww_contact_views.xml',
-        'views/ww_group_views.xml',
-        'views/ww_role_views.xml',
-        'views/ww_group_contact_rel_views.xml',
-        'views/marketing_activity_views.xml',
-        'views/whatsapp_message_views.xml',
-        'views/whatsapp_composer_views.xml',
-        'data/ir_cron.xml',
-    ],
-    'installable': True,
-    'application': True,
-    'auto_install': False,
-} 

+ 0 - 14
whatsapp_web_groups_local_backup_20251214231432/data/ir_cron.xml

@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo noupdate="1">
-
-    <record id="ir_cron_sync_ww_contacts_groups" model="ir.cron">
-        <field name="name">Sincronizar Contactos y Grupos WhatsApp Web</field>
-        <field name="model_id" ref="model_ww_group"/>
-        <field name="state">code</field>
-        <field name="code">model.sync_ww_contacts_groups()</field>
-        <field name="interval_number">1</field>
-        <field name="interval_type">hours</field>
-        <field name="active" eval="False"/>
-    </record>
-    
-</odoo> 

+ 0 - 7
whatsapp_web_groups_local_backup_20251214231432/models/__init__.py

@@ -1,7 +0,0 @@
-from . import ww_contact
-from . import ww_group
-from . import ww_role
-from . import ww_group_contact_rel
-from . import marketing_activity
-from . import whatsapp_message
-from . import whatsapp_composer 

+ 0 - 99
whatsapp_web_groups_local_backup_20251214231432/models/marketing_activity.py

@@ -1,99 +0,0 @@
-from odoo import models, fields
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class MarketingActivity(models.Model):
-    _inherit = 'marketing.activity'
-
-    # Campo para grupo de WhatsApp en actividades de marketing
-    whatsapp_group_id = fields.Many2one(
-        'ww.group', 
-        string='Grupo de WhatsApp',
-        help="Grupo de WhatsApp para enviar mensajes (opcional). Si está vacío, se envían a destinatarios individuales según la plantilla."
-    )
-
-    def _execute_whatsapp(self, traces):
-        """Override para soportar envío a grupos de WhatsApp usando campos nativos"""
-        _logger.info(f"Ejecutando WhatsApp para actividad {self.id}: {self.name}")
-        
-        # Si hay grupo configurado, usar la lógica simple que funcionaba antes
-        if self.whatsapp_group_id:
-            _logger.info(f"Enviando mensaje WhatsApp al grupo NATIVO: {self.whatsapp_group_id.name}")
-            
-            try:
-                # Usar el método original pero con mobile_number del grupo
-                res_ids = [res_id for res_id in set(traces.mapped('res_id')) if res_id]
-                now = self.env.cr.now()
-
-                composer_vals = {
-                    'res_model': self.model_name, 
-                    'res_ids': res_ids,
-                    'wa_template_id': self.whatsapp_template_id.id,
-                    'batch_mode': True,
-                    'phone': self.whatsapp_group_id.whatsapp_web_id,  # ✅ Usar ID del grupo como "teléfono"
-                }
-                
-                _logger.info(f"Usando método original con grupo ID: {self.whatsapp_group_id.whatsapp_web_id}")
-                composer = self.env['whatsapp.composer'].with_context(active_model=self.model_name).create(composer_vals)
-                messages = composer._create_whatsapp_messages(force_create=True)
-                message_by_res_id = {r.mail_message_id.res_id: r for r in messages}
-                
-                # Asignar mensajes a traces (como el original)
-                for trace in self.trace_ids:
-                    res_id = trace.res_id
-                    message = message_by_res_id.get(res_id, self.env['whatsapp.message'])
-                    if message:
-                        trace.whatsapp_message_id = message.id
-                        # Marcar como grupo
-                        message.write({
-                            'recipient_type': 'group',
-                            'whatsapp_group_id': self.whatsapp_group_id.id,
-                        })
-                    if not message.mobile_number:
-                        message.state = 'error'
-                        message.failure_type = 'phone_invalid'
-
-                # Enviar mensajes (como el original)
-                messages._send()
-                    
-                _logger.info(f"Mensaje enviado exitosamente al grupo {self.whatsapp_group_id.name}")
-                    
-            except Exception as e:
-                _logger.warning('Marketing Automation: actividad <%s> error al enviar a grupo WhatsApp %s', self.id, str(e))
-                traces.write({
-                    'state': 'error',
-                    'schedule_date': now,
-                    'state_msg': f'Error al enviar a grupo WhatsApp: {e}',
-                })
-            else:
-                # LÓGICA ORIGINAL: Marcar traces como procesados
-                cancelled_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'cancel')
-                error_traces = traces.filtered(lambda trace: trace.whatsapp_message_id.state == 'error')
-
-                if cancelled_traces:
-                    cancelled_traces.write({
-                        'state': 'canceled',
-                        'schedule_date': now,
-                        'state_msg': 'WhatsApp canceled'
-                    })
-                if error_traces:
-                    error_traces.write({
-                        'state': 'error',
-                        'schedule_date': now,
-                        'state_msg': 'WhatsApp failed'
-                    })
-
-                processed_traces = traces - (cancelled_traces | error_traces)
-                if processed_traces:
-                    processed_traces.write({
-                        'state': 'processed',
-                        'schedule_date': now,
-                    })
-        else:
-            _logger.info(f"Campo whatsapp_group_id VACÍO - Enviando a destinatarios individuales")
-            # Usar lógica original para envío individual
-            super()._execute_whatsapp(traces)
-        
-        return True
-

+ 0 - 115
whatsapp_web_groups_local_backup_20251214231432/models/whatsapp_composer.py

@@ -1,115 +0,0 @@
-from odoo import models, fields, api
-from odoo.exceptions import ValidationError
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class WhatsAppComposer(models.TransientModel):
-    _inherit = 'whatsapp.composer'
-
-    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
-    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
-                                        help="Select WhatsApp group to send message to",
-                                        ondelete='set null')
-
-    @api.onchange('whatsapp_group_id')
-    def _onchange_whatsapp_group_id(self):
-        """Actualizar campos cuando se selecciona un grupo"""
-        if self.whatsapp_group_id:
-            self.whatsapp_group_id_char = self.whatsapp_group_id.whatsapp_web_id
-            self.recipient_type = 'group'
-            self.phone = False
-
-    @api.onchange('recipient_type')
-    def _onchange_recipient_type(self):
-        """Limpiar campos al cambiar tipo de destinatario"""
-        super()._onchange_recipient_type()
-        if self.recipient_type != 'group':
-            self.whatsapp_group_id = False
-
-    @api.constrains('recipient_type', 'phone', 'whatsapp_group_id', 'whatsapp_group_id_char', 'wa_template_id', 'body')
-    def _check_recipient_configuration(self):
-        """Extender validación para incluir whatsapp_group_id"""
-        super()._check_recipient_configuration()
-        
-        for record in self:
-            if record.recipient_type == 'group':
-                if not record.whatsapp_group_id and not record.whatsapp_group_id_char:
-                    raise ValidationError("Please select a WhatsApp group or enter a Group ID when sending to groups")
-
-    def _send_whatsapp_web_message(self):
-        """Extender método para usar whatsapp_group_id si está disponible"""
-        records = self._get_active_records()
-        
-        for record in records:
-            # Determinar destinatario - priorizar whatsapp_group_id sobre whatsapp_group_id_char
-            if self.recipient_type == 'group':
-                if self.whatsapp_group_id:
-                    mobile_number = self.whatsapp_group_id.whatsapp_web_id
-                elif self.whatsapp_group_id_char:
-                    mobile_number = self.whatsapp_group_id_char
-                else:
-                    raise ValidationError("Please specify a group")
-            else:
-                mobile_number = self.phone
-                if not mobile_number:
-                    raise ValidationError("Please provide a phone number")
-            
-            # Crear mail.message con adjuntos si existen
-            post_values = {
-                'attachment_ids': [self.attachment_id.id] if self.attachment_id else [],
-                'body': self.body,
-                'message_type': 'whatsapp_message',
-                'partner_ids': hasattr(record, '_mail_get_partners') and record._mail_get_partners()[record.id].ids or record._whatsapp_get_responsible().partner_id.ids,
-            }
-            
-            if hasattr(records, '_message_log'):
-                message = record._message_log(**post_values)
-            else:
-                message = self.env['mail.message'].create(
-                    dict(post_values, res_id=record.id, model=self.res_model,
-                         subtype_id=self.env['ir.model.data']._xmlid_to_res_id("mail.mt_note"))
-                )
-            
-            # Crear mensaje WhatsApp
-            message_vals = {
-                'mail_message_id': message.id,
-                'mobile_number': mobile_number,
-                'mobile_number_formatted': mobile_number,
-                'recipient_type': self.recipient_type,
-                'wa_template_id': False,
-                'wa_account_id': self._get_whatsapp_web_account().id,
-                'state': 'outgoing',
-            }
-            
-            # Agregar whatsapp_group_id si está disponible
-            if self.whatsapp_group_id:
-                message_vals['whatsapp_group_id'] = self.whatsapp_group_id.id
-            
-            whatsapp_message = self.env['whatsapp.message'].create(message_vals)
-            
-            # Enviar mensaje
-            whatsapp_message._send_message()
-        
-        return {'type': 'ir.actions.act_window_close'}
-
-    def _prepare_whatsapp_message_values(self, record):
-        """Extender método para agregar información de grupo"""
-        values = super()._prepare_whatsapp_message_values(record)
-        
-        # Agregar información de grupo si está disponible
-        if (hasattr(self, 'recipient_type') and self.recipient_type == 'group'):
-            if self.whatsapp_group_id:
-                values.update({
-                    'whatsapp_group_id': self.whatsapp_group_id.id,
-                    'mobile_number': self.whatsapp_group_id.whatsapp_web_id,
-                    'mobile_number_formatted': self.whatsapp_group_id.whatsapp_web_id,
-                })
-            elif self.whatsapp_group_id_char:
-                values.update({
-                    'mobile_number': self.whatsapp_group_id_char,
-                    'mobile_number_formatted': self.whatsapp_group_id_char,
-                })
-        
-        return values
-

+ 0 - 64
whatsapp_web_groups_local_backup_20251214231432/models/whatsapp_message.py

@@ -1,64 +0,0 @@
-from odoo import models, fields, api
-from odoo.exceptions import ValidationError
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class WhatsAppMessage(models.Model):
-    _inherit = 'whatsapp.message'
-
-    # Campo Many2one para grupos - solo disponible cuando whatsapp_web_groups está instalado
-    whatsapp_group_id = fields.Many2one('ww.group', string='WhatsApp Group', 
-                                        help="WhatsApp group to send message to (if recipient_type is group)",
-                                        ondelete='set null')
-
-    @api.depends('recipient_type', 'mobile_number', 'whatsapp_group_id')
-    def _compute_final_recipient(self):
-        """Compute the final recipient based on type - extiende la lógica base"""
-        # Primero ejecutar la lógica base de whatsapp_web
-        super()._compute_final_recipient()
-        
-        # Si hay grupo seleccionado, usar su ID (sobrescribe la lógica base)
-        for record in self:
-            if record.recipient_type == 'group' and record.whatsapp_group_id:
-                record.final_recipient = record.whatsapp_group_id.whatsapp_web_id
-
-    @api.onchange('whatsapp_group_id')
-    def _onchange_whatsapp_group_id(self):
-        """Actualizar mobile_number cuando se selecciona un grupo"""
-        if self.whatsapp_group_id:
-            self.mobile_number = self.whatsapp_group_id.whatsapp_web_id
-            self.recipient_type = 'group'
-
-    @api.constrains('recipient_type', 'mobile_number', 'whatsapp_group_id')
-    def _check_recipient_configuration(self):
-        """Extender validación para incluir whatsapp_group_id"""
-        super()._check_recipient_configuration()
-        
-        for record in self:
-            if record.recipient_type == 'group':
-                if not record.whatsapp_group_id and not (record.mobile_number and record.mobile_number.endswith('@g.us')):
-                    raise ValidationError("Para mensajes a grupos, debe seleccionar un grupo o proporcionar un ID de grupo válido (@g.us)")
-
-    def _get_final_destination(self):
-        """Método mejorado para obtener destino final - extiende la lógica base"""
-        self.ensure_one()
-        
-        # Si hay grupo seleccionado, usar su ID
-        if self.recipient_type == 'group' and self.whatsapp_group_id:
-            return self.whatsapp_group_id.whatsapp_web_id
-        
-        # De lo contrario, usar la lógica base (incluye verificación de mobile_number @g.us)
-        result = super()._get_final_destination()
-        if result:
-            return result
-        
-        # Fallback adicional si no hay resultado
-        return False
-
-    def _send_message(self, with_commit=False):
-        """Extender método _send_message para manejar whatsapp_group_id"""
-        # El método _get_final_destination ya maneja whatsapp_group_id,
-        # así que la lógica base funcionará correctamente
-        return super()._send_message(with_commit)
-

+ 0 - 37
whatsapp_web_groups_local_backup_20251214231432/models/ww_contact.py

@@ -1,37 +0,0 @@
-from odoo import models, fields
-
-class WWContact(models.Model):
-    _inherit = 'res.partner'
-    _description = 'Contacto de WhatsApp Web'
-
-    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del contacto en WhatsApp Web')
-    group_ids = fields.Many2many(
-        comodel_name='ww.group',
-        relation='ww_group_contact_rel',
-        column1='contact_id',
-        column2='group_id',
-        string='Grupos',
-        readonly=True,
-    )
-    channel_ids = fields.Many2many(
-        comodel_name='discuss.channel',
-        relation='discuss_channel_member',
-        column1='partner_id',
-        column2='channel_id',
-        string='Canales',
-        readonly=True,
-    )
-    meeting_ids = fields.One2many(
-        comodel_name='calendar.event',
-        inverse_name='partner_id',
-        string='Reuniones',
-        readonly=True,
-    )
-    sla_ids = fields.Many2many(
-        comodel_name='helpdesk.sla',
-        relation='helpdesk_sla_partner',
-        column1='partner_id',
-        column2='sla_id',
-        string='SLAs',
-        readonly=True,
-    ) 

+ 0 - 331
whatsapp_web_groups_local_backup_20251214231432/models/ww_group.py

@@ -1,331 +0,0 @@
-from odoo import models, fields, api
-import logging
-from datetime import datetime
-
-_logger = logging.getLogger(__name__)
-
-class WWGroup(models.Model):
-    _name = 'ww.group'
-    _description = 'Grupo de WhatsApp Web'
-
-    name = fields.Char(string='Nombre del Grupo', required=True)
-    whatsapp_web_id = fields.Char(string='ID WhatsApp Web', index=True, help='ID único del grupo en WhatsApp Web')
-    whatsapp_account_id = fields.Many2one('whatsapp.account', string='Cuenta de WhatsApp', required=True)
-    channel_id = fields.Many2one('discuss.channel', string='Canal de Discusión', readonly=True)
-    contact_ids = fields.Many2many(
-        comodel_name='res.partner',
-        relation='ww_group_contact_rel',
-        column1='group_id',
-        column2='contact_id',
-        string='Contactos',
-        readonly=True,
-    )
-
-    def _process_messages(self, messages_data):
-        """Process WhatsApp messages and create them in the channel"""
-        self.ensure_one()
-        
-        if not messages_data or not self.channel_id:
-            return True
-
-        # Get existing message IDs to avoid duplicates
-        existing_ids = set(self.channel_id.message_ids.mapped('message_id'))
-        
-        # Prepare bulk create values
-        message_vals_list = []
-        for msg_data in messages_data:
-            msg_id = msg_data.get('id', {}).get('_serialized')
-            
-            # Skip if message already exists
-            if msg_id in existing_ids:
-                continue
-
-            # Get author partner
-            author_whatsapp_id = msg_data.get('author')
-            author = self.env['res.partner'].search([
-                ('whatsapp_web_id', '=', author_whatsapp_id)
-            ], limit=1) if author_whatsapp_id else False
-
-            # Get quoted message author if exists
-            quoted_author = False
-            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedParticipant'):
-                quoted_author = self.env['res.partner'].search([
-                    ('whatsapp_web_id', '=', msg_data['quotedParticipant'])
-                ], limit=1)
-
-            # Convert timestamp to datetime
-            timestamp = datetime.fromtimestamp(msg_data.get('timestamp', 0))
-
-            # Prepare message body with author and content
-            author_name = author.name if author else "Desconocido"
-            message_body = f"{msg_data.get('body', '')}"
-
-            # Add quoted message if exists
-            if msg_data.get('hasQuotedMsg') and msg_data.get('quotedMsg', {}).get('body'):
-                quoted_author_name = quoted_author.name if quoted_author else "Desconocido"
-                message_body += f"\n\n<blockquote><strong>{quoted_author_name}:</strong> {msg_data['quotedMsg']['body']}</blockquote>"
-
-            message_vals = {
-                'model': 'discuss.channel',
-                'res_id': self.channel_id.id,
-                'message_type': 'comment',
-                'subtype_id': self.env.ref('mail.mt_comment').id,
-                'body': message_body,
-                'date': timestamp,
-                'author_id': author.id if author else self.env.user.partner_id.id,
-                'message_id': msg_id,
-            }
-            message_vals_list.append(message_vals)
-
-        # Bulk create messages
-        if message_vals_list:
-            self.env['mail.message'].create(message_vals_list)
-
-        return True
-
-    def _create_discussion_channel(self):
-        """Create a discussion channel for the WhatsApp group"""
-        self.ensure_one()
-        
-        try:
-            # Verificar si ya existe un canal para este grupo
-            if self.channel_id:
-                return self.channel_id
-
-            # Create channel name with WhatsApp prefix
-            channel_name = f"📱 {self.name}"
-            
-            # Verificar que hay contactos
-            if not self.contact_ids:
-                _logger.warning(f"No hay contactos para crear el canal del grupo {self.name} - saltando creación de canal")
-                # No crear canal pero no fallar, permitir que el grupo exista
-                return False
-
-            # Obtener los IDs de los contactos de forma segura
-            partner_ids = []
-            for contact in self.contact_ids:
-                if contact and contact.id:
-                    partner_ids.append(contact.id)
-
-            if not partner_ids:
-                _logger.warning(f"No se encontraron IDs válidos de contactos para el grupo {self.name}")
-                return False
-
-            # Create the channel using channel_create
-            channel = self.env['discuss.channel'].channel_create(
-                name=channel_name,
-                group_id=self.env.user.groups_id[0].id,  # Usar el primer grupo del usuario actual
-            )
-            
-            # Add members to the channel
-            channel.add_members(partner_ids=partner_ids)
-            
-            # Link the channel to the group
-            self.write({'channel_id': channel.id})
-            return channel
-            
-        except Exception as e:
-            _logger.error(f"Error al crear el canal para el grupo {self.name}: {str(e)}")
-            return False
-
-    def _update_discussion_channel(self):
-        """Update the discussion channel members"""
-        self.ensure_one()
-        
-        try:
-            # Si no existe el canal, intentar crearlo
-            if not self.channel_id:
-                return self._create_discussion_channel()
-            
-            # Verificar que el canal aún existe
-            channel = self.env['discuss.channel'].browse(self.channel_id.id)
-            if not channel.exists():
-                _logger.warning(f"El canal para el grupo {self.name} ya no existe, creando uno nuevo")
-                self.write({'channel_id': False})
-                return self._create_discussion_channel()
-                
-            # Obtener los IDs de los contactos de forma segura
-            partner_ids = []
-            for contact in self.contact_ids:
-                if contact and contact.id:
-                    partner_ids.append(contact.id)
-
-            if not partner_ids:
-                _logger.warning(f"No hay contactos válidos para actualizar el canal del grupo {self.name} - saltando actualización")
-                # Si no hay contactos, no actualizar pero no fallar
-                return channel
-
-            # Update channel members using add_members
-            channel.add_members(partner_ids=partner_ids)
-            return channel
-            
-        except Exception as e:
-            _logger.error(f"Error al actualizar el canal para el grupo {self.name}: {str(e)}")
-            return False
-
-    @api.model
-    def sync_ww_contacts_groups(self):
-        """
-        Sincroniza los contactos y grupos de WhatsApp Web.
-        Solo sincroniza contactos que están dentro de grupos y valida que no se dupliquen,
-        verificando los últimos 10 dígitos del campo mobile.
-        """
-        accounts = self.env['whatsapp.account'].search([])
-        
-        for account in accounts:
-            try:
-                # Obtener grupos usando el método de la cuenta
-                groups_data = account.get_groups()
-                if not groups_data:
-                    continue
-                
-                # Procesar cada grupo
-                for group_data in groups_data:
-                    group_id = group_data.get('id').get('_serialized')
-                    group_name = group_data.get('name', 'Sin nombre')
-                    
-                    # Buscar o crear grupo
-                    group = self.search([
-                        ('whatsapp_web_id', '=', group_id),
-                        ('whatsapp_account_id', '=', account.id)
-                    ], limit=1)
-                    
-                    if not group:
-                        group = self.create({
-                            'name': group_name,
-                            'whatsapp_web_id': group_id,
-                            'whatsapp_account_id': account.id
-                        })
-                        # Crear canal solo después de procesar participantes
-                        # Se hará más abajo si hay contact_ids
-                    else:
-                        # Actualizar nombre del grupo si cambió
-                        if group.name != group_name:
-                            group.write({'name': group_name})
-                            # Actualizar nombre del canal si existe
-                            if group.channel_id:
-                                group.channel_id.write({'name': f"📱 {group_name}"})
-
-                    # Procesar participantes del grupo
-                    participants = group_data.get('members', [])
-                    contact_ids = []
-                    
-                    # Log para debug
-                    _logger.info(f"Procesando grupo {group_name}: {len(participants)} participantes encontrados")
-                    if not participants:
-                        _logger.warning(f"Grupo {group_name} no tiene participantes en la respuesta de la API")
-
-                    for participant in participants:
-                        whatsapp_web_id = participant.get('id', {}).get('_serialized')
-                        mobile = participant.get('number', '')
-                        is_admin = participant.get('isAdmin', False)
-                        is_super_admin = participant.get('isSuperAdmin', False)
-
-                        # Derive participant name
-                        participant_name = participant.get('name') or participant.get('pushname') or mobile
-
-                        # Search for existing contact
-                        contact = self.env['res.partner'].search([
-                            ('whatsapp_web_id', '=', whatsapp_web_id)
-                        ], limit=1)
-
-                        if not contact and mobile and len(mobile) >= 10:
-                            last_10_digits = mobile[-10:]
-                            contact = self.env['res.partner'].search([
-                                ('mobile', 'like', '%' + last_10_digits)
-                            ], limit=1)
-
-                        partner_vals = {
-                            'name': participant_name,
-                            'mobile': mobile,
-                            'whatsapp_web_id': whatsapp_web_id,
-                        }
-
-                        if contact:
-                            # Update existing contact
-                            contact.write(partner_vals)
-                        else:
-                            # Create new contact
-                            contact = self.env['res.partner'].create(partner_vals)
-                        
-                        if contact:
-                            contact_ids.append(contact.id)
-                    
-                    # Actualizar contactos del grupo
-                    group.write({'contact_ids': [(6, 0, contact_ids)]})
-                    
-                    # Update discussion channel members solo si hay contactos
-                    if contact_ids:
-                        # Si es un grupo nuevo sin canal, crear uno
-                        if not group.channel_id:
-                            group._create_discussion_channel()
-                        else:
-                            group._update_discussion_channel()
-                    else:
-                        _logger.info(f"Grupo {group_name} sincronizado sin contactos - no se creará canal de discusión")
-
-                    # Process messages if available
-                    messages = group_data.get('messages', [])
-                    if messages:
-                        group._process_messages(messages)
-
-            except Exception as e:
-                _logger.error("Error en la sincronización de grupos para la cuenta %s: %s", account.name, str(e))
-                continue
-
-        return True
-    
-    def send_whatsapp_message(self, body, attachment=None, wa_template_id=None):
-        """Enviar mensaje WhatsApp al grupo"""
-        self.ensure_one()
-        
-        if not self.whatsapp_account_id:
-            raise ValueError("Group must have a WhatsApp account configured")
-        
-        # Crear mail.message si hay adjunto
-        mail_message = None
-        if attachment:
-            mail_message = self.env['mail.message'].create({
-                'body': body,
-                'attachment_ids': [(4, attachment.id)]
-            })
-        
-        # Crear mensaje WhatsApp
-        message_vals = {
-            'body': body,
-            'recipient_type': 'group',
-            'whatsapp_group_id': self.id,
-            'mobile_number': self.whatsapp_web_id,
-            'wa_account_id': self.whatsapp_account_id.id,
-            'state': 'outgoing',
-        }
-        
-        if mail_message:
-            message_vals['mail_message_id'] = mail_message.id
-            
-        if wa_template_id:
-            message_vals['wa_template_id'] = wa_template_id
-        
-        whatsapp_message = self.env['whatsapp.message'].create(message_vals)
-        
-        # Enviar mensaje
-        whatsapp_message._send_message()
-        
-        return whatsapp_message
-    
-    def action_send_whatsapp_message(self):
-        """Acción para abrir el composer de WhatsApp para el grupo"""
-        self.ensure_one()
-        
-        return {
-            'type': 'ir.actions.act_window',
-            'name': f'Send Message to {self.name}',
-            'res_model': 'whatsapp.composer',
-            'view_mode': 'form',
-            'target': 'new',
-            'context': {
-                'default_recipient_type': 'group',
-                'default_whatsapp_group_id': self.id,
-                'default_wa_template_id': False,
-            }
-        } 

+ 0 - 22
whatsapp_web_groups_local_backup_20251214231432/models/ww_group_contact_rel.py

@@ -1,22 +0,0 @@
-from odoo import models, fields
-
-class WWGroupContactRel(models.Model):
-    _name = 'ww.group_contact_rel'
-    _description = 'Relación Contacto-Grupo de WhatsApp Web'
-    _table = 'ww_group_contact_rel'
-
-    # Explicitly define 'id' field. models.Model does this automatically,
-    # but being explicit can sometimes help clarify intent or resolve
-    # obscure schema generation issues if the table had a troubled history.
-    # fields.Id() is the standard way to define Odoo's automatic ID.
-    # id = fields.Id(string='ID')
-
-    group_id = fields.Many2one('ww.group', string='Grupo', required=True, ondelete='cascade', index=True)
-    contact_id = fields.Many2one('res.partner', string='Contacto', required=True, ondelete='cascade', index=True)
-    is_admin = fields.Boolean(string='Administrador del Grupo', default=False)
-    is_super_admin = fields.Boolean(string='Super Administrador del Grupo', default=False)
-    role_id = fields.Many2one('ww.role', string='Rol en el Grupo') 
-
-    _sql_constraints = [
-        ('group_contact_uniq', 'unique(group_id, contact_id)', 'El contacto debe ser único por grupo.')
-    ]

+ 0 - 8
whatsapp_web_groups_local_backup_20251214231432/models/ww_role.py

@@ -1,8 +0,0 @@
-from odoo import models, fields
-
-class WWRole(models.Model):
-    _name = 'ww.role'
-    _description = 'Rol de Grupo de WhatsApp Web'
-
-    name = fields.Char(string='Nombre del Rol', required=True)
-    description = fields.Text(string='Descripción') 

+ 0 - 7
whatsapp_web_groups_local_backup_20251214231432/security/ir.model.access.csv

@@ -1,7 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_ww_group_user,ww.group.user,model_ww_group,base.group_user,1,1,1,0
-access_ww_group_manager,ww.group.manager,model_ww_group,base.group_system,1,1,1,1
-access_ww_role_user,ww.role.user,model_ww_role,base.group_user,1,1,1,0
-access_ww_role_manager,ww.role.manager,model_ww_role,base.group_system,1,1,1,1
-access_ww_group_contact_rel_user,ww.group.contact.rel.user,model_ww_group_contact_rel,base.group_user,1,1,1,0
-access_ww_group_contact_rel_manager,ww.group.contact.rel.manager,model_ww_group_contact_rel,base.group_system,1,1,1,1

+ 0 - 19
whatsapp_web_groups_local_backup_20251214231432/views/marketing_activity_views.xml

@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <!-- Vista heredada que agrega el campo de grupo de WhatsApp después del template -->
-    <record id="marketing_activity_view_form_inherit_group" model="ir.ui.view">
-        <field name="name">marketing.activity.form.inherit.group</field>
-        <field name="model">marketing.activity</field>
-        <field name="inherit_id" ref="marketing_automation_whatsapp.marketing_activity_view_form"/>
-        <field name="priority">5</field>
-        <field name="arch" type="xml">
-            <!-- Agregar el campo de grupo de WhatsApp después del campo whatsapp_template_id -->
-            <xpath expr="//field[@name='whatsapp_template_id']" position="after">
-                <field name="whatsapp_group_id" 
-                       invisible="activity_type != 'whatsapp'" 
-                       placeholder="Seleccionar grupo de WhatsApp (opcional)..."
-                       help="Si se selecciona un grupo, todos los mensajes se enviarán al grupo. Si está vacío, se envían a destinatarios individuales según la plantilla."/>
-            </xpath>
-        </field>
-    </record>
-</odoo>

+ 0 - 22
whatsapp_web_groups_local_backup_20251214231432/views/whatsapp_composer_views.xml

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Extender vista de formulario del composer de WhatsApp para agregar campo Many2one -->
-        <record id="whatsapp_composer_view_form_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.composer.view.form.groups.m2o</field>
-            <field name="model">whatsapp.composer</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_composer_view_form_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar campo Many2one whatsapp_group_id antes de whatsapp_group_id_char -->
-                <xpath expr="//field[@name='whatsapp_group_id_char']" position="before">
-                    <field name="whatsapp_group_id" 
-                           invisible="recipient_type != 'group'"
-                           string="WhatsApp Group"
-                           placeholder="Select a WhatsApp group..."
-                           options="{'no_create': True}"/>
-                </xpath>
-            </field>
-        </record>
-    </data>
-</odoo>
-

+ 0 - 34
whatsapp_web_groups_local_backup_20251214231432/views/whatsapp_message_views.xml

@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Extender vista de formulario de WhatsApp Message para agregar campo Many2one -->
-        <record id="whatsapp_message_view_form_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.message.view.form.groups.m2o</field>
-            <field name="model">whatsapp.message</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_form_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar campo Many2one whatsapp_group_id después de recipient_type -->
-                <xpath expr="//field[@name='recipient_type']" position="after">
-                    <field name="whatsapp_group_id" 
-                           invisible="recipient_type != 'group'"
-                           required="recipient_type == 'group'"
-                           options="{'no_create': True}"/>
-                </xpath>
-            </field>
-        </record>
-
-        <!-- Extender vista de lista de WhatsApp Message -->
-        <record id="whatsapp_message_view_tree_groups_m2o" model="ir.ui.view">
-            <field name="name">whatsapp.message.view.tree.groups.m2o</field>
-            <field name="model">whatsapp.message</field>
-            <field name="inherit_id" ref="whatsapp_web.whatsapp_message_view_tree_groups"/>
-            <field name="arch" type="xml">
-                <!-- Agregar columna whatsapp_group_id -->
-                <xpath expr="//field[@name='recipient_type']" position="after">
-                    <field name="whatsapp_group_id" optional="hide"/>
-                </xpath>
-            </field>
-        </record>
-    </data>
-</odoo>
-

+ 0 - 63
whatsapp_web_groups_local_backup_20251214231432/views/ww_contact_views.xml

@@ -1,63 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <record id="view_res_partner_whatsapp_tree" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.tree</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_partner_tree"/>
-            <field name="arch" type="xml">
-                <xpath expr="//field[@name='complete_name']" position="after">
-                    <field name="whatsapp_web_id"/>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="view_res_partner_whatsapp_form" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.form</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_partner_form"/>
-            <field name="arch" type="xml">
-                <xpath expr="//notebook" position="inside">
-                    <page string="WhatsApp" name="whatsapp">
-                        <group>
-                            <field name="whatsapp_web_id"/>
-                        </group>
-                        <notebook>
-                            <page string="Grupos">
-                                <field name="group_ids" widget="many2many_tags">
-                                    <list string="Grupos">
-                                        <field name="name"/>
-                                    </list>
-                                </field>
-                            </page>
-                        </notebook>
-                    </page>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="view_res_partner_whatsapp_search" model="ir.ui.view">
-            <field name="name">res.partner.whatsapp.search</field>
-            <field name="model">res.partner</field>
-            <field name="inherit_id" ref="base.view_res_partner_filter"/>
-            <field name="arch" type="xml">
-                <xpath expr="//filter[@name='type_company']" position="after">
-                    <filter string="WhatsApp" name="whatsapp" domain="[('whatsapp_web_id', '!=', False)]"/>
-                </xpath>
-            </field>
-        </record>
-
-        <record id="action_res_partner_whatsapp" model="ir.actions.act_window">
-            <field name="name">Contactos WhatsApp Web</field>
-            <field name="res_model">res.partner</field>
-            <field name="view_mode">list,form</field>
-            <field name="domain">[('whatsapp_web_id', '!=', False)]</field>
-            <field name="context">{'search_default_whatsapp': 1}</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay contactos de WhatsApp
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 0 - 48
whatsapp_web_groups_local_backup_20251214231432/views/ww_group_contact_rel_views.xml

@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_group_contact_rel_list" model="ir.ui.view">
-            <field name="name">ww.group.contact.rel.list</field>
-            <field name="model">ww.group_contact_rel</field>
-            <field name="arch" type="xml">
-                <list string="Contactos de Grupo" editable="bottom">
-                    <field name="group_id"/>
-                    <field name="contact_id"/>
-                    <field name="is_admin"/>
-                    <field name="role_id"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_group_contact_rel_form" model="ir.ui.view">
-            <field name="name">ww.group.contact.rel.form</field>
-            <field name="model">ww.group_contact_rel</field>
-            <field name="arch" type="xml">
-                <form string="Contacto de Grupo">
-                    <sheet>
-                        <group>
-                            <field name="group_id"/>
-                            <field name="contact_id"/>
-                            <field name="is_admin"/>
-                            <field name="role_id"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción de ventana -->
-        <record id="action_ww_group_contact_rel" model="ir.actions.act_window">
-            <field name="name">Contactos de Grupo</field>
-            <field name="res_model">ww.group_contact_rel</field>
-            <field name="view_mode">list,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay contactos en grupos
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 0 - 70
whatsapp_web_groups_local_backup_20251214231432/views/ww_group_views.xml

@@ -1,70 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_group_list" model="ir.ui.view">
-            <field name="name">ww.group.list</field>
-            <field name="model">ww.group</field>
-            <field name="arch" type="xml">
-                <list string="Grupos WhatsApp" editable="bottom">
-                    <field name="name"/>
-                    <field name="whatsapp_web_id"/>
-                    <field name="whatsapp_account_id"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_group_form" model="ir.ui.view">
-            <field name="name">ww.group.form</field>
-            <field name="model">ww.group</field>
-            <field name="arch" type="xml">
-                <form string="Grupo WhatsApp">
-                    <header>
-                        <button name="action_send_whatsapp_message" 
-                                string="Send WhatsApp Message" 
-                                type="object" 
-                                class="btn-primary"
-                                invisible="not whatsapp_account_id"/>
-                    </header>
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                            <field name="whatsapp_web_id"/>
-                            <field name="whatsapp_account_id"/>
-                            <field name="channel_id" readonly="1"/>
-                        </group>
-                        <notebook>
-                            <page string="Contactos">
-                                <field name="contact_ids" widget="many2many_tags"/>
-                            </page>
-                        </notebook>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción -->
-        <record id="action_ww_group" model="ir.actions.act_window">
-            <field name="name">Grupos de WhatsApp</field>
-            <field name="res_model">ww.group</field>
-            <field name="view_mode">list,form</field>
-            <field name="view_id" ref="view_ww_group_list"/>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    Crear un nuevo grupo de WhatsApp
-                </p>
-                <p>
-                    Los grupos de WhatsApp te permiten enviar mensajes a múltiples contactos de forma simultánea.
-                </p>
-            </field>
-        </record>
-
-        <!-- Menú Grupos dentro del menú nativo de WhatsApp -->
-        <menuitem id="menu_whatsapp_web_groups"
-                  name="Grupos"
-                  parent="whatsapp.whatsapp_menu_main"
-                  action="action_ww_group"
-                  sequence="3"/>
-    </data>
-</odoo>

+ 0 - 44
whatsapp_web_groups_local_backup_20251214231432/views/ww_role_views.xml

@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <!-- Vista de lista -->
-        <record id="view_ww_role_list" model="ir.ui.view">
-            <field name="name">ww.role.list</field>
-            <field name="model">ww.role</field>
-            <field name="arch" type="xml">
-                <list string="Roles de WhatsApp" editable="bottom">
-                    <field name="name"/>
-                    <field name="description"/>
-                </list>
-            </field>
-        </record>
-
-        <!-- Vista de formulario -->
-        <record id="view_ww_role_form" model="ir.ui.view">
-            <field name="name">ww.role.form</field>
-            <field name="model">ww.role</field>
-            <field name="arch" type="xml">
-                <form string="Rol de WhatsApp">
-                    <sheet>
-                        <group>
-                            <field name="name"/>
-                            <field name="description"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <!-- Acción de ventana -->
-        <record id="action_ww_role" model="ir.actions.act_window">
-            <field name="name">Roles de WhatsApp</field>
-            <field name="res_model">ww.role</field>
-            <field name="view_mode">list,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">
-                    No hay roles de WhatsApp
-                </p>
-            </field>
-        </record>
-    </data>
-</odoo> 

+ 1 - 0
whatsapp_web_mail/__init__.py

@@ -0,0 +1 @@
+from . import models

+ 14 - 0
whatsapp_web_mail/__manifest__.py

@@ -0,0 +1,14 @@
+{
+    'name': 'WhatsApp Web Mail',
+    'version': '1.0',
+    'category': 'Tools',
+    'summary': 'Envía WhatsApp cuando se envía un correo desde plantillas',
+    'depends': ['mail', 'whatsapp_web'],
+    'data': [
+        'views/mail_template_views.xml',
+        'views/whatsapp_notifications_views.xml',
+        'views/res_partner_views.xml',
+        'security/ir.model.access.csv',
+    ],
+    'license': 'OEEL-1',
+}

+ 3 - 0
whatsapp_web_mail/models/__init__.py

@@ -0,0 +1,3 @@
+from . import mail_template
+from . import ir_model
+from . import res_partner

+ 6 - 0
whatsapp_web_mail/models/ir_model.py

@@ -0,0 +1,6 @@
+from odoo import models, fields
+
+class IrModel(models.Model):
+    _inherit = 'ir.model'
+
+    whatsapp_notifications_enabled = fields.Boolean(string="Notificaciones WhatsApp habilitadas", default=False)

+ 90 - 0
whatsapp_web_mail/models/mail_template.py

@@ -0,0 +1,90 @@
+from odoo import models, fields
+from odoo.tools import html2plaintext
+from odoo.tools.mail import email_split
+
+class MailTemplate(models.Model):
+    _inherit = 'mail.template'
+
+    send_whatsapp = fields.Boolean(string="Enviar también por WhatsApp", default=False)
+
+    def send_mail_batch(self, res_ids, force_send=False, raise_exception=False, email_values=None, email_layout_xmlid=False):
+        mails = super().send_mail_batch(res_ids, force_send=force_send, raise_exception=raise_exception, email_values=email_values, email_layout_xmlid=email_layout_xmlid)
+        if not self.send_whatsapp or not mails:
+            return mails
+        RecordModel = self.env[self.model]
+        partners_by_mail = {}
+        for mail in mails:
+            for partner in mail.recipient_ids:
+                if partner.email:
+                    partners_by_mail.setdefault(partner.email.strip().lower(), []).append(partner)
+        for mail in mails:
+            # Filtrar por modelo habilitado en ir.model
+            model_enabled = self.env['ir.model'].search([('model', '=', mail.model), ('whatsapp_notifications_enabled', '=', True)], limit=1)
+            if not model_enabled:
+                continue
+            to_emails = []
+            if mail.email_to:
+                to_emails = [e.strip().lower() for e in email_split(mail.email_to)]
+            recipients = set(to_emails)
+            for partner in mail.recipient_ids:
+                if partner.email:
+                    recipients.add(partner.email.strip().lower())
+            subject = mail.subject or ''
+            body_plain = html2plaintext(mail.body_html or '') if mail.body_html else ''
+            message_text = (subject + "\n\n" + body_plain).strip() if (subject or body_plain) else ''
+            sent_partner_ids = set()
+            for email in recipients:
+                partners = partners_by_mail.get(email, [])
+                if not partners:
+                    partners = self.env['res.partner'].search([('email', 'ilike', email), ('mobile', '!=', False)])
+                for partner in partners or []:
+                    if partner.id in sent_partner_ids:
+                        continue
+                    # Filtrar por preferencia del partner
+                    pref = self.env['partner.whatsapp.notification'].search([
+                        ('partner_id', '=', partner.id),
+                        ('model_id', '=', model_enabled.id),
+                        ('active', '=', True),
+                    ], limit=1)
+                    if not pref:
+                        continue
+                    if partner.mobile:
+                        msg = self.env['mail.message'].sudo().create({
+                            'body': message_text or subject,
+                            'message_type': 'whatsapp_message',
+                            'model': mail.model,
+                            'res_id': mail.res_id,
+                            'partner_ids': [partner.id],
+                            'subtype_id': self.env.ref("mail.mt_note").id,
+                        })
+                        wa_account = self.env['whatsapp.account'].search([('whatsapp_web_url', '!=', False)], limit=1)
+                        if not wa_account:
+                            continue
+                        self.env['whatsapp.message'].sudo().create({
+                            'mail_message_id': msg.id,
+                            'mobile_number': partner.mobile,
+                            'mobile_number_formatted': partner.mobile,
+                            'wa_template_id': False,
+                            'wa_account_id': wa_account.id,
+                            'state': 'outgoing',
+                        })._send_message()
+                        for attachment in mail.attachment_ids:
+                            msg_att = self.env['mail.message'].sudo().create({
+                                'body': message_text or subject,
+                                'message_type': 'whatsapp_message',
+                                'model': mail.model,
+                                'res_id': mail.res_id,
+                                'partner_ids': [partner.id],
+                                'attachment_ids': [(4, attachment.id)],
+                                'subtype_id': self.env.ref("mail.mt_note").id,
+                            })
+                            self.env['whatsapp.message'].sudo().create({
+                                'mail_message_id': msg_att.id,
+                                'mobile_number': partner.mobile,
+                                'mobile_number_formatted': partner.mobile,
+                                'wa_template_id': False,
+                                'wa_account_id': wa_account.id,
+                                'state': 'outgoing',
+                            })._send_message()
+                        sent_partner_ids.add(partner.id)
+        return mails

+ 18 - 0
whatsapp_web_mail/models/res_partner.py

@@ -0,0 +1,18 @@
+from odoo import models, fields
+
+class PartnerWhatsappNotification(models.Model):
+    _name = 'partner.whatsapp.notification'
+    _description = 'Preferencias de notificación WhatsApp por modelo'
+
+    partner_id = fields.Many2one('res.partner', required=True, ondelete='cascade')
+    model_id = fields.Many2one('ir.model', required=True, ondelete='cascade', domain="[('whatsapp_notifications_enabled','=',True)]")
+    active = fields.Boolean(string="Activo", default=True)
+
+    _sql_constraints = [
+        ('partner_model_unique', 'unique(partner_id, model_id)', 'La preferencia ya existe para este contacto y modelo')
+    ]
+
+class ResPartner(models.Model):
+    _inherit = 'res.partner'
+
+    whatsapp_notification_ids = fields.One2many('partner.whatsapp.notification', 'partner_id', string="Notificaciones WhatsApp")

+ 3 - 0
whatsapp_web_mail/security/ir.model.access.csv

@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_partner_whatsapp_notification_user,access.partner.whatsapp.notification.user,model_partner_whatsapp_notification,base.group_user,1,1,1,1
+access_partner_whatsapp_notification_admin,access.partner.whatsapp.notification.admin,model_partner_whatsapp_notification,base.group_system,1,1,1,1

+ 37 - 0
whatsapp_web_mail/views/mail_template_views.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+    <data>
+        <record id="mail_template_view_form_inherit_whatsapp" model="ir.ui.view">
+            <field name="name">mail.template.form.whatsapp.web.mail</field>
+            <field name="model">mail.template</field>
+            <field name="inherit_id" ref="mail.email_template_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//page[@name='email_settings']/group[1]" position="inside">
+                    <field name="send_whatsapp"/>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="mail_template_view_tree_inherit_whatsapp" model="ir.ui.view">
+            <field name="name">mail.template.tree.whatsapp.toggle</field>
+            <field name="model">mail.template</field>
+            <field name="inherit_id" ref="mail.email_template_tree"/>
+            <field name="arch" type="xml">
+                <xpath expr="//list" position="inside">
+                    <field name="send_whatsapp" optional="show"/>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="mail_template_action_server_activate_whatsapp" model="ir.actions.server">
+            <field name="name">Activar WhatsApp</field>
+            <field name="model_id" ref="mail.model_mail_template"/>
+            <field name="state">code</field>
+            <field name="code">
+                action = records.write({'send_whatsapp': True})
+            </field>
+            <field name="binding_model_id" ref="mail.model_mail_template"/>
+            <field name="binding_type">action</field>
+        </record>
+    </data>
+</odoo>

+ 17 - 0
whatsapp_web_mail/views/res_partner_views.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+    <data>
+        <record id="view_res_partner_form_inherit_whatsapp_notifications" model="ir.ui.view">
+            <field name="name">res.partner.form.whatsapp.notifications</field>
+            <field name="model">res.partner</field>
+            <field name="inherit_id" ref="base.view_partner_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//sheet/notebook" position="inside">
+                    <page string="Notificaciones WhatsApp">
+                        <field name="whatsapp_notification_ids"/>
+                    </page>
+                </xpath>
+            </field>
+        </record>
+    </data>
+</odoo>

+ 85 - 0
whatsapp_web_mail/views/whatsapp_notifications_views.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+    <data>
+        <record id="view_ir_model_whatsapp_list" model="ir.ui.view">
+            <field name="name">ir.model.list.whatsapp.notifications</field>
+            <field name="model">ir.model</field>
+            <field name="arch" type="xml">
+                <list string="Modelos Emails">
+                    <field name="name"/>
+                    <field name="model"/>
+                    <field name="whatsapp_notifications_enabled"/>
+                </list>
+            </field>
+        </record>
+
+        <record id="view_ir_model_whatsapp_form" model="ir.ui.view">
+            <field name="name">ir.model.form.whatsapp.notifications</field>
+            <field name="model">ir.model</field>
+            <field name="inherit_id" ref="base.view_model_form"/>
+            <field name="arch" type="xml">
+                <xpath expr="//field[@name='model']" position="after">
+                    <field name="whatsapp_notifications_enabled"/>
+                </xpath>
+            </field>
+        </record>
+
+        <record id="action_ir_model_whatsapp_notifications" model="ir.actions.act_window">
+            <field name="name">Modelos Emails</field>
+            <field name="res_model">ir.model</field>
+            <field name="view_mode">list,form</field>
+            <field name="view_id" ref="view_ir_model_whatsapp_list"/>
+            <field name="domain">[('is_mail_thread','=',True)]</field>
+            <field name="context">{}</field>
+        </record>
+
+        <record id="action_view_ir_model_whatsapp_form" model="ir.actions.act_window.view">
+            <field name="act_window_id" ref="action_ir_model_whatsapp_notifications"/>
+            <field name="view_mode">form</field>
+            <field name="view_id" ref="view_ir_model_whatsapp_form"/>
+            <field name="sequence">2</field>
+        </record>
+
+        <record id="view_partner_whatsapp_notification_list" model="ir.ui.view">
+            <field name="name">partner.whatsapp.notification.list</field>
+            <field name="model">partner.whatsapp.notification</field>
+            <field name="arch" type="xml">
+                <list string="Notificaciones WhatsApp">
+                    <field name="partner_id"/>
+                    <field name="model_id"/>
+                    <field name="active"/>
+                </list>
+            </field>
+        </record>
+
+        <record id="view_partner_whatsapp_notification_form" model="ir.ui.view">
+            <field name="name">partner.whatsapp.notification.form</field>
+            <field name="model">partner.whatsapp.notification</field>
+            <field name="arch" type="xml">
+                <form string="Notificación WhatsApp">
+                    <sheet>
+                        <group>
+                            <field name="partner_id"/>
+                            <field name="model_id"/>
+                            <field name="active"/>
+                        </group>
+                    </sheet>
+                </form>
+            </field>
+        </record>
+
+        <record id="action_partner_whatsapp_notification" model="ir.actions.act_window">
+            <field name="name">Notificaciones WhatsApp</field>
+            <field name="res_model">partner.whatsapp.notification</field>
+            <field name="view_mode">list,form</field>
+            <field name="view_id" ref="view_partner_whatsapp_notification_list"/>
+        </record>
+
+        <record id="menu_whatsapp_configuration_models_emails" model="ir.ui.menu">
+            <field name="name">Modelos Emails</field>
+            <field name="parent_id" ref="whatsapp.whatsapp_configuration_menu"/>
+            <field name="action" ref="action_ir_model_whatsapp_notifications"/>
+            <field name="sequence">50</field>
+        </record>
+    </data>
+</odoo>