Переглянути джерело

Merge commit 'a661165cfcf05bf9548e8d18c82d4290888e5c6c' as 'whatsapp_web_groups'

odoo 1 місяць тому
батько
коміт
217bddd979
100 змінених файлів з 1036 додано та 4192 видалено
  1. 53 1
      .gitignore
  2. 583 0
      API_REFERENCE.md
  3. 368 26
      README.md
  4. 1 0
      __init__.py
  5. 31 0
      __manifest__.py
  6. 0 1
      change_vat_in_partner/__init__.py
  7. 0 17
      change_vat_in_partner/__manifest__.py
  8. 0 3
      change_vat_in_partner/models/__init__.py
  9. 0 11
      change_vat_in_partner/models/account_edi_formart.py
  10. 0 19
      change_vat_in_partner/models/account_payment.py
  11. 0 14
      change_vat_in_partner/models/res_partner.py
  12. 0 12
      change_vat_in_partner/report/account_move_report.xml
  13. 0 13
      change_vat_in_partner/views/res_partner_view.xml
  14. 0 3
      custom_import_layout/__init__.py
  15. 0 22
      custom_import_layout/__manifest__.py
  16. BIN
      custom_import_layout/__pycache__/__init__.cpython-310.pyc
  17. BIN
      custom_import_layout/__pycache__/__init__.cpython-37.pyc
  18. 0 3
      custom_import_layout/controllers/__init__.py
  19. BIN
      custom_import_layout/controllers/__pycache__/__init__.cpython-37.pyc
  20. BIN
      custom_import_layout/controllers/__pycache__/controllers.cpython-37.pyc
  21. 0 21
      custom_import_layout/controllers/controllers.py
  22. 0 30
      custom_import_layout/demo/demo.xml
  23. 0 6
      custom_import_layout/models/__init__.py
  24. BIN
      custom_import_layout/models/__pycache__/__init__.cpython-310.pyc
  25. BIN
      custom_import_layout/models/__pycache__/__init__.cpython-37.pyc
  26. BIN
      custom_import_layout/models/__pycache__/account_move.cpython-310.pyc
  27. BIN
      custom_import_layout/models/__pycache__/import_layout.cpython-310.pyc
  28. BIN
      custom_import_layout/models/__pycache__/import_layout.cpython-37.pyc
  29. BIN
      custom_import_layout/models/__pycache__/import_layout_rule.cpython-310.pyc
  30. BIN
      custom_import_layout/models/__pycache__/import_layout_rule.cpython-37.pyc
  31. BIN
      custom_import_layout/models/__pycache__/import_layout_rule_line.cpython-310.pyc
  32. BIN
      custom_import_layout/models/__pycache__/import_layout_rule_line.cpython-37.pyc
  33. 0 20
      custom_import_layout/models/account_move.py
  34. 0 127
      custom_import_layout/models/import_layout.py
  35. 0 21
      custom_import_layout/models/import_layout_rule.py
  36. 0 27
      custom_import_layout/models/import_layout_rule_line.py
  37. 0 5
      custom_import_layout/security/ir.model.access.csv
  38. 0 16
      custom_import_layout/views/account_move_line.xml
  39. 0 54
      custom_import_layout/views/import_layout.xml
  40. 0 76
      custom_import_layout/views/import_layout_rule.xml
  41. 0 3
      custom_project/__init__.py
  42. 0 18
      custom_project/__manifest__.py
  43. 0 3
      custom_project/models/__init__.py
  44. 0 65
      custom_project/models/project_task.py
  45. 0 49
      custom_project/views/project_task.xml
  46. 0 5
      custom_sat_connection/__init__.py
  47. 0 30
      custom_sat_connection/__manifest__.py
  48. 0 3
      custom_sat_connection/controllers/__init__.py
  49. 0 23
      custom_sat_connection/controllers/controllers.py
  50. 0 14
      custom_sat_connection/models/__init__.py
  51. 0 19
      custom_sat_connection/models/account_account.py
  52. 0 831
      custom_sat_connection/models/account_cfdi.py
  53. 0 56
      custom_sat_connection/models/account_cfdi_line.py
  54. 0 17
      custom_sat_connection/models/account_cfdi_tax.py
  55. 0 149
      custom_sat_connection/models/account_esignature_certificate.py
  56. 0 17
      custom_sat_connection/models/account_journal.py
  57. 0 28
      custom_sat_connection/models/account_move.py
  58. 0 75
      custom_sat_connection/models/ir_attachment.py
  59. 0 882
      custom_sat_connection/models/portal_sat.py
  60. 0 159
      custom_sat_connection/models/res_company.py
  61. 0 7
      custom_sat_connection/models/res_config_settings.py
  62. 0 12
      custom_sat_connection/models/res_partner.py
  63. 0 19
      custom_sat_connection/security/ir.model.access.csv
  64. 0 28
      custom_sat_connection/security/res_groups.xml
  65. 0 25
      custom_sat_connection/views/account_account.xml
  66. 0 227
      custom_sat_connection/views/account_cfdi.xml
  67. 0 61
      custom_sat_connection/views/account_esignature_certificate.xml
  68. 0 23
      custom_sat_connection/views/account_journal.xml
  69. 0 33
      custom_sat_connection/views/account_move.xml
  70. 0 21
      custom_sat_connection/views/ir_attachment.xml
  71. 0 36
      custom_sat_connection/views/res_config_settings.xml
  72. 0 3
      custom_sat_connection/wizards/__init__.py
  73. 0 10
      custom_sat_connection/wizards/account_cfdi_link.py
  74. 0 19
      custom_sat_connection/wizards/account_cfdi_sat.py
  75. 0 44
      custom_sat_connection/wizards/account_cfdi_sat.xml
  76. 0 56
      custom_sat_connection/wizards/account_cfdi_xml.py
  77. 0 30
      custom_sat_connection/wizards/account_cfdi_xml.xml
  78. 0 76
      custom_sat_connection/wizards/account_cfdi_zip.py
  79. 0 38
      custom_sat_connection/wizards/account_cfdi_zip.xml
  80. 0 3
      custom_supplier_cfdi_data/__init__.py
  81. 0 19
      custom_supplier_cfdi_data/__manifest__.py
  82. BIN
      custom_supplier_cfdi_data/__pycache__/__init__.cpython-37.pyc
  83. 0 4
      custom_supplier_cfdi_data/models/__init__.py
  84. BIN
      custom_supplier_cfdi_data/models/__pycache__/__init__.cpython-37.pyc
  85. BIN
      custom_supplier_cfdi_data/models/__pycache__/account_edi_format.cpython-37.pyc
  86. BIN
      custom_supplier_cfdi_data/models/__pycache__/res_company.cpython-37.pyc
  87. 0 17
      custom_supplier_cfdi_data/models/account_edi_format.py
  88. 0 6
      custom_supplier_cfdi_data/models/res_company.py
  89. 0 19
      custom_supplier_cfdi_data/views/res_company.xml
  90. 0 1
      cutom_report_invoice/__init__.py
  91. 0 18
      cutom_report_invoice/__manifest__.py
  92. BIN
      cutom_report_invoice/__pycache__/__init__.cpython-310.pyc
  93. BIN
      cutom_report_invoice/i18n/es_MX.mo
  94. 0 296
      cutom_report_invoice/i18n/es_MX.po
  95. 0 1
      cutom_report_invoice/models/__init__.py
  96. BIN
      cutom_report_invoice/models/__pycache__/__init__.cpython-310.pyc
  97. BIN
      cutom_report_invoice/models/__pycache__/account_move.cpython-310.pyc
  98. 0 17
      cutom_report_invoice/models/account_move.py
  99. 0 13
      cutom_report_invoice/report/report_invoice.xml
  100. 0 16
      cutom_report_invoice/views/account_move_views.xml

+ 53 - 1
.gitignore

@@ -1 +1,53 @@
-*.pyc
+# 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
+
+

+ 583 - 0
API_REFERENCE.md

@@ -0,0 +1,583 @@
+# 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}")
+```
+

+ 368 - 26
README.md

@@ -1,44 +1,386 @@
-# m22proyectoodoointerno
+# Gestor de Grupos de WhatsApp Web para Odoo 18
 
-## Subtrees
+## Descripción
 
-Este proyecto incluye los siguientes repositorios como Git subtrees:
+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.
 
-- **theme_m22tc**: Tema personalizado M22 TechConsulting (rama `develop`)
-- **helpdesk_extras**: Extensiones del módulo Helpdesk (rama `develop`)
-- **whatsapp_web**: Integración de WhatsApp Web (rama `develop`)
+## Características Principales
 
-Los subtrees están integrados directamente en el repositorio principal, por lo que no requieren configuración adicional al clonar.
+- ✅ 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
 
-### Actualizar subtrees
+## Requisitos
 
-Para actualizar un subtree desde su repositorio remoto:
+- Odoo 18.0
+- Módulo `whatsapp_web` (dependencia obligatoria)
+- Módulos: `base`, `contacts`, `mail`, `calendar`, `helpdesk`
+- Servidor whatsapp-web.js configurado y funcionando
 
-```bash
-# Actualizar theme_m22tc
-git subtree pull --prefix=theme_m22tc theme_m22tc-remote develop --squash
+## 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
 
-# Actualizar helpdesk_extras
-git subtree pull --prefix=helpdesk_extras helpdesk_extras-remote develop --squash
+#### 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
 
-# Actualizar whatsapp_web
-git subtree pull --prefix=whatsapp_web whatsapp_web-remote develop --squash
+#### 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()
 ```
 
-### Agregar cambios a los subtrees
+#### `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()
+```
 
-Si necesitas hacer cambios en los subtrees y subirlos a sus repositorios:
+#### `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
-# Hacer cambios en theme_m22tc o helpdesk_extras
-# Luego hacer commit normalmente en el repositorio principal
+# 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)}")
+```
 
-# Para subir cambios a theme_m22tc
-git subtree push --prefix=theme_m22tc theme_m22tc-remote develop
+## Mantenimiento
 
-# Para subir cambios a helpdesk_extras
-git subtree push --prefix=helpdesk_extras helpdesk_extras-remote develop
+### Limpieza de Datos
+```python
+# Limpiar grupos sin contactos
+empty_groups = self.env['ww.group'].search([
+    ('contact_ids', '=', False)
+])
+empty_groups.unlink()
 
-# Para subir cambios a whatsapp_web
-git subtree push --prefix=whatsapp_web whatsapp_web-remote develop
+# 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
+

+ 1 - 0
__init__.py

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

+ 31 - 0
__manifest__.py

@@ -0,0 +1,31 @@
+{
+    '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 - 1
change_vat_in_partner/__init__.py

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

+ 0 - 17
change_vat_in_partner/__manifest__.py

@@ -1,17 +0,0 @@
-{
-    "name": "Change vat parent",
-    'description': """
-        Change the rfc of the client       
-    """,
-    "version": "17.1",
-    "category": "Partner",
-    "author": "M22",
-    'website': "https://www.m22.mx", 
-    "license": "AGPL-3",
-    "depends": ["account", "l10n_mx_edi", "contacts"],
-    "data": [
-        "views/res_partner_view.xml",
-        "report/account_move_report.xml",
-        ],
-    "installable": True,
-}

+ 0 - 3
change_vat_in_partner/models/__init__.py

@@ -1,3 +0,0 @@
-from . import res_partner
-from . import account_edi_formart
-from . import account_payment

+ 0 - 11
change_vat_in_partner/models/account_edi_formart.py

@@ -1,11 +0,0 @@
-
-from odoo import models
-
-
-class AccountEdiFormat(models.Model):
-    _inherit = 'account.edi.format'
-
-    def _l10n_mx_edi_get_40_values(self, move):
-        vals = super()._l10n_mx_edi_get_40_values(move)
-        vals["customer_name"] = self._l10n_mx_edi_clean_to_legal_name(move.partner_id.name)
-        return vals

+ 0 - 19
change_vat_in_partner/models/account_payment.py

@@ -1,19 +0,0 @@
-
-from odoo import models, api
-
-
-class AccountPayment(models.Model):
-    _inherit = "account.payment"
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        vals = super().create(vals_list)
-        if type(vals_list) == dict and vals_list.get("ref"):
-            partner = self.env['account.move'].search([("name", "=", vals_list["ref"])],limit=1)
-            if partner:
-                vals["partner_id"] = partner.partner_id.id
-        elif type(vals_list) == list and vals_list[0].get("ref"):
-            partner = self.env['account.move'].search([("name", "=", vals_list[0]["ref"])],limit=1)
-            if partner:
-                vals[0]["partner_id"] = partner.partner_id.id
-        return vals

+ 0 - 14
change_vat_in_partner/models/res_partner.py

@@ -1,14 +0,0 @@
-
-from odoo import api, models
-
-
-class ResPartner(models.Model):
-    _inherit = "res.partner"
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        vals = super().create(vals_list)
-        for val in vals:
-            if val.type == 'invoice':
-                val['vat'] = ""
-        return vals

+ 0 - 12
change_vat_in_partner/report/account_move_report.xml

@@ -1,12 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<odoo>
-    <template id="custom_report_invoice_document" inherit_id="l10n_mx_edi.report_invoice_document">
-        <xpath expr="//div[hasclass('row')]//div[@name='address_not_same_as_shipping']//t[@t-set='address']" position="replace">
-        </xpath>
-        <xpath expr="//div[hasclass('row')]//div[@name='address_same_as_shipping']//t[@t-set='address']" position="replace">
-        </xpath>
-        <xpath expr="//div[hasclass('row')]//div[@name='no_shipping']//t[@t-set='address']" position="replace">
-        </xpath>
-    </template>
-
-</odoo>

+ 0 - 13
change_vat_in_partner/views/res_partner_view.xml

@@ -1,13 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<odoo>
-    <record model="ir.ui.view" id="res_partner_view_change_vat">
-        <field name="name">res.partner.view.change.vat</field>
-        <field name="model">res.partner</field>
-        <field name="inherit_id" ref="base.view_partner_form"/>
-        <field name="arch" type="xml">
-            <xpath expr="//field[@name='vat']" position="replace">
-                <field name="vat" placeholder="e.g. BE0477472701"/>
-            </xpath>
-        </field>
-    </record>
-</odoo>

+ 0 - 3
custom_import_layout/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import models

+ 0 - 22
custom_import_layout/__manifest__.py

@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-{
-    'name': "Importación de layouts",
-    'summary': """
-        Importación de layouts por medio de excel.
-    """,
-    'description': """
-        Importación de layouts por medio de excel en los diferentes modelos.
-    """,
-    'author': "M22",
-    'website': "https://www.m22.mx",
-    'category': 'Import',
-    'version': '17.1',
-    'depends': ['base','sale','purchase', 'account'],
-    'data': [
-        'security/ir.model.access.csv',
-        'views/import_layout_rule.xml',
-        'views/import_layout.xml',
-        "views/account_move_line.xml",
-    ],
-    'license': 'AGPL-3'
-}

BIN
custom_import_layout/__pycache__/__init__.cpython-310.pyc


BIN
custom_import_layout/__pycache__/__init__.cpython-37.pyc


+ 0 - 3
custom_import_layout/controllers/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import controllers

BIN
custom_import_layout/controllers/__pycache__/__init__.cpython-37.pyc


BIN
custom_import_layout/controllers/__pycache__/controllers.cpython-37.pyc


+ 0 - 21
custom_import_layout/controllers/controllers.py

@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# from odoo import http
-
-
-# class CustomImportLayout(http.Controller):
-#     @http.route('/custom_import_layout/custom_import_layout', auth='public')
-#     def index(self, **kw):
-#         return "Hello, world"
-
-#     @http.route('/custom_import_layout/custom_import_layout/objects', auth='public')
-#     def list(self, **kw):
-#         return http.request.render('custom_import_layout.listing', {
-#             'root': '/custom_import_layout/custom_import_layout',
-#             'objects': http.request.env['custom_import_layout.custom_import_layout'].search([]),
-#         })
-
-#     @http.route('/custom_import_layout/custom_import_layout/objects/<model("custom_import_layout.custom_import_layout"):obj>', auth='public')
-#     def object(self, obj, **kw):
-#         return http.request.render('custom_import_layout.object', {
-#             'object': obj
-#         })

+ 0 - 30
custom_import_layout/demo/demo.xml

@@ -1,30 +0,0 @@
-<odoo>
-    <data>
-<!--
-          <record id="object0" model="custom_import_layout.custom_import_layout">
-            <field name="name">Object 0</field>
-            <field name="value">0</field>
-          </record>
-
-          <record id="object1" model="custom_import_layout.custom_import_layout">
-            <field name="name">Object 1</field>
-            <field name="value">10</field>
-          </record>
-
-          <record id="object2" model="custom_import_layout.custom_import_layout">
-            <field name="name">Object 2</field>
-            <field name="value">20</field>
-          </record>
-
-          <record id="object3" model="custom_import_layout.custom_import_layout">
-            <field name="name">Object 3</field>
-            <field name="value">30</field>
-          </record>
-
-          <record id="object4" model="custom_import_layout.custom_import_layout">
-            <field name="name">Object 4</field>
-            <field name="value">40</field>
-          </record>
--->
-    </data>
-</odoo>

+ 0 - 6
custom_import_layout/models/__init__.py

@@ -1,6 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import import_layout_rule
-from . import import_layout_rule_line
-from . import import_layout
-from . import account_move

BIN
custom_import_layout/models/__pycache__/__init__.cpython-310.pyc


BIN
custom_import_layout/models/__pycache__/__init__.cpython-37.pyc


BIN
custom_import_layout/models/__pycache__/account_move.cpython-310.pyc


BIN
custom_import_layout/models/__pycache__/import_layout.cpython-310.pyc


BIN
custom_import_layout/models/__pycache__/import_layout.cpython-37.pyc


BIN
custom_import_layout/models/__pycache__/import_layout_rule.cpython-310.pyc


BIN
custom_import_layout/models/__pycache__/import_layout_rule.cpython-37.pyc


BIN
custom_import_layout/models/__pycache__/import_layout_rule_line.cpython-310.pyc


BIN
custom_import_layout/models/__pycache__/import_layout_rule_line.cpython-37.pyc


+ 0 - 20
custom_import_layout/models/account_move.py

@@ -1,20 +0,0 @@
-from odoo import api, fields, models
-
-class AccountMoveLine(models.Model):
-    _inherit = "account.move.line"
-
-    import_name_taxes = fields.Char(string="Nombre Impuestos")
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        res = super().create(vals_list)
-        for line in res:
-            if not line.product_id:
-                if line.import_name_taxes:
-                    taxes = line.env["account.tax"].search([("name", "=", line.import_name_taxes), ("company_id", "=", line.company_id.id)])
-                    if taxes:
-                        line.tax_ids = False
-                        line.write({'tax_ids': [(6, 0, taxes.ids)]})
-                    else:
-                        line.tax_ids = False
-        return res

+ 0 - 127
custom_import_layout/models/import_layout.py

@@ -1,127 +0,0 @@
-from odoo import api, fields, models
-from odoo.exceptions import ValidationError
-import pandas as pd
-import os
-import base64
-
-
-class ImportLayput(models.TransientModel):
-    _name = 'import.layout'
-    _description = 'Importación de layout'
-    _rec_name = "rule_id"
-
-    rule_id = fields.Many2one(comodel_name="import.layout.rule", string="Plantilla")
-    file_data = fields.Binary(string="Archivo")
-    file_name = fields.Char(string="Nombre del archivo")
-    company_id = fields.Many2one(comodel_name="res.company", string="Empresa", default=lambda self: self.env.company)
-
-    def read_excel(self):
-        # Validar la extensión
-        self.extension_validator(self.file_name)
-        # Validar que se cargue el modelo correspondiente al menú
-        self.validate_model_id()
-        # Lectura del excel
-        data_decode = base64.b64decode(self.file_data)
-        df = pd.read_excel(data_decode)
-        group_line_id = self.rule_id.main_columns_ids.filtered(lambda line: line.group_by)
-        df_group_by = df.groupby(group_line_id.name)
-        move_ids = self.get_data(df_group_by)
-        if move_ids:
-            return {
-                'type': 'ir.actions.client',
-                'tag': 'display_notification',
-                'params': {
-                    'type': 'success',
-                    'message': (f'Se crearon los siguientes movimientos {move_ids}.'),
-                    'next': {'type': 'ir.actions.act_window_close'},
-                }
-            }
-
-    def validate_model_id(self):
-        if self.env.context.get("sale_import") and self.sudo().rule_id.main_model_id.model not in  ("sale.order", "purchase.order", "account.move"):
-            raise ValidationError("Es necesario asignar una regla relacionada al modelo seleccionado")
-
-    def extension_validator(self, file_name):
-        name, extension = os.path.splitext(file_name)
-        valid = True if str(extension).upper() == '.XLSX' or str(extension).upper() == '.XLS' else False
-        if not valid:
-            raise ValidationError(
-                "La extensión del archivo no es valida con el formato de excel, esta debe ser .xlsx o xls")
-
-    def get_data(self, df):
-        try:
-            main_name = []
-            for ciudad, grupo in df:
-                main_data = dict()
-                data_list = []
-                # Obtencion de los datos del cabecero
-                for column in self.rule_id.main_columns_ids:
-                    value = self.get_field_info(column.field_id, grupo.loc[grupo.index[0], column.name])
-                    main_data[f"{column.field_id.name}"] = value
-                # Creación del cabecero
-                if main_data:
-                    main_id = self.env[self.rule_id.main_model_id.model].sudo().create(main_data)
-                    main_name.append(main_id.name)
-                    # Obtenciónd de los datos de las lineas de la orden
-                    for line_index in range(grupo.shape[0]):
-                        data = {}
-                        for column in self.rule_id.column_ids:
-                            value = self.get_field_info(column.field_id,
-                                                        grupo.loc[grupo.index[line_index], column.name])
-                            data[f"{column.field_id.name}"] = value
-                            main_field = self.env[self.rule_id.model_id.model].fields_get()
-                            related_field = next((campo for campo, datos in main_field.items() if
-                                                  datos.get('relation') == f'{self.rule_id.main_model_id.model}'), None)
-                            # Relación de las lineas con su llave primaria
-                            data[f"{related_field}"] = main_id.id
-                        if data:
-                            data_list.append(data)
-                    # Creación de las lineas de las ordenes
-                    if data_list:
-                        line_ids = self.env[self.rule_id.model_id.model].sudo().create(data_list)
-            return main_name if main_name else False
-        except Exception as e:
-            raise ValidationError(
-                f"Hay algun problema con la configuración de la regla, posiblemente el nombre de las columnas no coincidan con el archivo, como por ejemplo: {e}")
-
-    # Obtener los valores de los campos dependiendo del tipo de campo
-    def get_field_info(self, field_id, value):
-        if field_id.ttype in ["many2one", "many2one_reference", "many2many", "many2many_reference"]:
-            if str(value).isnumeric():
-                if len(str(value)) <= 9:
-                    field_value = self.env[field_id.relation].sudo().search([("id", "=", int(value))], limit=1)
-                else:
-                    field_value = False
-                if not field_value and field_id.relation in ['product.product', 'product.template']:
-                    field_value = self.env[field_id.relation].sudo().search(["|",("default_code", "=", value),("barcode","=",value)], limit=1)
-                    if not field_value:
-                        raise ValidationError(
-                            f"No se encontro un registro en el modelo de {field_id.relation} con el id {value}")
-                elif not field_value:
-                    raise ValidationError(
-                        f"No se encontro un registro en el modelo de {field_id.relation} con el código {value}")
-                value = field_value.id
-            elif field_id.relation in ['product.product', 'product.template']:
-                field_value = self.env[field_id.relation].sudo().search(["|",("default_code", "=", value),("barcode","=",value)], limit=1)
-                if not field_value:
-                    field_value = self.env[field_id.relation].sudo().search([("name", "=", value)], limit=1)
-                    if not field_value:
-                        raise ValidationError(
-                            f"No se encontro un registro en el modelo de {field_id.relation} con el código {value}")
-                value = field_value.id
-            else:
-                field_value = self.env[field_id.relation].sudo().search([("name", "=", value)], limit=1)
-                if not field_value:
-                    raise ValidationError(
-                        f"No se encontro un registro en el modelo de {field_id.relation} con el nombre {value}")
-                value = field_value.id
-        return value
-
-
-
-
-
-
-
-
-

+ 0 - 21
custom_import_layout/models/import_layout_rule.py

@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from odoo import models, fields, api
-from odoo.exceptions import ValidationError
-
-
-class ImportLayoutRule(models.Model):
-    _name = "import.layout.rule"
-    _description = "Reglas de importación"
-
-    name = fields.Char(string="Nombre")
-    model_id = fields.Many2one(comodel_name="ir.model", string="Modelo de lineas")
-    main_model_id = fields.Many2one(comodel_name="ir.model", string="Modelo del cabero")
-    column_ids = fields.One2many("import.layout.rule.line", "rule_id", string="Regla de columnas")
-    main_columns_ids = fields.One2many("import.layout.rule.main.line", "rule_id", string="Reglas del cabecero")
-
-    @api.constrains("main_columns_ids")
-    def _check_main_group_by(self):
-        for rec in self:
-            if len(rec.main_columns_ids.filtered(lambda line: line.group_by)) != 1:
-                raise ValidationError("Es necesario indicar solamente un agrupador en las lineas del cabecero.")

+ 0 - 27
custom_import_layout/models/import_layout_rule_line.py

@@ -1,27 +0,0 @@
-from odoo import api, fields, models
-
-class ImportLayoutRuleMainLine(models.Model):
-    _name = 'import.layout.rule.main.line'
-    _description = 'Linea de reglas de importación del cabecero'
-
-    name = fields.Char(string="Nombre")
-    rule_id = fields.Many2one(comodel_name="import.layout.rule", string="Regla")
-    field_id = fields.Many2one(comodel_name="ir.model.fields", string="Campo")
-    sequence = fields.Integer(string="Secuencia")
-    group_by = fields.Boolean(string="Agrupar por")
-
-class ImportLayoutRuleLine(models.Model):
-    _name = 'import.layout.rule.line'
-    _description = 'Linea de reglas de importación'
-
-    name = fields.Char(string="Nombre")
-    rule_id = fields.Many2one(comodel_name="import.layout.rule", string="Regla")
-    field_id = fields.Many2one(comodel_name="ir.model.fields", string="Campo")
-    sequence = fields.Integer(string="Secuencia")
-
-
-
-
-
-
-

+ 0 - 5
custom_import_layout/security/ir.model.access.csv

@@ -1,5 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_import_layout_rule,import_layout_rule,model_import_layout_rule,base.group_erp_manager,1,1,1,1
-access_import_layout_rule_line,import_layout_rule_line,model_import_layout_rule_line,base.group_erp_manager,1,1,1,1
-access_import_layout_rule_main_line,import_layout_rule_main_line,model_import_layout_rule_main_line,base.group_erp_manager,1,1,1,1
-access_import_layout,import_layout,model_import_layout,base.group_user,1,1,1,1

+ 0 - 16
custom_import_layout/views/account_move_line.xml

@@ -1,16 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<odoo>
-    <record model="ir.ui.view" id="view_account_invoice_import_taxes">
-        <field name="name">account.move.import.taxes.form</field>
-        <field name="model">account.move</field>
-        <field name="inherit_id" ref="account.view_move_form" />
-        <field name="arch" type="xml">
-            <xpath expr="//field[@name='invoice_line_ids']/list/field[@name='tax_ids']" position="before">
-                <field name="import_name_taxes" />
-            </xpath>
-            <xpath expr="//field[@name='line_ids']/list/field[@name='tax_ids']" position="before">
-                <field name="import_name_taxes" invisible="1"/>
-            </xpath>
-        </field>
-    </record>
-</odoo>

+ 0 - 54
custom_import_layout/views/import_layout.xml

@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="import_layout_view_form" model="ir.ui.view">
-            <field name="name">import_layout_view_form</field>
-            <field name="model">import.layout</field>
-            <field name="arch" type="xml">
-                <form string="Importación de layout">
-                    <header>
-                        <button name="read_excel" string="Importar" type="object" class="btn btn-prumary"/>
-                    </header>
-                    <sheet>
-                        <group>
-                            <group>
-                                <field name="rule_id"/>
-                                <field name="file_name" invisible="1"/>
-                                <field name="file_data" filename="file_name"/>
-                            </group>
-                            <group>
-                                <field name="company_id" readonly="1"/>
-                            </group>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <record id="import_layout_sale_action" model="ir.actions.act_window">
-            <field name="name">Importación de layout</field>
-            <field name="type">ir.actions.act_window</field>
-            <field name="res_model">import.layout</field>
-            <field name="view_mode">form</field>
-            <field name="context">{'sale_import':True}</field>
-        </record>
-
-        <record id="import_layout_purchase_action" model="ir.actions.act_window">
-            <field name="name">Importación de layout</field>
-            <field name="type">ir.actions.act_window</field>
-            <field name="res_model">import.layout</field>
-            <field name="view_mode">form</field>
-            <field name="context">{'purchase_import':True}</field>
-        </record>
-
-        <menuitem id="import_layout_sale_menu" name="Importación de layout" parent="sale.sale_order_menu"
-                  action="import_layout_sale_action" sequence="200"/>
-
-        <menuitem id="import_layout_purchase_menu" name="Importación de layout" parent="purchase.menu_procurement_management"
-                  action="import_layout_purchase_action" sequence="200"/>
-
-        <menuitem id="import_layout_purchase_menu" name="Importación de layout" parent="account.menu_finance_configuration"
-                  action="import_layout_purchase_action" sequence="200"/>
-    </data>
-</odoo>

+ 0 - 76
custom_import_layout/views/import_layout_rule.xml

@@ -1,76 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="import_layout_rule_view_tree" model="ir.ui.view">
-            <field name="name">import_layout_rule_view_tree</field>
-            <field name="model">import.layout.rule</field>
-            <field name="arch" type="xml">
-                <list string="Reglas de importación">
-                    <field name="name"/>
-                    <field name="model_id"/>
-                </list>
-            </field>
-        </record>
-
-        <record id="import_layout_rule_view_form" model="ir.ui.view">
-            <field name="name">import_layout_rule_view_form</field>
-            <field name="model">import.layout.rule</field>
-            <field name="arch" type="xml">
-                <form string="Reglas de importación">
-                    <sheet>
-                        <group>
-                            <group>
-                                <field name="name" placeholder="Nombre de plantilla"/>
-                            </group>
-
-                        </group>
-                        <notebook>
-                            <page string="Cabecero">
-                                <group>
-                                    <field name="main_model_id" domain="[('model','in',['sale.order','purchase.order','account.move'])]" options="{'no_create':True, 'no_open': True}"/>
-                                </group>
-                                <field name="main_columns_ids">
-                                    <list editable="bottom">
-                                        <field name="sequence" widget="handle"/>
-                                        <field name="name"/>
-                                        <field name="field_id" domain="[('model_id','=',parent.main_model_id)]"
-                                               options="{'no_create':True, 'no_open': True}"/>
-                                        <field name="group_by"/>
-                                    </list>
-                                </field>
-                            </page>
-                            <page string="Lineas">
-                                <group>
-                                    <field name="model_id" domain="[('model','in',['sale.order.line','purchase.order.line','account.move.line'])]" options="{'no_create':True, 'no_open': True}"/>
-                                </group>
-                                <field name="column_ids">
-                                    <list editable="bottom">
-                                        <field name="sequence" widget="handle"/>
-                                        <field name="name"/>
-                                        <field name="field_id" domain="[('model_id','=',parent.model_id)]"
-                                               options="{'no_create':True, 'no_open': True}"/>
-                                    </list>
-                                </field>
-                            </page>
-                        </notebook>
-
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <record id="import_layout_rule_action" model="ir.actions.act_window">
-            <field name="name">Reglas de importación</field>
-            <field name="type">ir.actions.act_window</field>
-            <field name="res_model">import.layout.rule</field>
-            <field name="view_mode">list,form</field>
-        </record>
-
-        <menuitem id="import_layout_rule_categ_menu" name="Importación" parent="base.menu_administration"
-                  sequence="200"/>
-        <menuitem id="import_layout_rule_action_menu" name="Reglas de importación"
-                  parent="import_layout_rule_categ_menu" action="import_layout_rule_action" sequence="200"/>
-
-    </data>
-</odoo>

+ 0 - 3
custom_project/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import models

+ 0 - 18
custom_project/__manifest__.py

@@ -1,18 +0,0 @@
-# -*- coding: utf-8 -*-
-{
-    'name': "Personalización de proyectos y planeación",
-    'summary': "Personalización de proyectos añadiendo información extra y cambiando flujos de tiempos",
-    'description': """
-        Añadiendo flujos al momento de captura de fechas de planeación.
-    """,
-    'author': "M22 - Erick",
-    'website': "https://www.yourcompany.com",
-    'category': 'Services/Project',
-    'version': '18.1',
-    'depends': ['base','project','project_enterprise'],
-    'data': [
-        'views/project_task.xml'
-    ],
-    'license': 'AGPL-3'
-}
-

+ 0 - 3
custom_project/models/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import project_task

+ 0 - 65
custom_project/models/project_task.py

@@ -1,65 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from odoo import models, fields, api
-from datetime import datetime, timedelta
-
-class ProjectTask(models.Model):
-    _inherit = "project.task"
-
-    x_start_date = fields.Date(string="Fecha inicio")
-    x_end_date = fields.Date(string="Fecha final")
-    x_days_duration = fields.Integer(string="Duración (Días)", compute="compute_days_duration", store=True)
-
-    @api.depends("x_start_date","x_end_date")
-    def compute_days_duration(self):
-        for rec in self:
-            if rec.x_start_date and rec.x_end_date:
-                days = (rec.x_end_date - rec.x_start_date).days
-                day_date = rec.x_start_date
-                duration = 0
-                for day in range(days + 1):
-                    weekday =  day_date.weekday()
-                    if weekday not in [5,6]:
-                        duration += 1
-                    day_date += timedelta(days=1)
-                rec.x_days_duration = duration
-            else:
-                rec.x_days_duration = 0
-
-    @api.depends('date_deadline', 'planned_date_begin', 'user_ids')
-    def _compute_allocated_hours(self):
-        for rec in self:
-            rec.allocated_hours = 0
-
-    @api.depends('project_id')
-    def _compute_display_in_project(self):
-        for rec in self:
-            rec.display_in_project = True
-
-    @api.depends('project_id', 'parent_id')
-    def _compute_show_display_in_project(self):
-        for rec in self:
-            rec.show_display_in_project = False
-
-    @api.onchange("x_start_date")
-    def onchange_start_date(self):
-        for rec in self:
-            if rec.x_start_date:
-                rec.planned_date_begin = datetime.combine(rec.x_start_date, datetime.min.time()) + timedelta(hours=15)
-
-    @api.onchange("x_end_date")
-    def onchange_end_date(self):
-        for rec in self:
-            if rec.x_end_date:
-                rec.date_deadline = datetime.combine(rec.x_end_date, datetime.min.time()) + timedelta(hours=12)
-
-    def write(self, vals):
-        res = super().write(vals)
-        if vals and type(vals) == dict:
-            for rec in self:
-                if vals.get("x_start_date"):
-                    rec.planned_date_begin = datetime.combine(rec.x_start_date, datetime.min.time()) + timedelta(hours=15)
-                        
-                if vals.get("x_end_date"):
-                    rec.date_deadline = datetime.combine(rec.x_end_date, datetime.min.time()) + timedelta(hours=12)
-        return res

+ 0 - 49
custom_project/views/project_task.xml

@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="custom_project_project_task_form" model="ir.ui.view">
-            <field name="name">custom_project_project_task_form</field>
-            <field name="model">project.task</field>
-            <field name="inherit_id" ref="project_enterprise.project_task_view_form"/>
-            <field name="arch" type="xml">
-                <xpath expr="//div[@id='date_deadline_and_recurring_task']" position="attributes">
-                    <attribute name="invisible">1</attribute>
-                </xpath>
-                <xpath expr="//label[@for='date_deadline'][1]" position="attributes">
-                    <attribute name="invisible">1</attribute>
-                </xpath>
-
-                <xpath expr="//label[@for='date_deadline'][2]" position="attributes">
-                    <attribute name="invisible">1</attribute>
-                </xpath>
-
-                <xpath expr="//field[@name='recurring_task']" position="replace"/>
-
-                <xpath expr="//div[@id='date_deadline_and_recurring_task']" position="after">
-                    <label for="x_start_date" string="Fechas planeadas"/>
-                    <div id="planned_date_task" class="d-inline-flex w-100">
-                        <field name="x_end_date" invisible="1" required="1"/>
-                        <field name="x_start_date" required="1" widget="daterange" options="{'end_date_field': 'x_end_date'}"/>
-                        <field name="recurring_task" nolabel="1" class="ms-0" style="width: fit-content;" widget="boolean_icon" options="{'icon': 'fa-repeat'}" invisible="not active or parent_id" groups="project.group_project_recurring_tasks"/>
-                    </div>
-
-                    <field name="x_days_duration"/>
-
-                </xpath>
-                
-                <xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="attributes">
-                    <attribute name="column_invisible">1</attribute>
-                </xpath>
-
-                <xpath expr="//field[@name='child_ids']/list//field[@name='date_deadline']" position="after">
-                    <field name="x_end_date" column_invisible="1" required="1"/>
-                    <field name="x_start_date" string="Fechas planeadas" required="1" widget="daterange" options="{'end_date_field': 'x_end_date'}" optional="show"/>
-                    <field name="x_days_duration" optional="show"/>
-                </xpath>
-
-            </field>
-        </record>
-
-    </data>
-</odoo>

+ 0 - 5
custom_sat_connection/__init__.py

@@ -1,5 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import controllers
-from . import models
-from . import wizards

+ 0 - 30
custom_sat_connection/__manifest__.py

@@ -1,30 +0,0 @@
-# -*- coding: utf-8 -*-
-{
-    'name': "Conexión SAT",
-    'summary': """
-        Conexión con el portal del SAT para obtención de complementos CFDI.
-    """,
-    'description': """
-        Conexión con el portal del SAT para obtención de complementos CFDI.
-    """,
-    'author': "M22",
-    'website': "https://m22.mx",
-    'category': 'Account',
-    'version': '18.1',
-    'depends': ['base','account','accountant','l10n_mx_edi'],
-    'data': [
-        'security/res_groups.xml',
-        'security/ir.model.access.csv',
-        'views/account_cfdi.xml',
-        'views/res_config_settings.xml',
-        'views/account_journal.xml',
-        'views/account_account.xml',
-        'views/account_move.xml',
-        'views/account_esignature_certificate.xml',
-        'views/ir_attachment.xml',
-        'wizards/account_cfdi_sat.xml',
-        'wizards/account_cfdi_zip.xml',
-        'wizards/account_cfdi_xml.xml',
-    ],
-    'license': 'AGPL-3'
-}

+ 0 - 3
custom_sat_connection/controllers/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import controllers

+ 0 - 23
custom_sat_connection/controllers/controllers.py

@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo import http
-from odoo.http import request, content_disposition
-import base64
-
-class Binary(http.Controller):
-
-    @http.route('/web/binary/download_document', type='http', auth="public")
-    def download_document(self, model, id, filename=None, **kw):
-
-        record = request.env[model].browse(int(id))
-        binary_file = record.datas # aqui colocas el nombre del campo binario que almacena tu archivo
-        filecontent = base64.b64decode(binary_file or '')
-
-        if not filecontent:
-            return request.not_found()
-        else:
-            if not filename:
-                filename = '%s_%s' % (model.replace('.', '_'), id)
-            content_type = ('Content-Type', 'application/octet-stream')
-            disposition_content = ('Content-Disposition', content_disposition(filename))
-
-        return request.make_response(filecontent, [content_type, disposition_content])

+ 0 - 14
custom_sat_connection/models/__init__.py

@@ -1,14 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import account_esignature_certificate
-from . import portal_sat
-from . import account_journal
-from . import account_account
-from . import account_cfdi
-from . import account_cfdi_line
-from . import account_cfdi_tax
-from . import res_company
-from . import res_partner
-from . import res_config_settings
-from . import account_move
-from . import ir_attachment

+ 0 - 19
custom_sat_connection/models/account_account.py

@@ -1,19 +0,0 @@
-from odoo import api, fields, models
-
-class AccountAccount(models.Model):
-    _inherit = "account.account"
-
-    x_cfdi_type = fields.Selection([
-        ('I', 'Facturas de clientes'),
-        ('SI', 'Facturas de proveedor'),
-        ('E', 'Notas de crédito cliente'),
-        ('SE', 'Notas de crédito proveedor'),
-        ('P', 'REP de clientes'),
-        ('SP', 'REP de proveedores'),
-        ('N', 'Nóminas de empleados'),
-        ('SN', 'Nómina propia'),
-        ('T', 'Factura de traslado cliente'),
-        ('ST', 'Factura de traslado proveedor'),
-    ], string='Tipo de comprobante')
-
-

+ 0 - 831
custom_sat_connection/models/account_cfdi.py

@@ -1,831 +0,0 @@
-from odoo import api, fields, models, _, Command
-from odoo.exceptions import ValidationError
-from collections import OrderedDict
-from datetime import date
-from os.path import basename
-from zipfile import ZipFile
-from tempfile import TemporaryDirectory
-import xmltodict
-import base64
-import logging
-import os.path
-
-_logger = logging.getLogger(__name__)
-
-
-class AccountCFDI(models.Model):
-    _name = 'account.cfdi'
-    _inherit = ['mail.thread', 'mail.activity.mixin']
-    _description = 'Complemento CFDI'
-
-    code = fields.Char(string="Código")
-    name = fields.Char(string="Referencia")
-    uuid = fields.Char(string="UUID")
-    certificate = fields.Char(string="Certificado")
-    certificate_number = fields.Char(string="Nro. de certificado")
-    serie = fields.Char(string="Serie")
-    folio = fields.Char(string="Folio")
-    stamp = fields.Char(string="Sello")
-    version = fields.Char(string="Versión")
-    payment_condition = fields.Char(string="Condiciones de pago")
-    currency = fields.Char(string="Moneda")
-    payment_method = fields.Char(string="Forma de pago")
-    location = fields.Char(string="Lugar de expedición")
-    observations = fields.Text(string='Notas')
-    attachment_id = fields.Many2one(comodel_name="ir.attachment", string="Archivo adjunto")
-    pdf_id = fields.Many2one(comodel_name="ir.attachment", string="Representación impresa")
-    company_id = fields.Many2one(comodel_name="res.company", string="Empresa", default=lambda self: self.env.company)
-    emitter_id = fields.Many2one(comodel_name="res.partner", string="Emisor")
-    receiver_id = fields.Many2one(comodel_name="res.partner", string="Receptor")
-    move_id = fields.Many2one(comodel_name="account.move", string="Movimiento contable", copy=False)
-    payable_account_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable a pagar', tracking=True)
-    account_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable gasto', tracking=True)
-    account_analytic_account_id = fields.Many2one(comodel_name='account.analytic.account', string='Cuenta analítica')
-    fiscal_position_id = fields.Many2one(comodel_name='account.fiscal.position', string='Posición fiscal')
-    journal_id = fields.Many2one(comodel_name='account.journal', string='Diario', tracking=True)
-    tax_isr_id = fields.Many2one(comodel_name='account.tax', string='Retención ISR')
-    tax_iva_id = fields.Many2one(comodel_name='account.tax', string='Retención IVA')
-    analytic_distribution = fields.Json(string="Distribución analítica")
-    analytic_precision = fields.Integer(string="Precisión analítica", store=False, default=lambda self: self.env['decimal.precision'].precision_get("Percentage Analytic"))
-    concept_ids = fields.One2many("account.cfdi.line", "cfdi_id", string="Conceptos")
-    tax_ids = fields.One2many('account.cfdi.tax', 'cfdi_id', string='Impuestos')
-    date = fields.Date(string="Fecha")
-    subtotal = fields.Float(string="Subtotal", copy=False)
-    total = fields.Float(string="Total", copy=False)
-    tax_total = fields.Float(string="Impuestos", compute="compute_tax_total", store=True)
-    payment_type = fields.Selection(selection=[('PPD', 'PPD'), ('PUE', 'PUE')], string='Método de pago', readonly=True)
-    cfdi_type = fields.Selection(string="Tipo de comprobante", selection=[
-        ('I', 'Facturas de clientes'),
-        ('SI', 'Facturas de proveedor'),
-        ('E', 'Notas de crédito cliente'),
-        ('SE', 'Notas de crédito proveedor'),
-        ('P', 'REP de clientes'),
-        ('SP', 'REP de proveedores'),
-        ('N', 'Nóminas de empleados'),
-        ('SN', 'Nómina propia'),
-        ('T', 'Factura de traslado cliente'),
-        ('ST', 'Factura de traslado proveedor')
-    ], index=True, copy=False)
-    state = fields.Selection(string="Estado", selection=[
-        ('draft', 'Borrador'),
-        ('done', 'Procesada'),
-        ('cancel', 'Anulado')
-    ], copy=False, default='draft', tracking=True)
-    sat_state = fields.Selection(string="Estado SAT", selection=[
-        ("valid", "Valido"),
-        ("not_found", "No Encontrado"),
-        ("undefined", "No sincronizado Aún"),
-        ("none", "Estado no definido"),
-        ("cancelled", "Cancelado")
-    ], copy=False, tracking=True, default="valid")
-    #Datos de addenda
-    delivery_number = fields.Char(string="No. Entrega")
-    invoice_qty = fields.Integer(string="Unidades facturadas")
-
-    @api.depends("subtotal","total")
-    def compute_tax_total(self):
-        for rec in self:
-            rec.tax_total = rec.total - rec.subtotal
-
-    # ---------------------------------------------------Metodos de creación---------------------------------------------------------
-
-    @api.model
-    def create(self, vals_list):
-        self = self.with_context(skip_invoice_sync=True, check_move_validity=False)
-        res = super().create(vals_list)
-        for cfdi in res.filtered(lambda move: move.move_id):
-            cfdi.move_id.update({
-                "cfdi_id": cfdi.id
-            })
-        return res
-
-    def name_get(self):
-        result = []
-        for record in self:
-            folio = f"[{record.serie}-{record.folio}] - " if record.serie and record.folio else f"[{record.serie}] - " if record.serie and not record.folio else f"[{record.folio}] - " if not record.serie and record.folio else ""
-            name = f"{folio}" + record.uuid + ' - $' + str(record.total)
-            result.append((record.id, name))
-        return result
-
-    # Creación de CFDIS
-    def create_cfdis(self, attachment_data):
-        self = self.with_context(skip_invoice_sync=True, check_move_validity=False)
-        cfdi_list = []
-        cfdi_ids = self.env["account.cfdi"]
-        uuids = []
-        for data in attachment_data:
-            # Obtener la información para crear el cfdi
-            data_xml = data.get("xml")
-            xml_data = self.get_cfdi_data(data_xml.get("datas"))
-            if xml_data.get("Comprobante"):
-                cfdi_type = self.get_cfdi_type(xml_data)
-                uuid = self.validation_cfdi(xml_data, cfdi_type)
-                # Evitar UUID repetidos ya que el SAT manda en ocasiones el mismo XML dos veces
-                if uuid and uuid not in uuids:
-                    uuids.append(uuid)
-                    emiiter_partner_id, recipient_partner_id = self.get_cfdi_partners(xml_data)
-                    move_id = self.validate_cfdi_move(xml_data, uuid, cfdi_type, emiiter_partner_id)
-                    cfdi_data = self.add_cfdi_data(xml_data, uuid, emiiter_partner_id, recipient_partner_id, move_id, cfdi_type, data)
-                    cfdi_data = self.get_cfdi_lines(xml_data, cfdi_data, cfdi_type, emiiter_partner_id, recipient_partner_id)
-                    # cfdi_data = self.get_payment_tax(cfdi_type, xml_data, cfdi_data)
-                    cfdi_list.append(cfdi_data)
-                else:
-                    continue
-        if cfdi_list:
-            cfdi_ids = self.env["account.cfdi"].sudo().create(cfdi_list)
-        return cfdi_ids
-
-    # Obtener la información del xml
-    def get_cfdi_data(self, xml_file):
-        file_content = base64.b64decode(xml_file)
-        if b'xmlns:schemaLocation' in file_content and b'xsi:schemaLocation' not in file_content:
-            file_content = file_content.replace(b'xmlns:schemaLocation', b'xsi:schemaLocation')
-        file_content = file_content.replace(b'cfdi:', b'')
-        file_content = file_content.replace(b'tfd:', b'')
-        try:
-            xml_data = xmltodict.parse(file_content)
-            return xml_data
-        except Exception as e:
-            _logger.info(e)
-            return dict()
-
-    # Obtener el tipo de comprobante que es el CFDI
-    def get_cfdi_type(self, xml_data):
-        cfdi_type = xml_data['Comprobante']['@TipoDeComprobante'] if '@TipoDeComprobante' in xml_data['Comprobante'] else 'I'
-        if cfdi_type in ['I', 'E', 'P']:
-            if xml_data['Comprobante']['Emisor']['@Rfc'] != self.env.company.vat:
-                cfdi_type = 'S' + cfdi_type
-        return cfdi_type
-
-    # Validaciones antes de la creación del CFDI
-    def validation_cfdi(self, xml_data, cfdi_type):
-        if '@UUID' in xml_data['Comprobante']['Complemento']['TimbreFiscalDigital']:
-            uuid = xml_data['Comprobante']['Complemento']['TimbreFiscalDigital']['@UUID']
-            cfdi_id = self.env['account.cfdi'].sudo().search([('uuid', '=', uuid)], limit=1)
-            if cfdi_id:
-                _logger.info(f"El CFDI con UUID {uuid}, ya existe en la base de datos, se omitirá en el proceso.")
-                return False
-            # Evitar que se suban xml que no pertenecen a la empresa
-            if "S" in cfdi_type:
-                partner_rfc = xml_data['Comprobante']['Receptor']['@Rfc']
-            else:
-                partner_rfc = xml_data['Comprobante']['Emisor']['@Rfc']
-            if partner_rfc != self.env.company.vat:
-                return False
-        else:
-            return False
-        return uuid
-
-    # Buscar el asiento contable de odoo relacionado si es que existe
-    def validate_cfdi_move(self, xml_data, uuid, cfdi_type, emitter_partner_id):
-        move_id = self.env["account.move"]
-        delivery_number, invoice_qty = self.get_addenda_data(xml_data)
-        if cfdi_type in ["I", "E"]:
-            move_id = move_id.sudo().search(
-                [("move_type", "in", ["out_invoice", "out_refund"]), ("l10n_mx_edi_cfdi_uuid", "=", uuid),
-                 ("state", "=", "posted"), ("cfdi_id", "=", False)], limit=1)
-        elif cfdi_type in ["SI", "SE"]:
-            invoice_date = xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else ""
-            folio = xml_data['Comprobante']['@Folio'] if '@Folio' in xml_data['Comprobante'] else ''
-            move_id = self.env['account.move'].sudo().search(
-                [('partner_id.vat', '=', emitter_partner_id.vat), ('ref', '=', folio),
-                 ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"), ("cfdi_id", "=", False),
-                 ("invoice_date", "=", invoice_date[:10])], limit=1)
-            if not move_id:
-                if '@Serie' in xml_data['Comprobante']:
-                    folio = xml_data['Comprobante']['@Serie'] + folio
-                    move_id = self.env['account.move'].sudo().search(
-                        [('partner_id.vat', '=', emitter_partner_id.vat), ('ref', '=', folio),
-                         ('move_type', 'in', ['in_invoice', 'in_refund']), ("cfdi_id", "=", False),
-                         ("state", "=", "posted"), ("invoice_date", "=", invoice_date[:10])], limit=1)
-                if not move_id:
-                    move_id = self.env['account.move'].sudo().search(
-                        [('partner_id.vat', '=', emitter_partner_id.vat),
-                         ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"),
-                         ("cfdi_id", "=", False),
-                         ("invoice_date", "=", invoice_date[:10])], limit=1)
-                if not move_id and delivery_number:
-                    move_id = self.env['account.move'].sudo().search(
-                        [('partner_id.vat', '=', emitter_partner_id.vat),
-                         ('move_type', 'in', ['in_invoice', 'in_refund']), ("state", "=", "posted"),
-                         ("cfdi_id", "=", False), ("x_delivery_number","!=",False),
-                         ("x_delivery_number", "=", delivery_number)], limit=1)
-                if not move_id:
-                    amount_untaxed = xml_data['Comprobante']['@SubTotal'] if '@SubTotal' in xml_data['Comprobante'] else 0
-                    invoice_date = xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else ""
-                    move_id = self.env['account.move'].sudo().search(
-                        [('partner_id.vat', '=', emitter_partner_id.vat), ('move_type', 'in', ['in_invoice']),
-                         ("cfdi_id", "=", False), ("state", "=", "posted"), ("amount_untaxed", "=", amount_untaxed),
-                         ("invoice_date", "=", invoice_date[:10])], limit=1)
-        return move_id
-
-    # Preparar la informacion del CFDI
-    def add_cfdi_data(self, xml_data, uuid, emitter_partner_id, recipient_partner_id, move_id, cfdi_type, attachment_data):
-        journal_id, account_id = self.get_cfdi_journal_id(cfdi_type, emitter_partner_id, recipient_partner_id)
-        partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
-        payable_account_id = self.get_payable_cfdi_account_id(cfdi_type, partner_id)
-        attachment_id = self.env["ir.attachment"].sudo().create(attachment_data.get("xml"))
-        pdf_id = self.env["ir.attachment"].sudo().create(attachment_data.get("pdf")) if attachment_data.get("pdf") else False
-        delivery_number, invoice_qty = self.get_addenda_data(xml_data)
-        
-        data = {
-            "attachment_id": attachment_id.id,
-            "pdf_id": pdf_id.id if pdf_id else False,
-            "code": uuid,
-            "uuid": uuid,
-            "certificate": xml_data['Comprobante']['@Certificado'] if '@Certificado' in xml_data['Comprobante'] else '',
-            "date": xml_data['Comprobante']['@Fecha'] if '@Fecha' in xml_data['Comprobante'] else '',
-            "folio": xml_data['Comprobante']['@Folio'] if '@Folio' in xml_data['Comprobante'] else '',
-            "payment_method": xml_data['Comprobante']['@FormaPago'] if '@FormaPago' in xml_data['Comprobante'] else '',
-            "location": xml_data['Comprobante']['@LugarExpedicion'] if '@LugarExpedicion' in xml_data['Comprobante'] else '',
-            "payment_type": xml_data['Comprobante']['@MetodoPago'] if '@MetodoPago' in xml_data['Comprobante'] else '',
-            "currency": xml_data['Comprobante']['@Moneda'] if '@Moneda' in xml_data['Comprobante'] else '',
-            "certificate_number": xml_data['Comprobante']['@NoCertificado'] if '@NoCertificado' in xml_data['Comprobante'] else '',
-            "stamp": xml_data['Comprobante']['@Sello'] if '@Sello' in xml_data['Comprobante'] else '',
-            "serie": xml_data['Comprobante']['@Serie'] if '@Serie' in xml_data['Comprobante'] else '',
-            "subtotal": xml_data['Comprobante']['@SubTotal'] if '@SubTotal' in xml_data['Comprobante'] else '',
-            "cfdi_type": cfdi_type,
-            "total": xml_data['Comprobante']['@Total'] if '@Total' in xml_data['Comprobante'] else '',
-            "version": xml_data['Comprobante']['@Version'] if '@Version' in xml_data['Comprobante'] else '',
-            "payment_condition": xml_data['Comprobante']['@CondicionesDePago'] if '@CondicionesDePago' in xml_data['Comprobante'] else '',
-            "emitter_id": emitter_partner_id.id,
-            "receiver_id": recipient_partner_id.id,
-            "move_id": move_id.id if move_id else False,
-            "state": "draft" if not move_id else "done",
-            "journal_id": journal_id.id if journal_id else False,
-            "account_id": account_id.id if account_id else False,
-            "fiscal_position_id": partner_id.property_account_position_id.id if partner_id.property_account_position_id else False,
-            "tax_iva_id": partner_id.x_tax_iva_id.id if partner_id.x_tax_iva_id else False,
-            "tax_isr_id": partner_id.x_tax_isr_id.id if partner_id.x_tax_isr_id else False,
-            "payable_account_id": payable_account_id.id if payable_account_id else False,
-        }
-        data["name"] = data["serie"] + data["folio"] if data.get("serie") and data.get("folio") else data["serie"] if data.get("serie") else data.get("folio")
-        data["delivery_number"] = delivery_number
-        data["invoice_qty"] = invoice_qty
-        return data
-
-    # Se obtienen las lineas de CFDI
-    def get_cfdi_lines(self, xml_data, cfdi_data, cfdi_type, emitter_partner_id, recipient_partner_id):
-        if type(xml_data['Comprobante']['Conceptos']['Concepto']) is list:
-            lines = xml_data['Comprobante']['Conceptos']['Concepto']
-        elif type(xml_data['Comprobante']['Conceptos']['Concepto']) is OrderedDict:
-            lines = xml_data['Comprobante']['Conceptos'].items()
-        else:
-            lines = [xml_data['Comprobante']['Conceptos']['Concepto']]
-        i = 1
-        data_list = []
-        for line_value in lines:
-            if type(xml_data['Comprobante']['Conceptos']['Concepto']) is list:
-                line = line_value
-            elif type(xml_data['Comprobante']['Conceptos']['Concepto']) is OrderedDict:
-                line = line_value[1]
-            else:
-                line = line_value
-            if float(line['@Importe']) >= 0:
-                uom_id = False
-                if '@ClaveUnidad' in line:
-                    uom_unspsc_id = self.env['product.unspsc.code'].sudo().search([('code', '=', line['@ClaveUnidad'])])
-                    if uom_unspsc_id:
-                        uom_id = self.env['uom.uom'].sudo().search([('unspsc_code_id', '=', uom_unspsc_id.id)], limit=1)
-                if uom_id:
-                    unidad_id = uom_id.id
-                else:
-                    unidad_id = False
-                product_category_id = False
-                if '@ClaveProdServ' in line:
-                    unspsc_product_category_id = self.env['product.unspsc.code'].sudo().search([('code', '=', line['@ClaveProdServ'])])
-                    if unspsc_product_category_id:
-                        product_category_id = unspsc_product_category_id.id
-                    else:
-                        product_category_id = False
-
-                data_line = {
-                    'sequence': i,
-                    'code_cfdi': cfdi_data.get("code"),
-                    'date': cfdi_data.get("date"),
-                    'folio': cfdi_data.get("folio"),
-                    'payment_method': cfdi_data.get("payment_method"),
-                    'location': cfdi_data.get("location"),
-                    'payment_type': cfdi_data.get("payment_type"),
-                    'currency': cfdi_data.get("currency"),
-                    'certificate_number': cfdi_data.get("certificate_number"),
-                    'stamp': cfdi_data.get("stamp"),
-                    'serie': cfdi_data.get("serie"),
-                    'subtotal': cfdi_data.get("subtotal"),
-                    'cfdi_type': cfdi_data.get("cfdi_type"),
-                    'total': cfdi_data.get("total"),
-                    'version': cfdi_data.get("version"),
-                    'emitter_id': cfdi_data.get("emitter_id"),
-                    'receiver_id': cfdi_data.get("receiver_id"),
-                    'product_code': line['@ClaveProdServ'] if '@ClaveProdServ' in line else '',
-                    'no_identification': line['@NoIdentificacion'] if '@NoIdentificacion' in line else '',
-                    'quantity': float(line['@Cantidad']),
-                    'uom_code': line['@ClaveUnidad'] if '@ClaveUnidad' in line else '',
-                    'uom': line['@Unidad'] if '@Unidad' in line else '',
-                    'description': line['@Descripcion'],
-                    'discount': float(line['@Descuento']) if '@Descuento' in line else 0,
-                    'unit_price': float(line['@ValorUnitario']),
-                    'uom_id': unidad_id,
-                    'unspsc_product_category_id': product_category_id,
-                    'amount': float(line['@Importe']),
-                }
-                data_line = self.search_cfdi_product(line, cfdi_type, data_line, emitter_partner_id, recipient_partner_id)
-                data_line = self.get_cfdi_tax_lines(data_line, line, cfdi_type, recipient_partner_id)
-                data_list.append(Command.create(data_line))
-            i += 1
-        if data_list:
-            cfdi_data["concept_ids"] = data_list
-        return cfdi_data
-
-    #Obtener intereses de pago
-    def get_payment_tax(self, cfdi_type, xml_data, cfdi_data):
-        if cfdi_type in ['P', 'SP']:
-            payment_tax_list = []
-            payments_list = xml_data['Comprobante']['Complemento']['pago20:Pagos']['pago20:Pago']
-            payments_list = self.get_data_iterable(payments_list)
-            if payments_list:
-                for payment_list in payments_list:
-                    payment_date = payment_list["@FechaPago"][:10],
-                    payments = payment_list["pago20:DoctoRelacionado"]
-                    payments = self.get_data_iterable(payments)
-
-                    if payments:
-                        for payment in payments:
-                            if payment.get("@ObjetoImpDR") and payment.get("@ObjetoImpDR") == '02':
-                                payment_taxes = payment["pago20:ImpuestosDR"]["pago20:TrasladosDR"]["pago20:TrasladoDR"]
-                                payment_taxes = self.get_data_iterable(payment_taxes)
-                                if payment_taxes:
-                                    for payment_tax in payment_taxes:
-                                        payment_tax_data = {
-                                            "name": payment["@IdDocumento"],
-                                            "serie": payment.get("@Serie"),
-                                            "folio": payment.get("@Folio"),
-                                            "currency": payment["@MonedaDR"],
-                                            "currency_rate": payment["@EquivalenciaDR"],
-                                            "paid_amount": payment["@ImpPagado"],
-                                            "previous_balance": payment["@ImpSaldoAnt"],
-                                            "current_balance": payment["@ImpSaldoInsoluto"],
-                                            "subject_tax": payment["@ObjetoImpDR"],
-                                            "payment_date": payment_date[0],
-                                            "tax_amount": payment_tax.get("@ImporteDR"),
-                                            "base_amount": payment_tax["@BaseDR"],
-                                            "type_tax": payment_tax["@ImpuestoDR"],
-                                            "base_tax": float(payment_tax["@TasaOCuotaDR"]) * 100 if payment_tax.get("@TasaOCuotaDR") else 0,
-                                            "exempt_tax": True if payment_tax.get("@TipoFactorDR") == 'Exento' else False,
-                                        }
-                                        payment_tax_list.append(Command.create(payment_tax_data))
-            if payment_tax_list:
-                cfdi_data["tax_paymnent_ids"] = payment_tax_list
-        return cfdi_data
-
-    def get_addenda_data(self, xml_data):
-        try:
-            addenda = xml_data["Comprobante"]["Addenda"]
-            addenda_header = addenda["customized"]["NEW_ERA"]["Cabecera"]
-            addenda_footer = addenda["customized"]["NEW_ERA"]["DatosPie"]
-            return addenda_header["@DL_VBLEN"], addenda_footer["@SUMQTYEA"]
-        except:
-            return False, False
-            
-    #Obtener el producto del cfdi si existe
-    def search_cfdi_product(self, line, cfdi_type, data_line, emitter_partner_id, recipient_partner_id):
-        partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
-        product_tmpl_id = self.env['product.template']
-        concept_id = self.env['account.cfdi.line']
-        account_line_id = self.env['account.move.line']
-        # Buscar el producto para poder relacionarlo a la linea del concepto
-        if '@NoIdentificacion' in line:
-            # Si el comprobante es una factura de proveedor o una nota de cliente del proveedor
-            if cfdi_type in ['SI', 'SE']:
-                # Se busca si es que se cuenta con una lista de precios a proveedor que identifique el producto mediante el codigo del producto
-                product_supplier_id = self.env['product.supplierinfo'].sudo().search([('partner_id', '=', partner_id.id), ('product_code', '=', line['@NoIdentificacion'])], limit=1)
-                if product_supplier_id:
-                    product_tmpl_id = product_supplier_id.product_tmpl_id
-            if not product_tmpl_id and cfdi_type in ["I", "E"]:
-                product_tmpl_id = self.env['product.template'].sudo().search([('default_code', '=', line['@NoIdentificacion'])], limit=1)
-        # Identificar si se tiene registro de algun producto que coincida exactamente con la descripción del concepto
-        if not product_tmpl_id and '@Descripcion' in line:
-            if cfdi_type in ['SI', 'SE']:
-                product_supplier_id = self.env['product.supplierinfo'].sudo().search([('partner_id', '=', partner_id.id), ('product_name', '=', line['@Descripcion'])], limit=1)
-                if product_supplier_id:
-                    product_tmpl_id = product_supplier_id.product_tmpl_id
-            elif cfdi_type in ['I', 'E']:
-                product_tmpl_id = self.env['product.template'].sudo().search(['|', ("name", "=", line['@Descripcion']), ('default_code', '=', line['@Descripcion'])], limit=1)
-        # Se busca el producto si ya ha habido lineas de producto con el mismo emisor y misma clave de producto o descripcion
-        if not product_tmpl_id and '@ClaveProdServ' in line and partner_id:
-            concept_id = self.env['account.cfdi.line'].sudo().search([("product_tmpl_id", "!=", False), ("emitter_id.id", "=", partner_id.id if cfdi_type in ["SI", "SE"] else emitter_partner_id.id), ("product_code", "=", line['@ClaveProdServ']), ("company_id.id", "=", self.env.company.id)], limit=1)
-            if concept_id:
-                product_tmpl_id = concept_id.product_tmpl_id
-            else:
-                account_line_id = self.env["account.move.line"].sudo().search([("product_id", "!=", False), ("product_id.unspsc_code_id.code", "=", line['@ClaveProdServ']), ("partner_id.id", "=", partner_id.id)], limit=1)
-                if account_line_id:
-                    product_tmpl_id = account_line_id.product_id.product_tmpl_id
-        # Identificar si existen lineas de conceptos que coincidan con la misma descripción y del mismo emisor
-        if not product_tmpl_id and '@Descripcion' in line and partner_id:
-            concept_id = self.env['account.cfdi.line'].sudo().search([("product_tmpl_id", "!=", False), ("emitter_id.id", "=", partner_id.id if cfdi_type in ["SI", "SE"] else emitter_partner_id.id), ("description", "=", line['@Descripcion']), ("company_id.id", "=", self.env.company.id)], limit=1)
-            if concept_id:
-                product_tmpl_id = concept_id.product_tmpl_id
-        if not product_tmpl_id and partner_id and partner_id.x_product_tmpl_id:
-            product_tmpl_id = partner_id.x_product_tmpl_id
-
-        if product_tmpl_id:
-            product_id = self.env['product.product'].sudo().search([('product_tmpl_id', '=', product_tmpl_id.id)], limit=1)
-            data_line["product_id"] = product_id.id if product_id else False
-            data_line["product_tmpl_id"] = product_tmpl_id.id
-            categ_id = product_tmpl_id.categ_id
-            if cfdi_type in ["SI", "SE"]:
-                account_id = product_tmpl_id.property_account_expense_id if product_tmpl_id.property_account_expense_id else categ_id.property_account_expense_categ_id if categ_id else False
-            elif cfdi_type in ["I", "E"]:
-                account_id = product_tmpl_id.property_account_income_id if product_tmpl_id.property_account_income_id else categ_id.property_account_income_categ_id if categ_id else False
-            data_line["account_id"] = concept_id.account_id.id if concept_id and concept_id.account_id else account_line_id.account_id.id if account_line_id else account_id.id if account_id else False
-        return data_line
-
-    #Obtener lineas de impuestos
-    def get_cfdi_tax_lines(self, data_line, line, cfdi_type, recipient_partner_id):
-        tax_list = []
-        if 'Impuestos' in line:
-            j = 1
-            if 'Traslados' in line['Impuestos']:
-                if type(line['Impuestos']['Traslados']['Traslado']) is list:
-                    impuestos = line['Impuestos']['Traslados']['Traslado']
-                elif type(line['Impuestos']['Traslados']['Traslado']) is OrderedDict:
-                    impuestos = line['Impuestos']['Traslados'].items()
-                else:
-                    impuestos = [line['Impuestos']['Traslados']['Traslado']]
-                for value_tax in impuestos:
-                    if type(line['Impuestos']['Traslados']['Traslado']) is list:
-                        impuesto = value_tax
-                    elif type(line['Impuestos']['Traslados']['Traslado']) is OrderedDict:
-                        impuesto = value_tax[1]
-                    else:
-                        impuesto = value_tax
-                    if str(impuesto['@TipoFactor']).strip().upper() == 'EXENTO':
-                        tasa_o_cuota = 0
-                        importe = 0
-                    else:
-                        tasa_o_cuota = float(impuesto['@TasaOCuota'])
-                        importe = float(impuesto['@Importe'])
-                    tax_data = {
-                        'sequence': j,
-                        'base': float(impuesto['@Base']),
-                        'code': impuesto['@Impuesto'],
-                        'factor_type': impuesto['@TipoFactor'],
-                        'rate': tasa_o_cuota,
-                        'amount': importe,
-                        'tax_type': 'traslado'
-                    }
-                    j = j + 1
-                    amount = tasa_o_cuota * 100
-                    tax_domain = [('amount', '=', amount), ('company_id', '=', self.env.company.id)]
-                    t_id = self.env["account.tax"]
-                    if tax_data.get("impuesto") == '002':
-                        tax_domain.append(("name", "ilike", "iva"))
-                    elif tax_data.get("impuesto") == '003':
-                        tax_domain.append(("name", "ilike", "ieps"))
-                    elif tax_data.get("impuesto") == '001':
-                        tax_domain.append(("name", "ilike", "isr"))
-
-                    if cfdi_type in ['I', 'E']:
-                        tax_domain.append(('type_tax_use', '=', 'sale'))
-                        t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
-                    elif cfdi_type in ['SI', 'SE']:
-                        tax_domain.append(('type_tax_use', '=', 'purchase'))
-                        t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
-                    if t_id:
-                        tax_data["tax_id"] = t_id.id
-                    tax_list.append(Command.create(tax_data))
-
-            if 'Retenciones' in line['Impuestos']:
-                if type(line['Impuestos']['Retenciones']['Retencion']) is list:
-                    retenciones = line['Impuestos']['Retenciones']['Retencion']
-                elif type(line['Impuestos']['Retenciones']['Retencion']) is OrderedDict:
-                    retenciones = line['Impuestos']['Retenciones'].items()
-                else:
-                    retenciones = [line['Impuestos']['Retenciones']['Retencion']]
-                for value_tax in retenciones:
-                    if type(line['Impuestos']['Retenciones']['Retencion']) is list:
-                        retencion = value_tax
-                    elif type(line['Impuestos']['Retenciones']['Retencion']) is OrderedDict:
-                        retencion = value_tax[1]
-                    else:
-                        retencion = value_tax
-                    tasa_o_cuota = float(retencion['@TasaOCuota'])
-                    importe = float(retencion['@Importe'])
-                    tax_data = {
-                        'sequence': j,
-                        'base': float(retencion['@Base']),
-                        'code': retencion['@Impuesto'],
-                        'factor_type': retencion['@TipoFactor'],
-                        'rate': tasa_o_cuota,
-                        'amount': importe,
-                        'tax_type': 'retencion'
-                    }
-                    j = j + 1
-
-                    amount = round(-(tasa_o_cuota * 100), 2)
-                    tax_domain = [('amount', '=', amount), ('company_id', '=', self.env.company.id)]
-                    t_id = self.env['account.tax']
-                    if tax_data.get("impuesto") == '002':
-                        tax_domain.append(("name", "ilike", "iva"))
-                    elif tax_data.get("impuesto") == '003':
-                        tax_domain.append(("name", "ilike", "ieps"))
-                    elif tax_data.get("impuesto") == '001':
-                        tax_domain.append(("name", "ilike", "isr"))
-                    if cfdi_type in ['I', 'E']:
-                        tax_domain.append(('type_tax_use', '=', 'sale'))
-                        t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
-                    elif cfdi_type in ['SI', 'SE']:
-                        tax_domain.append(('type_tax_use', '=', 'purchase'))
-                        t_id = self.env['account.tax'].sudo().search(tax_domain, limit=1)
-                    if t_id:
-                        tax_data["tax_id"] = t_id.id
-
-                    if not t_id and cfdi_type in ['SI', 'SE']:
-                        if tax_data.get("impuesto") == '001':
-                            if recipient_partner_id.tax_isr_id:
-                                tax_data["tax_id"] = recipient_partner_id.tax_isr_id.id
-                        elif tax_data.get("impuesto") == '002':
-                            if recipient_partner_id.tax_iva_id:
-                                tax_data["tax_id"] = recipient_partner_id.tax_iva_id.id
-                    tax_list.append(Command.create(tax_data))
-            if tax_list:
-                data_line["tax_ids"] = tax_list
-        return data_line
-
-    # Obteniendo el diario contable dependiendo del tipo de comprobante
-    def get_cfdi_journal_id(self, cfdi_type, emitter_partner_id, recipient_partner_id):
-        journal_id = self.env['account.journal'].sudo().search([('x_cfdi_type', '=', cfdi_type), ("company_id.id", "=", self.env.company.id)], limit=1)
-        # Buscamos si ya hubo un CFDI al mismo cliente y receptor y del mismo tipo y que tenga un diario colocado
-        if not journal_id:
-            cfdi_id = self.env["account.cfdi"].sudo().search(
-                [("emitter_id.id", "=", emitter_partner_id.id),
-                 ("receiver_id.id", "=", recipient_partner_id.id), ("cfdi_type", "=", cfdi_type),
-                 ("journal_id", "!=", False)], limit=1)
-            journal_id = cfdi_id.journal_id if cfdi_id else False
-        account_id = journal_id.default_account_id if journal_id and journal_id.default_account_id else False
-        if not account_id:
-            partner_id = recipient_partner_id if cfdi_type in ["I", "E"] else emitter_partner_id
-            account_id = self.get_expense_cfdi_account_id(cfdi_type, partner_id)
-        return journal_id, account_id
-
-    # Obtener la cuenta de gastos
-    def get_expense_cfdi_account_id(self, cfdi_type, partner_id):
-        if partner_id:
-            expense_account_id = partner_id.x_account_expense_id
-            # Buscamos un CFDI parecido para obtener la cuenta por pagar
-            if not expense_account_id and cfdi_type in ['I', 'E']:
-                cfdi_id = self.env["account.cfdi"].sudo().search(
-                    [("receiver_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
-                     ("account_id", "!=", False)], limit=1)
-                expense_account_id = cfdi_id.payable_account_id if cfdi_id else False
-            elif not expense_account_id and cfdi_type in ['SI', 'SE']:
-                cfdi_id = self.env["account.cfdi"].sudo().search(
-                    [("emitter_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
-                     ("account_id", "!=", False)], limit=1)
-                expense_account_id = cfdi_id.payable_account_id if cfdi_id else False
-        return expense_account_id
-
-    # Obtener cuenta por pagar dependiendo del tipo de comprobante
-    def get_payable_cfdi_account_id(self, cfdi_type, partner_id):
-        # Se busca si se tiene configurado la cuenta por pagar en la cuenta sino se busca por el contacto y por ultimo se busca un cfdi con el mismo contacto y tipo
-        payable_account_id = self.env['account.account'].sudo().search([('x_cfdi_type', '=', cfdi_type), ("company_ids.id", "=", self.env.company.id)], limit=1)
-        if not payable_account_id and partner_id:
-            payable_account_id = partner_id.property_account_payable_id
-            # Buscamos un CFDI parecido para obtener la cuenta por pagar
-            if not payable_account_id and cfdi_type in ['I', 'E']:
-                cfdi_id = self.env["account.cfdi"].sudo().search(
-                    [("receiver_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
-                     ("payable_account_id", "!=", False)], limit=1)
-                payable_account_id = cfdi_id.payable_account_id if cfdi_id else False
-            elif not payable_account_id and cfdi_type in ['SI', 'SE']:
-                cfdi_id = self.env["account.cfdi"].sudo().search(
-                    [("emitter_id.id", "=", partner_id.id), ("cfdi_type", "=", cfdi_type),
-                     ("payable_account_id", "!=", False)], limit=1)
-                payable_account_id = cfdi_id.payable_account_id if cfdi_id else False
-        return payable_account_id
-
-    # Obtener el emisor y receptor del cfdi
-    def get_cfdi_partners(self, xml_data):
-        emitter_id = self.env['res.partner'].sudo().search([('vat', '=', xml_data['Comprobante']['Emisor']['@Rfc'])],	limit=1)
-        receiver_id = self.env['res.partner'].sudo().search([('vat', '=', xml_data['Comprobante']['Receptor']['@Rfc'])], limit=1)
-        if not emitter_id:
-            emitter_id = self.create_cfdi_partner(xml_data, "Emisor")
-        if not receiver_id:
-            receiver_id = self.create_cfdi_partner(xml_data, "Receptor")
-        return emitter_id, receiver_id
-
-    # Crear los contactos del CFDI si es necesario
-    def create_cfdi_partner(self, xml_data, partner_type):
-        data = {
-            'name': xml_data['Comprobante'][partner_type]['@Nombre'],
-            'vat': xml_data['Comprobante'][partner_type]['@Rfc'],
-            'l10n_mx_edi_fiscal_regime': xml_data['Comprobante'][partner_type][
-                '@RegimenFiscal'] if partner_type == "Emisor" else xml_data['Comprobante'][partner_type][
-                '@RegimenFiscalReceptor'],
-            'country_id': self.env.company.country_id.id,
-            'company_type': 'company'
-        }
-        partner_id = self.env["res.partner"].create(data)
-        return partner_id
-
-    # --------------------------------------------------------------------------------------------------------------------
-
-
-    # ---------------------------------------------------Metodos de descarga masiva---------------------------------------------------------
-
-    def download_massive_pdf_zip(self):
-        return self.download_massive_zip("PDF Masivos.zip", "pdf_id")
-
-    def download_massive_xml_zip(self):
-        return self.download_massive_zip("XML Masivos.zip", "attachment_id")
-
-    def download_massive_zip(self, filename, field_name):
-        self = self.with_user(1)
-        zip_file = self.env['ir.attachment'].sudo().search([('name', '=', filename)], limit=1)
-        if zip_file:
-            zip_file.sudo().unlink()
-
-        # Funcion para decodificar el archivo
-        def isBase64_decodestring(s):
-            try:
-                decode_archive = base64.decodebytes(s)
-                return decode_archive
-            except Exception as e:
-                raise ValidationError('Error:', + str(e))
-
-        tempdir_file = TemporaryDirectory()
-        location_tempdir = tempdir_file.name
-        # Creando ruta dinamica para poder guardar el archivo zip
-        date_act = date.today()
-        file_name = 'DescargaMasiva(Fecha de descarga' + " - " + str(date_act) + ")"
-        file_name_zip = file_name + ".zip"
-        zipfilepath = os.path.join(location_tempdir, file_name_zip)
-        path_files = os.path.join(location_tempdir)
-
-        # Creando zip
-        for file in self.mapped(field_name):
-            object_name = file.name
-            ruta_ob = object_name
-            object_handle = open(os.path.join(location_tempdir, ruta_ob), "wb")
-            object_handle.write(isBase64_decodestring(file.datas))
-            object_handle.close()
-
-        with ZipFile(zipfilepath, 'w') as zip_obj:
-            for file in os.listdir(path_files):
-                file_path = os.path.join(path_files, file)
-                if file_path != zipfilepath:
-                    zip_obj.write(file_path, basename(file_path))
-
-        with open(zipfilepath, 'rb') as file_data:
-            bytes_content = file_data.read()
-            encoded = base64.b64encode(bytes_content)
-
-        data = {
-            'name': filename,
-            'type': 'binary',
-            'datas': encoded,
-            'company_id': self.env.company.id
-        }
-        attachment = self.env['ir.attachment'].sudo().create(data)
-        return self.download_zip(file_name_zip, attachment.id)
-
-    def download_zip(self, filename, id_file):
-        path = "/web/binary/download_document?"
-        model = "ir.attachment"
-
-        url = path + "model={}&id={}&filename={}".format(model, id_file, filename)
-        return {
-            'type': 'ir.actions.act_url',
-            'url': url,
-            'target': 'self',
-        }
-
-    # ---------------------------------------------------------------------------------------------------------------------
-
-    # ---------------------------------------------------Metodos de conciliación---------------------------------------------------------
-
-    def action_done(self):
-        self = self.with_user(1)
-        invoice_list = []
-        folio_names = []
-        for rec in self.filtered(lambda cfdi: not cfdi.move_id and cfdi.attachment_id and cfdi.cfdi_type in ["SI", "I","SE"]):
-            cfdi_type = rec.cfdi_type
-            partner_id = rec.emitter_id if cfdi_type in ["SI",'SE','SP'] else rec.receiver_id
-            folio = f"{rec.serie}-{rec.folio}" if rec.serie and rec.folio else f"{rec.serie}" if rec.serie and not rec.folio else f"{rec.folio}" if not rec.serie and rec.folio else ''
-            move_type = {
-                "SI": "in_invoice",
-                "I": "out_invoice",
-                "SE": "in_refund",
-                "E": "out_refund",
-            }
-            invoice_data = {
-                'move_type': move_type.get(cfdi_type),
-                'partner_id': partner_id.id,
-                'date': rec.date,
-                'invoice_date': rec.date,
-                'invoice_date_due': rec.date,
-                'fiscal_position_id': partner_id.property_account_position_id.id if partner_id.property_account_position_id else rec.fiscal_position_id.id,
-                'ref': folio,
-                'amount_total_signed': rec.total,
-                'amount_total': rec.total,
-                'journal_id': rec.journal_id.id,
-                'company_id': rec.company_id.id,
-                'cfdi_id': rec.id,
-                'x_uuid': rec.uuid,
-                "x_delivery_number": rec.delivery_number,
-                "x_invoice_qty": rec.invoice_qty
-            }
-            if folio != '' and not self.env["account.move"].sudo().search([("name", "=", folio), ("state", "=", "posted")], limit=1) and folio not in folio_names and cfdi_type == "I":
-                invoice_data["name"] = folio
-            folio_names.append(folio)
-            i = 1
-            line_list = []
-            for line in rec.concept_ids:
-                if float(line.amount) > 0:
-                    data_line = {
-                        'sequence': i,
-                        'name': line.description,
-                        'quantity': float(line.quantity),
-                        'product_uom_id': line.uom_id.id,
-                        'discount': (float(line.discount) * 100) / float(line.amount),
-                        'price_unit': float(line.unit_price),
-                        'tax_ids': line.mapped("tax_ids.tax_id").ids,
-                        'account_id': line.account_id.id if line.account_id else rec.account_id.id if rec.account_id else False,
-                        'analytic_distribution': line.analytic_distribution if line.analytic_distribution else rec.analytic_distribution,
-                        'partner_id': partner_id.id,
-                    }
-
-                    if line.product_tmpl_id:
-                        product_id = self.env['product.product'].sudo().search([('product_tmpl_id', '=', line.product_tmpl_id.id)], limit=1)
-                        data_line["product_id"] = product_id.id
-                    line_list.append(Command.create(data_line))
-                    i = i + 1
-            invoice_data["invoice_line_ids"] = line_list
-            invoice_list.append(invoice_data)
-        invoice_ids = self.env['account.move'].with_context(check_move_validity=False).sudo().create(invoice_list)
-
-        for invoice_id in invoice_ids:
-            attachment_id = invoice_id.cfdi_id.attachment_id
-            invoice_id.cfdi_id.write({
-                "move_id": invoice_id.id,
-                "state": "done"
-            })
-            attachment_id.write({
-                'res_model': 'account.move',
-                'res_id': invoice_id.id,
-            })
-            if invoice_id.move_type == "out_invoice":
-                if attachment_id:
-                    id_edi_format = self.env['account.edi.format'].sudo().search([('name', '=', 'CFDI (4.0)')], limit=1)
-                    if not invoice_id.edi_document_ids:
-                        if id_edi_format:
-                            create_edi = self.env['account.edi.document'].sudo().create(
-                                {'edi_format_id': id_edi_format.id, 'attachment_id': attachment_id.id, 'state': 'sent',
-                                 'move_id': invoice_id.id, 'error': False})
-                            invoice_id.write({'edi_document_ids': [(6, False, [create_edi.id])], 'l10n_mx_edi_cfdi_uuid': invoice_id.l10n_mx_edi_cfdi_uuid_cusom})
-                        # Se colocaria la factura como timbrada
-                        elif id_edi_format and invoice_id.edi_document_ids:
-                            edi_format_id = invoice_id.edi_document_ids.filtered(
-                                lambda edi: edi.edi_format_id.id == id_edi_format.id)
-                            if edi_format_id:
-                                edi_format_id["attachment_id"] = attachment_id.id
-                                edi_format_id["error"] = False
-                                edi_format_id["state"] = "sent"
-                            else:
-                                data = {
-                                    'move_id': invoice_id.id,
-                                    'attachment_id': attachment_id.id,
-                                    'state': 'sent',
-                                    'error': False,
-                                    'edi_format_id': id_edi_format.id
-                                }
-                                self.env["account.edi.document"].sudo().create([data])
-                            invoice_id.write({'l10n_mx_edi_cfdi_uuid': invoice_id.l10n_mx_edi_cfdi_uuid_cusom})
-
-            invoice_id.write({
-                "state": "posted"
-            })
-        return {
-            "name": _("Facturas"),
-            "view_mode": "list,form",
-            "res_model": "account.move",
-            "type": "ir.actions.act_window",
-            "target": "current",
-            "domain": [('id', 'in', invoice_ids.ids)]
-        }
-
-    def action_unlink_move(self):
-        for rec in self:
-            if rec.move_id:
-                rec.attachment_id.write({
-                    'res_model': 'account.cfdi',
-                    'res_id': rec.id,
-                })
-                rec.write({
-                    "state": "draft",
-                    "move_id": False
-                })
-                rec.move_id.write({
-                    'cfdi_id': False,
-                    'x_uuid': False
-                })

+ 0 - 56
custom_sat_connection/models/account_cfdi_line.py

@@ -1,56 +0,0 @@
-from odoo import api, fields, models
-
-class AccountCfdiLine(models.Model):
-    _name = 'account.cfdi.line'
-    _description = 'Linea de complemento CFDI'
-
-    sequence = fields.Integer(string='Secuencia')
-    name = fields.Char()
-    cfdi_id = fields.Many2one(comodel_name="account.cfdi", string="CFDI")
-    company_id = fields.Many2one(comodel_name='res.company', string='Empresa', related='cfdi_id.company_id')
-    code_cfdi = fields.Char(string='UUID')
-    date = fields.Date(string='Fecha')
-    folio = fields.Char(string='Folio')
-    payment_method = fields.Char(string='Forma de pago')
-    location = fields.Char(string='Lugar de expedición')
-    payment_type = fields.Selection(string='Método de pago', selection=[('PPD', 'PPD'), ('PUE', 'PUE')])
-    currency = fields.Char(string='Moneda')
-    certificate_number = fields.Char(string='Nro. de certificado')
-    stamp = fields.Char(string='Sello')
-    serie = fields.Char(string='Serie')
-    subtotal = fields.Float(string='Subtotal')
-    cfdi_type = fields.Selection(selection=[
-        ('I', u'Facturas de clientes'),
-        ('SI', u'Facturas de proveedor'),
-        ('E', u'Notas de crédito cliente'),
-        ('SE', u'Notas de crédito proveedor'),
-        ('P', u'REP de clientes'),
-        ('SP', u'REP de proveedores'),
-        ('N', u'Nóminas de empleados'),
-        ('SN', u'Nómina propia'),
-        ('T', u'Factura de traslado cliente'),
-        ('ST', u'Factura de traslado proveedor'),
-    ], string='Tipo de comprobante', index=True)
-    total = fields.Float(string='Total', readonly=True)
-    version = fields.Char(string='Versión', readonly=True)
-    emitter_id = fields.Many2one(comodel_name='res.partner', string='Emisor')
-    receiver_id = fields.Many2one(comodel_name='res.partner', string='Receptor')
-    quantity = fields.Float(string='Cantidad', digits='Product Unit of Measure')
-    product_code = fields.Char(string='Clave')
-    uom_code = fields.Char(string='Clave unidad')
-    description = fields.Char(string='Descripción')
-    discount = fields.Float(string='Descuento')
-    amount = fields.Float(string='Importe')
-    unit_price = fields.Float(string='Valor unitario', digits='Product Price')
-    no_identification = fields.Char(string='Identificación', readonly=True)
-    uom = fields.Char(string='Unidad', readonly=True)
-    uom_id = fields.Many2one(comodel_name='uom.uom', string='Unidad de medida')
-    unspsc_product_category_id = fields.Many2one(comodel_name='product.unspsc.code', string='Categoria')
-    product_tmpl_id = fields.Many2one(comodel_name='product.template', string='Plantilla del producto')
-    product_id = fields.Many2one(comodel_name='product.product', string='Producto')
-    account_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable')
-    account_analytic_account_id = fields.Many2one(comodel_name='account.analytic.account', string='Cuenta analítica')
-    analytic_distribution = fields.Json(string="Distribución analítica")
-    analytic_precision = fields.Integer(string="Precisión analítica", store=False, default=lambda self: self.env['decimal.precision'].precision_get("Percentage Analytic"))
-    tax_ids = fields.One2many('account.cfdi.tax', 'concept_id', string='Impuestos')
-

+ 0 - 17
custom_sat_connection/models/account_cfdi_tax.py

@@ -1,17 +0,0 @@
-from odoo import api, fields, models
-
-class AccountCfdiTax(models.Model):
-    _name = 'account.cfdi.tax'
-    _description = 'Impuestos de conceptos de CFDI'
-
-    sequence = fields.Integer(string='Secuencia', required=True)
-    company_id = fields.Many2one(comodel_name='res.company', string='Empresa', related='concept_id.company_id', store=True)
-    base = fields.Float(string='Base')
-    code = fields.Char(string='Código')
-    factor_type = fields.Char(string='Código de porcentaje')
-    rate = fields.Float(string='Tasa o cuota', digits=(6, 4))
-    amount = fields.Float(string='Importe')
-    tax_id = fields.Many2one(comodel_name='account.tax', string='Impuesto')
-    concept_id = fields.Many2one(comodel_name='account.cfdi.line', string='Concepto de CFDI', ondelete="cascade")
-    cfdi_id = fields.Many2one(comodel_name='account.cfdi', string='CFDI', related="concept_id.cfdi_id", store=True, ondelete="cascade")
-    tax_type = fields.Selection(string="Tipo de impuesto", selection=[('retencion', 'Retención'), ('traslado', 'Traslados')])

+ 0 - 149
custom_sat_connection/models/account_esignature_certificate.py

@@ -1,149 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import base64
-import logging
-import ssl
-from datetime import datetime
-from cryptography.hazmat.primitives import serialization
-_logger = logging.getLogger(__name__)
-try:
-    from OpenSSL import crypto
-except ImportError:
-    _logger.warning('OpenSSL library not found. If you plan to use l10n_mx_edi, please install the library from https://pypi.python.org/pypi/pyOpenSSL')
-
-from pytz import timezone
-from odoo import _, api, fields, models, tools
-from odoo.exceptions import ValidationError, UserError
-from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
-
-def convert_key_cer_to_pem(key, password):
-    # TODO compute it from a python way
-    private_key = serialization.load_der_private_key(base64.b64decode(key), password.encode())
-    return private_key.private_bytes(
-        encoding=serialization.Encoding.PEM,
-        format=serialization.PrivateFormat.PKCS8,
-        encryption_algorithm=serialization.NoEncryption()
-    )
-
-def str_to_datetime(dt_str, tz=timezone('America/Mexico_City')):
-    return tz.localize(fields.Datetime.from_string(dt_str))
-
-class AccountEsignatureCertificate(models.Model):
-    _name = 'account.esignature.certificate'
-    _description = 'Certificado de México'
-
-    content = fields.Binary(string='Certificado', required=True)
-    key = fields.Binary(string='Llave de certificado',required=True)
-    password = fields.Char(string='Contraseña', required=True)
-    holder = fields.Char(string='Nombre', required=False)
-    holder_vat = fields.Char(string="RFC", required=False)
-    serial_number = fields.Char(string='Número de serie', readonly=True, index=True)
-    date_start = fields.Datetime(string='Fecha inicio', readonly=True)
-    date_end = fields.Datetime(string='Fecha final', readonly=True)
-
-    @tools.ormcache('content')
-    def get_pem_cer(self, content):
-        '''Get the current content in PEM format
-        '''
-        self.ensure_one()
-        return ssl.DER_cert_to_PEM_cert(base64.decodebytes(content)).encode('UTF-8')
-
-    @tools.ormcache('key', 'password')
-    def get_pem_key(self, key, password):
-        '''Get the current key in PEM format
-        '''
-        self.ensure_one()
-        private_key = serialization.load_der_private_key(base64.b64decode(key), password.encode())
-        return private_key.private_bytes(
-            encoding=serialization.Encoding.PEM,
-            format=serialization.PrivateFormat.PKCS8,
-            encryption_algorithm=serialization.NoEncryption()
-        )
-
-    def get_data(self):
-        '''Return the content (b64 encoded) and the certificate decrypted
-        '''
-        self.ensure_one()
-        cer_pem = self.get_pem_cer(self.content)
-        certificate = crypto.load_certificate(crypto.FILETYPE_PEM, cer_pem)
-        for to_del in ['\n', ssl.PEM_HEADER, ssl.PEM_FOOTER]:
-            cer_pem = cer_pem.replace(to_del.encode('UTF-8'), b'')
-        return cer_pem, certificate
-
-    def get_mx_current_datetime(self):
-        '''Get the current datetime with the Mexican timezone.
-        '''
-        mexican_tz = timezone('America/Mexico_City')
-        return datetime.now(mexican_tz)
-
-    def get_valid_certificate(self):
-        '''Search for a valid certificate that is available and not expired.
-        '''
-        mexican_dt = self.get_mx_current_datetime()
-        for record in self:
-            date_start = str_to_datetime(record.date_start)
-            date_end = str_to_datetime(record.date_end)
-            if date_start <= mexican_dt <= date_end:
-                return record
-        return None
-
-    def get_encrypted_cadena(self, cadena):
-        '''Encrypt the cadena using the private key.
-        '''
-        self.ensure_one()
-        key_pem = self.get_pem_key(self.key, self.password)
-        private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
-        encrypt = 'sha256WithRSAEncryption'
-        cadena_crypted = crypto.sign(private_key, cadena, encrypt)
-        return base64.b64encode(cadena_crypted)
-
-    @api.constrains('content', 'key', 'password')
-    def _check_credentials(self):
-        '''Check the validity of content/key/password and fill the fields
-        with the certificate values.
-        '''
-        mexican_tz = timezone('America/Mexico_City')
-        mexican_dt = self.get_mx_current_datetime()
-        date_format = '%Y%m%d%H%M%SZ'
-        for record in self:
-            try:
-                cer_pem, certificate = record.get_data()
-                before = mexican_tz.localize(
-                    datetime.strptime(certificate.get_notBefore().decode("utf-8"), date_format))
-                after = mexican_tz.localize(
-                    datetime.strptime(certificate.get_notAfter().decode("utf-8"), date_format))
-                serial_number = certificate.get_serial_number()
-                subject = certificate.get_subject()
-                holder = subject.CN
-            except Exception as e:
-                raise ValidationError(_('El contenido del certificado no es valido.'))
-
-            record.holder = holder
-            record.serial_number = ('%x' % serial_number)[1::2]
-            record.date_start = before.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
-            record.date_end = after.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
-            if mexican_dt > after:
-                raise ValidationError(_('El certificado expiro desde %s') % record.date_end)
-            try:
-                key_pem = self.get_pem_key(self.key, self.password)
-                crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
-            except Exception:
-                raise ValidationError(_('La contraseña no corresponde, favor de validar'))
-
-    @api.model
-    def create(self, data):
-        res = super(AccountEsignatureCertificate, self).create(data)
-        self.clear_caches()
-        return res
-
-    def write(self, data):
-        res = super(AccountEsignatureCertificate, self).write(data)
-        self.clear_caches()
-        return res
-
-    def unlink(self):
-        if self.env['account.move'].search([('l10n_mx_edi_cfdi_certificate_id', 'in', self.ids)]):
-            raise UserError(_('No es posible eliminar un certificado valido y vigente.'))
-        res = super(AccountEsignatureCertificate, self).unlink()
-        self.clear_caches()
-        return res

+ 0 - 17
custom_sat_connection/models/account_journal.py

@@ -1,17 +0,0 @@
-from odoo import api, fields, models
-
-class AccountJournal(models.Model):
-	_inherit = 'account.journal'
-
-	x_cfdi_type = fields.Selection(selection=[
-		('I', 'Facturas de clientes'),
-		('SI', 'Facturas de proveedor'),
-		('E', 'Notas de crédito cliente'),
-		('SE', 'Notas de crédito proveedor'),
-		('P', 'REP de clientes'),
-		('SP', 'REP de proveedores'),
-		('N', 'Nóminas de empleados'),
-		('SN', 'Nómina propia'),
-		('T', 'Factura de traslado cliente'),
-		('ST', 'Factura de traslado proveedor'),
-	], string='Tipo de comprobante')

+ 0 - 28
custom_sat_connection/models/account_move.py

@@ -1,28 +0,0 @@
-from odoo import api, fields, models
-
-class AccountMove(models.Model):
-    _inherit = "account.move"
-
-    cfdi_id = fields.Many2one(comodel_name="account.cfdi", string="CFDI")
-    x_uuid = fields.Char(string="CFDI UIID", related="cfdi_id.uuid", store=True)
-    x_delivery_number = fields.Char(string="No. Entrega")
-    x_invoice_qty = fields.Integer(string="Unidades facturadas")
-
-    #Eliminar relacion con el cfdi si se cancela el movimiento
-    def button_cancel(self):
-        res = super(AccountMove, self).button_cancel()
-        for rec in self:
-            if rec.cfdi_id:
-                rec.cfdi_id.attachment_id.write({
-                    'res_model': 'account.cfdi',
-                    'res_id': rec.cfdi_id.id,
-                })
-                rec.cfdi_id.write({
-                    "state": "draft",
-                    "move_id": False
-                })
-                rec.write({
-                    'cfdi_id': False,
-                    'x_uuid': False
-                })
-        return res

+ 0 - 75
custom_sat_connection/models/ir_attachment.py

@@ -1,75 +0,0 @@
-from odoo import api, fields, models
-from os.path import basename
-from zipfile import ZipFile
-from tempfile import TemporaryDirectory
-import base64
-import os.path
-
-class IrAttachment(models.Model):
-    _inherit = "ir.attachment"
-
-    def download_massive_zip(self):
-        self = self.with_user(1)
-        zip_name = "Descarga masiva.zip"
-        zip_file = self.env['ir.attachment'].sudo().search([('name', '=', zip_name)], limit=1)
-        if zip_file:
-            zip_file.sudo().unlink()
-
-        # Funcion para decodificar el archivo
-        def isBase64_decodestring(s):
-            try:
-                decode_archive = base64.decodebytes(s)
-                return decode_archive
-            except Exception as e:
-                raise ValidationError('Error:', + str(e))
-
-        tempdir_file = TemporaryDirectory()
-        location_tempdir = tempdir_file.name
-        # Creando ruta dinamica para poder guardar el archivo zip
-        date_act = fields.Date.today()
-        file_name = 'DescargaMasiva(Fecha de descarga' + " - " + str(date_act) + ")"
-        file_name_zip = file_name + ".zip"
-        zipfilepath = os.path.join(location_tempdir, file_name_zip)
-        path_files = os.path.join(location_tempdir)
-
-        # Creando zip
-        for file in self:
-            object_name = file.name
-            ruta_ob = object_name
-            object_handle = open(os.path.join(location_tempdir, ruta_ob), "wb")
-            object_handle.write(isBase64_decodestring(file.datas))
-            object_handle.close()
-
-        with ZipFile(zipfilepath, 'w') as zip_obj:
-            for file in os.listdir(path_files):
-                file_path = os.path.join(path_files, file)
-                if file_path != zipfilepath:
-                    zip_obj.write(file_path, basename(file_path))
-
-        with open(zipfilepath, 'rb') as file_data:
-            bytes_content = file_data.read()
-            encoded = base64.b64encode(bytes_content)
-
-        data = {
-            'name': zip_name,
-            'type': 'binary',
-            'datas': encoded,
-            'company_id': self.env.company.id
-        }
-        attachment = self.env['ir.attachment'].sudo().create(data)
-        self.unlink()
-        return self.download_zip(file_name_zip, attachment.id)
-
-    def download_zip(self, filename, id_file):
-        path = "/web/binary/download_document?"
-        model = "ir.attachment"
-
-        url = path + "model={}&id={}&filename={}".format(model, id_file, filename)
-        return {
-            'type': 'ir.actions.act_url',
-            'url': url,
-            'target': 'self',
-        }
-    
-    
-

+ 0 - 882
custom_sat_connection/models/portal_sat.py

@@ -1,882 +0,0 @@
-# -*- coding: utf-8 -*-
-# !/usr/bin/env python
-
-import base64
-import calendar
-import datetime
-from copy import deepcopy
-from html.parser import HTMLParser
-from uuid import UUID
-from OpenSSL import crypto
-from requests import exceptions, adapters
-import httpx
-import urllib3
-import ssl
-
-urllib3.disable_warnings()
-import logging
-
-_logger = logging.getLogger(__name__)
-
-TIMEOUT = 120
-TRY_COUNT = 3
-VERIFY_CERT = True
-CONTEXT = ssl.create_default_context()
-CONTEXT.set_ciphers('HIGH:!DH:!aNULL')
-
-#LE DA FORMATO A LOS VALORES DEL HTML
-class FormValues(HTMLParser):
-    _description = 'Elementos del HTML'
-
-    def __init__(self):
-        super().__init__()
-        self.values = {}
-
-    def handle_starttag(self, tag, attrs):
-        if tag in ('input', 'select'):
-            a = dict(attrs)
-            if a.get('type', '') and a['type'] == 'hidden':
-                if 'name' in a and 'value' in a:
-                    self.values[a['name']] = a['value']
-
-#LE DA FORMATO A LOS VALORES DEL HTML DEL INICIO DE SESION
-class FormLoginValues(HTMLParser):
-    _description = 'Elementos del HTML del inicio de sesión'
-
-    def __init__(self):
-        super().__init__()
-        self.values = {}
-
-    def handle_starttag(self, tag, attrs):
-        if tag == 'input':
-            attrib = dict(attrs)
-            try:
-                self.values[attrib['id']] = attrib['value']
-            except:
-                pass
-
-class Filters(object):
-    _description = 'Filters'
-
-    def __init__(self, args):
-        self.date_from = args['date_from']
-        self.day = args.get('day', False)
-        self.emitidas = args['emitidas']
-        self.date_to = None
-        if self.date_from:
-            self.date_to = args.get('date_to', self._now()).replace(hour=23, minute=59, second=59, microsecond=0)
-        self.uuid = str(args.get('uuid', ''))
-        self.stop = False
-        self.hour = False
-        self.minute = False
-        self._init_values(args)
-
-    def __str__(self):
-        if self.uuid:
-            msg = 'Descargar por UUID'
-        elif self.hour:
-            msg = 'Descargar por HORA'
-        elif self.day:
-            msg = 'Descargar por DIA'
-        else:
-            msg = 'Descargar por MES'
-        tipo = 'Recibidas'
-        if self.emitidas:
-            tipo = 'Emitidas'
-        if self.uuid:
-            return '{} - {} - {}'.format(msg, self.uuid, tipo)
-        else:
-            return '{} - {} - {} - {}'.format(msg, self.date_from, self.date_to, tipo)
-
-    def _now(self):
-        if self.day:
-            n = self.date_from
-        else:
-            last_day = calendar.monthrange(
-                self.date_from.year, self.date_from.month)[1]
-            n = datetime.datetime(self.date_from.year, self.date_from.month, last_day)
-        return n
-
-    def _init_values(self, args):
-        status = '-1'
-        type_cfdi = args.get('type_cfdi', '-1')
-        center_filter = 'RdoFechas'
-        if self.uuid:
-            center_filter = 'RdoFolioFiscal'
-        rfc_receptor = args.get('rfc_emisor', False)
-        if self.emitidas:
-            rfc_receptor = args.get('rfc_receptor', False)
-        script_manager = 'ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda'
-        self._post = {
-            '__ASYNCPOST': 'true',
-            '__EVENTTARGET': '',
-            '__EVENTARGUMENT': '',
-            '__LASTFOCUS': '',
-            '__VIEWSTATEENCRYPTED': '',
-            'ctl00$ScriptManager1': script_manager,
-            'ctl00$MainContent$hfInicialBool': 'false',
-            'ctl00$MainContent$BtnBusqueda': 'Buscar CFDI',
-            'ctl00$MainContent$TxtUUID': self.uuid,
-            'ctl00$MainContent$FiltroCentral': center_filter,
-            'ctl00$MainContent$DdlEstadoComprobante': status,
-            'ctl00$MainContent$ddlComplementos': type_cfdi,
-        }
-        return
-
-    def get_post(self):
-        start_hour = '0'
-        start_minute = '0'
-        start_second = '0'
-        end_hour = '0'
-        end_minute = '0'
-        end_second = '0'
-        if self.date_from:
-            start_hour = str(self.date_from.hour)
-            start_minute = str(self.date_from.minute)
-            start_second = str(self.date_from.second)
-            end_hour = str(self.date_to.hour)
-            end_minute = str(self.date_to.minute)
-            end_second = str(self.date_to.second)
-        if self.emitidas:
-            year1 = '0'
-            year2 = '0'
-            start = ''
-            end = ''
-            if self.date_from:
-                year1 = str(self.date_from.year)
-                year2 = str(self.date_to.year)
-                start = self.date_from.strftime('%d/%m/%Y')
-                end = self.date_to.strftime('%d/%m/%Y')
-            data = {
-                'ctl00$MainContent$hfInicial': year1,
-                'ctl00$MainContent$CldFechaInicial2$Calendario_text': start,
-                'ctl00$MainContent$CldFechaInicial2$DdlHora': start_hour,
-                'ctl00$MainContent$CldFechaInicial2$DdlMinuto': start_minute,
-                'ctl00$MainContent$CldFechaInicial2$DdlSegundo': start_second,
-                'ctl00$MainContent$hfFinal': year2,
-                'ctl00$MainContent$CldFechaFinal2$Calendario_text': end,
-                'ctl00$MainContent$CldFechaFinal2$DdlHora': end_hour,
-                'ctl00$MainContent$CldFechaFinal2$DdlMinuto': end_minute,
-                'ctl00$MainContent$CldFechaFinal2$DdlSegundo': end_second,
-            }
-        else:
-            year = '0'
-            month = '0'
-            if self.date_from:
-                year = str(self.date_from.year)
-                month = str(self.date_from.month)
-            day = '00'
-            if self.day:
-                day = '{:02d}'.format(self.date_from.day)
-            data = {
-                'ctl00$MainContent$CldFecha$DdlAnio': year,
-                'ctl00$MainContent$CldFecha$DdlMes': month,
-                'ctl00$MainContent$CldFecha$DdlDia': day,
-                'ctl00$MainContent$CldFecha$DdlHora': start_hour,
-                'ctl00$MainContent$CldFecha$DdlMinuto': start_minute,
-                'ctl00$MainContent$CldFecha$DdlSegundo': start_second,
-                'ctl00$MainContent$CldFecha$DdlHoraFin': end_hour,
-                'ctl00$MainContent$CldFecha$DdlMinutoFin': end_minute,
-                'ctl00$MainContent$CldFecha$DdlSegundoFin': end_second,
-            }
-        self._post.update(data)
-        return self._post
-
-
-class Invoice(HTMLParser):
-    _description = 'Invoice'
-
-    START_PAGE = 'ContenedorDinamico'
-    URL = 'https://portalcfdi.facturaelectronica.sat.gob.mx/'
-    END_PAGE = 'ctl00_MainContent_pageNavPosition'
-    LIMIT_RECORDS = 'ctl00_MainContent_PnlLimiteRegistros'
-    NOT_RECORDS = 'ctl00_MainContent_PnlNoResultados'
-    TEMPLATE_DATE = '%Y-%m-%dT%H:%M:%S'
-
-    def __init__(self):
-        super().__init__()
-        self._is_div_page = False
-        self._col = 0
-        self._current_tag = ''
-        self._last_link = ''
-        self._last_link_pdf = ''
-        self._last_uuid = ''
-        self._last_status = ''
-        self._last_date_cfdi = ''
-        self._last_date_timbre = ''
-        self._last_pac = ''
-        self._last_total = ''
-        self._last_type = ''
-        self._last_date_cancel = ''
-        self._last_emisor_rfc = ''
-        self._last_emisor = ''
-        self._last_receptor_rfc = ''
-        self._last_receptor = ''
-        self.invoices = []
-        self.not_found = False
-        self.limit = False
-
-    def handle_starttag(self, tag, attrs):
-        self._current_tag = tag
-        if tag == 'div':
-            attrib = dict(attrs)
-            if 'id' in attrib and attrib['id'] == self.NOT_RECORDS \
-                    and 'inline' in attrib['style']:
-                self.not_found = True
-            elif 'id' in attrib and attrib['id'] == self.LIMIT_RECORDS:
-                self.limit = True
-            elif 'id' in attrib and attrib['id'] == self.START_PAGE:
-                self._is_div_page = True
-            elif 'id' in attrib and attrib['id'] == self.END_PAGE:
-                self._is_div_page = False
-        elif self._is_div_page and tag == 'td':
-            self._col += 1
-        elif tag == 'span':
-            attrib = dict(attrs)
-            if attrib.get('id', '') == 'BtnDescarga':
-                self._last_link = attrib['onclick'].split("'")[1]
-            if attrib.get('id', '') == 'BtnRI':
-                self._last_link_pdf = attrib['onclick'].split("'")[1]
-
-    def handle_endtag(self, tag):
-        if self._is_div_page and tag == 'tr':
-            if self._last_uuid:
-                url_xml = ''
-                if self._last_link:
-                    url_xml = '{}{}'.format(self.URL, self._last_link)
-                    self._last_link = ''
-                url_pdf = ''
-                if self._last_link_pdf:
-                    url_pdf = '{}{}{}'.format(self.URL, "RepresentacionImpresa.aspx?Datos=", self._last_link_pdf)
-
-                date_cancel = None
-                if self._last_date_cancel:
-                    date_cancel = datetime.datetime.strptime(
-                        self._last_date_cancel, self.TEMPLATE_DATE)
-                invoice = (self._last_uuid,
-                           {
-                               'url': url_xml,
-                               'acuse': url_pdf,
-                               'estatus': self._last_status,
-                               'date_cfdi': datetime.datetime.strptime(
-                                   self._last_date_cfdi, self.TEMPLATE_DATE),
-                               'date_timbre': datetime.datetime.strptime(
-                                   self._last_date_timbre, self.TEMPLATE_DATE),
-                               'date_cancel': date_cancel,
-                               'rfc_pac': self._last_pac,
-                               'total': float(self._last_total),
-                               'tipo': self._last_type,
-                               'emisor': self._last_emisor,
-                               'rfc_emisor': self._last_emisor_rfc,
-                               'receptor': self._last_receptor,
-                               'rfc_receptor': self._last_receptor_rfc,
-                           }
-                           )
-                self.invoices.append(invoice)
-            self._last_uuid = ''
-            self._last_status = ''
-            self._last_date_cancel = ''
-            self._last_emisor_rfc = ''
-            self._last_emisor = ''
-            self._last_receptor_rfc = ''
-            self._last_receptor = ''
-            self._last_date_cfdi = ''
-            self._last_date_timbre = ''
-            self._last_pac = ''
-            self._last_total = ''
-            self._last_type = ''
-            self._col = 0
-
-    def handle_data(self, data):
-        cv = data.strip()
-        if self._is_div_page and self._current_tag == 'span' and cv:
-            if self._col == 1:
-                try:
-                    UUID(cv)
-                    self._last_uuid = cv
-                except ValueError:
-                    pass
-            elif self._col == 2:
-                self._last_emisor_rfc = cv
-            elif self._col == 3:
-                self._last_emisor = cv
-            elif self._col == 4:
-                self._last_receptor_rfc = cv
-            elif self._col == 5:
-                self._last_receptor = cv
-            elif self._col == 6:
-                self._last_date_cfdi = cv
-            elif self._col == 7:
-                self._last_date_timbre = cv
-            elif self._col == 8:
-                self._last_pac = cv
-            elif self._col == 9:
-                self._last_total = cv.replace('$', '').replace(',', '')
-            elif self._col == 10:
-                self._last_type = cv.lower()
-            elif self._col == 12:
-                self._last_status = cv
-            elif self._col == 14:
-                self._last_date_cancel = cv
-
-
-# CONEXION Y OBTENCION DE ELEMENTOS DEL SAT
-class PortalSAT(object):
-    _description = 'Conexion al portal del SAT inicio de sesion y descarga'
-
-    # CONSTANTES PARA LA CONEXION
-    URL_MAIN = 'https://portal.facturaelectronica.sat.gob.mx/'
-    HOST = 'cfdiau.sat.gob.mx'
-    BROWSER = 'Mozilla/5.0 (X11; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0'
-    REFERER = 'https://cfdiau.sat.gob.mx/nidp/app/login?id=SATx509Custom&sid=0&option=credential&sid=0'
-    PORTAL = 'portalcfdi.facturaelectronica.sat.gob.mx'
-    URL_LOGIN = 'https://{}/nidp/app/login'.format(HOST)
-    URL_FORM = 'https://{}/nidp/app/login?sid=0&sid=0'.format(HOST)
-    URL_PORTAL = 'https://portalcfdi.facturaelectronica.sat.gob.mx/'
-    URL_CONTROL = 'https://cfdicontribuyentes.accesscontrol.windows.net/v2/wsfederation'
-    URL_CONSULTA = URL_PORTAL + 'Consulta.aspx'
-    URL_RECEPTOR = URL_PORTAL + 'ConsultaReceptor.aspx'
-    URL_EMISOR = URL_PORTAL + 'ConsultaEmisor.aspx'
-    URL_LOGOUT = URL_PORTAL + 'logout.aspx?salir=y'
-    DIR_EMITIDAS = 'emitidas'
-    DIR_RECIBIDAS = 'recibidas'
-    COMPANY_ID = ""
-
-    def __init__(self, rfc, target, sin):
-        self._rfc = rfc
-        self.error = ''
-        self.is_connect = False
-        self.not_network = False
-        self.only_search = False
-        self.only_test = False
-        self.sin_sub = sin
-        self._only_status = False
-        self._init_values(target)
-
-    def _init_values(self, target):
-        self._folder = target
-        self._emitidas = False
-        self._current_year = datetime.datetime.now().year
-
-        self._session = httpx.Client(http2=True, timeout=TIMEOUT, verify=CONTEXT)
-        a = adapters.HTTPAdapter(pool_connections=512, pool_maxsize=512, max_retries=5)
-        return
-
-    def _get_post_form_dates(self):
-        post = {}
-        post['__ASYNCPOST'] = 'true'
-        post['__EVENTARGUMENT'] = ''
-        post['__EVENTTARGET'] = 'ctl00$MainContent$RdoFechas'
-        post['__LASTFOCUS'] = ''
-        post['ctl00$MainContent$CldFecha$DdlAnio'] = str(self._current_year)
-        post['ctl00$MainContent$CldFecha$DdlDia'] = '0'
-        post['ctl00$MainContent$CldFecha$DdlHora'] = '0'
-        post['ctl00$MainContent$CldFecha$DdlHoraFin'] = '23'
-        post['ctl00$MainContent$CldFecha$DdlMes'] = '1'
-        post['ctl00$MainContent$CldFecha$DdlMinuto'] = '0'
-        post['ctl00$MainContent$CldFecha$DdlMinutoFin'] = '59'
-        post['ctl00$MainContent$CldFecha$DdlSegundo'] = '0'
-        post['ctl00$MainContent$CldFecha$DdlSegundoFin'] = '59'
-        post['ctl00$MainContent$DdlEstadoComprobante'] = '-1'
-        post['ctl00$MainContent$FiltroCentral'] = 'RdoFechas'
-        post['ctl00$MainContent$TxtRfcReceptor'] = ''
-        post['ctl00$MainContent$TxtUUID'] = ''
-        post['ctl00$MainContent$ddlComplementos'] = '-1'
-        post['ctl00$MainContent$hfInicialBool'] = 'true'
-        post['ctl00$ScriptManager1'] = 'ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$RdoFechas'
-        return post
-
-    #OBTENER RESPUESTAS DE LAS PETICIONES QUE SE REALIZAN AL SAT
-    def _response(self, url, method='get', headers={}, data={}):
-        try:
-            if method == 'get':
-                result = self._session.get(url, timeout=TIMEOUT)
-            else:
-                result = self._session.post(url, data=data, timeout=TIMEOUT)
-            msg = '{} {} {}'.format(result.status_code, method.upper(), url)
-            if result.status_code == 200:
-                return result.text
-            else:
-                _logger.error(msg)
-                return ''
-        except exceptions.Timeout:
-            msg = 'Tiempo de espera agotado'
-            self.not_network = True
-            _logger.error(msg)
-            return ''
-        except exceptions.ConnectionError:
-            msg = 'Revisa la conexión a Internet'
-            self.not_network = True
-            _logger.error(msg)
-            return ''
-
-    #LECTURA Y OBTENCION DE CIERTOS ELEMENTOS QUE SE PRESENTAN EN EL HTML
-    def _read_form(self, html, form=''):
-        if form == 'login':
-            parser = FormLoginValues()
-        else:
-            parser = FormValues()
-        parser.feed(html)
-        return parser.values
-
-    #OBTENCION DEL CABECERO QUE SE ENVIARA EN ALGUNAS PETICIONES
-    def _get_headers(self, host, referer, ajax=False):
-        acept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
-        headers = {
-            'Accept': acept,
-            'Accept-Encoding': 'gzip, deflate, br',
-            'Accept-Language': 'es-ES,es;q=0.5',
-            'Connection': 'keep-alive',
-            'DNT': '1',
-            'Host': host,
-            'Referer': referer,
-            'Upgrade-Insecure-Requests': '1',
-            'User-Agent': self.BROWSER,
-            'Content-Type': 'application/x-www-form-urlencoded',
-        }
-        if ajax:
-            headers.update({
-                'Cache-Control': 'no-cache',
-                'X-MicrosoftAjax': 'Delta=true',
-                'x-requested-with': 'XMLHttpRequest',
-                'Pragma': 'no-cache',
-            })
-        return headers
-
-    #OBTENER LOS VALORES DE LOS CAMPOS Y BOTONES DE LA PANTALLA DE CONSULTA
-    def _get_post_type_search(self, html):
-        tipo_busqueda = 'RdoTipoBusquedaReceptor'
-        if self._emitidas:
-            tipo_busqueda = 'RdoTipoBusquedaEmisor'
-        sm = 'ctl00$MainContent$UpnlBusqueda|ctl00$MainContent$BtnBusqueda'
-        post = self._read_form(html)
-        post['ctl00$MainContent$TipoBusqueda'] = tipo_busqueda
-        post['__ASYNCPOST'] = 'true'
-        post['__EVENTTARGET'] = ''
-        post['__EVENTARGUMENT'] = ''
-        post['ctl00$ScriptManager1'] = sm
-        return post
-
-    #OBTENER INFORMACION DEL CERTIFICADO
-    def _get_data_cert(self, fiel_cert_data):
-        cert = crypto.load_certificate(crypto.FILETYPE_ASN1, fiel_cert_data)
-        rfc = cert.get_subject().x500UniqueIdentifier.split(' ')[0]
-        serie = '{0:x}'.format(cert.get_serial_number())[1::2]
-        fert = cert.get_notAfter().decode()[2:]
-        return rfc, serie, fert
-
-    #OBTENER UNA FIRMA PARA PODER OBTENER UN TOKEN MAS ADELANTE
-    def _sign(self, fiel_pem_data, data):
-        key = crypto.load_privatekey(crypto.FILETYPE_PEM, fiel_pem_data)
-        sign = base64.b64encode(crypto.sign(key, data, 'sha256'))
-        return base64.b64encode(sign).decode('utf-8')
-
-    #OBTENCION DEL TOKEN QUE NOS PERMITIRA MANTENER LA SESION INICIADA
-    def _get_token(self, firma, co):
-        co = base64.b64encode(co.encode('utf-8')).decode('utf-8')
-        data = '{}#{}'.format(co, firma).encode('utf-8')
-        token = base64.b64encode(data).decode('utf-8')
-        return token
-
-    #OBTENCION DE LA INFORMACION QUE SE ENVIARA AL SAT PARA EL INICIO DE SESION
-    def _make_data_form(self, fiel_cert_data, fiel_pem_data, values):
-        rfc, serie, fert = self._get_data_cert(fiel_cert_data)
-        co = '{}|{}|{}'.format(values['tokenuuid'], rfc, serie)
-        firma = self._sign(fiel_pem_data, co)
-        token = self._get_token(firma, co)
-        keys = ('credentialsRequired', 'guid', 'ks', 'urlApplet')
-        data = {k: values[k] for k in keys}
-        data['fert'] = fert
-        data['token'] = token
-        data['arc'] = ''
-        data['placer'] = ''
-        data['secuence'] = ''
-        data['seeder'] = ''
-        data['tan'] = ''
-        return data
-
-    # CONEXION CON EL PORTAL DEL SAT
-    def login_fiel(self, fiel_cert_data, fiel_pem_data, certificate, company_id):
-        # CREAMOS SESION PERSISTENTE
-        client = self._session
-        # MANDAMOS LA SOLICITUD DE OBTENCION DEL SITIO WEB https://portal.facturaelectronica.sat.gob.mx/ PARA OBTENER REDIRECCIONAMIENTO
-        response = client.get(url=self.URL_MAIN)
-        # PETICION AL LOGIN CON FIEL
-        headers = {
-            "referer": self.get_url(response.url),
-        }
-        response = client.post(url="https://cfdiau.sat.gob.mx/nidp/wsfed/ep?id=SATUPCFDiCon&sid=0&option=credential&sid=0", headers=headers)
-        # PETICION PARA OBTENER EL FORMULARIO
-        headers["referer"] = self.get_url(response.url)
-        response = client.get(url="https://cfdiau.sat.gob.mx/nidp/app/login?id=SATx509Custom&sid=0&option=credential&sid=0", headers=headers)
-        values = self._read_form(response.text, 'login')
-        data = self._make_data_form(fiel_cert_data, fiel_pem_data, values)
-        headers["referer"] = self.get_url(response.url)
-        headers.update(self._get_headers(self.HOST, self.get_url(response.url)))
-        headers = {
-            "cache-control": "max-age=0",
-            "origin": "https://cfdiau.sat.gob.mx",
-            "content-type": "application/x-www-form-urlencoded",
-            "upgrade-insecure-requests": "1",
-            "user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0",
-            "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
-            "sec-gpc": "1",
-            "accept-language": "es-ES,es;q=0.5",
-            "sec-fetch-site": "same-origin",
-            "sec-fetch-dest": "document",
-            "referer": "https://cfdiau.sat.gob.mx/nidp/app/login?id=SATx509Custom&sid=0&option=credential&sid=0",
-            "accept-encoding": "gzip, deflate, br, zstd",
-            "priority": "u=0, i",
-        }
-        #NOS IDENTIFICAMOS EN EL SAT PARA PODER SEGUIR CON PETICIONES Y CONSULTAS
-        response = client.post(url="https://cfdiau.sat.gob.mx/nidp/app/login?id=SATx509Custom&sid=0&option=credential&sid=0", headers=headers, data=data)
-        headers["referer"] = "https://portal.facturaelectronica.sat.gob.mx/"
-        headers["Host"] = self.HOST
-        #SE OBTIENE LA PAGINA DE CONSULTA PARA OBTENER DATOS NECESARIOS PARA SU POSTERIOR USO EN LAS BUSQUEDAS
-        response = client.get(url=self.URL_CONSULTA)
-        data = self._read_form(response.text)
-        #SE MANDA LA INFORMACION PARA PODER SER REDIRIGIDOS CORRECTAMENTE A LA PAGINA DE CONSULTA
-        response = client.post(url=self.URL_CONSULTA, data=data)
-        self._session.headers.update(headers=headers)
-        self.is_connect = True
-        return True
-
-    def get_url(self, url_object):
-        url = f"{url_object.scheme}/{url_object.host}{url_object.full_path}"
-        return url
-
-    def _merge(self, list1, list2):
-        result = list1.copy()
-        result.update(list2)
-        return result
-
-    def _last_day(self, date):
-        last_day = calendar.monthrange(date.year, date.month)[1]
-        return datetime.datetime(date.year, date.month, last_day)
-
-    def _get_dates(self, d1, d2):
-        end = d2
-        dates = []
-        while True:
-            d2 = self._last_day(d1)
-            if d2 >= end:
-                dates.append((d1, end))
-                break
-            dates.append((d1, d2))
-            d1 = d2 + datetime.timedelta(days=1)
-        return dates
-
-    def _get_dates_recibidas(self, d1, d2):
-        days = (d2 - d1).days + 1
-        return [d1 + datetime.timedelta(days=d) for d in range(days)]
-
-    def _time_delta(self, days):
-        now = datetime.datetime.now()
-        date_from = now.replace(
-            hour=0, minute=0, second=0, microsecond=0) - datetime.timedelta(days=days)
-        date_to = now.replace(hour=23, minute=59, second=59, microsecond=0)
-        return date_from, date_to
-
-    def _time_delta_recibidas(self, days):
-        now = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
-        return [now - datetime.timedelta(days=d) for d in range(days)]
-
-    #FILTROS PARA LA CONSULTA
-    def _get_filters(self, args, emitidas=True):
-        filters = []
-        data = {}
-        data['day'] = bool(args['dia'])
-        data['uuid'] = ''
-        if args['uuid']:
-            data['uuid'] = str(args['uuid'])
-        data['emitidas'] = emitidas
-        data['rfc_emisor'] = args.get('rfc_emisor', '')
-        data['rfc_receptor'] = args.get('rfc_receptor', '')
-        data['type_cfdi'] = args.get('tipo_complemento', '-1')
-
-        if args['fecha_inicial'] and args['fecha_final'] and emitidas:
-            dates = self._get_dates(args['fecha_inicial'], args['fecha_final'])
-            for start, end in dates:
-                data['date_from'] = start
-                data['date_to'] = end
-                filters.append(Filters(data))
-        elif args['fecha_inicial'] and args['fecha_final']:
-            dates = self._get_dates_recibidas(args['fecha_inicial'], args['fecha_final'])
-            is_first_date = False
-            for d in dates:
-                if not is_first_date:
-                    data['date_from'] = d
-                    is_first_date = True
-                else:
-                    d = d.replace(hour=0, minute=0, second=0, microsecond=0)
-                    data['date_from'] = d
-                data['day'] = True
-                filters.append(Filters(data))
-        elif args['intervalo_dias'] and emitidas:
-            data['date_from'], data['date_to'] = self._time_delta(args['intervalo_dias'])
-            filters.append(Filters(data))
-        elif args['intervalo_dias']:
-            dates = self._time_delta_recibidas(args['intervalo_dias'])
-            for d in dates:
-                data['date_from'] = d
-                data['day'] = True
-                filters.append(Filters(data))
-        elif args['uuid']:
-            data['date_from'] = None
-            filters.append(Filters(data))
-        else:
-            day = args['dia'] or 1
-            data['date_from'] = datetime.datetime(args['ano'], args['mes'], day)
-            filters.append(Filters(data))
-
-        return tuple(filters)
-
-    def _segment_filter(self, filters):
-        new_filters = []
-        if filters.stop:
-            return new_filters
-        date = filters.date_from
-        date_to = filters.date_to
-
-        if filters.minute:
-            for m in range(10):
-                nf = deepcopy(filters)
-                nf.stop = True
-                nf.date_from = date + datetime.timedelta(minutes=m)
-                nf.date_to = date + datetime.timedelta(minutes=m + 1)
-                new_filters.append(nf)
-        elif filters.hour:
-            minutes = tuple(range(0, 60, 10)) + (0,)
-            minutes = tuple(zip(minutes, minutes[1:]))
-            for m in minutes:
-                nf = deepcopy(filters)
-                nf.minute = True
-                nf.date_from = date + datetime.timedelta(minutes=m[0])
-                nf.date_to = date + datetime.timedelta(minutes=m[1])
-                if m[0] == 50 and nf.date_to.hour == 23:
-                    nf.date_to = nf.date_to.replace(
-                        hour=nf.date_to.hour, minute=59, second=59)
-                elif m[0] == 50 and nf.date_to.hour != 23:
-                    nf.date_to = nf.date_to.replace(
-                        hour=nf.date_to.hour + 1, minute=0, second=0)
-                new_filters.append(nf)
-        elif filters.day:
-            hours = tuple(range(0, 25))
-            hours = tuple(zip(hours, hours[1:]))
-            for h in hours:
-                nf = deepcopy(filters)
-                nf.hour = True
-                nf.date_from = date + datetime.timedelta(hours=h[0])
-                nf.date_to = date + datetime.timedelta(hours=h[1])
-                if h[1] == 24:
-                    nf.date_to = nf.date_from.replace(
-                        minute=59, second=59, microsecond=0)
-                new_filters.append(nf)
-        else:
-            last_day = calendar.monthrange(date.year, date.month)[1]
-            for d in range(last_day):
-                nf = deepcopy(filters)
-                nf.day = True
-                nf.date_from = date + datetime.timedelta(days=d)
-                nf.date_to = nf.date_from.replace(
-                    hour=23, minute=59, second=59, microsecond=0)
-                new_filters.append(nf)
-                if date_to == nf.date_to:
-                    break
-        return new_filters
-
-    #OBTENER INFORMACION QUE SE ENVIARA EN LA CONSULTA PARA OBTENER LOS CFDIS
-    def _get_post(self, html):
-        validos = ('EVENTTARGET', '__EVENTARGUMENT', '__LASTFOCUS', '__VIEWSTATE')
-        values = html.split('|')
-        post = {v: values[i + 1] for i, v in enumerate(values) if v in validos}
-        return post
-
-    #ASIGNAR UN HEADER PARA LA CONSULTA DE CFDIS
-    def _set_search_headers(self):
-        self._session.headers = {
-            "cache-control": "no-cache",
-            "x-requested-with": "XMLHttpRequest",
-            "user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0",
-            "x-microsoftajax": "Delta=true",
-            "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
-            "accept": "*/*",
-            "sec-gpc": "1",
-            "accept-language": "es-ES,es;q=0.5",
-            "origin": "https://portalcfdi.facturaelectronica.sat.gob.mx",
-            "sec-fetch-site": "same-origin",
-            "sec-fetch-mode": "cors",
-            "sec-fetch-dest": "empty",
-            "referer": "https://portalcfdi.facturaelectronica.sat.gob.mx/ConsultaEmisor.aspx",
-            "accept-encoding": "gzip, deflate, br, zstd",
-            "priority": "u=1, i",
-        }
-        return True
-
-    #OBTENCION DE LA PAGINA DE CONSULTA POR FECHAS
-    def _change_to_date(self, url_search):
-        client = self._session
-        self._set_search_headers()
-        response = client.get(url_search)
-        data = self._read_form(response.text)
-        post = self._merge(data, self._get_post_form_dates())
-        headers = self._get_headers(self.PORTAL, url_search, True)
-        response = client.post(url=url_search, headers=headers, data=post)
-        post = self._get_post(response.text)
-        return data, post
-
-    #BUSQUEDA DE CFDIS RECIBIDOS CREADOS POR UN PROVEEDOR
-    def _search_recibidas(self, filters):
-        url_search = self.URL_RECEPTOR
-        values, post_source = self._change_to_date(url_search)
-        invoice_content = {}
-        for f in filters:
-            post = self._merge(values, f.get_post())
-            post = self._merge(post, post_source)
-            headers = self._get_headers(self.PORTAL, url_search, True)
-            html = self._response(url_search, 'post', headers, post)
-            not_found, limit, invoices = self._get_download_links(html)
-            if not_found or not invoices:
-                msg = '\n\tNo se encontraron documentos en el filtro:\n\t{}'.format(str(f))
-                _logger.info(msg)
-            else:
-                data = self._download(invoices, limit, f)
-                if data and type(data) == dict:
-                    invoice_content.update(data)
-        return invoice_content
-
-    #BUSQUEDA DE CFDIS EMITIDOS CREADOS POR LA EMPRESA
-    def _search_emitidas(self, filters):
-        url_search = self.URL_EMISOR
-        values, post_source = self._change_to_date(url_search)
-        invoice_content = {}
-        for f in filters:
-            _logger.info(str(f))
-            post = self._merge(values, f.get_post())
-            post = self._merge(post, post_source)
-            headers = self._get_headers(self.PORTAL, url_search, True)
-            html = self._response(url_search, 'post', headers, post)
-            not_found, limit, invoices = self._get_download_links(html)
-            if not_found or not invoices:
-                msg = '\n\tNo se encontraron documentos en el filtro:\n\t{}'.format(str(f))
-                _logger.info(msg)
-            else:
-                data = self._download(invoices, limit, f, self.DIR_EMITIDAS)
-                if data and type(data) == dict:
-                    invoice_content.update(data)
-        return invoice_content
-
-    #PROCESO DE BUSQUEDA DE CFDIS PARA SU DESCARGA
-    def search(self, opt, download_option='both'):
-        self._only_status = opt['estatus']
-        invoice_content_e, invoice_content_r = {}, {}
-        if download_option == 'both':
-            filters_e = self._get_filters(opt, True)
-            invoice_content_e = self._search_emitidas(filters_e)
-            filters_r = self._get_filters(opt, False)
-            invoice_content_r = self._search_recibidas(filters_r)
-        elif download_option == 'supplier':
-            filters_r = self._get_filters(opt, False)
-            invoice_content_r = self._search_recibidas(filters_r)
-        elif download_option == 'customer':
-            filters_e = self._get_filters(opt, True)
-            invoice_content_e = self._search_emitidas(filters_e)
-        return invoice_content_r, invoice_content_e
-
-    #PROCESO DE DESCARGA
-    def _download(self, invoices, limit=False, filters=None, folder=DIR_RECIBIDAS):
-        if not invoices and not limit:
-            msg = '\n\tTodos los documentos han sido previamente descargados para el filtro.\n\t{}'.format(str(filters))
-            _logger.info(msg)
-            return {}
-
-        invoices_content = {}
-        if invoices and not self.only_search:
-            invoices_content = self._thread_download(invoices, folder, filters)
-        if limit:
-            sf = self._segment_filter(filters)
-            if folder == self.DIR_RECIBIDAS:
-                data = self._search_recibidas(sf)
-                if data and type(data) == dict:
-                    invoices_content.update(data)
-            else:
-                data = self._search_emitidas(sf)
-                if data and type(data) == dict:
-                    invoices_content.update(data)
-        return invoices_content
-
-    #OBTENCION DE LOS VALORE DE LA PETICION PARA OBTENER LAS URL DE LOS CFDI
-    def _thread_download(self, invoices, folder, filters):
-        for_download = invoices[:]
-        current = 1
-        total = len(for_download)
-        invoice_content = {}
-        for i in range(TRY_COUNT):
-            for uuid, values in for_download:
-                data = {
-                    'url': values['url'],
-                    'acuse': values['acuse'],
-                }
-                content = self._get_xml(uuid, data, current, total)
-                pdf_content = self._get_pdf(uuid, data, current, total)
-                if content:
-                    invoice_content.update({uuid: [values, content, pdf_content]})
-                current += 1
-
-            if len(invoice_content) == len(for_download):
-                break
-        if total:
-            msg = '{} documentos por descargar en: {}'.format(total, str(filters))
-            _logger.info(msg)
-        return invoice_content
-
-    #OBTENER EL DOCUMENTO XML POR MEDIO DE LA URL DE DESCARGAR
-    def _get_xml(self, uuid, values, current, count):
-        for i in range(TRY_COUNT):
-            try:
-                r = self._session.get(values['url'], timeout=TIMEOUT)
-                if r.status_code == 200:
-                    return r.content
-
-            except exceptions.Timeout:
-                _logger.debug('Tiempo de espera sobrepasado')
-                continue
-            except Exception as e:
-                _logger.error(str(e))
-                return
-        msg = 'Tiempo de espera agotado para el documento: {}'.format(uuid)
-        _logger.error(msg)
-        return
-
-        # OBTENER EL DOCUMENTO XML POR MEDIO DE LA URL DE DESCARGAR
-    def _get_pdf(self, uuid, values, current, count):
-        for i in range(TRY_COUNT):
-            try:
-                r = self._session.get(values['acuse'], timeout=TIMEOUT)
-                if r.status_code == 200:
-                    return r.content
-
-            except exceptions.Timeout:
-                _logger.debug('Tiempo de espera sobrepasado')
-                continue
-            except Exception as e:
-                _logger.error(str(e))
-                return
-        msg = 'Tiempo de espera agotado para el documento: {}'.format(uuid)
-        _logger.error(msg)
-        return
-
-    def _get_download_links(self, html):
-        parser = Invoice()
-        parser.feed(html)
-        return parser.not_found, parser.limit, parser.invoices
-
-    def logout(self):
-        msg = 'Cerrando sessión en el SAT'
-        _logger.debug(msg)
-        response = self._response(self.URL_LOGOUT)
-        self.is_connect = False
-        self._session.close()
-        msg = 'Sesión cerrada en el SAT'
-        _logger.info(msg)
-        return

+ 0 - 159
custom_sat_connection/models/res_company.py

@@ -1,159 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from odoo import models, api, fields, _
-from odoo.exceptions import ValidationError, UserError
-import base64
-import time
-import logging
-from datetime import date, datetime
-from dateutil.relativedelta import relativedelta
-from .account_esignature_certificate import convert_key_cer_to_pem
-from .portal_sat import PortalSAT
-
-_logger = logging.getLogger(__name__)
-TRY_COUNT = 3  # INTENTOS DE CONEXION
-
-class ResCompany(models.Model):
-    _inherit = 'res.company'
-
-    x_esignature_ids = fields.Many2many(comodel_name='account.esignature.certificate', string='Certificado FIEL')
-    x_last_cfdi_fetch_date = fields.Datetime("Última sincronización")
-    x_only_supplier_cfdi = fields.Boolean("Solo documentos de proveedor")
-
-    @api.model
-    def auto_import_cfdi_invoices(self):
-        for company in self.search([('x_esignature_ids', '!=', False)]):
-            company.with_company(company.id).download_cfdi_invoices_sat()
-        return True
-
-    # =================================================PORTAL SAT===========================================================
-
-    # DESCARGAR LOS XML DESDE EL SAT
-    def download_cfdi_invoices_sat(self, start_date=False, end_Date=False, document_type=False):
-        esignature_ids = self.x_esignature_ids
-        esignature = esignature_ids.with_user(self.env.user).get_valid_certificate()
-        if not esignature:
-            raise ValidationError("No se encontraron certificados validos, favor de revisar.")
-
-        if not esignature.content or not esignature.key or not esignature.password:
-            raise ValidationError("Seleccine los archivos FIEL .cer o FIEL .pem.")
-
-        fiel_cert_data = base64.b64decode(esignature.content)
-        fiel_pem_data = convert_key_cer_to_pem(esignature.key, esignature.password)
-
-        opt = {'credenciales': None, 'rfc': None, 'uuid': None, 'ano': None, 'mes': None, 'dia': 0,
-               'intervalo_dias': None, 'fecha_inicial': None, 'fecha_final': None, 'tipo': 't',
-               'tipo_complemento': '-1', 'rfc_emisor': None, 'rfc_receptor': None, 'sin_descargar': False,
-               'base_datos': False, 'directorio_fiel': '', 'archivo_uuids': '', 'estatus': False}
-        today = datetime.utcnow()
-        if start_date and end_Date:
-            opt['fecha_inicial'] = datetime.combine(start_date, datetime.min.time())
-            opt['fecha_final'] = datetime.combine(end_Date, datetime.max.time())
-        elif self.x_last_cfdi_fetch_date:
-            last_import_date = self.x_last_cfdi_fetch_date
-            last_import_date - relativedelta(days=2)
-
-            fecha_inicial = last_import_date - relativedelta(days=2)
-            fecha_final = today + relativedelta(days=2)
-            opt['fecha_inicial'] = fecha_inicial
-            opt['fecha_final'] = fecha_final
-        else:
-            year = today.year
-            month = today.month
-            opt['ano'] = year
-            opt['mes'] = month
-
-        sat = False
-        for i in range(TRY_COUNT):
-            sat = PortalSAT(opt['rfc'], 'cfdi-descarga', False)
-            if sat.login_fiel(fiel_cert_data, fiel_pem_data, esignature, self.env.company):
-                time.sleep(1)
-                break
-        invoice_content_receptor, invoice_content_emisor = {}, {}
-        if sat and sat.is_connect:
-            if document_type == "supplier":
-                invoice_content_receptor, invoice_content_emisor = sat.search(opt, 'supplier')
-            elif document_type == "customer":
-                invoice_content_receptor, invoice_content_emisor = sat.search(opt, 'customer')
-            else:
-                invoice_content_receptor, invoice_content_emisor = sat.search(opt)
-            sat.logout()
-        elif sat:
-            sat.logout()
-
-        attachment_data = []
-        if invoice_content_receptor:
-            attachment_data += self.get_cfdi_data(invoice_content_receptor, attachment_data)
-        if invoice_content_emisor:
-            attachment_data += self.get_cfdi_data(invoice_content_emisor, attachment_data)
-        if attachment_data:
-            cfdi_ids = self.env['account.cfdi'].create_cfdis(attachment_data=attachment_data)
-            if cfdi_ids:
-                self.write({'x_last_cfdi_fetch_date': date.today()})
-                return {
-                    "name": _("CFDIs importados"),
-                    "view_mode": "list,form",
-                    "res_model": "account.cfdi",
-                    "type": "ir.actions.act_window",
-                    "target": "current",
-                    "domain": [("id", "in", cfdi_ids.ids)]
-                }
-            else:
-                return {
-                    'type': 'ir.actions.client',
-                    'tag': 'display_notification',
-                    'params': {
-                        'title': _(
-                            "No se cargaron nuevos CFDIs al sistema ya que estos ya existen o no se encontraron, favor de validar."),
-                        'type': 'warning',
-                        'sticky': True,
-                    },
-                }
-        else:
-            return {
-                'type': 'ir.actions.client',
-                'tag': 'display_notification',
-                'params': {
-                    'title': _(
-                        "No se encontraron CFDIs que coincidan con las fechas o estos ya se encuentran en el sistema, favor de validar."),
-                    'type': 'warning',
-                    'sticky': True,
-                },
-            }
-
-    # ======================================================================================================================
-
-    def get_cfdi_data(self, content_sat, attachment_data):
-        uuids = list(content_sat.keys())
-        cfdi_ids = self.env["account.cfdi"].sudo().search([('uuid', 'in', uuids)])
-        exist_uuids = cfdi_ids.mapped('uuid')
-
-        for uuid, data in content_sat.items():
-            if uuid in exist_uuids:
-                continue
-            xml_content = data[1]
-            pdf_content = data[2]
-            filename = uuid + ".xml"
-            filepdf = uuid + ".pdf"
-            data = dict(
-                name=filename,
-                store_fname=filename,
-                type='binary',
-                datas=base64.b64encode(xml_content),
-                company_id=self.id,
-                mimetype='application/xml'
-            )
-            data_pdf = dict(
-                name=filepdf,
-                store_fname=filepdf,
-                type='binary',
-                datas=base64.b64encode(pdf_content) if pdf_content else False,
-                company_id=self.id,
-                mimetype='application/pdf'
-            )
-            data_uuid = {
-                "xml": data,
-                "pdf": data_pdf
-            }
-            attachment_data.append(data_uuid)
-        return attachment_data

+ 0 - 7
custom_sat_connection/models/res_config_settings.py

@@ -1,7 +0,0 @@
-from odoo import api, fields, models
-
-class ResConfigSettings(models.TransientModel):
-    _inherit = "res.config.settings"
-
-    x_esignature_ids = fields.Many2many(related='company_id.x_esignature_ids', string='Certificados México', readonly=False)
-

+ 0 - 12
custom_sat_connection/models/res_partner.py

@@ -1,12 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from odoo import models, fields, api
-
-
-class Respartner(models.Model):
-    _inherit = 'res.partner'
-
-    x_account_expense_id = fields.Many2one(comodel_name='account.account', string='Cuenta contable gasto')
-    x_product_tmpl_id = fields.Many2one(comodel_name='product.template', string='Producto')
-    x_tax_isr_id = fields.Many2one(comodel_name='account.tax', string='Retención ISR')
-    x_tax_iva_id = fields.Many2one(comodel_name='account.tax', string='Retención IVA')

+ 0 - 19
custom_sat_connection/security/ir.model.access.csv

@@ -1,19 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_account_cfdi_user,account_cfdi_user,model_account_cfdi,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_manager,account_cfdi_manager,model_account_cfdi,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_cfdi_line_user,account_cfdi_line_user,model_account_cfdi_line,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_line_manager,account_cfdi_line_manager,model_account_cfdi_line,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_cfdi_tax_user,account_cfdi_tax_user,model_account_cfdi_tax,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_tax_manager,account_cfdi_tax_manager,model_account_cfdi_tax,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_cfdi_zip_user,account_cfdi_zip_user,model_account_cfdi_zip,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_zip_manager,account_cfdi_zip_manager,model_account_cfdi_zip,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_cfdi_xml_user,account_cfdi_xml_user,model_account_cfdi_xml,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_xml_manager,account_cfdi_xml_manager,model_account_cfdi_xml,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_cfdi_account_user,account_cfdi_account_user,model_account_cfdi,account.group_account_user,1,0,0,0
-access_account_cfdi_account_manager,iia_boveda_fiscal_cfdi,model_account_cfdi,account.group_account_manager,1,0,0,0
-access_account_cfdi_account_other_user,iia_boveda_fiscal_cfdi,model_account_cfdi,account.group_account_invoice,1,0,0,0
-access_account_cfdi_sat_user,account_cfdi_sat_user,model_account_cfdi_sat,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_cfdi_sat_manager,account_cfdi_sat_manager,model_account_cfdi_sat,custom_sat_connection.manager_account_cfdi,1,1,1,1
-access_account_esignature_certificate_user,access_account_esignature_certificate_user,model_account_esignature_certificate,custom_sat_connection.user_account_cfdi,1,1,1,0
-access_account_esignature_certificate_manager,account_esignature_certificate_manager,model_account_esignature_certificate,custom_sat_connection.manager_account_cfdi,1,1,1,1
-

+ 0 - 28
custom_sat_connection/security/res_groups.xml

@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data noupdate="1">
-
-        <record id="category_account_cfdi" model="ir.module.category">
-            <field name="name">Complementos SAT</field>
-            <field name="sequence">1000</field>
-        </record>
-
-        <record id="user_type_account_cfdi" model="ir.module.category">
-            <field name="name">Complementos SAT</field>
-            <field name="sequence">10</field>
-            <field name="parent_id" ref="custom_sat_connection.category_account_cfdi"/>
-        </record>
-
-        <record id="user_account_cfdi" model="res.groups">
-            <field name="name">Usuario complementos SAT</field>
-            <field name="category_id" ref="custom_sat_connection.user_type_account_cfdi"/>
-        </record>
-
-        <record id="manager_account_cfdi" model="res.groups">
-            <field name="name">Administrador complementos SAT</field>
-            <field name="category_id" ref="custom_sat_connection.user_type_account_cfdi"/>
-            <field name="implied_ids" eval="[(6, 0, [ref('custom_sat_connection.user_account_cfdi')])]"/>
-        </record>
-
-    </data>
-</odoo>

+ 0 - 25
custom_sat_connection/views/account_account.xml

@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="sat_connection_account_account_form" model="ir.ui.view">
-            <field name="name">sat_connection_account_account_form</field>
-            <field name="model">account.account</field>
-            <field name="inherit_id" ref="account.view_account_form"/>
-            <field name="arch" type="xml">
-
-                <xpath expr="//page[@name='accounting']" position="after">
-                    <page name="SAT" string="SAT">
-                        <group>
-                            <group>
-                                <field name="x_cfdi_type"/>
-                            </group>
-                        </group>
-                    </page>
-                </xpath>
-
-            </field>
-        </record>
-
-    </data>
-</odoo>

+ 0 - 227
custom_sat_connection/views/account_cfdi.xml

@@ -1,227 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="account_cfdi_view_tree" model="ir.ui.view">
-            <field name="name">account_cfdi_view_tree</field>
-            <field name="model">account.cfdi</field>
-            <field name="arch" type="xml">
-                <list string="Complementos CFDI">
-                    <header>
-                        <button name="action_done" string="Procesar" type="object" class="btn btn-primary"/>
-                        <button name="download_massive_xml_zip" string="Descargar XML" type="object"
-                                class="btn btn-secondary"/>
-                        <button name="download_massive_pdf_zip" string="Descargar PDF" type="object"
-                                class="btn btn-secondary"/>
-                    </header>
-                    <field name="name"/>
-                    <field name="date"/>
-                    <field name="emitter_id"/>
-                    <field name="receiver_id"/>
-                    <field name="uuid"/>
-                    <field name="serie"/>
-                    <field name="folio"/>
-                    <field name="move_id"/>
-                    <field name="cfdi_type"/>
-                    <field name="subtotal"/>
-                    <field name="total"/>
-                    <field name="state" widget="badge" decoration-success="state == 'done'"
-                           decoration-muted="state == 'cancel'" decoration-info="state == 'draft'"/>
-                    <field name="sat_state" width="badge" decoration-success="sat_state == 'valid'"
-                           decoration-danger="sat_state == 'cancelled'" decoration-warning="sat_state == 'not_found'"
-                           decoration-info="sat_state == 'none'" decoration-bf="sat_state == 'undefined'"/>
-                </list>
-            </field>
-        </record>
-
-        <record id="account_cfdi_view_form" model="ir.ui.view">
-            <field name="name">account_cfdi_view_form</field>
-            <field name="model">account.cfdi</field>
-            <field name="arch" type="xml">
-                <form string="Complementos CFDI">
-                    <header>
-                        <button name="action_done" default_focus="1" string="Procesar" icon="fa-gear" type="object"
-                                class="btn btn-primary" invisible="cfdi_type not in ('SI','I','SE') or (cfdi_type in ('SI','I','SE') and state != 'draft')"
-                        />
-                        <button name="action_unlink_move" default_focus="1" string="Desvincular" type="object"
-                                class="btn btn-secondary" invisible="cfdi_type not in ('SI','I','SE') or (cfdi_type in ('SI','I','SE') and state != 'done')"
-                        />
-                        <field name="state" widget="statusbar"/>
-                    </header>
-                    <sheet>
-                        <div>
-                            <h1>
-                                <field name="name" readonly="1"/>
-                            </h1>
-                        </div>
-                        <group col="3">
-                            <group string="Complemento">
-                                <field name="uuid" readonly="1"/>
-                                <field name="cfdi_type" readonly="1"/>
-                                <field name="attachment_id" readonly="1"/>
-                                <field name="pdf_id" readonly="1"/>
-                                <field name="date" readonly="1"/>
-                                <field name="emitter_id" options="{'no_create': True, 'no_create_edit':True}"
-                                       readonly="1"/>
-                                <field name="receiver_id" options="{'no_create': True, 'no_create_edit':True}"
-                                       readonly="1"/>
-                                <field name="version" readonly="1"/>
-                                <field name="serie" readonly="1"/>
-                                <field name="folio" readonly="1"/>
-                            </group>
-                            <group string="Detalle">
-                                <field name="payment_method" readonly="1"/>
-                                <field name="certificate_number" readonly="1"/>
-                                <field name="payment_condition" readonly="1"/>
-                                <field name="subtotal" readonly="1"/>
-                                <field name="tax_total"/>                                
-                                <field name="total" readonly="1"/>  
-                                <field name="currency" readonly="1"/>
-                                <field name="cfdi_type" readonly="1"/>
-                                <field name="payment_type" readonly="1"/>
-                                <field name="location" readonly="1"/>
-                                <field name="move_id" readonly="1"/>
-                                <field name="company_id" readonly="1"/>
-                            </group>
-                            <group string="Contable">
-                                <field name="journal_id" options="{'no_create':True}"
-                                       domain="[('type','in',('purchase','sale'))]"/>
-                                <field name="payable_account_id" options="{'no_create':True}"/>
-                                <field name="account_id" options="{'no_create':True}"/>
-                                <field name="account_analytic_account_id" invisible="1"/>
-                                <field string="Distribución analítica" name="analytic_distribution"
-                                       widget="analytic_distribution"
-                                       groups="analytic.group_analytic_accounting"
-                                       optional="show"
-                                       options="{'product_field': 'product_tmpl_id', 'account_field': 'account_id', 'force_applicability': 'optional'}"
-                                />
-                                <field name="fiscal_position_id" options="{'no_create':True}"/>
-                                <field name="tax_iva_id" options="{'no_create':True}"/>
-                                <field name="tax_isr_id" options="{'no_create':True}"/>
-                            </group>
-                        </group>
-                        <notebook>
-                            <page string="Conceptos/Impuestos">
-                                <group string="Conceptos">
-                                    <field name="concept_ids" colspan="2" nolabel="1">
-                                        <list string="Conceptos" create="false" delete="false" editable="top">
-                                            <field name="description"/>
-                                            <field name="no_identification" optional="hide"/>
-                                            <field name="product_tmpl_id" string="Producto"
-                                                   options="{'no_create':True}"/>
-                                            <field name="account_id" options="{'no_create':True}"/>
-                                            <field name="product_code"/>
-                                            <field invisible="1" name="account_analytic_account_id"/>
-                                            <field string="Distribución analítica" name="analytic_distribution"
-                                                   widget="analytic_distribution"
-                                                   groups="analytic.group_analytic_accounting"
-                                                   optional="show"
-                                                   options="{'product_field': 'product_tmpl_id', 'account_field': 'account_id', 'force_applicability': 'optional'}"
-                                            />
-                                            <field name="quantity" sum="quantity"/>
-                                            <field name="uom_code"/>
-                                            <field name="unit_price" sum="unit_price"/>
-                                            <field name="discount" sum="Total"/>
-                                            <field name="amount" sum="amount"/>
-                                        </list>
-                                    </field>
-                                </group>
-                                <group string="Impuestos">
-                                    <field name="tax_ids" colspan="2" nolabel="1">
-                                        <list string="Impuestos" editable="bottom" create="0" delete="0">
-                                            <field name="base" readonly="1"/>
-                                            <field name="code" readonly="1"/>
-                                            <field name="factor_type" readonly="1"/>
-                                            <field name="rate" readonly="1"/>
-                                            <field name="amount" sum="Total" readonly="1"/>
-                                            <field name="tax_id" options="{'no_create':True}"/>
-                                        </list>
-                                    </field>
-                                </group>
-                            </page>
-                            <page string="Addenda" name="addenda_page" invisible="1">
-                                <group name="addenda_group">
-                                    <field name="delivery_number"/>
-                                    <field name="invoice_qty"/>
-                                </group>
-                            </page>
-                        </notebook>
-                    </sheet>
-                    <chatter/>
-                </form>
-            </field>
-        </record>
-
-        <record id="account_cfdi_view_search" model="ir.ui.view">
-            <field name="name">account_cfdi_view_search</field>
-            <field name="model">account.cfdi</field>
-            <field name="arch" type="xml">
-                <search>
-                    <field name="company_id" groups="base.group_multi_company" optional="show"/>
-                    <field name="uuid"/>
-                    <field name="serie"/>
-                    <field name="folio"/>
-                    <field name="cfdi_type"/>
-                    <field name="date"/>
-                    <field name="emitter_id"/>
-                    <field name="receiver_id"/>
-                    <field name="move_id"/>
-                    <field name="journal_id"/>
-                    <filter name="facturas_clientes" string="Facturas de clientes"
-                            domain="[('cfdi_type','=','I')]"/>
-                    <filter name="facturas_proveedor" string="Facturas de proveedor"
-                            domain="[('cfdi_type','=','SI')]"/>
-                    <filter name="notas_credito_clientes" string="Notas de crédito de clientes"
-                            domain="[('cfdi_type','=','E')]"/>
-                    <filter name="notas_credito_proveedor" string="Notas de crédito de proveedor"
-                            domain="[('cfdi_type','=','SE')]"/>
-                    <filter name="rep_clientes" string="REP de clientes" domain="[('cfdi_type','=','P')]"/>
-                    <filter name="rep_proveedor" string="REP de proveedor" domain="[('cfdi_type','=','SP')]"/>
-                    <filter name="nomina_empleados" string="Nómina de empleados"
-                            domain="[('cfdi_type','=','N')]"/>
-                    <filter name="nomina_propia" string="Nómina propia" domain="[('cfdi_type','=','SN')]"/>
-                    <filter name="facturas_traslado_clientes" string="Facturas de traslado de clientes"
-                            domain="[('cfdi_type','=','T')]"/>
-                    <filter name="facturas_traslado_proveedor" string="Facturas de traslado de proveedor"
-                            domain="[('cfdi_type','=','ST')]"/>
-                    <separator orientation="vertical"/>
-                    <filter name="state_draft" string="Por procesar" domain="[('state','=','draft')]"/>
-                    <filter name="state_done" string="Procesados" domain="[('state','=','done')]"/>
-                    <filter name="state_cancel" string="Anulados" domain="[('state','=','cancel')]"/>
-                    <separator orientation="vertical"/>
-                    <group expand="0" string="Agrupar por">
-                        <filter name="groupby_company" string="Empresa" domain="" context="{'group_by':'company_id'}"/>
-                        <separator orientation="vertical"/>
-                        <filter name="groupby_tipo_de_comprobante" string="Tipo de comprobante"
-                                context="{'group_by':'cfdi_type'}"/>
-                        <separator orientation="vertical"/>
-                        <filter name="groupby_fecha" string="Fecha" domain="" context="{'group_by':'date'}"/>
-                        <separator orientation="vertical"/>
-                        <filter name="groupby_emisor" string="Emisor" domain=""
-                                context="{'group_by':'emitter_id'}"/>
-                        <filter name="groupby_receptor" string="Receptor" context="{'group_by':'receiver_id'}"/>
-                        <separator orientation="vertical"/>
-                        <filter name="groupby_version" string="Versión" domain="" context="{'group_by':'version'}"/>
-                        <filter name="groupby_metodo_pago" string="Método de pago"
-                                context="{'group_by':'payment_method'}"/>
-                        <filter name="groupby_moneda" string="Moneda" domain="" context="{'group_by':'currency'}"/>
-                        <separator orientation="vertical"/>
-                    </group>
-                </search>
-            </field>
-        </record>
-
-        <record id="account_cfdi_action" model="ir.actions.act_window">
-            <field name="name">Complementos CFDI</field>
-            <field name="type">ir.actions.act_window</field>
-            <field name="res_model">account.cfdi</field>
-            <field name="view_mode">list,form</field>
-        </record>
-
-        <menuitem id="account_cfdi_action_parent_menu" parent="accountant.menu_accounting" sequence="10"
-                  name="SAT"/>
-        <menuitem action="account_cfdi_action" id="menu_action_boveda_fiscal_cfdi"
-                  parent="custom_sat_connection.account_cfdi_action_parent_menu" sequence="100"
-                  name="Complementos CFDI"/>
-    </data>
-</odoo>

+ 0 - 61
custom_sat_connection/views/account_esignature_certificate.xml

@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<openerp>
-    <data>
-
-        <record id="sat_connection_account_esignature_certificate_tree" model="ir.ui.view">
-            <field name="name">sat_connection_account_esignature_certificate_tree</field>
-            <field name="model">account.esignature.certificate</field>
-            <field name="arch" type="xml">
-                <list string="Certificados">
-                    <field name="holder" string="Titular"/>
-                    <field name="holder_vat" string="RFC"/>
-                    <field name="serial_number" string="Número serial"/>
-                    <field name="date_start" string="Fecha de inicio"/>
-                    <field name="date_end" string="Fecha final"/>
-                </list>
-            </field>
-        </record>
-
-        <record id="sat_connection_account_esignature_certificate_form" model="ir.ui.view">
-            <field name="name">sat_connection_account_esignature_certificate_form</field>
-            <field name="model">account.esignature.certificate</field>
-            <field name="arch" type="xml">
-                <form string="Certificados">
-                    <sheet>
-                        <group>
-                            <field name="content" string="Certificado"/>
-                            <field name="key" string="Clave de certificado"/>
-                            <field name="password" password="True" string="Contraseña"/>
-                            <label for="date_start" string="Fecha de validación"/>
-                            <div>
-                                <field name="date_start" string="Fecha de inicio"/> -
-                                <field name="date_end" string="Fecha final"/>
-                            </div>
-                            <field name="serial_number" string="Número serial"/>
-                        </group>
-                    </sheet>
-                </form>
-            </field>
-        </record>
-
-        <record id="sat_connection_account_esignature_certificate_search" model="ir.ui.view">
-            <field name="name">sat_connection_account_esignature_certificate_search</field>
-            <field name="model">account.esignature.certificate</field>
-            <field name="arch" type="xml">
-                <search>
-                    <field name="holder" string="Titular"/>
-                    <field name="holder_vat" string="RFC"/>
-                </search>
-            </field>
-        </record>
-
-        <record id="sat_connection_account_esignature_certificate_action" model="ir.actions.act_window">
-            <field name="name">sat_connection_account_esignature_certificate_action</field>
-            <field name="res_model">account.esignature.certificate</field>
-            <field name="view_mode">tree,form</field>
-            <field name="help" type="html">
-                <p class="o_view_nocontent_smiling_face">Crear el primer certificado</p>
-            </field>
-        </record>
-    </data>
-</openerp>

+ 0 - 23
custom_sat_connection/views/account_journal.xml

@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="sat_connection_account_journal_form" model="ir.ui.view">
-            <field name="name">sat_connection_account_journal_form</field>
-            <field name="model">account.journal</field>
-            <field name="inherit_id" ref="account.view_account_journal_form"/>
-            <field name="arch" type="xml">
-
-                <xpath expr="//notebook/page[@name='advanced_settings']" position="after">
-                    <page name="SAT" string="SAT">
-                        <group>
-                            <field name="x_cfdi_type" string="Tipo de comprobante"/>
-                        </group>
-                    </page>
-                </xpath>
-
-            </field>
-        </record>
-
-    </data>
-</odoo>

+ 0 - 33
custom_sat_connection/views/account_move.xml

@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-        <record id="sat_connection_account_move_form" model="ir.ui.view">
-            <field name="name">sat_connection_account_move_form</field>
-            <field name="model">account.move</field>
-            <field name="inherit_id" ref="account.view_move_form"/>
-            <field name="arch" type="xml">
-
-                <xpath expr="//field[@name='ref']" position="after">
-                    <field name="cfdi_id" invisible="1"/>
-                    <field name="x_uuid" readonly="1"/>
-                </xpath>
-
-                <xpath expr="//notebook" position="inside">
-                    <page name="cfdi_data" string="CFDI información" invisible="1">
-                        <group>
-                            <group>
-                                <field name="x_delivery_number"/>
-                                <field name="x_invoice_qty"/>
-                            </group>
-                            <group>
-                            </group>
-                        </group>
-                    </page>
-                </xpath>
-
-            </field>
-        </record>
-
-
-    </data>
-</odoo>

+ 0 - 21
custom_sat_connection/views/ir_attachment.xml

@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="sat_connection_ir_attachment_list" model="ir.ui.view">
-            <field name="name">sat_connection_ir_attachment_list</field>
-            <field name="model">ir.attachment</field>
-            <field name="inherit_id" ref="data_cleaning.view_data_storage_attachment_tree"/>
-            <field name="arch" type="xml">
-
-                <xpath expr="//field[@name='name']" position="before">
-                    <header>
-                        <button name="download_massive_zip" string="Descargar zip" class="btn btn-primary" type="object"/>
-                    </header>
-                </xpath>
-
-            </field>
-        </record>
-
-    </data>
-</odoo>

+ 0 - 36
custom_sat_connection/views/res_config_settings.xml

@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <record id="sat_connection_res_config_settings_form" model="ir.ui.view">
-        <field name="name">sat_connection_res_config_settings_form</field>
-        <field name="model">res.config.settings</field>
-        <field name="inherit_id" ref="account.res_config_settings_view_form"/>
-        <field name="arch" type="xml">
-            <xpath expr="//block[@id='quick_edit_mode']" position="after">
-                <h2>Importación SAT</h2>
-                <div class="row mt16 o_settings_container" id="settings_sat_sync_configuration">
-                    <div class="col-12 col-lg-12 o_setting_box" title="Parametro para configurar el certificado de México.">
-                        <div class="o_setting_left_pane"/>
-                        <div class="o_setting_right_pane">
-                            <span class="o_form_label">Certificados MX</span>
-                            <div class="text-muted">
-                                Configuración de certificados fiscales.
-                            </div>
-                            <div class="content-group">
-                                <div class="row mt16">
-                                    <field name="x_esignature_ids">
-                                        <list>
-                                            <field name="date_start"/>
-                                            <field name="date_end"/>
-                                            <field name="holder"/>
-                                        </list>
-                                    </field>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </xpath>
-
-        </field>
-    </record>
-</odoo>

+ 0 - 3
custom_sat_connection/wizards/__init__.py

@@ -1,3 +0,0 @@
-from . import account_cfdi_sat
-from . import account_cfdi_zip
-from . import account_cfdi_xml

+ 0 - 10
custom_sat_connection/wizards/account_cfdi_link.py

@@ -1,10 +0,0 @@
-from odoo import api, fields, models
-
-class AccountCfdiLink(models.TransientModel):
-    _name = 'account.cfdi.link'
-    _description = 'Vinculación CFDI y asiento contable'
-    _rec_name = "cfdi_id"
-
-    cfdi_id = fields.Many2one(comodel_name="account.cfdi", string="CFDI")
-
-

+ 0 - 19
custom_sat_connection/wizards/account_cfdi_sat.py

@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo import models, fields, api
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class ImportXML(models.TransientModel):
-    _name = 'account.cfdi.sat'
-    _description = 'Importación CFDI desde el SAT'
-
-    date_from = fields.Date(string='Desde', default=fields.Date.today())
-    date_to = fields.Date(string='Hasta', default=fields.Date.today())
-    type = fields.Selection(selection=[('0', 'Todo'), ('1', 'Emitidas'), ('2', 'Recibidas')], string='Tipo', default='0')
-    company_id = fields.Many2one(comodel_name='res.company', string='Empresa', default=lambda self: self.env.company, readonly=True)
-
-    def import_sat(self):
-        response = self.company_id.download_cfdi_invoices_sat(self.date_from, self.date_to, "supplier")
-        return response

+ 0 - 44
custom_sat_connection/wizards/account_cfdi_sat.xml

@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-        <record id="account_cfdi_sat_view_form" model="ir.ui.view">
-            <field name="name">account_cfdi_sat_view_form</field>
-            <field name="model">account.cfdi.sat</field>
-            <field name="arch" type="xml">
-                <form string="Importación SAT">
-                    <sheet>
-                        <group>
-                            <group>
-                                <label for="date_from" string="Fechas"/>
-                                <div class="o_row">
-                                    <field name="date_from"/><span><![CDATA[&nbsp;]]>a<![CDATA[&nbsp;]]></span><field
-                                        name="date_to"/>
-                                </div>
-                            </group>
-                            <group>
-                                <field name="company_id"/>
-                            </group>
-                        </group>
-                    </sheet>
-                    <footer>
-                        <button name="import_sat" string="Importar" type="object" default_focus="1"
-                                class="btn btn-primary"/>
-                        <button string="Cancelar" class="btn btn-secondary" special="cancel"/>
-                    </footer>
-                </form>
-            </field>
-        </record>
-
-        <record id="account_cfdi_sat_action" model="ir.actions.act_window">
-            <field name="name">Importación SAT</field>
-            <field name="res_model">account.cfdi.sat</field>
-            <field name="type">ir.actions.act_window</field>
-            <field name="view_mode">form</field>
-            <field name="target">new</field>
-        </record>
-
-        <menuitem action="account_cfdi_sat_action" id="menu_import_sat" parent="custom_sat_connection.account_cfdi_action_parent_menu" sequence="30"/>
-
-    </data>
-</odoo>

+ 0 - 56
custom_sat_connection/wizards/account_cfdi_xml.py

@@ -1,56 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo import _, api, fields, models
-from odoo.exceptions import UserError, ValidationError
-import logging
-_logger = logging.getLogger(__name__)
-
-class AccountCfdiXml(models.TransientModel):
-    _name = 'account.cfdi.xml'
-    _description = 'Importación por XML'
-
-    company_id = fields.Many2one(comodel_name='res.company', string='Compañía', default=lambda self: self.env.company, readonly=True)
-    filedata_file = fields.Many2many(comodel_name='ir.attachment', string='Archivos XML')
-    filedata_name = fields.Char(string="Nombre de archivo")
-
-    def import_file(self):
-        if len(self.filedata_file) > 0:
-            attachment_list = []
-            for content in self.filedata_file:
-                try:
-                    attachment_data = {
-                        'name': content.name,
-                        'type': 'binary',
-                        'company_id': self.company_id.id,
-                        'datas': content.datas,
-                        'store_fname': content.name,
-                        'mimetype': 'application/xml'
-                    }
-                    data_uuid = {
-                        "xml": attachment_data,
-                    }
-                    attachment_list.append(data_uuid)
-                except Exception as e:
-                    _logger.info(e)
-            if attachment_list:
-                cfdi_ids = self.env['account.cfdi'].create_cfdis(attachment_list)
-                if cfdi_ids:
-                    return {
-                        "name": _("CFDIs importados"),
-                        "view_mode": "list,form",
-                        "res_model": "account.cfdi",
-                        "type": "ir.actions.act_window",
-                        "target": "current",
-                        "domain": [("id", "=", cfdi_ids.ids)]
-                    }
-                else:
-                    return {
-                        'type': 'ir.actions.client',
-                        'tag': 'display_notification',
-                        'params': {
-                            'title': _("No se cargaron nuevos CFDIs al sistema ya que estos ya existen o no pertenecen a la empresa, favor de validar."),
-                            'type': 'warning',
-                            'sticky': True,
-                        },
-                    }
-        else:
-            raise ValidationError(_('No ha subido ningún archivo XML'))

+ 0 - 30
custom_sat_connection/wizards/account_cfdi_xml.xml

@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<odoo>
-    <record id="sat_connection_account_cfdi_xml_form" model="ir.ui.view">
-        <field name="name">sat_connection_account_cfdi_xml_form</field>
-        <field name="model">account.cfdi.xml</field>
-        <field name="arch" type="xml">
-            <form class="form-xml">
-                <sheet>
-                    <field name="filedata_file"  widget="many2many_binary" required="1" placeholder="Seleccione los xml a subir..."/>
-                    <field name="filedata_name" invisible="1"/>
-                </sheet>
-                <footer>
-                    <button name="import_file" string="Cargar" type="object" class="btn-primary"/>
-                    <button string="Cerrar" class="btn-secondary" special="cancel"/>
-                </footer>
-            </form>
-        </field>
-    </record>
-
-    <record id="sat_connection_account_cfdi_xml_action" model="ir.actions.act_window">
-        <field name="name">Importación por XML</field>
-        <field name="type">ir.actions.act_window</field>
-        <field name='res_model'>account.cfdi.xml</field>
-        <field name="view_mode">form</field>
-        <field name="context">{'l10n_mx_edi_invoice_type': 'in'}</field>
-        <field name="target">new</field>
-    </record>
-
-    <menuitem action="sat_connection_account_cfdi_xml_action" id="sat_connection_account_cfdi_xml_action_menu" name="Importación por XML" parent="custom_sat_connection.account_cfdi_action_parent_menu" sequence="50"/>
-</odoo>

+ 0 - 76
custom_sat_connection/wizards/account_cfdi_zip.py

@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-from odoo import models, fields, api
-from odoo.exceptions import RedirectWarning, ValidationError
-from zipfile import ZipFile
-
-import base64
-import tempfile
-import os
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class AccountCfdiZip(models.TransientModel):
-    _name = 'account.cfdi.zip'
-    _description = 'Importación con archivo ZIP'
-
-    file = fields.Binary(string='Archivo', required=True)
-    file_name = fields.Char(string='Nombre del archivo', required=True)
-    company_id = fields.Many2one(comodel_name='res.company', string='Empresa', default=lambda self: self.env.company, readonly=True)
-    result = fields.Char(string='Resultado')
-    state = fields.Selection(selection=[('draft', 'Seleccionar'), ('done', 'Importado'), ], string='Estado', default='draft')
-
-    def import_zip(self):
-        count_xml = 0
-        if self.file:
-            zip_file_id = self.env['ir.attachment'].create({
-                'name': self.file_name,
-                'type': 'binary',
-                'company_id': self.company_id.id,
-                'datas': self.file,
-                'store_fname': self.file_name,
-                'mimetype': 'application/zip'
-            })
-            fd, path = tempfile.mkstemp()
-            with os.fdopen(fd, 'wb') as tmp:
-                tmp.write(base64.b64decode(zip_file_id.datas))
-
-            try:
-                with ZipFile(path, 'r') as zip:
-                    attachment_list = []
-                    for filename in zip.namelist():
-                        with zip.open(filename) as file:
-                            xml_content = file.read()
-                            attachment_data = {
-                                'name': filename,
-                                'type': 'binary',
-                                'company_id': self.company_id.id,
-                                'datas': base64.b64encode(xml_content),
-                                'store_fname': filename,
-                                'mimetype': 'application/xml'
-                            }
-                            data_uuid = {
-                                "xml": attachment_data,
-                            }
-                            attachment_list.append(data_uuid)
-                    if attachment_list:
-                        cfdi_ids = self.env['account.cfdi'].create_cfdis(attachment_list)
-                        count_xml = len(cfdi_ids)
-            except Exception as e:
-                raise ValidationError(e)
-
-        self.write({
-            'result': "Archivos XML procesados correctamente: " + str(count_xml),
-            'state': 'done',
-        })
-
-        return {
-            'type': 'ir.actions.act_window',
-            'res_model': 'account.cfdi.zip',
-            'view_mode': 'form',
-            'view_type': 'form',
-            'res_id': self.id,
-            'views': [(False, 'form')],
-            'target': 'new',
-        }

+ 0 - 38
custom_sat_connection/wizards/account_cfdi_zip.xml

@@ -1,38 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <record id="sat_connection_account_cfdi_zip_form" model="ir.ui.view">
-        <field name="name">sat_connection_account_cfdi_zip_form</field>
-        <field name="model">account.cfdi.zip</field>
-        <field name="arch" type="xml">
-            <form string="Importar archivo ZIP">
-                <sheet>
-                    <field name="state" invisible="1"/>
-                    <group name="datos" string="Subir archivo" >
-                        <field name="file" filename="file_name"/>
-                        <field name="file_name" invisible="1"/>
-                        <field name="company_id"/>
-                    </group>
-                    <group>
-                        <separator string="Importación finalizada" colspan="4"/>
-                    </group>
-                    <field name="result" readonly="1" nolabel="1"/>
-                </sheet>
-                <footer>
-                    <button name="import_zip" string="Importar" type="object" default_focus="1" class="oe_highlight"/>
-                    <button string="Cerrar" class="oe_link" special="cancel"/>
-                </footer>
-            </form>
-        </field>
-    </record>
-
-    <record id="sat_connection_account_cfdi_zip_action" model="ir.actions.act_window">
-        <field name="name">Importación ZIP</field>
-        <field name="res_model">account.cfdi.zip</field>
-        <field name="type">ir.actions.act_window</field>
-        <field name="view_mode">form</field>
-        <field name="target">new</field>
-    </record>
-
-    <menuitem action="sat_connection_account_cfdi_zip_action" id="menu_import_zip" parent="custom_sat_connection.account_cfdi_action_parent_menu" sequence="40"/>
-
-</odoo>

+ 0 - 3
custom_supplier_cfdi_data/__init__.py

@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import models

+ 0 - 19
custom_supplier_cfdi_data/__manifest__.py

@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-{
-    'name': "Cambio de emisor CFDI",
-    'summary': """
-        Cambio del contacto de donde se obtiene la información del emisor para crear la factura.
-    """,
-    'description': """
-        Cambio del contacto de donde se obtiene la información del emisor para crear la factura de cliente, agregando
-        un campo nuevo en el catálogo de empresas.
-    """,
-    'author': "M22",
-    'website': "http://www.m22.mx",
-    'category': 'Contabilidad',
-    'version': '18.1',
-    'depends': ['base','l10n_mx_edi'],
-    'data': ['views/res_company.xml'],
-    'license': 'AGPL-3' 
-
-}

BIN
custom_supplier_cfdi_data/__pycache__/__init__.cpython-37.pyc


+ 0 - 4
custom_supplier_cfdi_data/models/__init__.py

@@ -1,4 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from . import res_company
-from . import account_edi_format

BIN
custom_supplier_cfdi_data/models/__pycache__/__init__.cpython-37.pyc


BIN
custom_supplier_cfdi_data/models/__pycache__/account_edi_format.cpython-37.pyc


BIN
custom_supplier_cfdi_data/models/__pycache__/res_company.cpython-37.pyc


+ 0 - 17
custom_supplier_cfdi_data/models/account_edi_format.py

@@ -1,17 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from odoo import models, fields, api
-
-class AccountEdiFormat(models.Model):
-    _inherit = 'l10n_mx_edi.document'
-
-    @api.model
-    def _add_certificate_cfdi_values(self, cfdi_values):
-        res = super(AccountEdiFormat, self)._add_certificate_cfdi_values(cfdi_values)
-        root_company = cfdi_values['root_company']
-        if root_company.x_commercial_partner_id and cfdi_values.get("emisor"):
-            supplier = root_company.x_commercial_partner_id.with_user(self.env.user)
-            cfdi_values["emisor"]["supplier"] = supplier
-            cfdi_values["emisor"]["rfc"] = supplier.vat
-            cfdi_values["emisor"]["nombre"] = self._cfdi_sanitize_to_legal_name(supplier.name)
-            cfdi_values["emisor"]["supplier"] = supplier.zip

+ 0 - 6
custom_supplier_cfdi_data/models/res_company.py

@@ -1,6 +0,0 @@
-from odoo import api, fields, models
-
-class ResCompany(models.Model):
-    _inherit = 'res.company'
-
-    x_commercial_partner_id = fields.Many2one(comodel_name="res.partner", string="Entidad")

+ 0 - 19
custom_supplier_cfdi_data/views/res_company.xml

@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<odoo>
-    <data>
-
-       <record id="custom_supplier_cfdi_data_res_company_form_view" model="ir.ui.view">
-           <field name="name">custom_supplier_cfdi_data_res_company_form_view</field>
-           <field name="model">res.company</field>
-           <field name="inherit_id" ref="base.view_company_form"/>
-           <field name="arch" type="xml">
-
-               <xpath expr="//field[@name='company_registry']" position="after">
-                   <field name="x_commercial_partner_id" options="{'no_create':True}"/>
-               </xpath>
-
-           </field>
-       </record>
-
-    </data>
-</odoo>

+ 0 - 1
cutom_report_invoice/__init__.py

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

+ 0 - 18
cutom_report_invoice/__manifest__.py

@@ -1,18 +0,0 @@
-{
-    "name": "Custom Report Invoice",
-    'description': """
-        Custom Report Invoice M22      
-    """,
-    "version": "17.1",
-    "category": "Partner",
-    "author": "M22",
-    'website': "https://www.m22.mx",
-    "license": "AGPL-3",
-    "depends": ["account", "l10n_mx_edi"],
-    "data": [
-        "views/report_invoice_view.xml",
-        "views/account_move_views.xml",
-        "report/report_invoice.xml",
-        ],
-    "installable": True,
-}

BIN
cutom_report_invoice/__pycache__/__init__.cpython-310.pyc


BIN
cutom_report_invoice/i18n/es_MX.mo


+ 0 - 296
cutom_report_invoice/i18n/es_MX.po

@@ -1,296 +0,0 @@
-# Translation of Odoo Server.
-# This file contains the translation of the following modules:
-# 	* cutom_report_invoice
-#
-msgid ""
-msgstr ""
-"Project-Id-Version: Odoo Server 16.0+e\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-09-26 03:08+0000\n"
-"PO-Revision-Date: 2025-04-25 21:56-0600\n"
-"Last-Translator: \n"
-"Language-Team: \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"
-"X-Generator: Poedit 3.5\n"
-
-#. module: cutom_report_invoice
-#: model:ir.actions.report,print_report_name:cutom_report_invoice.custom_report_invoice
-msgid "(object._get_report_base_filename())"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid ""
-"<span groups=\"account.group_show_line_subtotals_tax_excluded\">Amount</span>\n"
-"                                            <span groups=\"account.group_show_line_subtotals_tax_included\">Total Price</span>"
-msgstr ""
-"<span groups=\"account.group_show_line_subtotals_tax_excluded\">Importe</span>\n"
-"                                            <span groups=\"account.group_show_line_subtotals_tax_included\">Total</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> - </span>"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | Certification Date:</span>"
-msgstr "<span> | Fecha de Certificación:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | Emission Date:</span>"
-msgstr "<span> | Fecha de Emisión:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | Expedition place:</span>"
-msgstr "<span> | Lugar de expedición:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | Fiscal Folio:</span>"
-msgstr "<span> | Folio Fiscal:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | Fiscal Regime:</span>"
-msgstr "<span> | Régimen Fiscal:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span> | SAT Certificate:</span>"
-msgstr "<span> | Certificado SAT:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Description</span>"
-msgstr "<span>DESCRIPCIÓN</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Digital stamp SAT</span>"
-msgstr "<span>Sello digital de SAT</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Digital stamp of the emitter</span>"
-msgstr "<span>Sello digital del emisor</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Disc.%</span>"
-msgstr "<span>Desc.%</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Emitter certificate:</span>"
-msgstr "<span>Certificado del emisor:</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Extra Info</span>"
-msgstr "<span>Información Extra</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Original chain complement of digital certification SAT</span>"
-msgstr "<span>Cadena original del complemento del certificado digital del SAT</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Product code</span>"
-msgstr "<span>CÓDIGO PRODUCTO</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Taxes</span>"
-msgstr "<span>IMPUESTOS</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<span>Unit code</span>"
-msgstr "<span>CÓDIGO UNIDAD</span>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong class=\"mr16\">Subtotal</strong>"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong class=\"text-center\">Scan me with your banking app.</strong><br/><br/>"
-msgstr "<strong class=\"text-center\">Escanéame con tu aplicación bancaria.</strong><br/><br/>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>A signature of this invoice is required, but it is not signed.</strong>"
-msgstr "<strong>Se requiere una firma de esta factura, pero no está firmada.</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Bank Account:</strong>"
-msgstr "<strong>Cuenta Bancaria:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Credit Note Date:</strong>"
-msgstr "<strong>Fecha de la nota de crédito:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_rate_report_invoice_document
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Currency:</strong>"
-msgstr "<strong>Divisa:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Customer Code:</strong>"
-msgstr "<strong>Código de cliente:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Date:</strong>"
-msgstr "<strong>Fecha:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Due Date:</strong>"
-msgstr "<strong>Fecha de vencimiento:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_rate_report_invoice_document
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Exchange rate:</strong>"
-msgstr "<strong>Tipo de cambio:</strong"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Incoterm: </strong>"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Invoice Date:</strong>"
-msgstr "<strong>Fecha de factura:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Payment Method:</strong>"
-msgstr "<strong>Método de Pago:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Payment Way:</strong>"
-msgstr "<strong>Forma de Pago:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Receipt Date:</strong>"
-msgstr "<strong>Fecha de recepción:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Reference:</strong>"
-msgstr "<strong>Referencia:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Source:</strong>"
-msgstr "<strong>Origen:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>This document is a printed representation of a CFDI</strong>"
-msgstr "<strong>Este documento es una representación impresa de un CFDI</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "<strong>Usage:</strong>"
-msgstr "<strong>Uso:</strong>"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Amount Due"
-msgstr "Monto adeudado"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Barcode"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Cancelled Invoice"
-msgstr "Factura cancelada"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Credit Note"
-msgstr "Nota de crédito"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Discount"
-msgstr "Descuento"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Draft Invoice"
-msgstr "Factura borrador"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Due Date"
-msgstr "Fecha de vencimiento:"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Invoice"
-msgstr "Factura"
-
-#. module: cutom_report_invoice
-#: model:ir.actions.report,name:cutom_report_invoice.custom_report_invoice
-msgid "Invoice Implementation"
-msgstr "Factura Implementación"
-
-#. module: cutom_report_invoice
-#: model:ir.model,name:cutom_report_invoice.model_account_move
-msgid "Journal Entry"
-msgstr "Asiento contable"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Paid on"
-msgstr "Pagado en"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Please use the following communication for your payment :"
-msgstr "Utilice la siguiente referencia al realizar su pago:"
-
-#. module: cutom_report_invoice
-#: model:ir.model.fields,field_description:cutom_report_invoice.field_account_bank_statement_line__rate
-#: model:ir.model.fields,field_description:cutom_report_invoice.field_account_move__rate
-#: model:ir.model.fields,field_description:cutom_report_invoice.field_account_payment__rate
-msgid "Rate"
-msgstr ""
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Vendor Bill"
-msgstr "Factura de proveedor"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "Vendor Credit Note"
-msgstr "Nota de crédito del proveedor"
-
-#. module: cutom_report_invoice
-#: model_terms:ir.ui.view,arch_db:cutom_report_invoice.custom_report_invoice_document
-msgid "if paid before"
-msgstr "si se paga antes"

+ 0 - 1
cutom_report_invoice/models/__init__.py

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

BIN
cutom_report_invoice/models/__pycache__/__init__.cpython-310.pyc


BIN
cutom_report_invoice/models/__pycache__/account_move.cpython-310.pyc


+ 0 - 17
cutom_report_invoice/models/account_move.py

@@ -1,17 +0,0 @@
-
-from odoo import models, fields
-
-
-class AccountMove(models.Model):
-    _inherit = "account.move"
-
-    rate = fields.Float(compute="_get_last_exchannge_rate")
-
-    def _get_last_exchannge_rate(self):
-        if self.currency_id.rate_ids:
-            rate = self.currency_id.rate_ids[0]
-            if rate:
-                self.rate = rate.inverse_company_rate
-        else:
-            self.rate = 0
-                

+ 0 - 13
cutom_report_invoice/report/report_invoice.xml

@@ -1,13 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<odoo>
-    <record id="custom_report_invoice" model="ir.actions.report">
-        <field name="name">Invoice Implementation</field>
-        <field name="model">account.move</field>
-        <field name="report_type">qweb-pdf</field>
-        <field name="report_name">cutom_report_invoice.custom_report_invoice_document</field>
-        <field name="report_file">cutom_report_invoice.custom_report_invoice_document</field>
-        <field name="attachment" />
-        <field name="binding_model_id" ref="account.model_account_move" />
-        <field name="binding_type">report</field>
-    </record>
-</odoo>

+ 0 - 16
cutom_report_invoice/views/account_move_views.xml

@@ -1,16 +0,0 @@
-<?xml version='1.0' encoding='utf-8'?>
-<odoo>
-    <template id="custom_rate_report_invoice_document" inherit_id="account.report_invoice_document">
-        <xpath expr="//div[@name='reference']" position="after">
-            <div class="col-auto col-3 mw-100 mb-2" t-if="o.currency_id" name="currency">
-                <strong>Currency:</strong>
-                <p class="m-0" t-field="o.currency_id"/>
-            </div>
-            <div class="col-auto col-3 mw-100 mb-2" t-if="o.rate and o.currency_id.name == 'USD'" name="rate">
-                <strong>Exchange rate:</strong>
-                <p class="m-0" t-field="o.rate"/>
-            </div>
-        </xpath>
-    </template>
-
-</odoo>

Деякі файли не було показано, через те що забагато файлів було змінено