Mudanza de repo

This commit is contained in:
2025-09-22 18:43:29 -06:00
parent 26fe36ca52
commit d11d543bdc
193 changed files with 10998 additions and 0 deletions

15
.dockerignore Normal file
View File

@@ -0,0 +1,15 @@
.git
__pycache__/
*.pyc
*.pyo
*.pyd
media/
static/
logs/
*.log
.env
.DS_Store
*.sqlite3
celerybeat-schedule
supervisord.log
supervisord.pid

20
.env.example Normal file
View File

@@ -0,0 +1,20 @@
SITE_URL=http://localhost:5173/
SECRET_KEY=django-insecure-*b7u3902e^k2&i=pg4hh0*^t=s%)$h9#6u0zjt64d6_ng#c*ei
DEBUG=True
ALLOWED_HOSTS=localhost,host.docker.internal,127.0.0.1,192.168.1.195,192.168.1.195:8000,192.168.1.195:8001
CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:8000,http://localhost:8001,http://192.168.1.195:5173,http://192.168.1.195:8001,http://192.168.1.195
DB_PASSWORD=Soluciones01
DB_NAME=postgres
DB_USER=postgres
DB_PORT=5432
DB_HOST=postgres_dev
EMAIL_HOST_USER=noreply@aduanasoft.com.mx
EMAIL_HOST_PASSWORD=N036p7y!
EMAIL_PORT=587
EMAIL_HOST=secure.emailsrvr.com
SERVICE_API_URL=http://localhost:8001/api/v1

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.zip filter=lfs diff=lfs merge=lfs -text

181
.gitignore vendored Normal file
View File

@@ -0,0 +1,181 @@
# Created by https://www.toptal.com/developers/gitignore/api/django
# Edit at https://www.toptal.com/developers/gitignore?templates=django
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
db.sqlite3
db.sqlite3-journal
media
.env
*.log.*
*.pid
*.err
media.*
static
staticfiles
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
### Django.Python Stack ###
# Byte-compiled / optimized / DLL files
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
# Django stuff:
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# End of https://www.toptal.com/developers/gitignore/api/django

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install flower
COPY . .
EXPOSE 8000
EXPOSE 5555
# El comando se define en docker-compose para cada servicio (backend, worker, beat, flower)

22
Dockerfile.prod Normal file
View File

@@ -0,0 +1,22 @@
FROM python:3.11-slim
WORKDIR /app
# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Copiar e instalar dependencias de Python
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt && pip install gunicorn
# Copiar código de la aplicación
COPY . .
## Ya no se usa supervisord, ni se copia su configuración
EXPOSE 8000
# El comando se define en docker-compose para cada servicio (backend, worker, beat, flower)

0
api/__init__.py Normal file
View File

0
api/cards/__init__.py Normal file
View File

3
api/cards/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
api/cards/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CardsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.cards'

3
api/cards/models.py Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

79
api/cards/tests.py Normal file
View File

@@ -0,0 +1,79 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from api.organization.models import Organizacion
from api.record.models import Document
from api.logger.models import UserActivity, RequestLog
from api.customs.models import ProcesamientoPedimento
User = get_user_model()
class CardsViewsTests(APITestCase):
def setUp(self):
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
self.org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
self.admin.groups.create(name="admin")
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
self.importador.groups.create(name="importador")
self.client = APIClient()
def test_admin_sees_only_own_org_documents(self):
from api.record.models import Document
doc1 = Document.objects.create(archivo="file1.pdf", organizacion=self.org)
doc2 = Document.objects.create(archivo="file2.pdf", organizacion=self.org2)
self.client.force_authenticate(user=self.admin)
url = reverse('document-util-information')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Only doc1 should be counted
self.assertGreaterEqual(response.data['archivos_ultimas_1_dia'], 0)
def test_superuser_sees_all_documents(self):
from api.record.models import Document
doc1 = Document.objects.create(archivo="file1.pdf", organizacion=self.org)
doc2 = Document.objects.create(archivo="file2.pdf", organizacion=self.org2)
self.client.force_authenticate(user=self.superuser)
url = reverse('document-util-information')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Both docs should be counted
self.assertGreaterEqual(response.data['archivos_ultimas_1_dia'], 0)
def test_importador_cannot_create_document(self):
self.client.force_authenticate(user=self.importador)
url = reverse('document-util-information')
response = self.client.post(url, {})
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_document_util_information(self):
url = reverse('document-util-information')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('archivos_ultimas_1_dia', response.data)
def test_services_util_information(self):
url = reverse('pedimento-services-util-information')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('en_espera', response.data)
def test_user_activity_analysis(self):
url = reverse('user-activity-analysis')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions_count', response.data)
def test_request_log_analysis(self):
url = reverse('request-log-analysis')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('methods_count', response.data)
def test_last_document_view(self):
url = reverse('downloaded-documents')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('documentos', response.data)

21
api/cards/urls.py Normal file
View File

@@ -0,0 +1,21 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
DocumentUtilInformation,
ViewPedimentoServicesUtilInformation,
UserActivityAnalysis,
RequestLogAnalysis,
LastDocumentView,
)
# Create a router and register our viewset with it.
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('document-util-information/', DocumentUtilInformation.as_view(), name='document-util-information'),
path('services-util-information/', ViewPedimentoServicesUtilInformation.as_view(), name='pedimento-services-util-information'),
path('user-activity-analysis/', UserActivityAnalysis.as_view(), name='user-activity-analysis'),
path('request-log-analysis/', RequestLogAnalysis.as_view(), name='request-log-analysis'),
path('downloaded-documents/', LastDocumentView.as_view(), name='downloaded-documents'),
]

457
api/cards/views.py Normal file
View File

@@ -0,0 +1,457 @@
from django.shortcuts import render
from django.contrib.auth import get_user_model
from django.utils import timezone
from django.db.models import F
from datetime import timedelta
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from api.organization.models import UsoAlmacenamiento, Organizacion
from api.record.models import Document
from api.customs.models import ProcesamientoPedimento
from api.logger.models import UserActivity, RequestLog
from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import FiltroPorOrganizacionMixin, DocumentosFiltradosMixin
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from api.logger.models import UserActivity, RequestLog, UserActivity
# Create your views here.
class DocumentUtilInformation(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
"""
View to get the total storage used by the organization and stats of documents added in last 1, 7, and 30 days.
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = Document
my_tags = ['Cards']
@swagger_auto_schema(
operation_description="Get total storage used and document stats. Permite filtrar por fecha de documentos.",
manual_parameters=[
openapi.Parameter('fecha_inicio', openapi.IN_QUERY, description="Fecha de inicio (YYYY-MM-DD)", type=openapi.TYPE_STRING),
openapi.Parameter('fecha_fin', openapi.IN_QUERY, description="Fecha de fin (YYYY-MM-DD)", type=openapi.TYPE_STRING),
],
responses={
200: openapi.Response(
description="Document stats",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"archivos_ultimas_1_dia": openapi.Schema(type=openapi.TYPE_INTEGER, description="Archivos en el último día"),
"archivos_ultimos_7_dias": openapi.Schema(type=openapi.TYPE_INTEGER, description="Archivos en los últimos 7 días"),
"archivos_ultimos_30_dias": openapi.Schema(type=openapi.TYPE_INTEGER, description="Archivos en los últimos 30 días"),
"archivos_filtrados": openapi.Schema(type=openapi.TYPE_INTEGER, description="Archivos en el rango de fechas")
}
),
examples={
"application/json": {
"archivos_ultimas_1_dia": 5,
"archivos_ultimos_7_dias": 20,
"archivos_ultimos_30_dias": 50,
"archivos_filtrados": 10
}
}
)
}
)
def get_queryset(self):
return self.get_queryset_filtrado()
def get(self, request):
queryset = self.get_queryset()
now = timezone.now()
count_1 = queryset.filter(created_at__gte=now - timedelta(days=1)).count()
count_7 = queryset.filter(created_at__gte=now - timedelta(days=7)).count()
count_30 = queryset.filter(created_at__gte=now - timedelta(days=30)).count()
fecha_inicio = request.query_params.get('fecha_inicio')
fecha_fin = request.query_params.get('fecha_fin')
docs_filtrados = queryset
if fecha_inicio:
docs_filtrados = docs_filtrados.filter(created_at__gte=fecha_inicio)
if fecha_fin:
docs_filtrados = docs_filtrados.filter(created_at__lte=fecha_fin)
count_filtrados = docs_filtrados.count()
return Response({
"archivos_ultimas_1_dia": count_1,
"archivos_ultimos_7_dias": count_7,
"archivos_ultimos_30_dias": count_30,
"archivos_filtrados": count_filtrados
})
class ViewPedimentoServicesUtilInformation(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
"""
View para obtener información de uso de servicios relacionados con pedimentos.
Devuelve la cantidad de procesos por estado (1: espera, 2: proceso, 3: finalizado, 4: error) para la organización.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = Document
my_tags = ['Cards']
@swagger_auto_schema(
operation_description="Get services stats. Permite filtrar por fecha de procesos.",
manual_parameters=[
openapi.Parameter('fecha_inicio', openapi.IN_QUERY, description="Fecha de inicio (YYYY-MM-DD)", type=openapi.TYPE_STRING),
openapi.Parameter('fecha_fin', openapi.IN_QUERY, description="Fecha de fin (YYYY-MM-DD)", type=openapi.TYPE_STRING),
],
responses={
200: openapi.Response(
description="Services stats",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"en_espera": openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad de procesos en espera"),
"en_proceso": openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad de procesos en proceso"),
"finalizados": openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad de procesos finalizados"),
"con_error": openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad de procesos con error"),
"procesos_filtrados": openapi.Schema(type=openapi.TYPE_INTEGER, description="Procesos en el rango de fechas")
}
),
examples={
"application/json": {
"en_espera": 1,
"en_proceso": 2,
"finalizados": 3,
"con_error": 4,
"procesos_filtrados": 5
}
}
)
}
)
def get_queryset(self):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
return None
# Si es super usuario, devuelve todos los procesos
if self.request.user.is_superuser:
return ProcesamientoPedimento.objects.all()
# Si es Administrador de la organizacion devuelve todos los servicios de la organizacion
if self.request.user.is_authenticated and self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
# Si es Desarrollador de la organizacion devuelve todos los servicios de la organizacion
if self.request.user.is_authenticated and self.request.user.groups.filter(name='developer').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return self.request.user.organizacion.procesamiento_pedimentos.all()
if self.request.user.is_authenticated and self.request.user.groups.filter(name='user').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
return self.request.user.organizacion.procesamiento_pedimentos.all()
# Si es importador de la organizacion, devuelve los servicios relacionados con sus pedimentos
if self.request.user.is_authenticated and self.request.user.groups.filter(name='importador').exists() and self.request.user.is_importador and self.request.user.groups.filter(name='user').exists():
return self.request.user.organizacion.procesamiento_pedimentos.filter(pedimento__contribuyente=self.request.user.rfc)
# Si es parte de una organización, filtrar por esa organización
return ProcesamientoPedimento.objects.filter(pedimento__organizacion=self.request.user.organizacion)
def get(self, request):
queryset = self.get_queryset()
if queryset is None:
return Response({"error": "Usuario no autenticado o sin organización"}, status=401)
en_espera = queryset.filter(estado=1).count()
en_proceso = queryset.filter(estado=2).count()
finalizados = queryset.filter(estado=3).count()
con_error = queryset.filter(estado=4).count()
fecha_inicio = request.query_params.get('fecha_inicio')
fecha_fin = request.query_params.get('fecha_fin')
procesos_filtrados = queryset
if fecha_inicio:
procesos_filtrados = procesos_filtrados.filter(created_at__gte=fecha_inicio)
if fecha_fin:
procesos_filtrados = procesos_filtrados.filter(created_at__lte=fecha_fin)
count_filtrados = procesos_filtrados.count()
return Response({
"en_espera": en_espera,
"en_proceso": en_proceso,
"finalizados": finalizados,
"con_error": con_error,
"procesos_filtrados": count_filtrados
})
class UserActivityAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
"""
Endpoint para análisis de actividades de usuario.
Devuelve el conteo de acciones por tipo y los 5 usuarios más activos.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = UserActivity
my_tags = ['Cards']
@swagger_auto_schema(
operation_description="Get analysis of user activities. Permite filtrar por fecha de actividades.",
manual_parameters=[
openapi.Parameter('fecha_inicio', openapi.IN_QUERY, description="Fecha de inicio (YYYY-MM-DD)", type=openapi.TYPE_STRING),
openapi.Parameter('fecha_fin', openapi.IN_QUERY, description="Fecha de fin (YYYY-MM-DD)", type=openapi.TYPE_STRING),
],
responses={
200: openapi.Response(
description="User activity analysis",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"actions_count": openapi.Schema(
type=openapi.TYPE_OBJECT,
additional_properties=openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad por acción")
),
"top_users": openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"username": openapi.Schema(type=openapi.TYPE_STRING),
"activity_count": openapi.Schema(type=openapi.TYPE_INTEGER)
}
),
description="Top 5 usuarios más activos"
),
"actividades_filtradas": openapi.Schema(type=openapi.TYPE_INTEGER, description="Actividades en el rango de fechas")
}
),
examples={
"application/json": {
"actions_count": {
"login": 20,
"logout": 18,
"create": 15,
"update": 10,
"delete": 5,
"view": 30,
"search": 12,
"export": 3,
"import": 2
},
"top_users": [
{"username": "admin", "activity_count": 25},
{"username": "user1", "activity_count": 20}
],
"actividades_filtradas": 10
}
}
)
}
)
def get_queryset(self):
return self.get_queryset_filtrado()
def get(self, request):
queryset = self.get_queryset()
User = get_user_model()
actions = [a[0] for a in UserActivity.ACTIONS]
actions_count = {a: queryset.filter(action=a).count() for a in actions}
# Top 5 usuarios más activos
top_users = []
from django.db.models import Count
top_users_qs = queryset.values('user').annotate(activity_count=Count('id')).order_by('-activity_count')[:5]
for entry in top_users_qs:
try:
user_obj = User.objects.get(pk=entry['user'])
top_users.append({"username": user_obj.username, "activity_count": entry['activity_count']})
except User.DoesNotExist:
continue
fecha_inicio = request.query_params.get('fecha_inicio')
fecha_fin = request.query_params.get('fecha_fin')
actividades_filtradas = queryset
if fecha_inicio:
actividades_filtradas = actividades_filtradas.filter(timestamp__gte=fecha_inicio)
if fecha_fin:
actividades_filtradas = actividades_filtradas.filter(timestamp__lte=fecha_fin)
count_filtrados = actividades_filtradas.count()
return Response({
"actions_count": actions_count,
"top_users": top_users,
"actividades_filtradas": count_filtrados
})
class RequestLogAnalysis(LoggingMixin, APIView, FiltroPorOrganizacionMixin):
"""
Endpoint para análisis de logs de peticiones.
Devuelve el conteo por método, los paths más solicitados y el promedio de tiempo de respuesta.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = RequestLog
my_tags = ['Cards']
@swagger_auto_schema(
operation_description="Get analysis of request logs. Permite filtrar por fecha de logs.",
manual_parameters=[
openapi.Parameter('fecha_inicio', openapi.IN_QUERY, description="Fecha de inicio (YYYY-MM-DD)", type=openapi.TYPE_STRING),
openapi.Parameter('fecha_fin', openapi.IN_QUERY, description="Fecha de fin (YYYY-MM-DD)", type=openapi.TYPE_STRING),
],
responses={
200: openapi.Response(
description="Request log analysis",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"methods_count": openapi.Schema(
type=openapi.TYPE_OBJECT,
additional_properties=openapi.Schema(type=openapi.TYPE_INTEGER, description="Cantidad por método")
),
"top_paths": openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"path": openapi.Schema(type=openapi.TYPE_STRING),
"count": openapi.Schema(type=openapi.TYPE_INTEGER)
}
),
description="Top 5 paths más solicitados"
),
"avg_response_time": openapi.Schema(type=openapi.TYPE_NUMBER, format='float', description="Promedio de tiempo de respuesta (ms)"),
"logs_filtrados": openapi.Schema(type=openapi.TYPE_INTEGER, description="Logs en el rango de fechas")
}
),
examples={
"application/json": {
"methods_count": {
"GET": 120,
"POST": 30,
"PUT": 10,
"DELETE": 5
},
"top_paths": [
{"path": "/api/v1/record/documents/", "count": 50},
{"path": "/api/v1/customs/pedimentos/", "count": 40}
],
"avg_response_time": 120.5,
"logs_filtrados": 15
}
}
)
}
)
def get_queryset(self):
return self.get_queryset_filtrado()
def get(self, request):
queryset = self.get_queryset()
from django.db.models import Count, Avg
from api.logger.models import RequestLog
methods = [m[0] for m in RequestLog.METHODS]
methods_count = {m: queryset.filter(method=m).count() for m in methods}
top_paths_qs = queryset.values('path').annotate(count=Count('id')).order_by('-count')[:5]
top_paths = [{"path": entry['path'], "count": entry['count']} for entry in top_paths_qs]
avg_response_time = queryset.aggregate(avg=Avg('response_time'))['avg'] or 0.0
fecha_inicio = request.query_params.get('fecha_inicio')
fecha_fin = request.query_params.get('fecha_fin')
logs_filtrados = queryset
if fecha_inicio:
logs_filtrados = logs_filtrados.filter(timestamp__gte=fecha_inicio)
if fecha_fin:
logs_filtrados = logs_filtrados.filter(timestamp__lte=fecha_fin)
count_filtrados = logs_filtrados.count()
return Response({
"methods_count": methods_count,
"top_paths": top_paths,
"avg_response_time": round(avg_response_time, 2),
"logs_filtrados": count_filtrados
})
class LastDocumentView(LoggingMixin, APIView, DocumentosFiltradosMixin):
"""
View que obtiene los ultimos 10 documentos agregados.
Permite filtrar por fecha usando los parámetros ?fecha_inicio=YYYY-MM-DD&fecha_fin=YYYY-MM-DD
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = Document
my_tags = ['Cards']
@swagger_auto_schema(
operation_description="Obtiene los últimos 10 documentos agregados. Permite filtrar por fecha de creación.",
manual_parameters=[
openapi.Parameter('fecha_inicio', openapi.IN_QUERY, description="Fecha de inicio (YYYY-MM-DD)", type=openapi.TYPE_STRING),
openapi.Parameter('fecha_fin', openapi.IN_QUERY, description="Fecha de fin (YYYY-MM-DD)", type=openapi.TYPE_STRING),
],
responses={
200: openapi.Response(
description="Lista de los últimos 10 documentos",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"documentos": openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"id": openapi.Schema(type=openapi.TYPE_STRING, format="uuid"),
"archivo": openapi.Schema(type=openapi.TYPE_STRING, description="Ruta del archivo"),
"extension": openapi.Schema(type=openapi.TYPE_STRING),
"size": openapi.Schema(type=openapi.TYPE_INTEGER),
"created_at": openapi.Schema(type=openapi.TYPE_STRING, format="date-time"),
"updated_at": openapi.Schema(type=openapi.TYPE_STRING, format="date-time"),
"organizacion": openapi.Schema(type=openapi.TYPE_STRING),
"pedimento": openapi.Schema(type=openapi.TYPE_STRING)
}
),
description="Últimos 10 documentos agregados"
),
"total_filtrados": openapi.Schema(type=openapi.TYPE_INTEGER, description="Total de documentos en el rango de fechas")
}
),
examples={
"application/json": {
"documentos": [
{"id": "b7e6c8e2-1a2b-4c3d-8e9f-123456789abc", "archivo": "documents/doc1.pdf", "extension": "pdf", "size": 123456, "created_at": "2025-07-14T10:00:00Z", "updated_at": "2025-07-14T10:00:00Z", "organizacion": "Org1", "pedimento": "Ped1"}
],
"total_filtrados": 1
}
}
)
}
)
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def get(self, request):
queryset = self.get_queryset()
fecha_inicio = request.query_params.get('fecha_inicio')
fecha_fin = request.query_params.get('fecha_fin')
documentos = queryset
if fecha_inicio:
documentos = documentos.filter(created_at__gte=fecha_inicio)
if fecha_fin:
documentos = documentos.filter(created_at__lte=fecha_fin)
total_filtrados = documentos.count()
ultimos = documentos[:10]
docs_serializados = []
for doc in ultimos:
docs_serializados.append({
"id": str(doc.id),
"archivo": doc.archivo.name if doc.archivo else '',
"extension": doc.extension,
"size": doc.size,
"created_at": doc.created_at.isoformat() if doc.created_at else '',
"updated_at": doc.updated_at.isoformat() if doc.updated_at else '',
"organizacion": str(doc.organizacion) if doc.organizacion else '',
"pedimento": str(doc.pedimento) if doc.pedimento else ''
})
return Response({
"documentos": docs_serializados,
"total_filtrados": total_filtrados
})

0
api/cuser/__init__.py Normal file
View File

51
api/cuser/admin.py Normal file
View File

@@ -0,0 +1,51 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = CustomUser
fields = ('username', 'email', 'first_name', 'last_name', 'organizacion', 'profile_picture', 'password1', 'password2')
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = CustomUser
fields = ('username', 'email', 'first_name', 'last_name', 'organizacion', 'profile_picture')
class CustomUserAdmin(UserAdmin):
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = CustomUser
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'is_importador', 'organizacion')
list_filter = ('is_staff', 'is_active', 'organizacion')
search_fields = ('username', 'email', 'first_name', 'last_name')
ordering = ('username',)
# Fieldsets para editar un usuario
fieldsets = (
(None, {'fields': ('username', 'password')}),
('Información personal', {'fields': ('first_name', 'last_name', 'email', 'organizacion', 'profile_picture', 'is_importador', 'rfc')}),
('Permisos', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
('Fechas importantes', {'fields': ('last_login', 'date_joined')}),
)
# Fieldsets para crear un nuevo usuario
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'username', 'email', 'first_name', 'last_name',
'organizacion', 'profile_picture',
'password1', 'password2',
'is_staff', 'is_active', 'is_importador'
)
}),
)
admin.site.register(CustomUser, CustomUserAdmin)

6
api/cuser/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CuserConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.cuser'

View File

@@ -0,0 +1,45 @@
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework.response import Response
from rest_framework import status
from django.conf import settings
class CookieTokenObtainPairView(TokenObtainPairView):
"""
Custom view to set JWT tokens as HttpOnly cookies.
"""
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access = response.data.get('access')
refresh = response.data.get('refresh')
# Remove tokens from body (optional, for extra security)
response.data.pop('access', None)
response.data.pop('refresh', None)
# Set cookies
cookie_settings = {
'httponly': True,
'secure': True, # Set to True if using HTTPS
'samesite': 'Lax',
'path': '/'
}
response.set_cookie('access_token', access, max_age=60*5, **cookie_settings) # 5 min
response.set_cookie('refresh_token', refresh, max_age=60*60*24*7, **cookie_settings) # 7 days
return response
class CookieTokenRefreshView(TokenRefreshView):
"""
Custom view to refresh JWT tokens and set as HttpOnly cookies.
"""
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access = response.data.get('access')
response.data.pop('access', None)
cookie_settings = {
'httponly': True,
'secure': True, # Set to True if using HTTPS
'samesite': 'Lax',
'path': '/'
}
response.set_cookie('access_token', access, max_age=60*5, **cookie_settings) # 5 min
return response

View File

@@ -0,0 +1,51 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.contrib.auth.models
import django.contrib.auth.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('organization', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('profile_picture', models.ImageField(blank=True, null=True, upload_to='profile_pictures/')),
('is_importador', models.BooleanField(default=False, help_text='Indicates if the user is an importer')),
('rfc', models.CharField(blank=True, help_text='RFC of the user', max_length=13, null=True, unique=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='users', to='organization.organizacion')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Custom User',
'verbose_name_plural': 'Custom Users',
'ordering': ['username'],
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-07-30 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cuser', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='rfc',
field=models.CharField(blank=True, help_text='RFC of the user', max_length=13, null=True),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.3 on 2025-07-31 16:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cuser', '0002_alter_customuser_rfc'),
]
operations = [
migrations.AlterField(
model_name='customuser',
name='rfc',
field=models.CharField(blank=True, help_text='RFC of the user', max_length=1, null=True),
),
]

View File

23
api/cuser/models.py Normal file
View File

@@ -0,0 +1,23 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class CustomUser(AbstractUser):
"""
Custom user model that extends the default Django user model.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, null=True, blank=True, related_name='users')
profile_picture = models.ImageField(upload_to='profile_pictures/', null=True, blank=True)
is_importador = models.BooleanField(default=False, help_text="Indicates if the user is an importer")
rfc = models.CharField(max_length=1, null=True, blank=True, help_text="RFC of the user")
def __str__(self):
return self.username
class Meta:
verbose_name = 'Custom User'
verbose_name_plural = 'Custom Users'
ordering = ['username']

View File

@@ -0,0 +1,24 @@
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
from django.urls import reverse
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
from django.template.loader import render_to_string
def send_password_reset_email(user, request):
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
# Enlace directo al frontend
reset_link = f"https://efc-aduanasoft.com/user/password-reset-confirm/{uid}/{token}/"
subject = 'Recuperación de contraseña'
context = {
'username': user.username,
'reset_link': reset_link,
}
text_content = f'Hola {user.username},\nPara restablecer tu contraseña haz clic en el siguiente enlace: {reset_link}'
html_content = render_to_string('email/password_reset_email.html', context)
email = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [user.email])
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)

29
api/cuser/serializers.py Normal file
View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from .models import CustomUser
from django.contrib.auth.models import Group
class CustomUserSerializer(serializers.ModelSerializer):
"""
Serializer for the CustomUser model.
"""
password = serializers.CharField(write_only=True)
groups = serializers.PrimaryKeyRelatedField(queryset=Group.objects.all(), many=True, required=False)
rfc = serializers.CharField(max_length=20, required=False, allow_blank=True)
class Meta:
model = CustomUser
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'password', 'profile_picture', 'organizacion', 'is_importador', 'rfc', 'is_active', 'is_superuser', 'groups']
read_only_fields = ['id', 'organizacion', 'is_superuser']
def create(self, validated_data):
groups = validated_data.pop('groups', [])
password = validated_data.pop('password')
user = CustomUser(**validated_data)
user.set_password(password)
user.save()
if groups:
user.groups.set(groups)
return user

91
api/cuser/tests.py Normal file
View File

@@ -0,0 +1,91 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from api.organization.models import Organizacion
from .models import CustomUser
User = get_user_model()
class CustomUserViewSetTests(APITestCase):
def setUp(self):
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
self.org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
self.admin.groups.create(name="admin")
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
self.importador.groups.create(name="importador")
self.user = User.objects.create_user(username="user1", password="userpass", organizacion=self.org)
self.client = APIClient()
def test_admin_sees_only_own_org_users(self):
user2 = User.objects.create_user(username="user2", password="userpass2", organizacion=self.org2)
self.client.force_authenticate(user=self.admin)
url = reverse('customuser-list')
response = self.client.get(url)
usernames = [u['username'] for u in response.data]
self.assertIn("admin", usernames)
self.assertIn("user1", usernames)
self.assertNotIn("user2", usernames)
def test_superuser_sees_all_users(self):
user2 = User.objects.create_user(username="user2", password="userpass2", organizacion=self.org2)
self.client.force_authenticate(user=self.superuser)
url = reverse('customuser-list')
response = self.client.get(url)
usernames = [u['username'] for u in response.data]
self.assertIn("admin", usernames)
self.assertIn("user1", usernames)
self.assertIn("user2", usernames)
def test_importador_cannot_create_user(self):
self.client.force_authenticate(user=self.importador)
url = reverse('customuser-list')
data = {
"username": "newuser",
"email": "newuser@example.com",
"first_name": "New",
"last_name": "User",
"password": "newpass123"
}
response = self.client.post(url, data)
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_list_users(self):
url = reverse('customuser-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(len(response.data) >= 1)
def test_me_endpoint(self):
url = reverse('customuser-me')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['username'], self.admin.username)
def test_create_user_as_admin(self):
url = reverse('customuser-list')
data = {
"username": "newuser",
"email": "newuser@example.com",
"first_name": "New",
"last_name": "User",
"password": "newpass123"
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['username'], "newuser")
def test_update_user_as_admin(self):
url = reverse('customuser-detail', args=[str(self.user.id)])
data = {"first_name": "Updated"}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['first_name'], "Updated")
def test_profile_picture_view(self):
# No profile picture, should return 404
url = reverse('profile-picture', args=[str(self.user.id)])
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

18
api/cuser/urls.py Normal file
View File

@@ -0,0 +1,18 @@
from django.urls import path, include
from .views import ProfilePictureView, ActivateUserView, PasswordResetRequestView, PasswordResetConfirmView, CustomUserViewSet
customuser_me = CustomUserViewSet.as_view({'get': 'me'})
from rest_framework.routers import DefaultRouter
from .views import CustomUserViewSet
router = DefaultRouter()
router.register(r'users', CustomUserViewSet, basename='customuser')
urlpatterns = [
path('', include(router.urls)),
path('me/', customuser_me, name='user-me'),
path('profile-picture/<uuid:user_id>/', ProfilePictureView.as_view(), name='profile-picture'),
path('activate/<uidb64>/<token>/', ActivateUserView.as_view(), name='activate'),
path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset'),
path('password-reset-confirm/<uidb64>/<token>/', PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
]

28
api/cuser/utils.py Normal file
View File

@@ -0,0 +1,28 @@
import uuid
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.contrib.auth.tokens import default_token_generator
def send_activation_email(user, request):
"""
Envía un correo de activación al usuario con un link único.
"""
token = default_token_generator.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
activation_link = request.build_absolute_uri(
reverse('cuser:activate', kwargs={'uidb64': uid, 'token': token})
)
subject = 'Activa tu cuenta'
context = {
'username': user.username,
'activation_link': activation_link,
}
text_content = f'Hola {user.username},\nPor favor haz clic en el siguiente enlace para activar tu cuenta: {activation_link}'
html_content = render_to_string('email/activation_email.html', context)
email = EmailMultiAlternatives(subject, text_content, settings.DEFAULT_FROM_EMAIL, [user.email])
email.attach_alternative(html_content, "text/html")
email.send(fail_silently=False)

275
api/cuser/views.py Normal file
View File

@@ -0,0 +1,275 @@
from .password_reset_utils import send_password_reset_email
from django.contrib.auth import get_user_model
from django.utils.encoding import force_str
# Vista para solicitar recuperación de contraseña
from rest_framework import status
import uuid
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404, redirect
from rest_framework.views import APIView
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import PermissionDenied
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from .serializers import CustomUserSerializer
from .models import CustomUser
from api.logger.mixins import LoggingMixin
from api.vucem.models import Vucem
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
from .utils import send_activation_email
from django.utils.http import urlsafe_base64_decode
from django.contrib.auth.tokens import default_token_generator
from rest_framework.views import APIView
from django.utils.encoding import force_str
from django.conf import settings
class CustomPagination(PageNumberPagination):
"""
Paginación personalizada con parámetros flexibles
- Si no se especifica page_size, devuelve todos los resultados (sin paginación)
- Si se especifica page_size, usa paginación normal
"""
page_size = None # Sin paginación por defecto
page_size_query_param = 'page_size'
max_page_size = 1000 # Límite máximo de seguridad
page_query_param = 'page'
def paginate_queryset(self, queryset, request, view=None):
"""
Si no se especifica page_size en los parámetros, devolver None (sin paginación)
Si se especifica, usar paginación normal
"""
# Verificar si se especificó page_size en la query
if self.page_size_query_param not in request.query_params:
# No hay page_size, devolver None para indicar "sin paginación"
return None
# Hay page_size, usar paginación normal
try:
page_size = int(request.query_params[self.page_size_query_param])
if page_size <= 0:
return None
# Establecer el page_size temporalmente para esta request
self.page_size = min(page_size, self.max_page_size)
except (ValueError, TypeError):
return None
return super().paginate_queryset(queryset, request, view)
class CustomUserViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for CustomUser model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSameOrganization )]
pagination_class = CustomPagination
model = CustomUser
serializer_class = CustomUserSerializer
filterset_fields = ['username', 'email', 'first_name', 'last_name', 'organizacion', 'is_importador']
my_tags = ['User Profile']
def get_permissions(self):
# Permitir eliminar usuarios solo a admin, Agente Aduanal y user de la misma organización
if self.action == 'destroy':
user = self.request.user
if not (
user.is_superuser or
user.groups.filter(name='admin').exists() or
user.groups.filter(name='Agente Aduanal').exists() or
user.groups.filter(name='user').exists()
):
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin, Agente Aduanal o user pueden eliminar usuarios.")
elif self.action in ['create', 'update', 'partial_update']:
if not (self.request.user.is_superuser or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Importador').exists()) :
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo admin o superusuario pueden modificar usuarios.")
return super().get_permissions()
def perform_destroy(self, instance):
# Solo permitir eliminar usuarios de la misma organización
if self.request.user.is_superuser or instance.organizacion == self.request.user.organizacion:
instance.delete()
else:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo puedes eliminar usuarios de tu organización.")
def get_queryset(self):
# Si es importador, solo puede ver su propio usuario
if self.request.user.groups.filter(name='importador').exists() or self.request.user.groups.filter(name='Importador').exists():
return CustomUser.objects.filter(pk=self.request.user.pk)
# Otros roles: filtrar por organización
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
# Always assign the creator's organization
if self.request.user.groups.filter(name='admin').exists() and self.request.user.groups.filter(name='Agente Aduanal').exists():
if not self.request.user.organizacion:
raise PermissionDenied("Los administradores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.is_superuser:
# If superuser, allow creating users without organization
user = serializer.save(is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.groups.filter(name='developer').exists():
# Developers can create users but must assign an organization
if not self.request.user.organizacion:
raise PermissionDenied("Los desarrolladores deben tener una organización asignada para crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
if self.request.user.groups.filter(name='importador').exists():
# No puedes crear un usuario si eres importador
raise PermissionDenied("Los importadores no pueden crear usuarios.")
user = serializer.save(organizacion=self.request.user.organizacion, is_active=False)
send_activation_email(user, self.request) # Usa template HTML
return
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def me(self, request):
"""
Endpoint para obtener la información del usuario autenticado.
GET /api/v1/user/me/
"""
serializer = self.get_serializer(request.user)
return Response(serializer.data)
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
def change_password(self, request, pk=None):
"""
Endpoint para cambiar la contraseña de un usuario.
Solo el propio usuario o un admin/superuser pueden cambiarla.
POST /user/users/{id}/change_password/
Body: {"old_password": "actual", "new_password": "nueva"}
"""
user = self.get_object()
current_user = request.user
# Solo el propio usuario, admin o superuser pueden cambiar la contraseña
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists() or user == current_user):
raise PermissionDenied("No tienes permiso para cambiar la contraseña de este usuario.")
old_password = request.data.get('old_password')
new_password = request.data.get('new_password')
if not new_password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
# Si no es admin/superuser, debe validar old_password
if not (current_user.is_superuser or current_user.groups.filter(name='admin').exists()):
if not old_password or not user.check_password(old_password):
return Response({'detail': 'La contraseña actual es incorrecta.'}, status=400)
user.set_password(new_password)
user.save()
return Response({'detail': 'Contraseña cambiada correctamente.'}, status=200)
class ActivateUserView(APIView):
"""
Vista para activar usuario desde el link enviado por correo.
"""
permission_classes = [] # Permitir acceso público a la activación de usuario
my_tags = ['User Authentication']
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
from .models import CustomUser
user = CustomUser.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist):
user = None
if user is not None and default_token_generator.check_token(user, token):
user.is_active = True
user.save()
# Aquí puedes redirigir a una página de éxito o login
return redirect(settings.SITE_URL + 'login?activated=1')
else:
return Response({'detail': 'El enlace de activación no es válido o ha expirado.'}, status=400)
def perform_update(self, serializer):
# Only allow update if user is in the same organization
instance = self.get_object()
if instance.organizacion != self.request.user.organizacion:
from rest_framework.exceptions import PermissionDenied
raise PermissionDenied("Solo puedes actualizar usuarios de tu organización.")
password = serializer.validated_data.pop('password', None)
user = serializer.save()
if password:
user.set_password(password)
user.save()
class ProfilePictureView(LoggingMixin, APIView):
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
my_tags = ['User Profile']
def get(self, request, user_id):
# Obtiene el usuario (automáticamente 404 si no existe)
user = get_object_or_404(CustomUser, pk=user_id)
# El permiso IsOwnerOrAdmin ya verificó que request.user == user o es admin
# Así que no necesitas validar manualmente los permisos aquí.
if not user.profile_picture:
raise Http404("El usuario no tiene imagen de perfil")
return FileResponse(user.profile_picture.open('rb'))
class PasswordResetRequestView(APIView):
permission_classes = [] # Permitir acceso público a la recuperación de contraseña
my_tags = ['User Authentication']
def post(self, request):
email = request.data.get('email')
username = request.data.get('username')
if not email or not username:
return Response({'detail': 'Se requieren username y email.'}, status=400)
User = get_user_model()
try:
user = User.objects.get(email=email, username=username)
except User.DoesNotExist:
return Response({'detail': 'No existe usuario con ese username y email.'}, status=404)
send_password_reset_email(user, request) # Usa template HTML
return Response({'detail': 'Se ha enviado un correo para restablecer la contraseña.'}, status=status.HTTP_200_OK)
# Vista para confirmar recuperación de contraseña
class PasswordResetConfirmView(APIView):
permission_classes = [] # Permitir acceso público a la confirmación de recuperación de contraseña
my_tags = ['User Authentication']
def post(self, request, uidb64, token):
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode
User = get_user_model()
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return Response({'detail': 'Enlace inválido.'}, status=400)
if not default_token_generator.check_token(user, token):
return Response({'detail': 'Token inválido o expirado.'}, status=400)
password = request.data.get('password')
if not password:
return Response({'detail': 'La nueva contraseña es requerida.'}, status=400)
user.set_password(password)
user.save()
return Response({'detail': 'Contraseña restablecida correctamente.'}, status=status.HTTP_200_OK)

0
api/customs/__init__.py Normal file
View File

65
api/customs/admin.py Normal file
View File

@@ -0,0 +1,65 @@
from django.contrib import admin
from .models import (
EstadoDeProcesamiento,
Pedimento,
ProcesamientoPedimento,
Servicio,
TipoDeProcesamiento,
TipoOperacion,
EDocument,
Importador
)
class TipoOperacionAdmin(admin.ModelAdmin):
model = TipoOperacion
list_display = ('id', 'tipo')
search_fields = ('nombre',)
class PedimentoAdmin(admin.ModelAdmin):
model = Pedimento
list_display = ('id', 'pedimento', 'aduana', 'patente')
search_fields = ('numero',)
list_filter = ('aduana', 'agente_aduanal', 'organizacion')
class ProcesamientoPedimentoAdmin(admin.ModelAdmin):
model = ProcesamientoPedimento
list_display = ('id', 'estado', 'pedimento', 'created_at', 'updated_at')
search_fields = ('pedimento__pedimento_app', 'organizacion__nombre', 'estado__estado', 'servicio__endpoint')
list_filter = ('estado', 'organizacion__nombre')
class EstadoDeProcesamientoAdmin(admin.ModelAdmin):
model = EstadoDeProcesamiento
list_display = ('id', 'estado')
search_fields = ('estado',)
class TipoDeProcesamientoAdmin(admin.ModelAdmin):
model = TipoDeProcesamiento
list_display = ('id', 'tipo')
# Solo 'tipo' es campo directo, los demás no existen en el modelo
list_filter = ['tipo']
search_fields = ('tipo', 'organizacion', 'estado', 'servicio')
class ServicioAdmin(admin.ModelAdmin):
model = Servicio
list_display = ('id', 'endpoint', 'descripcion')
search_fields = ('endpoint', 'descripcion')
class EDocumentAdmin(admin.ModelAdmin):
model = EDocument
list_display = ('id', 'pedimento', 'numero_edocument', 'organizacion')
search_fields = ('numero_edocument', 'pedimento', 'pedimento__pedimento_app')
list_filter = ['organizacion']
class ImportadorAdmin(admin.ModelAdmin):
model = Importador
list_display = ('id', 'nombre', 'rfc')
search_fields = ('nombre', 'rfc')
admin.site.register(TipoOperacion, TipoOperacionAdmin)
admin.site.register(Pedimento, PedimentoAdmin)
admin.site.register(ProcesamientoPedimento, ProcesamientoPedimentoAdmin)
admin.site.register(EstadoDeProcesamiento, EstadoDeProcesamientoAdmin)
admin.site.register(TipoDeProcesamiento, TipoDeProcesamientoAdmin)
admin.site.register(Servicio, ServicioAdmin)
admin.site.register(EDocument, EDocumentAdmin)
admin.site.register(Importador)

9
api/customs/apps.py Normal file
View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class CustomsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.customs'
def ready(self):
import api.customs.signals

View File

@@ -0,0 +1,224 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('organization', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Aduana',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('seccion', models.CharField(max_length=10)),
('descripcion', models.CharField(max_length=200)),
],
options={
'verbose_name': 'Aduana',
'verbose_name_plural': 'Aduanas',
'db_table': 'aduana',
'ordering': ['seccion'],
},
),
migrations.CreateModel(
name='ClavePedimento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('clave', models.CharField(max_length=10)),
('descripcion', models.CharField(max_length=200)),
],
options={
'verbose_name': 'Clave de Pedimento',
'verbose_name_plural': 'Claves de Pedimento',
'db_table': 'clave_pedimento',
'ordering': ['clave'],
},
),
migrations.CreateModel(
name='EstadoDeProcesamiento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('estado', models.CharField(max_length=50)),
],
options={
'verbose_name': 'Estado de Procesamiento',
'verbose_name_plural': 'Estados de Procesamiento',
'db_table': 'estado_de_procesamiento',
'ordering': ['estado'],
},
),
migrations.CreateModel(
name='Patente',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero', models.CharField(max_length=20)),
('descripcion', models.CharField(max_length=200)),
],
options={
'verbose_name': 'Patente',
'verbose_name_plural': 'Patentes',
'db_table': 'patente',
'ordering': ['numero'],
},
),
migrations.CreateModel(
name='Regimen',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('clave', models.CharField(max_length=10)),
('descripcion', models.CharField(max_length=200)),
],
options={
'verbose_name': 'Regimen',
'verbose_name_plural': 'Regimenes',
'db_table': 'regimen',
'ordering': ['clave'],
},
),
migrations.CreateModel(
name='Servicio',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('endpoint', models.CharField(max_length=100)),
('descripcion', models.TextField(blank=True, null=True)),
('hora_inicio', models.TimeField(blank=True, max_length=50, null=True)),
('hora_fin', models.TimeField(blank=True, max_length=50, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Servicio',
'verbose_name_plural': 'Servicios',
'db_table': 'servicio',
'ordering': ['endpoint'],
},
),
migrations.CreateModel(
name='TipoDeProcesamiento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tipo', models.CharField(max_length=50)),
],
options={
'verbose_name': 'Tipo de Procesamiento',
'verbose_name_plural': 'Tipos de Procesamiento',
'db_table': 'tipo_de_procesamiento',
'ordering': ['tipo'],
},
),
migrations.CreateModel(
name='TipoOperacion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tipo', models.CharField(max_length=100)),
('descripcion', models.CharField(max_length=200)),
],
options={
'verbose_name': 'Tipo de Operacion',
'verbose_name_plural': 'Tipos de Operacion',
'db_table': 'tipo_operacion',
'ordering': ['tipo'],
},
),
migrations.CreateModel(
name='AgenteAduanal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100)),
('rfc', models.CharField(blank=True, max_length=13, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('id_aduana', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agentes_aduanales', to='customs.aduana')),
('id_patente', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agentes_aduanales', to='customs.patente')),
],
options={
'verbose_name': 'Agente Aduanal',
'verbose_name_plural': 'Agentes Aduanales',
'db_table': 'agente_aduanal',
'ordering': ['nombre'],
},
),
migrations.CreateModel(
name='Pedimento',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('pedimento', models.CharField(help_text='Número de pedimento aduanal', max_length=20, unique=True)),
('patente', models.CharField(blank=True, help_text='Número de patente aduanal', max_length=20, null=True)),
('aduana', models.CharField(blank=True, help_text='Clave de la aduana según la clasificación aduanera', max_length=10, null=True)),
('regimen', models.CharField(blank=True, help_text='Clave del régimen aduanero según la clasificación aduanera', max_length=10, null=True)),
('clave_pedimento', models.CharField(blank=True, help_text='Clave del pedimento según la clasificación aduanera', max_length=10, null=True)),
('fecha_inicio', models.DateField(blank=True, help_text='Fecha de inicio del pedimento', null=True)),
('fecha_fin', models.DateField(blank=True, help_text='Fecha de fin del pedimento', null=True)),
('fecha_pago', models.DateField(blank=True, help_text='Fecha de pago del pedimento', null=True)),
('alerta', models.BooleanField(default=False, help_text='Indica si el pedimento tiene una alerta asociada')),
('contribuyente', models.CharField(blank=True, help_text='Nombre del contribuyente/importador asociado al pedimento', max_length=100, null=True)),
('agente_aduanal', models.CharField(blank=True, help_text='RFC del agente aduanal', max_length=100, null=True)),
('curp_apoderado', models.CharField(blank=True, help_text='CURP del apoderado aduanal', max_length=18, null=True)),
('importe_total', models.DecimalField(blank=True, decimal_places=2, help_text='Importe total del pedimento', max_digits=10, null=True)),
('saldo_disponible', models.DecimalField(blank=True, decimal_places=2, help_text='Saldo disponible del pedimento', max_digits=10, null=True)),
('importe_pedimento', models.DecimalField(blank=True, decimal_places=2, help_text='Importe del pedimento', max_digits=10, null=True)),
('existe_expediente', models.BooleanField(default=False)),
('remesas', models.BooleanField(default=False, help_text='Indica si el pedimento tiene remesas asociadas')),
('numero_partidas', models.PositiveIntegerField(blank=True, default=0, help_text='Número de partidas asociadas al pedimento', null=True)),
('numero_operacion', models.CharField(blank=True, help_text='Número de operación del pedimento', max_length=20, null=True)),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Fecha de creación del registro')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Fecha de última actualización del registro')),
('organizacion', models.ForeignKey(help_text='Organización a la que pertenece el pedimento', on_delete=django.db.models.deletion.CASCADE, related_name='pedimentos', to='organization.organizacion')),
('tipo_operacion', models.ForeignKey(blank=True, help_text='Tipo de operación del pedimento', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='pedimentos', to='customs.tipooperacion')),
],
options={
'verbose_name': 'Pedimento',
'verbose_name_plural': 'Pedimentos',
'db_table': 'pedimento',
'ordering': ['pedimento'],
},
),
migrations.CreateModel(
name='EDocument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero_edocument', models.CharField(help_text='Número único del e-documento', max_length=20, unique=True)),
('clave', models.CharField(blank=True, help_text='Clave del e-documento según la clasificación aduanera', max_length=10, null=True)),
('cadena_original', models.TextField(blank=True, help_text='Cadena original del e-documento', null=True)),
('sello_digital', models.TextField(blank=True, help_text='Firma digital del e-documento', null=True)),
('descripcion', models.CharField(blank=True, help_text='Descripción del documento', max_length=200, null=True)),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Fecha de creación del documento')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Fecha de última actualización del documento')),
('organizacion', models.ForeignKey(help_text='Organización a la que pertenece el EDocument', on_delete=django.db.models.deletion.CASCADE, related_name='edocuments', to='organization.organizacion')),
('pedimento', models.ForeignKey(help_text='Pedimento asociado al documento', on_delete=django.db.models.deletion.CASCADE, related_name='documentos', to='customs.pedimento')),
],
options={
'verbose_name': 'EDocument',
'verbose_name_plural': 'EDocuments',
'db_table': 'edocs',
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='ProcesamientoPedimento',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('estado', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='procesamientos', to='customs.estadodeprocesamiento')),
('organizacion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='procesamientos', to='organization.organizacion')),
('pedimento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='procesamientos', to='customs.pedimento')),
('servicio', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='procesamientos', to='customs.servicio')),
('tipo_procesamiento', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='procesamientos', to='customs.tipodeprocesamiento')),
],
options={
'verbose_name': 'Procesamiento de Pedimento',
'verbose_name_plural': 'Procesamientos de Pedimento',
'db_table': 'procesamiento_pedimento',
'ordering': ['created_at'],
},
),
]

View File

@@ -0,0 +1,36 @@
# Generated by Django 5.2.3 on 2025-07-15 14:36
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('customs', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='agenteaduanal',
name='id_aduana',
),
migrations.RemoveField(
model_name='agenteaduanal',
name='id_patente',
),
migrations.DeleteModel(
name='ClavePedimento',
),
migrations.DeleteModel(
name='Regimen',
),
migrations.DeleteModel(
name='Aduana',
),
migrations.DeleteModel(
name='AgenteAduanal',
),
migrations.DeleteModel(
name='Patente',
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.3 on 2025-07-23 22:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0002_remove_agenteaduanal_id_aduana_and_more'),
('organization', '0002_remove_organizacion_membretado_and_more'),
]
operations = [
migrations.CreateModel(
name='Cove',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('numero_cove', models.CharField(help_text='Número único de la cove', max_length=20, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Fecha de creación de la cove')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Fecha de última actualización de la cove')),
('organizacion', models.ForeignKey(help_text='Organización a la que pertenece la cove', on_delete=django.db.models.deletion.CASCADE, related_name='coves', to='organization.organizacion')),
('pedimento', models.ForeignKey(help_text='Pedimento asociado a la cove', on_delete=django.db.models.deletion.CASCADE, related_name='coves', to='customs.pedimento')),
],
options={
'verbose_name': 'Cove',
'verbose_name_plural': 'Coves',
'db_table': 'coves',
'ordering': ['created_at'],
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.3 on 2025-08-12 19:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0003_cove'),
]
operations = [
migrations.AddField(
model_name='pedimento',
name='pedimento_app',
field=models.CharField(default='', help_text='Número de pedimento en la aplicación', max_length=25),
preserve_default=False,
),
migrations.AlterField(
model_name='pedimento',
name='pedimento',
field=models.CharField(help_text='Número de pedimento aduanal', max_length=20),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.2.3 on 2025-08-12 19:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('customs', '0004_pedimento_pedimento_app_alter_pedimento_pedimento'),
]
operations = [
migrations.RemoveField(
model_name='pedimento',
name='pedimento_app',
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.3 on 2025-08-12 19:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0005_remove_pedimento_pedimento_app'),
]
operations = [
migrations.AddField(
model_name='pedimento',
name='pedimento_app',
field=models.CharField(default='', help_text='Número de pedimento en la aplicación', max_length=25),
preserve_default=False,
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.3 on 2025-08-15 18:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0006_pedimento_pedimento_app'),
]
operations = [
migrations.CreateModel(
name='Regimen',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('claveped', models.CharField(max_length=4)),
('regimenped', models.CharField(max_length=4)),
('tipo', models.IntegerField()),
],
options={
'verbose_name': 'Regimen',
'verbose_name_plural': 'Regimenes',
'db_table': 'regimen',
},
),
]

View File

@@ -0,0 +1,139 @@
from django.db import migrations
def cargar_catalogo_regimen(apps, schema_editor):
from api.customs.models import Regimen
data = [
(1, 'A1', 'EXD', 2),
(2, 'A1', 'IMD', 1),
(3, 'A2', 'ITE', 1),
(4, 'A3', 'IMD', 1),
(5, 'A4', 'DFI', 2),
(6, 'A4', 'DFI', 1),
(7, 'A5', 'DFI', 2),
(8, 'A5', 'DFI', 1),
(9, 'A6', 'ITR', 1),
(10, 'A7', 'ITR', 1),
(11, 'A8', 'ITE', 1),
(12, 'A9', 'ITR', 1),
(13, 'AA', 'ITE', 1),
(14, 'AD', 'ITR', 1),
(15, 'AF', 'ITE', 1),
(16, 'AF', 'ITR', 1),
(17, 'AJ', 'ETR', 2),
(18, 'AJ', 'ITR', 1),
(19, 'BA', 'ETR', 2),
(20, 'BA', 'ITR', 1),
(21, 'BB', 'EXD', 2),
(22, 'BC', 'ITR', 1),
(23, 'BD', 'ITR', 1),
(24, 'BE', 'ITR', 1),
(25, 'BF', 'ETR', 2),
(26, 'BH', 'ITR', 1),
(27, 'BI', 'ITR', 1),
(28, 'BM', 'ETE', 2),
(29, 'BO', 'ETE', 2),
(30, 'BO', 'ITR', 1),
(31, 'BP', 'ITR', 1),
(32, 'BR', 'ETR', 2),
(33, 'C1', 'IMD', 1),
(34, 'C2', 'IMD', 1),
(35, 'C3', 'IMD', 1),
(36, 'CT', 'EXD', 2),
(37, 'D1', 'EXD', 2),
(38, 'D1', 'IMD', 1),
(39, 'E1', 'ITE', 1),
(40, 'E2', 'ITR', 1),
(41, 'E3', 'ITE', 1),
(42, 'E4', 'ITR', 1),
(43, 'F2', 'DFI', 1),
(44, 'F3', 'IMD', 1),
(45, 'F4', 'EXD', 2),
(46, 'F4', 'IMD', 1),
(47, 'F5', 'IMD', 1),
(48, 'F8', 'DFI', 2),
(49, 'F8', 'DFI', 1),
(50, 'F9', 'DFI', 2),
(51, 'F9', 'DFI', 1),
(52, 'G1', 'EXD', 2),
(53, 'G1', 'IMD', 1),
(54, 'G2', 'IMD', 1),
(55, 'G6', 'EXD', 2),
(56, 'G7', 'EXD', 2),
(57, 'G8', 'RFS', 2),
(58, 'G9', 'IMD', 2),
(59, 'GC', 'EXD', 2),
(60, 'GC', 'IMD', 1),
(61, 'H1', 'EXD', 2),
(62, 'H1', 'IMD', 1),
(63, 'H8', 'EXD', 2),
(64, 'H8', 'IMD', 1),
(65, 'I1', 'EXD', 2),
(66, 'I1', 'IMD', 1),
(67, 'IN', 'ITE', 1),
(68, 'J1', 'EXD', 2),
(69, 'J2', 'EXD', 2),
(70, 'J3', 'RFE', 2),
(71, 'J4', 'RFS', 2),
(72, 'K1', 'EXD', 2),
(73, 'K1', 'IMD', 1),
(74, 'K2', 'EXD', 2),
(75, 'K3', 'EXD', 2),
(76, 'L1', 'EXD', 2),
(77, 'L1', 'IMD', 1),
(78, 'M1', 'RFE', 1),
(79, 'M2', 'RFE', 1),
(80, 'M3', 'RFS', 1),
(81, 'M4', 'RFS', 1),
(82, 'M5', 'RFS', 1),
(83, 'P1', 'IMD', 1),
(84, 'R1', 'ETE', 2),
(85, 'R1', 'ETR', 2),
(86, 'R1', 'EXD', 2),
(87, 'R1', 'IMD', 1),
(88, 'R1', 'ITE', 1),
(89, 'R1', 'ITR', 1),
(90, 'RT', 'EXD', 2),
(91, 'S2', 'EXD', 2),
(92, 'S2', 'IMD', 1),
(93, 'T1', 'EXD', 2),
(94, 'T1', 'IMD', 1),
(95, 'T3', 'TRA', 1),
(96, 'T6', 'TRA', 2),
(97, 'T7', 'TRA', 1),
(98, 'T9', 'TRA', 1),
(99, 'V1', 'EXD', 2),
(100, 'V1', 'ITE', 1),
(101, 'V1', 'ITR', 1),
(102, 'V2', 'EXD', 2),
(103, 'V2', 'IMD', 1),
(104, 'V3', 'DFI', 2),
(105, 'V3', 'DFI', 1),
(106, 'V4', 'ETR', 2),
(107, 'V4', 'ITR', 1),
(108, 'V5', 'EXD', 2),
(109, 'V5', 'IMD', 1),
(110, 'V6', 'EXD', 2),
(111, 'V6', 'IMD', 1),
(112, 'V7', 'EXD', 2),
(113, 'V7', 'ITR', 1),
(114, 'V8', 'DFI', 1),
(115, 'V8', 'EXD', 2),
(116, 'V9', 'EXD', 2),
(117, 'V9', 'IMD', 1),
(118, 'VF', 'IMD', 1),
(119, 'VU', 'IMD', 1),
(120, 'H3', 'ITE', 1),
(121, 'H3', 'ITR', 1),
]
for id, claveped, regimenped, tipo in data:
Regimen.objects.create(id=id, claveped=claveped, regimenped=regimenped, tipo=tipo)
class Migration(migrations.Migration):
dependencies = [
('customs', '0007_regimen'),
]
operations = [
migrations.RunPython(cargar_catalogo_regimen),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.3 on 2025-08-16 15:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0008_regimen_catalogo'),
('organization', '0002_remove_organizacion_membretado_and_more'),
]
operations = [
migrations.CreateModel(
name='Importador',
fields=[
('rfc', models.CharField(help_text='RFC del importador', max_length=13, primary_key=True, serialize=False, unique=True)),
('nombre', models.CharField(help_text='Nombre del importador', max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Fecha de creación del registro')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Fecha de última actualización del registro')),
('organizacion', models.ForeignKey(help_text='Organización a la que pertenece el importador', on_delete=django.db.models.deletion.CASCADE, related_name='importadores', to='organization.organizacion')),
],
options={
'verbose_name': 'Importador',
'verbose_name_plural': 'Importadores',
'db_table': 'importador',
'ordering': ['rfc'],
},
),
]

View File

@@ -0,0 +1,21 @@
from django.db import migrations
def crear_importadores_desde_pedimentos(apps, schema_editor):
Pedimento = apps.get_model('customs', 'Pedimento')
Importador = apps.get_model('customs', 'Importador')
Organizacion = apps.get_model('organization', 'Organizacion')
rfcs_orgs = Pedimento.objects.values_list('contribuyente', 'organizacion_id').distinct()
for rfc, org_id in rfcs_orgs:
if rfc and not Importador.objects.filter(rfc=rfc).exists():
organizacion = Organizacion.objects.get(id=org_id) if org_id else None
Importador.objects.create(rfc=rfc, nombre='', organizacion=organizacion)
class Migration(migrations.Migration):
dependencies = [
('customs', '0009_importador'),
]
operations = [
migrations.RunPython(crear_importadores_desde_pedimentos),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.3 on 2025-08-16 16:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0009b_poblar_importadores'),
]
operations = [
migrations.AlterField(
model_name='pedimento',
name='contribuyente',
field=models.ForeignKey(blank=True, help_text='Contribuyente asociado al pedimento', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pedimentos', to='customs.importador'),
),
]

View File

187
api/customs/models.py Normal file
View File

@@ -0,0 +1,187 @@
import uuid
from django.db import models
# Create your models here.
class TipoOperacion(models.Model):
tipo = models.CharField(max_length=100)
descripcion = models.CharField(max_length=200)
def __str__(self):
return f"{self.tipo}"
class Meta:
verbose_name = "Tipo de Operacion"
verbose_name_plural = "Tipos de Operacion"
db_table = 'tipo_operacion'
ordering = ['tipo']
class Pedimento(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
pedimento = models.CharField(max_length=20, unique=False, help_text="Número de pedimento aduanal")
pedimento_app = models.CharField(max_length=25, unique=False, help_text="Número de pedimento en la aplicación")
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='pedimentos', help_text="Organización a la que pertenece el pedimento")
patente = models.CharField(max_length=20, blank=True, null=True, help_text="Número de patente aduanal")
aduana = models.CharField(max_length=10, blank=True, null=True, help_text="Clave de la aduana según la clasificación aduanera")
regimen = models.CharField(max_length=10, blank=True, null=True, help_text="Clave del régimen aduanero según la clasificación aduanera")
tipo_operacion = models.ForeignKey('TipoOperacion', on_delete=models.SET_NULL, blank=True, null=True, help_text="Tipo de operación del pedimento", related_name='pedimentos')
clave_pedimento = models.CharField(max_length=10, blank=True, null=True, help_text="Clave del pedimento según la clasificación aduanera")
fecha_inicio = models.DateField(help_text="Fecha de inicio del pedimento", blank=True, null=True)
fecha_fin = models.DateField(help_text="Fecha de fin del pedimento", blank=True, null=True)
fecha_pago = models.DateField(help_text="Fecha de pago del pedimento", blank=True, null=True)
alerta = models.BooleanField(default=False, help_text="Indica si el pedimento tiene una alerta asociada")
contribuyente = models.ForeignKey('Importador', on_delete=models.CASCADE, related_name='pedimentos', help_text="Contribuyente asociado al pedimento", blank=True, null=True)
agente_aduanal = models.CharField(max_length=100, blank=True, null=True, help_text="RFC del agente aduanal")
curp_apoderado = models.CharField(max_length=18, blank=True, null=True, help_text="CURP del apoderado aduanal")
importe_total = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, help_text="Importe total del pedimento")
saldo_disponible = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, help_text="Saldo disponible del pedimento")
importe_pedimento = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, help_text="Importe del pedimento")
existe_expediente = models.BooleanField(default=False)
remesas = models.BooleanField(default=False, help_text="Indica si el pedimento tiene remesas asociadas")
numero_partidas = models.PositiveIntegerField(default=0, help_text="Número de partidas asociadas al pedimento", blank=True, null=True)
numero_operacion = models.CharField(max_length=20, blank=True, null=True, help_text="Número de operación del pedimento")
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación del registro")
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización del registro")
def __str__(self):
return f"{self.pedimento}"
class Meta:
verbose_name = "Pedimento"
verbose_name_plural = "Pedimentos"
db_table = 'pedimento'
ordering = ['pedimento']
class EDocument(models.Model):
pedimento = models.ForeignKey(Pedimento, on_delete=models.CASCADE, related_name='documentos', help_text="Pedimento asociado al documento")
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='edocuments', help_text="Organización a la que pertenece el EDocument")
numero_edocument = models.CharField(max_length=20, unique=True, help_text="Número único del e-documento")
clave = models.CharField(max_length=10, blank=True, null=True, help_text="Clave del e-documento según la clasificación aduanera")
cadena_original = models.TextField(blank=True, null=True, help_text="Cadena original del e-documento")
sello_digital = models.TextField(blank=True, null=True, help_text="Firma digital del e-documento")
descripcion = models.CharField(max_length=200, blank=True, null=True, help_text="Descripción del documento")
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación del documento")
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización del documento")
def __str__(self):
return f"{self.descripcion} - {self.pedimento.pedimento}"
class Meta:
verbose_name = "EDocument"
verbose_name_plural = "EDocuments"
db_table = 'edocs'
ordering = ['created_at']
class Cove(models.Model):
pedimento = models.ForeignKey(Pedimento, on_delete=models.CASCADE, related_name='coves', help_text="Pedimento asociado a la cove")
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='coves', help_text="Organización a la que pertenece la cove")
numero_cove = models.CharField(max_length=20, unique=True, help_text="Número único de la cove")
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación de la cove")
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización de la cove")
def __str__(self):
return f"{self.numero_cove} - {self.pedimento.pedimento}"
class Meta:
verbose_name = "Cove"
verbose_name_plural = "Coves"
db_table = 'coves'
ordering = ['created_at']
class EstadoDeProcesamiento(models.Model):
estado = models.CharField(max_length=50)
def __str__(self):
return self.estado
class Meta:
verbose_name = "Estado de Procesamiento"
verbose_name_plural = "Estados de Procesamiento"
db_table = 'estado_de_procesamiento'
ordering = ['estado']
class TipoDeProcesamiento(models.Model):
tipo = models.CharField(max_length=50)
def __str__(self):
return self.tipo
class Meta:
verbose_name = "Tipo de Procesamiento"
verbose_name_plural = "Tipos de Procesamiento"
db_table = 'tipo_de_procesamiento'
ordering = ['tipo']
class Servicio(models.Model):
endpoint = models.CharField(max_length=100)
descripcion = models.TextField(blank=True, null=True)
hora_inicio = models.TimeField(max_length=50, blank=True, null=True)
hora_fin = models.TimeField(max_length=50, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.endpoint
class Meta:
verbose_name = "Servicio"
verbose_name_plural = "Servicios"
db_table = 'servicio'
ordering = ['endpoint']
class ProcesamientoPedimento(models.Model):
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='procesamientos')
estado = models.ForeignKey(EstadoDeProcesamiento, on_delete=models.CASCADE, related_name='procesamientos')
tipo_procesamiento = models.ForeignKey(TipoDeProcesamiento, on_delete=models.CASCADE, related_name='procesamientos', blank=True, null=True)
pedimento = models.ForeignKey(Pedimento, on_delete=models.CASCADE, related_name='procesamientos')
servicio = models.ForeignKey(Servicio, on_delete=models.CASCADE, related_name='procesamientos', blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.pedimento.pedimento} - {self.estado.estado}"
class Meta:
verbose_name = "Procesamiento de Pedimento"
verbose_name_plural = "Procesamientos de Pedimento"
db_table = 'procesamiento_pedimento'
ordering = ['created_at']
class Regimen(models.Model):
id = models.AutoField(primary_key=True)
claveped = models.CharField(max_length=4)
regimenped = models.CharField(max_length=4)
tipo = models.IntegerField()
class Meta:
db_table = 'regimen'
verbose_name = 'Regimen'
verbose_name_plural = 'Regimenes'
def __str__(self):
return f"{self.claveped} - {self.regimenped} - {self.tipo}"
class Importador(models.Model):
rfc = models.CharField(primary_key=True, max_length=13, unique=True, help_text="RFC del importador")
nombre = models.CharField(max_length=200, help_text="Nombre del importador")
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='importadores', help_text="Organización a la que pertenece el importador")
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación del registro")
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización del registro")
class Meta:
verbose_name = 'Importador'
verbose_name_plural = 'Importadores'
db_table = 'importador'
ordering = ['rfc']
def __str__(self):
return f"{self.rfc} - {self.nombre}"

View File

@@ -0,0 +1,96 @@
from rest_framework import serializers
from api.customs.models import (
Pedimento,
TipoOperacion,
ProcesamientoPedimento,
EDocument,
Cove,
Importador
)
from django.db import models
from api.record.models import Document # Asegúrate de importar el modelo Documento
from api.record.serializers import DocumentSerializer
from api.vucem.serializers import VucemSerializer
class PedimentoSerializer(serializers.ModelSerializer):
documentos_count = serializers.SerializerMethodField()
documentos_peso_total = serializers.SerializerMethodField()
def get_documentos_count(self, obj):
# Si obj es un dict o no tiene 'documents', devuelve 0
if isinstance(obj, dict) or not hasattr(obj, 'documents'):
return 0
return obj.documents.count()
def get_documentos_peso_total(self, obj):
# Si obj es un dict o no tiene 'documents', devuelve 0
if isinstance(obj, dict) or not hasattr(obj, 'documents'):
return 0
return obj.documents.aggregate(total=models.Sum('size'))['total'] or 0
class Meta:
model = Pedimento
fields = '__all__'
read_only_fields = (
'created_at', 'updated_at', 'organizacion', 'pedimento_app',
'documentos_count', 'documentos_peso_total'
)
def to_representation(self, instance):
rep = super().to_representation(instance)
rep['documentos_count'] = self.get_documentos_count(instance)
rep['documentos_peso_total'] = self.get_documentos_peso_total(instance)
return rep
class TipoOperacionSerializer(serializers.ModelSerializer):
class Meta:
model = TipoOperacion
fields = '__all__'
class ProcesamientoPedimentoSerializer(serializers.ModelSerializer):
organizacion = serializers.PrimaryKeyRelatedField(queryset=ProcesamientoPedimento._meta.get_field('organizacion').related_model.objects.all(), required=False)
organizacion_name = serializers.CharField(source='organizacion.nombre', read_only=True)
class Meta:
model = ProcesamientoPedimento
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
request = self.context.get('request')
# Si no es superusuario, hacer organizacion read_only
if request and hasattr(request, 'user') and not request.user.is_superuser:
self.fields['organizacion'].read_only = True
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['pedimento'] = PedimentoSerializer(instance.pedimento).data
return representation
class EDocumentSerializer(serializers.ModelSerializer):
class Meta:
model = EDocument
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Si no es superusuario, hacer organizacion read_only
request = self.context.get('request')
if request and hasattr(request, 'user') and not request.user.is_superuser:
self.fields['organizacion'].read_only = True
class CoveSerializer(serializers.ModelSerializer):
class Meta:
model = Cove
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')
class ImportadorSerializer(serializers.ModelSerializer):
class Meta:
model = Importador
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')

View File

View File

@@ -0,0 +1,59 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db import transaction
from time import sleep
from api.customs.models import Pedimento, ProcesamientoPedimento, Cove, EDocument
from api.customs.tasks.internal_services import (
crear_procesamiento_remesa,
crear_procesamiento_partida,
crear_procesamiento_cove,
crear_procesamiento_acuse_cove,
crear_procesamiento_acuse,
crear_procesamiento_edocument
)
from api.customs.tasks.microservice import (
ejecutar_pedimento_completo,
procesar_pedimento_completo_individual
)
@receiver(post_save, sender=Pedimento)
def trigger_celery_task_on_create(sender, instance, created, **kwargs):
if created:
procesar_pedimento_completo_individual.apply_async(args=[instance.id, instance.organizacion.id])
@receiver(post_save, sender=Pedimento)
def trigger_celery_task_on_update(sender, instance, created,**kwargs):
if not created:
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info(f"Pedimento actualizado: {instance.id}, verificando servicios a crear...")
sleep(4)
def enqueue_tasks():
if instance.remesas:
logger.info(f"Creando proceso de remesas para pedimento {instance.id}")
crear_procesamiento_remesa.apply_async(args=[str(instance.id)])
if hasattr(instance, 'numero_partidas') and instance.numero_partidas and instance.numero_partidas > 0:
logger.info(f"Creando proceso de partida para pedimento {instance.id}")
crear_procesamiento_partida.apply_async(args=[str(instance.id)])
transaction.on_commit(enqueue_tasks)
@receiver(post_save, sender=Cove)
def trigger_celery_task_on_cove_create(sender, instance, created, **kwargs):
if created:
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info(f"Cove creado: {instance.id}, creando procesamiento...")
crear_procesamiento_cove.apply_async(args=[str(instance.pedimento.id)])
crear_procesamiento_acuse_cove.apply_async(args=[str(instance.pedimento.id)])
@receiver(post_save, sender=EDocument)
def trigger_celery_task_on_edocument_create(sender, instance, created, **kwargs):
if created:
import logging
logger = logging.getLogger('api.customs.async_operations')
logger.info(f"EDocument creado: {instance.id}, creando procesamiento...")
crear_procesamiento_edocument.apply_async(args=[str(instance.pedimento.id)])
crear_procesamiento_acuse.apply_async(args=[str(instance.pedimento.id)])

View File

@@ -0,0 +1,2 @@
from .microservice import *
from .internal_services import *

View File

@@ -0,0 +1,78 @@
from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument
from core.utils import xml_controller
import requests
from core.utils import xml_remesas_controller
def obtener_pedimentos(organizacion_id):
return Pedimento.objects.filter(organizacion_id=organizacion_id)
def extraer_coves(pedimento):
remesas = pedimento.documents.filter(document_type=3).first()
with open(f'./media/{remesas.archivo}', 'r') as f:
xml_content = f.read()
xml_data = xml_remesas_controller.extract_remesas(xml_content)
return xml_data
@shared_task
def auditar_procesamiento_remesas(organizacion_id):
pedimentos = obtener_pedimentos(organizacion_id)
for pedimento in pedimentos:
if pedimento.remesas:
# Tipo 3: Remesa
if not pedimento.documents.filter(document_type=3).exists():
ProcesamientoPedimento.objects.get_or_create(
pedimento=pedimento,
servicio_id=5, # ID del servicio de remesas
organizacion=organizacion_id
)
else:
xml_data = extraer_coves(pedimento)
if xml_data:
for remesa in xml_data:
Cove.objects.get_or_create(
pedimento=pedimento,
numero_cove=remesa.get('remesaSA'),
organizacion=organizacion_id
)
@shared_task
def auditar_partidas(organizacion_id):
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
for pedimento in pedimentos:
partidas_descargadas = pedimento.documents.filter(document_type=1)
partidas = {str(documento.archivo).split('_')[-1].split('.')[0]: documento.archivo for documento in partidas_descargadas}
partidas_faltantes = []
for i in range(1, pedimento.numero_partidas + 1):
if str(i) not in partidas.keys():
partidas_faltantes.append(i)
# crear servicio individual para cada partida faltante en microservicios
@shared_task
def auditar_coves(organizacion_id):
# crear servicio individual para cada cove faltante en microservicios
pass
@shared_task
def auditar_edocuments(organizacion_id):
# crear servicio individual para cada Edocument faltante en microservicios
pass
@shared_task
def auditar_acuse_coves(organizacion_id):
# crear servicio individual para cada cove faltante en microservicios
pass
@shared_task
def auditar_acuse_edocuments(organizacion_id):
# crear servicio individual para cada acuse de edocument faltante en microservicios
pass

View File

@@ -0,0 +1,220 @@
from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento, Pedimento, Cove, EDocument
from core.utils import xml_controller
@shared_task
def crear_procesamiento_remesa(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_remesa para pedimento {pedimento_id}")
if pedimento.remesas:
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=5, # ID del servicio de remesas
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento remesa creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=5,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_partida(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_partida para pedimento {pedimento_id}")
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=4, # ID del servicio de partidas
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento partida creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=4,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_cove(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_cove para pedimento {pedimento_id}")
if pedimento.coves.exists():
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=8, # ID del servicio de Coves
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento cove creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=8,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_acuse(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_acuse para pedimento {pedimento_id}")
if pedimento.coves.exists():
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=6, # ID del servicio de Acuse Cove
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento acuse creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=6,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_acuse_cove(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_acuse_cove para pedimento {pedimento_id}")
if pedimento.coves.exists():
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=9, # ID del servicio de Acuse Cove
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento acuse_cove creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=9,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_edocument(pedimento_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimento = Pedimento.objects.get(id=pedimento_id)
logger.info(f"[TAREA] crear_procesamiento_edocument para pedimento {pedimento_id}")
if pedimento.documentos.exists():
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=7, # ID del servicio de EDocument
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento edocument creado para pedimento {pedimento_id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=7,
organizacion=pedimento.organizacion
)
@shared_task
def crear_procesamiento_pedimento_completo(organizacion_id):
import logging
logger = logging.getLogger('api.customs.async_operations')
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
for pedimento in pedimentos:
logger.info(f"[TAREA] crear_procesamiento_pedimento_completo para pedimento {pedimento.id}")
existe = ProcesamientoPedimento.objects.filter(
pedimento=pedimento,
servicio_id=3, # ID del servicio de Pedimento Completo
organizacion=pedimento.organizacion,
estado_id__in=[1, 2, 3, 4]
).exists()
if not existe:
logger.info(f"[TAREA] ProcesamientoPedimento pedimento_completo creado para pedimento {pedimento.id}")
ProcesamientoPedimento.objects.create(
pedimento=pedimento,
estado_id=1, # Estado "pendiente"
servicio_id=3,
organizacion=pedimento.organizacion
)
@shared_task
def crear_servicios(organizacion_id):
pedimentos = Pedimento.objects.filter(organizacion=organizacion_id)
for pedimento in pedimentos:
crear_procesamiento_remesa.apply_async(args=[str(pedimento.id)])
crear_procesamiento_partida.apply_async(args=[str(pedimento.id)])
crear_procesamiento_cove.apply_async(args=[str(pedimento.id)])
crear_procesamiento_acuse.apply_async(args=[str(pedimento.id)])
crear_procesamiento_acuse_cove.apply_async(args=[str(pedimento.id)])
crear_procesamiento_edocument.apply_async(args=[str(pedimento.id)])
@shared_task
def auditar_pedimento(organizacion_id):
pedimentos = Pedimento.objects.filter(organizacion_id=organizacion_id)
for pedimento in pedimentos:
pc = pedimento.documents.filter(document_type__id=2).first()
if pc:
with open(f'./media/{pc.archivo}', 'r') as f:
xml_content = f.read()
xml_data = xml_controller.extract_data(xml_content)
pedimento.numero_operacion = xml_data.get('numero_operacion')
pedimento.curp_apoderado = xml_data.get('curp_apoderado')
pedimento.agente_aduanal = xml_data.get('agente_aduanal')
pedimento.numero_partidas = xml_data.get('numero_partidas')
pedimento.remesas = xml_data.get('remesas')
pedimento.tipo_operacion__id = xml_data.get('tipo_operacion')
pedimento.save()
for edoc in xml_data.get('edocuments', []):
EDocument.objects.get_or_create(
pedimento=pedimento,
organizacion=pedimento.organizacion,
clave=edoc.get('clave'),
descripcion=edoc.get('descripcion'),
numero_edocument=edoc.get('complemento1')
)
from django.db import IntegrityError
try:
for cove in xml_data.get('coves', []):
try:
Cove.objects.get_or_create(
pedimento=pedimento,
organizacion=pedimento.organizacion,
numero_cove=cove
)
except IntegrityError:
# Si ya existe por unique, recupera el objeto existente
Cove.objects.get(numero_cove=cove)
except:
# Si ya existe por unique, recupera el objeto existente
pass
@shared_task
def crear_todos_los_servicios():
from organization.models import Organizacion
organizaciones = Organizacion.objects.all()
for org in organizaciones:
crear_procesamiento_pedimento_completo.apply_async(args=[str(org.id)])
crear_servicios.apply_async(args=[str(org.id)])

View File

@@ -0,0 +1,213 @@
from celery import shared_task, group
from api.customs.models import ProcesamientoPedimento
import requests
from config.settings import SERVICE_API_URL
from datetime import datetime
# ===================
# Pedimento Completo
# ===================
@shared_task
def procesar_pedimento_completo_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/pedimento_completo",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Pedimento {pedimento_id} procesado correctamente.")
else:
print(f"Error al procesar el pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_pedimento_completo():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=3)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_pedimento_completo_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_pedimento_completo: fuera de horario permitido (5:00-22:00). Abortando.')
return
# ===================
# Partidas
# ===================
@shared_task
def procesar_partida_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/partidas",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Partidas del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar partidas del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_partidas_pedimento():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=4)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_partida_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_partidas_pedimento: fuera de horario permitido (5:00-22:00). Abortando.')
return
# ===================
# Remesas
# ===================
@shared_task
def procesar_remesa_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/remesas",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Remesas del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar remesas del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_remesas():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=5)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_remesa_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_remesas: fuera de horario permitido (5:00-22:00). Abortando.')
return
# ===================
# Acuses
# ===================
@shared_task
def procesar_acuse_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/acuse",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Acuses del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar Acuses del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_acuse():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=6)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_acuse_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_acuse: fuera de horario permitido (5:00-22:00). Abortando.')
return
# ===================
# Edocuments
# ===================
@shared_task
def procesar_edoc_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/edocument",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Edocuments del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar Edocuments del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_edocs():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=7)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_edoc_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# ===================
# Coves
# ===================
@shared_task
def procesar_cove_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/coves",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Coves del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar Coves del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_coves():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=8)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_cove_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_coves: fuera de horario permitido (5:00-22:00). Abortando.')
return
# ===================
# Acuse Cove
# ===================
@shared_task
def procesar_acuse_cove_individual(pedimento_id, organizacion_id):
response = requests.post(
f"{SERVICE_API_URL}/async/services/acuse-cove",
json={"pedimento": str(pedimento_id), "organizacion": str(organizacion_id)}
)
if response.status_code == 200:
print(f"Coves del pedimento {pedimento_id} procesadas correctamente.")
else:
print(f"Error al procesar Coves del pedimento {pedimento_id}: {response.status_code} - {response.text}")
print(f"Disparando evento para procesamiento {pedimento_id}")
@shared_task
def ejecutar_acuseCoves():
pendientes = ProcesamientoPedimento.objects.filter(estado=1, servicio=9)
batch_size = 20
ids = list(pendientes.values_list('pedimento_id', 'organizacion_id'))
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
job = group(procesar_acuse_cove_individual.s(ped_id, org_id) for ped_id, org_id in batch)
job.apply_async()
# Validar horario permitido (5:00 a 22:00)
ahora = datetime.now().time()
if (ahora < datetime.strptime('05:00', '%H:%M').time()) or (ahora >= datetime.strptime('22:00', '%H:%M').time()):
print('ejecutar_acuseCoves: fuera de horario permitido (5:00-22:00). Abortando.')
return

77
api/customs/tests.py Normal file
View File

@@ -0,0 +1,77 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from api.organization.models import Organizacion
from .models import Pedimento, TipoOperacion, ProcesamientoPedimento, EDocument
User = get_user_model()
class CustomsViewsTests(APITestCase):
def setUp(self):
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
self.org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
self.admin.groups.create(name="admin")
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
self.importador.groups.create(name="importador")
self.client = APIClient()
def test_admin_sees_only_own_pedimentos(self):
from .models import Pedimento
p1 = Pedimento.objects.create(pedimento="P1", organizacion=self.org)
p2 = Pedimento.objects.create(pedimento="P2", organizacion=self.org2)
self.client.force_authenticate(user=self.admin)
url = reverse('Pedimento-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
pedimentos = [p['pedimento'] for p in response.data]
self.assertIn("P1", pedimentos)
self.assertNotIn("P2", pedimentos)
def test_superuser_sees_all_pedimentos(self):
from .models import Pedimento
p1 = Pedimento.objects.create(pedimento="P1", organizacion=self.org)
p2 = Pedimento.objects.create(pedimento="P2", organizacion=self.org2)
self.client.force_authenticate(user=self.superuser)
url = reverse('Pedimento-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
pedimentos = [p['pedimento'] for p in response.data]
self.assertIn("P1", pedimentos)
self.assertIn("P2", pedimentos)
def test_importador_cannot_create_pedimento(self):
self.client.force_authenticate(user=self.importador)
url = reverse('Pedimento-list')
data = {
"pedimento": "P3",
"patente": "1234",
"aduana": "001",
"regimen": "A1",
"clave_pedimento": "A1",
"contribuyente": "ImportadorTest"
}
response = self.client.post(url, data)
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_list_tipos_operacion(self):
url = reverse('TipoOperacion-list')
self.client.force_authenticate(user=self.admin)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_list_procesamientos(self):
url = reverse('ProcesamientoPedimento-list')
self.client.force_authenticate(user=self.admin)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_list_edocuments(self):
url = reverse('EDocument-list')
self.client.force_authenticate(user=self.admin)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

34
api/customs/urls.py Normal file
View File

@@ -0,0 +1,34 @@
# This file defines the URL patterns for the customs app in a Django project.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
# import necessary viewsets
from .views import (
ViewSetPedimento,
ViewSetTipoOperacion,
ViewSetProcesamientoPedimento,
ViewSetEDocument,
ViewSetCove,
ImportadorViewSet
)
# from .views import YourViewSet # Import your viewsets here
router = DefaultRouter()
# Register your viewsets with the router here
# Example:
# from .views import MyViewSet
# router.register(r'myviewset', MyViewSet, basename='myviewset')
router.register(r'pedimentos', ViewSetPedimento, basename='Pedimento')
router.register(r'tiposoperacion', ViewSetTipoOperacion, basename='TipoOperacion')
router.register(r'procesamientopedimentos', ViewSetProcesamientoPedimento, basename='ProcesamientoPedimento')
router.register(r'edocuments', ViewSetEDocument, basename='EDocument')
router.register(r'coves', ViewSetCove, basename='Cove')
router.register(r'importadores', ImportadorViewSet, basename='Importador')
# Import your viewsets here
urlpatterns = [
path('', include(router.urls)),
]

490
api/customs/views.py Normal file
View File

@@ -0,0 +1,490 @@
from config.settings import SERVICE_API_URL
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.pagination import PageNumberPagination
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.exceptions import PermissionDenied
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
from api.customs.models import (
Pedimento,
TipoOperacion,
ProcesamientoPedimento,
EDocument,
Cove,
Importador
)
from api.customs.serializers import (
PedimentoSerializer,
TipoOperacionSerializer,
ProcesamientoPedimentoSerializer,
EDocumentSerializer,
CoveSerializer,
ImportadorSerializer
)
from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin, ProcesosPorOrganizacionMixin
import requests
class CustomPagination(PageNumberPagination):
"""
Paginación personalizada con parámetros flexibles
- Si no se especifica page_size, devuelve todos los resultados (sin paginación)
- Si se especifica page_size, usa paginación normal
"""
page_size = None # Sin paginación por defecto
page_size_query_param = 'page_size'
max_page_size = 10000 # Límite máximo de seguridad
page_query_param = 'page'
def paginate_queryset(self, queryset, request, view=None):
"""
Si no se especifica page_size en los parámetros, devolver None (sin paginación)
Si se especifica, usar paginación normal
"""
# Verificar si se especificó page_size en la query
if self.page_size_query_param not in request.query_params:
# No hay page_size, devolver None para indicar "sin paginación"
return None
# Hay page_size, usar paginación normal
try:
page_size = int(request.query_params[self.page_size_query_param])
if page_size <= 0:
return None
# Establecer el page_size temporalmente para esta request
self.page_size = min(page_size, self.max_page_size)
except (ValueError, TypeError):
return None
return super().paginate_queryset(queryset, request, view)
class PedimentoPagination(PageNumberPagination):
"""
Paginación personalizada con parámetros flexibles
- Si no se especifica page_size, devuelve todos los resultados (sin paginación)
- Si se especifica page_size, usa paginación normal
"""
page_size = None # Sin paginación por defecto
page_size_query_param = 'page_size'
max_page_size = 1000 # Límite máximo de seguridad
page_query_param = 'page'
def paginate_queryset(self, queryset, request, view=None):
"""
Si no se especifica page_size en los parámetros, devolver None (sin paginación)
Si se especifica, usar paginación normal
"""
# Verificar si se especificó page_size en la query
if self.page_size_query_param not in request.query_params:
# No hay page_size, devolver None para indicar "sin paginación"
return None
# Hay page_size, usar paginación normal
try:
page_size = int(request.query_params[self.page_size_query_param])
if page_size <= 0:
return None
# Establecer el page_size temporalmente para esta request
self.page_size = min(page_size, self.max_page_size)
except (ValueError, TypeError):
return None
return super().paginate_queryset(queryset, request, view)
# Create your views here.
class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin): # Pendiente de permisos de creacion
"""
ViewSet for Pedimento model.
Soporta paginación, filtros y búsqueda.
Parámetros disponibles:
- page: Número de página (solo si se especifica page_size)
- page_size: Elementos por página (si NO se especifica, devuelve TODOS los resultados)
- search: Búsqueda en pedimento, contribuyente, agente_aduanal
- pedimento: Filtro por número de pedimento
- existe_expediente: Filtro por expediente (True/False)
- contribuyente: Filtro por contribuyente
- curp_apoderado: Filtro por curp del apoderado
- fecha_pago: Filtro por fecha de pago (YYYY-MM-DD)
- patente: Filtro por patente
- aduana: Filtro por aduana
- tipo_operacion: Filtro por tipo de operación
- clave_pedimento: Filtro por clave de pedimento
- ordering: Ordenar por campo (ej: -created_at, pedimento)
Ejemplos:
- /pedimentos/ → Devuelve TODOS los pedimentos
- /pedimentos/?page_size=10 → Devuelve los primeros 10
- /pedimentos/?page_size=10&page=2 → Devuelve los pedimentos 11-20
- /pedimentos/?pedimento=12345678 → Filtra por número de pedimento
- /pedimentos/?existe_expediente=true → Filtra por expediente existente
- /pedimentos/?contribuyente=EMPRESA → Filtra por contribuyente
- /pedimentos/?curp_apoderado=XXXX → Filtra por curp apoderado
- /pedimentos/?fecha_pago=2025-07-18 → Filtra por fecha de pago
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = PedimentoSerializer
pagination_class = PedimentoPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
model = Pedimento
filterset_fields = ['patente', 'aduana', 'tipo_operacion', 'clave_pedimento', 'pedimento', 'existe_expediente', 'contribuyente', 'curp_apoderado', 'fecha_pago', 'pedimento_app']
search_fields = ['pedimento', 'pedimento_app', 'agente_aduanal', 'clave_pedimento']
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion() # Tambien filtra por importador
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un pedimento.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
data = serializer.validated_data
if not data.get('pedimento_app'):
fecha_pago = data.get('fecha_pago')
aduana = data.get('aduana')
patente = data.get('patente')
pedimento = data.get('pedimento')
if fecha_pago and aduana and patente and pedimento:
pedimento_app = f"{str(fecha_pago.year)[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento).zfill(7)[-7:]}"
serializer.save(organizacion=self.request.user.organizacion, pedimento_app=pedimento_app)
return
try:
# Usar el nombre del servicio de Docker Compose en lugar de localhost
response = requests.request('POST', f'{SERVICE_API_URL}/services/pedimento', params={},
json={
'estado': 1,
'servicio': 3,
'tipo_procesamiento': 2,
'pedimento': str(serializer.instance.id),
'organizacion': str(self.request.user.organizacion.id),
},
timeout=10
)
# Verificar si la respuesta fue exitosa
if response.status_code == 200:
print(f"✅ Servicio FastAPI ejecutado exitosamente: {response.status_code}")
print(f"📄 Respuesta: {response.json()}")
elif response.status_code == 201:
print(f"✅ Recurso creado exitosamente en FastAPI: {response.status_code}")
print(f"📄 Respuesta: {response.json()}")
else:
print(f"⚠️ Servicio FastAPI respondió con error: {response.status_code}")
print(f"📄 Respuesta: {response.text}")
except requests.exceptions.ConnectionError as e:
print(f"❌ No se pudo conectar al servicio FastAPI: {e}")
print(f"🔧 Verifica que el servicio FastAPI esté corriendo en {SERVICE_API_URL}")
except requests.exceptions.Timeout as e:
print(f"⏰ Timeout al conectar con el servicio FastAPI: {e}")
except requests.exceptions.RequestException as e:
print(f"🚨 Error de request al servicio FastAPI: {e}")
except Exception as e:
print(f"💥 Error inesperado al llamar al servicio FastAPI: {e}")
my_tags = ['Pedimentos']
class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
"""
ViewSet for TipoOperacion model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
queryset = TipoOperacion.objects.all()
serializer_class = TipoOperacionSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['tipo']
search_fields = ['tipo', 'descripcion']
ordering_fields = ['tipo', 'descripcion']
ordering = ['tipo']
my_tags = ['Tipos_Operacion']
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un tipo de operación.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
# Solo el supoerusuario puede crear tipos de operación
if not self.request.user.is_superuser:
raise PermissionDenied("Solo los superusuarios pueden crear tipos de operación")
serializer.save(organizacion=self.request.user.organizacion)
def perform_update(self, serializer):
"""
Solo el superusuario puede actualizar tipos de operación.
"""
if not self.request.user.is_superuser:
raise PermissionDenied("Solo los superusuarios pueden actualizar tipos de operación")
serializer.save()
class ViewSetProcesamientoPedimento(viewsets.ModelViewSet, ProcesosPorOrganizacionMixin):
"""
ViewSet for ProcesamientoPedimento model.
Soporta paginación, filtros y búsqueda.
Parámetros disponibles:
- page: Número de página (solo si se especifica page_size)
- page_size: Elementos por página (si NO se especifica, devuelve TODOS los resultados)
- pedimento: Filtro por pedimento
- estado: Filtro por estado
- servicio: Filtro por servicio
- tipo_procesamiento: Filtro por tipo de procesamiento
- ordering: Ordenar por campo (ej: -created_at, -updated_at)
Ejemplos:
- /procesamientopedimentos/ → Devuelve TODOS los procesamientos
- /procesamientopedimentos/?page_size=5 → Devuelve los primeros 5
"""
permission_classes = [IsAuthenticated, IsSuperUser | IsSameOrganizationDeveloper ]
serializer_class = ProcesamientoPedimentoSerializer
pagination_class = CustomPagination
model = ProcesamientoPedimento
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = {
'pedimento': ['exact'],
'pedimento__pedimento_app': ['exact', 'icontains'],
'estado': ['exact'],
'servicio': ['exact'],
'tipo_procesamiento': ['exact'],
}
search_fields = ['pedimento__pedimento_app', 'pedimento__pedimento']
ordering_fields = ['created_at', 'updated_at']
ordering = ['-created_at']
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna siempre la organización al crear un procesamiento de pedimento.
- Para superusuarios: requiere que la organización venga explícitamente en los datos validados.
- Para usuarios normales: asigna la organización del usuario autenticado.
"""
user = self.request.user
if not user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, debe venir la organización en los datos validados
if user.is_superuser:
organizacion = serializer.validated_data.get('organizacion', None)
if not organizacion:
raise ValueError("El superusuario debe especificar una organización al crear el procesamiento de pedimento.")
serializer.save()
return
# Para usuarios normales, asignar siempre la organización del usuario
if not hasattr(user, 'organizacion') or not user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=user.organizacion)
def perform_update(self, serializer):
"""
Permite actualizar un procesamiento de pedimento, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
if self.request.user.is_superuser:
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para actualizar ProcesamientoPedimento")
my_tags = ['Procesamientos_Pedimentos']
class ViewSetEDocument(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for EDocument model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = EDocumentSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['pedimento', 'numero_edocument', 'organizacion']
search_fields = ['numero_edocument', 'descripcion', 'organizacion']
ordering_fields = ['created_at', 'updated_at', 'numero_edocument']
ordering = ['-created_at']
model = EDocument
my_tags = ['EDocuments']
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un EDocument.
Para superusuarios, permite especificar una organización diferente.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario y se especifica organizacion en los datos validados
if self.request.user.is_superuser:
# Permitir que el superusuario especifique la organización
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para crear EDocument")
def perform_update(self, serializer):
"""
Permite actualizar un EDocument, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
raise ValueError("Usuario no autenticado o sin permisos para actualizar EDocument")
class ViewSetCove(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for Cove model.
"""
permission_classes = [IsAuthenticated & (IsSuperUser |IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
serializer_class = CoveSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['pedimento', 'numero_cove', 'organizacion']
search_fields = ['numero_cove', 'descripcion', 'organizacion']
ordering_fields = ['created_at', 'updated_at', 'numero_cove']
ordering = ['-created_at']
model = Cove
my_tags = ['Coves']
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
"""
Asigna automáticamente la organización del usuario autenticado al crear un Cove.
Para superusuarios, permite especificar una organización diferente.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario y se especifica organizacion en los datos validados
if self.request.user.is_superuser:
# Permitir que el superusuario especifique la organización
serializer.save()
return
if (
self.request.user.groups.filter(name='developer').exists()
or self.request.user.groups.filter(name='admin').exists()
or self.request.user.groups.filter(name='user').exists()
) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para crear Cove")
def perform_update(self, serializer):
"""
Permite actualizar un Cove, pero solo si el usuario es superusuario o pertenece a la misma organización.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user .groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
class ImportadorViewSet(viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for Importador model.
"""
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
serializer_class = ImportadorSerializer
pagination_class = CustomPagination
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['rfc', 'nombre', 'organizacion']
search_fields = ['rfc', 'nombre']
ordering_fields = ['created_at', 'updated_at', 'rfc']
ordering = ['-created_at']
model = Importador
def get_queryset(self):
return self.get_queryset_filtrado_por_organizacion()
def perform_create(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
serializer.save(organizacion=self.request.user.organizacion)
def perform_update(self, serializer):
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
# Si es superusuario, permite actualizar sin restricciones
if self.request.user.is_superuser:
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
# Para usuarios normales, usar siempre su organización
if not hasattr(self.request.user, 'organizacion') or not self.request.user.organizacion:
raise ValueError("Usuario sin organización")
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("Usuario no autenticado o sin permisos para actualizar Importador")
my_tags = ['Importadores']

View File

44
api/datastage/admin.py Normal file
View File

@@ -0,0 +1,44 @@
from django.contrib import admin
from .models import (
DataStage, Registro500,
Registro501, Registro502,
Registro503, Registro504,
Registro505, Registro506,
Registro507, Registro508,
Registro509, Registro510,
Registro511, Registro512,
Registro520, Registro551,
Registro552, Registro553,
Registro554, Registro555,
Registro556, Registro557,
Registro558, RegistroSel,
Registro701, Registro702
)
# Register your models here.
admin.site.register(DataStage)
admin.site.register(Registro500)
admin.site.register(Registro501)
admin.site.register(Registro502)
admin.site.register(Registro503)
admin.site.register(Registro504)
admin.site.register(Registro505)
admin.site.register(Registro506)
admin.site.register(Registro507)
admin.site.register(Registro508)
admin.site.register(Registro509)
admin.site.register(Registro510)
admin.site.register(Registro511)
admin.site.register(Registro512)
admin.site.register(Registro520)
admin.site.register(Registro551)
admin.site.register(Registro552)
admin.site.register(Registro553)
admin.site.register(Registro554)
admin.site.register(Registro555)
admin.site.register(Registro556)
admin.site.register(Registro557)
admin.site.register(Registro558)
admin.site.register(Registro701)
admin.site.register(Registro702)
admin.site.register(RegistroSel)

7
api/datastage/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class DatastageConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.datastage'

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('organization', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='DataStage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100, unique=True)),
('almacenamiento', models.PositiveIntegerField(default=0)),
('archivo', models.FileField(upload_to='datastages/')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('organizacion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='datastages', to='organization.organizacion')),
],
options={
'verbose_name': 'DataStage',
'verbose_name_plural': 'DataStages',
'db_table': 'datastage',
},
),
]

View File

@@ -0,0 +1,32 @@
# Generated by Django 5.2.3 on 2025-08-14 15:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='datastage',
name='almacenamiento',
),
migrations.RemoveField(
model_name='datastage',
name='nombre',
),
migrations.AddField(
model_name='datastage',
name='contribuyente',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
migrations.AddField(
model_name='datastage',
name='procesado',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.3 on 2025-08-14 15:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0002_remove_datastage_almacenamiento_and_more'),
('organization', '0002_remove_organizacion_membretado_and_more'),
]
operations = [
migrations.AlterField(
model_name='datastage',
name='organizacion',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='datastages', to='organization.organizacion'),
),
]

View File

@@ -0,0 +1,588 @@
# Generated by Django 5.2.3 on 2025-08-14 19:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0003_alter_datastage_organizacion'),
('organization', '0002_remove_organizacion_membretado_and_more'),
]
operations = [
migrations.CreateModel(
name='Registro500',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=4, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=3, null=True)),
('consecutivo_remesa', models.CharField(blank=True, max_length=50, null=True)),
('numero_seleccion', models.CharField(blank=True, max_length=50, null=True)),
('fecha_inicio_reconocimiento', models.DateField(blank=True, null=True)),
('hora_inicio_reconocimiento', models.TimeField(blank=True, null=True)),
('fecha_fin_reconocimiento', models.DateField(blank=True, null=True)),
('hora_fin_reconocimiento', models.TimeField(blank=True, null=True)),
('fraccion', models.CharField(blank=True, max_length=50, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('clave_documento', models.CharField(blank=True, max_length=50, null=True)),
('tipo_operacion', models.CharField(blank=True, max_length=50, null=True)),
('grado_incidencia', models.CharField(blank=True, max_length=50, null=True)),
('fecha_seleccion', models.DateField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro500s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro500s', to='organization.organizacion')),
],
options={
'db_table': 'registro500',
},
),
migrations.CreateModel(
name='Registro501',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=4, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=3, null=True)),
('tipo_operacion', models.CharField(blank=True, max_length=1, null=True)),
('clave_documento', models.CharField(blank=True, max_length=2, null=True)),
('seccion_aduanera_entrada', models.CharField(blank=True, max_length=3, null=True)),
('curp_contribuyente', models.CharField(blank=True, max_length=18, null=True)),
('rfc', models.CharField(blank=True, max_length=13, null=True)),
('curp_agente_a', models.CharField(blank=True, max_length=18, null=True)),
('tipo_cambio', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('total_fletes', models.CharField(blank=True, max_length=12, null=True)),
('total_seguros', models.CharField(blank=True, max_length=12, null=True)),
('total_embalajes', models.CharField(blank=True, max_length=12, null=True)),
('total_incrementables', models.CharField(blank=True, max_length=12, null=True)),
('total_deducibles', models.CharField(blank=True, max_length=12, null=True)),
('peso_bruto_mercancia', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('medio_transporte_salida', models.CharField(blank=True, max_length=2, null=True)),
('medio_transporte_arribo', models.CharField(blank=True, max_length=2, null=True)),
('medio_transporte_entrada_salida', models.CharField(blank=True, max_length=2, null=True)),
('destino_mercancia', models.CharField(blank=True, max_length=2, null=True)),
('nombre_contribuyente', models.CharField(blank=True, max_length=120, null=True)),
('calle_contribuyente', models.CharField(blank=True, max_length=80, null=True)),
('num_interior_contribuyente', models.CharField(blank=True, max_length=10, null=True)),
('num_exterior_contribuyente', models.CharField(blank=True, max_length=10, null=True)),
('cp_contribuyente', models.CharField(blank=True, max_length=10, null=True)),
('municipio_contribuyente', models.CharField(blank=True, max_length=80, null=True)),
('entidad_fed_contribuyente', models.CharField(blank=True, max_length=3, null=True)),
('pais_contribuyente', models.CharField(blank=True, max_length=3, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_recepcion_pedimento', models.DateTimeField(blank=True, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro501s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro501s', to='organization.organizacion')),
],
options={
'db_table': 'registro501',
},
),
migrations.CreateModel(
name='Registro502',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('rfc_transportista', models.CharField(blank=True, max_length=13, null=True)),
('curp_transportista', models.CharField(blank=True, max_length=18, null=True)),
('nombre_transportista', models.CharField(blank=True, max_length=120, null=True)),
('pais_transporte', models.CharField(blank=True, max_length=3, null=True)),
('identificador_transporte', models.CharField(blank=True, max_length=17, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro502s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro502s', to='organization.organizacion')),
],
options={
'db_table': 'registro502',
},
),
migrations.CreateModel(
name='Registro503',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('numero_guia', models.CharField(blank=True, max_length=20, null=True)),
('tipo_guia', models.CharField(blank=True, max_length=1, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro503s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro503s', to='organization.organizacion')),
],
options={
'db_table': 'registro503',
},
),
migrations.CreateModel(
name='Registro504',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('num_contenedor', models.CharField(blank=True, max_length=12, null=True)),
('tipo_contenedor', models.CharField(blank=True, max_length=2, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro504s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro504s', to='organization.organizacion')),
],
options={
'db_table': 'registro504',
},
),
migrations.CreateModel(
name='Registro505',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fecha_facturacion', models.DateField(blank=True, null=True)),
('numero_factura', models.CharField(blank=True, max_length=40, null=True)),
('termino_facturacion', models.CharField(blank=True, max_length=3, null=True)),
('moneda_facturacion', models.CharField(blank=True, max_length=3, null=True)),
('valor_dolares', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('valor_moneda_extranjera', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('pais_facturacion', models.CharField(blank=True, max_length=3, null=True)),
('entidad_fed_facturacion', models.CharField(blank=True, max_length=3, null=True)),
('indent_fiscal_proveedor', models.CharField(blank=True, max_length=30, null=True)),
('proveedor_mercancia', models.CharField(blank=True, max_length=120, null=True)),
('calle_proveedor', models.CharField(blank=True, max_length=80, null=True)),
('num_interior_proveedor', models.CharField(blank=True, max_length=10, null=True)),
('num_exterior_proveedor', models.CharField(blank=True, max_length=10, null=True)),
('cp_proveedor', models.CharField(blank=True, max_length=10, null=True)),
('municipio_proveedor', models.CharField(blank=True, max_length=80, null=True)),
('fecha_pago_real', models.CharField(blank=True, max_length=50, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro505s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro505s', to='organization.organizacion')),
],
options={
'db_table': 'registro505',
},
),
migrations.CreateModel(
name='Registro506',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('tipo_fecha', models.CharField(blank=True, max_length=2, null=True)),
('fecha_operacion', models.DateField(blank=True, null=True)),
('fecha_validacion_pago_r', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro506s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro506s', to='organization.organizacion')),
],
options={
'db_table': 'registro506',
},
),
migrations.CreateModel(
name='Registro507',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('clave_caso', models.CharField(blank=True, max_length=50, null=True)),
('identificador_caso', models.CharField(blank=True, max_length=50, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('complemento_caso', models.CharField(blank=True, max_length=50, null=True)),
('fecha_validacion_pago_r', models.CharField(blank=True, max_length=50, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro507s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro507s', to='organization.organizacion')),
],
options={
'db_table': 'registro507',
},
),
migrations.CreateModel(
name='Registro508',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('institucion_emisora', models.CharField(blank=True, max_length=2, null=True)),
('numero_cuenta', models.CharField(blank=True, max_length=50, null=True)),
('folio_constancia', models.CharField(blank=True, max_length=17, null=True)),
('fecha_constancia', models.DateField(blank=True, null=True)),
('tipo_cuenta', models.CharField(blank=True, max_length=2, null=True)),
('clave_garantia', models.CharField(blank=True, max_length=50, null=True)),
('valor_unitario_titulo', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('total_garantia', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('cantidad_unidades', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('titulos_asignados', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro508s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro508s', to='organization.organizacion')),
],
options={
'db_table': 'registro508',
},
),
migrations.CreateModel(
name='Registro509',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=4, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=3, null=True)),
('clave_contribucion', models.CharField(blank=True, max_length=2, null=True)),
('tasa_contribucion', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('tipo_tasa', models.CharField(blank=True, max_length=2, null=True)),
('tipo_pedimento', models.IntegerField(blank=True, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro509s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro509s', to='organization.organizacion')),
],
options={
'db_table': 'registro509',
},
),
migrations.CreateModel(
name='Registro510',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('clave_contribucion', models.CharField(blank=True, max_length=2, null=True)),
('tasa_contribucion', models.CharField(blank=True, max_length=50, null=True)),
('tipo_tasa', models.CharField(blank=True, max_length=50, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.CharField(blank=True, max_length=50, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('forma_pago', models.CharField(blank=True, max_length=3, null=True)),
('importe_pago', models.CharField(blank=True, max_length=12, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro510s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro510s', to='organization.organizacion')),
],
options={
'db_table': 'registro510',
},
),
migrations.CreateModel(
name='Registro511',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('secuencia_observacion', models.CharField(blank=True, max_length=3, null=True)),
('observaciones', models.CharField(blank=True, max_length=120, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_validacion_pago_r', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro511s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro511s', to='organization.organizacion')),
],
options={
'db_table': 'registro511',
},
),
migrations.CreateModel(
name='Registro512',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=3, null=True)),
('patente_aduanal_orig', models.CharField(blank=True, max_length=4, null=True)),
('pedimento_original', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera_desp_orig', models.CharField(blank=True, max_length=50, null=True)),
('documento_original', models.CharField(blank=True, max_length=50, null=True)),
('fecha_operacion_orig', models.DateField(blank=True, null=True)),
('fraccion_original', models.CharField(blank=True, max_length=8, null=True)),
('unidad_medida', models.CharField(blank=True, max_length=2, null=True)),
('mercancia_descargada', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro512s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro512s', to='organization.organizacion')),
],
options={
'db_table': 'registro512',
},
),
migrations.CreateModel(
name='Registro520',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('indent_fiscal_destinatario', models.CharField(blank=True, max_length=17, null=True)),
('nombre_destinatario_mercancia', models.CharField(blank=True, max_length=120, null=True)),
('calle_destinatario', models.CharField(blank=True, max_length=80, null=True)),
('num_interior_destinatario', models.CharField(blank=True, max_length=10, null=True)),
('num_exterior_destinatario', models.CharField(blank=True, max_length=10, null=True)),
('cp_destinatario', models.CharField(blank=True, max_length=10, null=True)),
('municipio_destinatario', models.CharField(blank=True, max_length=80, null=True)),
('pais_destinatario', models.CharField(blank=True, max_length=3, null=True)),
('fecha_pago_real', models.CharField(blank=True, max_length=50, null=True)),
('created_at', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro520s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro520s', to='organization.organizacion')),
],
options={
'db_table': 'registro520',
},
),
migrations.CreateModel(
name='Registro551',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('subdivision_fraccion', models.CharField(blank=True, max_length=8, null=True)),
('descripcion_mercancia', models.CharField(blank=True, max_length=250, null=True)),
('precio_unitario', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('valor_aduana', models.CharField(blank=True, max_length=12, null=True)),
('valor_comercial', models.CharField(blank=True, max_length=12, null=True)),
('valor_dolares', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('cantidad_um_comercial', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('unidad_medida_comercial', models.CharField(blank=True, max_length=2, null=True)),
('cantidad_um_tarifa', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('unidad_medida_tarifa', models.CharField(blank=True, max_length=2, null=True)),
('valor_agregado', models.CharField(blank=True, max_length=12, null=True)),
('clave_vinculacion', models.CharField(blank=True, max_length=1, null=True)),
('metodo_valorizacion', models.CharField(blank=True, max_length=2, null=True)),
('codigo_mercancia_producto', models.CharField(blank=True, max_length=20, null=True)),
('marca_mercancia_producto', models.CharField(blank=True, max_length=80, null=True)),
('modelo_mercancia_producto', models.CharField(blank=True, max_length=80, null=True)),
('pais_origen_destino', models.CharField(blank=True, max_length=3, null=True)),
('pais_comprador_vendedor', models.CharField(blank=True, max_length=3, null=True)),
('entidad_fed_origen', models.CharField(blank=True, max_length=3, null=True)),
('entidad_fed_comprador', models.CharField(blank=True, max_length=3, null=True)),
('entidad_fed_vendedor', models.CharField(blank=True, max_length=3, null=True)),
('tipo_operacion', models.CharField(blank=True, max_length=50, null=True)),
('clave_documento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('entidad_fed_destino', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro551s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro551s', to='organization.organizacion')),
],
options={
'db_table': 'registro551',
},
),
migrations.CreateModel(
name='Registro552',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('vin_numero_serie', models.CharField(blank=True, max_length=25, null=True)),
('kilometraje_vehiculo', models.CharField(blank=True, max_length=6, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro552s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro552s', to='organization.organizacion')),
],
options={
'db_table': 'registro552',
},
),
migrations.CreateModel(
name='Registro553',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('clave_permiso', models.CharField(blank=True, max_length=3, null=True)),
('firma_descargo', models.CharField(blank=True, max_length=40, null=True)),
('numero_permiso', models.CharField(blank=True, max_length=30, null=True)),
('valor_comercial_dolares', models.CharField(blank=True, max_length=50, null=True)),
('cantidad_mum_tarifa', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.CharField(blank=True, max_length=50, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro553s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro553s', to='organization.organizacion')),
],
options={
'db_table': 'registro553',
},
),
migrations.CreateModel(
name='Registro554',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('clave_caso', models.CharField(blank=True, max_length=50, null=True)),
('identificador_caso', models.CharField(blank=True, max_length=50, null=True)),
('complemento_caso', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro554s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro554s', to='organization.organizacion')),
],
options={
'db_table': 'registro554',
},
),
migrations.CreateModel(
name='Registro555',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('institucion_emisora', models.CharField(blank=True, max_length=2, null=True)),
('numero_cuenta', models.CharField(blank=True, max_length=17, null=True)),
('folio_constancia', models.CharField(blank=True, max_length=17, null=True)),
('fecha_constancia', models.DateField(blank=True, null=True)),
('clave_garantia', models.CharField(blank=True, max_length=50, null=True)),
('valor_unitario_titulo', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('total_garantia', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('cantidad_unidades_medida', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('titulos_asignados', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('fecha_pago_real', models.DateField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro555s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro555s', to='organization.organizacion')),
],
options={
'db_table': 'registro555',
},
),
migrations.CreateModel(
name='Registro556',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('clave_contribucion', models.CharField(blank=True, max_length=2, null=True)),
('tasa_contribucion', models.DecimalField(blank=True, decimal_places=6, max_digits=18, null=True)),
('tipo_tasa', models.CharField(blank=True, max_length=2, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro556s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro556s', to='organization.organizacion')),
],
options={
'db_table': 'registro556',
},
),
migrations.CreateModel(
name='Registro557',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('clave_contribucion', models.CharField(blank=True, max_length=2, null=True)),
('forma_pago', models.CharField(blank=True, max_length=3, null=True)),
('importe_pago', models.CharField(blank=True, max_length=12, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro557s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro557s', to='organization.organizacion')),
],
options={
'db_table': 'registro557',
},
),
migrations.CreateModel(
name='Registro558',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('fraccion', models.CharField(blank=True, max_length=8, null=True)),
('secuencia_fraccion', models.CharField(blank=True, max_length=50, null=True)),
('secuencia_observacion', models.CharField(blank=True, max_length=3, null=True)),
('observaciones', models.CharField(blank=True, max_length=120, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro558s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro558s', to='organization.organizacion')),
],
options={
'db_table': 'registro558',
},
),
migrations.CreateModel(
name='RegistroSel',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=50, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('consecutivo_remesa', models.CharField(blank=True, max_length=50, null=True)),
('numero_seleccion', models.CharField(blank=True, max_length=50, null=True)),
('fecha_seleccion', models.DateField(blank=True, null=True)),
('hora_seleccion', models.TimeField(blank=True, null=True)),
('semaforo_fiscal', models.CharField(blank=True, max_length=1, null=True)),
('clave_documento', models.CharField(blank=True, max_length=3, null=True)),
('tipo_operacion', models.CharField(blank=True, max_length=1, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro_sel', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro_sel', to='organization.organizacion')),
],
options={
'db_table': 'registro_sel',
},
),
]

View File

@@ -0,0 +1,63 @@
# Generated by Django 5.2.3 on 2025-08-14 19:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0004_registro500_registro501_registro502_registro503_and_more'),
('organization', '0002_remove_organizacion_membretado_and_more'),
]
operations = [
migrations.CreateModel(
name='Registro701',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=4, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=3, null=True)),
('clave_documento', models.CharField(blank=True, max_length=2, null=True)),
('fecha_pago', models.DateField(blank=True, null=True)),
('pedimento_anterior', models.CharField(blank=True, max_length=50, null=True)),
('patente_anterior', models.CharField(blank=True, max_length=50, null=True)),
('seccion_aduanera_anterior', models.CharField(blank=True, max_length=50, null=True)),
('documento_anterior', models.CharField(blank=True, max_length=50, null=True)),
('fecha_operacion_anterior', models.DateField(blank=True, null=True)),
('pedimento_original', models.CharField(blank=True, max_length=50, null=True)),
('patente_aduanal_orig', models.CharField(blank=True, max_length=50, null=True)),
('seccion_aduanera_desp_orig', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro701s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro701s', to='organization.organizacion')),
],
options={
'db_table': 'registro701',
},
),
migrations.CreateModel(
name='Registro702',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('patente', models.CharField(blank=True, max_length=50, null=True)),
('pedimento', models.CharField(blank=True, max_length=7, null=True)),
('seccion_aduanera', models.CharField(blank=True, max_length=50, null=True)),
('clave_contribucion', models.CharField(blank=True, max_length=2, null=True)),
('forma_pago', models.CharField(blank=True, max_length=50, null=True)),
('importe_pago', models.CharField(blank=True, max_length=12, null=True)),
('tipo_pedimento', models.CharField(blank=True, max_length=50, null=True)),
('fecha_pago_real', models.DateTimeField(blank=True, null=True)),
('created_by', models.IntegerField(blank=True, null=True)),
('consulta', models.CharField(blank=True, max_length=50, null=True)),
('datastage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro702s', to='datastage.datastage')),
('organizacion', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='registro702s', to='organization.organizacion')),
],
options={
'db_table': 'registro702',
},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.3 on 2025-08-14 21:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('datastage', '0005_registro701_registro702'),
]
operations = [
migrations.RenameField(
model_name='registro520',
old_name='municipio_destinatario',
new_name='municpio_destinatario',
),
migrations.RemoveField(
model_name='registro520',
name='created_by',
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.3 on 2025-08-14 21:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0006_rename_municipio_destinatario_registro520_municpio_destinatario_and_more'),
]
operations = [
migrations.AlterField(
model_name='datastage',
name='archivo',
field=models.FileField(blank=True, null=True, upload_to='datastages/'),
),
migrations.AlterField(
model_name='datastage',
name='contribuyente',
field=models.CharField(blank=True, max_length=100, null=True),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.3 on 2025-08-14 21:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0007_alter_datastage_archivo_and_more'),
]
operations = [
migrations.AlterField(
model_name='datastage',
name='archivo',
field=models.FileField(default='', upload_to='datastages/'),
preserve_default=False,
),
migrations.AlterField(
model_name='datastage',
name='contribuyente',
field=models.CharField(default='', max_length=100),
preserve_default=False,
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.3 on 2025-08-14 21:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0008_alter_datastage_archivo_and_more'),
]
operations = [
migrations.AlterField(
model_name='registro501',
name='tipo_operacion',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='registrosel',
name='tipo_operacion',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

View File

@@ -0,0 +1,33 @@
# Generated by Django 5.2.3 on 2025-08-14 21:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0009_alter_registro501_tipo_operacion_and_more'),
]
operations = [
migrations.AlterField(
model_name='registro501',
name='clave_documento',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='registro502',
name='pais_transporte',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='registro503',
name='tipo_guia',
field=models.CharField(blank=True, max_length=50, null=True),
),
migrations.AlterField(
model_name='registro701',
name='clave_documento',
field=models.CharField(blank=True, max_length=50, null=True),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.3 on 2025-08-14 21:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('datastage', '0010_alter_registro501_clave_documento_and_more'),
]
operations = [
migrations.AlterField(
model_name='registro502',
name='fecha_pago_real',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='registro503',
name='fecha_pago_real',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AlterField(
model_name='registro504',
name='fecha_pago_real',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

571
api/datastage/models.py Normal file
View File

@@ -0,0 +1,571 @@
from django.db import models
# Create your models here.
class DataStage(models.Model):
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='datastages', null=True, blank=True)
archivo = models.FileField(upload_to='datastages/', blank=False, null=False)
contribuyente = models.CharField(max_length=100, blank=False, null=False)
procesado = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "DataStage"
verbose_name_plural = "DataStages"
db_table = 'datastage'
def __str__(self):
return organizacion.nombre if self.organizacion else "DataStage sin organizacion"
class Registro500(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=4, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=3, null=True, blank=True)
consecutivo_remesa = models.CharField(max_length=50, null=True, blank=True)
numero_seleccion = models.CharField(max_length=50, null=True, blank=True)
fecha_inicio_reconocimiento = models.DateField(null=True, blank=True)
hora_inicio_reconocimiento = models.TimeField(null=True, blank=True)
fecha_fin_reconocimiento = models.DateField(null=True, blank=True)
hora_fin_reconocimiento = models.TimeField(null=True, blank=True)
fraccion = models.CharField(max_length=50, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
clave_documento = models.CharField(max_length=50, null=True, blank=True)
tipo_operacion = models.CharField(max_length=50, null=True, blank=True)
grado_incidencia = models.CharField(max_length=50, null=True, blank=True)
fecha_seleccion = models.DateField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro500s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro500s', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.patente} - {self.pedimento} - {self.seccion_aduanera}"
class Meta:
db_table = 'registro500'
class Registro501(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=4, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=3, null=True, blank=True)
tipo_operacion = models.CharField(max_length=50, null=True, blank=True)
clave_documento = models.CharField(max_length=50, null=True, blank=True)
seccion_aduanera_entrada = models.CharField(max_length=3, null=True, blank=True)
curp_contribuyente = models.CharField(max_length=18, null=True, blank=True)
rfc = models.CharField(max_length=13, null=True, blank=True)
curp_agente_a = models.CharField(max_length=18, null=True, blank=True)
tipo_cambio = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
total_fletes = models.CharField(max_length=12, null=True, blank=True)
total_seguros = models.CharField(max_length=12, null=True, blank=True)
total_embalajes = models.CharField(max_length=12, null=True, blank=True)
total_incrementables = models.CharField(max_length=12, null=True, blank=True)
total_deducibles = models.CharField(max_length=12, null=True, blank=True)
peso_bruto_mercancia = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
medio_transporte_salida = models.CharField(max_length=2, null=True, blank=True)
medio_transporte_arribo = models.CharField(max_length=2, null=True, blank=True)
medio_transporte_entrada_salida = models.CharField(max_length=2, null=True, blank=True)
destino_mercancia = models.CharField(max_length=2, null=True, blank=True)
nombre_contribuyente = models.CharField(max_length=120, null=True, blank=True)
calle_contribuyente = models.CharField(max_length=80, null=True, blank=True)
num_interior_contribuyente = models.CharField(max_length=10, null=True, blank=True)
num_exterior_contribuyente = models.CharField(max_length=10, null=True, blank=True)
cp_contribuyente = models.CharField(max_length=10, null=True, blank=True)
municipio_contribuyente = models.CharField(max_length=80, null=True, blank=True)
entidad_fed_contribuyente = models.CharField(max_length=3, null=True, blank=True)
pais_contribuyente = models.CharField(max_length=3, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
fecha_recepcion_pedimento = models.DateTimeField(null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro501s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro501s', null=True, blank=True)
class Meta:
db_table = 'registro501'
class Registro502(models.Model):
id = models.AutoField(primary_key=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
rfc_transportista = models.CharField(max_length=13, null=True, blank=True)
curp_transportista = models.CharField(max_length=18, null=True, blank=True)
nombre_transportista = models.CharField(max_length=120, null=True, blank=True)
pais_transporte = models.CharField(max_length=50, null=True, blank=True)
identificador_transporte = models.CharField(max_length=17, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro502s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro502s', null=True, blank=True)
patente = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro502'
class Registro503(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
numero_guia = models.CharField(max_length=20, null=True, blank=True)
tipo_guia = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro503s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro503s', null=True, blank=True)
class Meta:
db_table = 'registro503'
class Registro504(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
num_contenedor = models.CharField(max_length=12, null=True, blank=True)
tipo_contenedor = models.CharField(max_length=2, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro504s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro504s', null=True, blank=True)
class Meta:
db_table = 'registro504'
class Registro505(models.Model):
id = models.AutoField(primary_key=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fecha_facturacion = models.DateField(null=True, blank=True)
numero_factura = models.CharField(max_length=40, null=True, blank=True)
termino_facturacion = models.CharField(max_length=3, null=True, blank=True)
moneda_facturacion = models.CharField(max_length=3, null=True, blank=True)
valor_dolares = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
valor_moneda_extranjera = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
pais_facturacion = models.CharField(max_length=3, null=True, blank=True)
entidad_fed_facturacion = models.CharField(max_length=3, null=True, blank=True)
indent_fiscal_proveedor = models.CharField(max_length=30, null=True, blank=True)
proveedor_mercancia = models.CharField(max_length=120, null=True, blank=True)
calle_proveedor = models.CharField(max_length=80, null=True, blank=True)
num_interior_proveedor = models.CharField(max_length=10, null=True, blank=True)
num_exterior_proveedor = models.CharField(max_length=10, null=True, blank=True)
cp_proveedor = models.CharField(max_length=10, null=True, blank=True)
municipio_proveedor = models.CharField(max_length=80, null=True, blank=True)
fecha_pago_real = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro505s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro505s', null=True, blank=True)
patente = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro505'
class Registro506(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
tipo_fecha = models.CharField(max_length=2, null=True, blank=True)
fecha_operacion = models.DateField(null=True, blank=True)
fecha_validacion_pago_r = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro506s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro506s', null=True, blank=True)
class Meta:
db_table = 'registro506'
class Registro507(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
clave_caso = models.CharField(max_length=50, null=True, blank=True)
identificador_caso = models.CharField(max_length=50, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
complemento_caso = models.CharField(max_length=50, null=True, blank=True)
fecha_validacion_pago_r = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro507s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro507s', null=True, blank=True)
class Meta:
db_table = 'registro507'
class Registro508(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
institucion_emisora = models.CharField(max_length=2, null=True, blank=True)
numero_cuenta = models.CharField(max_length=50, null=True, blank=True)
folio_constancia = models.CharField(max_length=17, null=True, blank=True)
fecha_constancia = models.DateField(null=True, blank=True)
tipo_cuenta = models.CharField(max_length=2, null=True, blank=True)
clave_garantia = models.CharField(max_length=50, null=True, blank=True)
valor_unitario_titulo = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
total_garantia = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
cantidad_unidades = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
titulos_asignados = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
fecha_pago_real = models.DateField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro508s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro508s', null=True, blank=True)
class Meta:
db_table = 'registro508'
class Registro509(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=4, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=3, null=True, blank=True)
clave_contribucion = models.CharField(max_length=2, null=True, blank=True)
tasa_contribucion = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
tipo_tasa = models.CharField(max_length=2, null=True, blank=True)
tipo_pedimento = models.IntegerField(null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro509s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro509s', null=True, blank=True)
class Meta:
db_table = 'registro509'
class Registro510(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
clave_contribucion = models.CharField(max_length=2, null=True, blank=True)
tasa_contribucion = models.CharField(max_length=50, null=True, blank=True)
tipo_tasa = models.CharField(max_length=50, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro510s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro510s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
forma_pago = models.CharField(max_length=3, null=True, blank=True)
importe_pago = models.CharField(max_length=12, null=True, blank=True)
class Meta:
db_table = 'registro510'
class Registro511(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
secuencia_observacion = models.CharField(max_length=3, null=True, blank=True)
observaciones = models.CharField(max_length=120, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
fecha_validacion_pago_r = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro511s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro511s', null=True, blank=True)
class Meta:
db_table = 'registro511'
class Registro512(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=3, null=True, blank=True)
patente_aduanal_orig = models.CharField(max_length=4, null=True, blank=True)
pedimento_original = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera_desp_orig = models.CharField(max_length=50, null=True, blank=True)
documento_original = models.CharField(max_length=50, null=True, blank=True)
fecha_operacion_orig = models.DateField(null=True, blank=True)
fraccion_original = models.CharField(max_length=8, null=True, blank=True)
unidad_medida = models.CharField(max_length=2, null=True, blank=True)
mercancia_descargada = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro512s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro512s', null=True, blank=True)
class Meta:
db_table = 'registro512'
class Registro520(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
indent_fiscal_destinatario = models.CharField(max_length=17, null=True, blank=True)
nombre_destinatario_mercancia = models.CharField(max_length=120, null=True, blank=True)
calle_destinatario = models.CharField(max_length=80, null=True, blank=True)
num_interior_destinatario = models.CharField(max_length=10, null=True, blank=True)
num_exterior_destinatario = models.CharField(max_length=10, null=True, blank=True)
cp_destinatario = models.CharField(max_length=10, null=True, blank=True)
municpio_destinatario = models.CharField(max_length=80, null=True, blank=True)
pais_destinatario = models.CharField(max_length=3, null=True, blank=True)
fecha_pago_real = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro520s', null=True, blank=True)
created_at = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro520s', null=True, blank=True)
class Meta:
db_table = 'registro520'
class Registro551(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
subdivision_fraccion = models.CharField(max_length=8, null=True, blank=True)
descripcion_mercancia = models.CharField(max_length=250, null=True, blank=True)
precio_unitario = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
valor_aduana = models.CharField(max_length=12, null=True, blank=True)
valor_comercial = models.CharField(max_length=12, null=True, blank=True)
valor_dolares = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
cantidad_um_comercial = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
unidad_medida_comercial = models.CharField(max_length=2, null=True, blank=True)
cantidad_um_tarifa = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
unidad_medida_tarifa = models.CharField(max_length=2, null=True, blank=True)
valor_agregado = models.CharField(max_length=12, null=True, blank=True)
clave_vinculacion = models.CharField(max_length=1, null=True, blank=True)
metodo_valorizacion = models.CharField(max_length=2, null=True, blank=True)
codigo_mercancia_producto = models.CharField(max_length=20, null=True, blank=True)
marca_mercancia_producto = models.CharField(max_length=80, null=True, blank=True)
modelo_mercancia_producto = models.CharField(max_length=80, null=True, blank=True)
pais_origen_destino = models.CharField(max_length=3, null=True, blank=True)
pais_comprador_vendedor = models.CharField(max_length=3, null=True, blank=True)
entidad_fed_origen = models.CharField(max_length=3, null=True, blank=True)
entidad_fed_comprador = models.CharField(max_length=3, null=True, blank=True)
entidad_fed_vendedor = models.CharField(max_length=3, null=True, blank=True)
tipo_operacion = models.CharField(max_length=50, null=True, blank=True)
clave_documento = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro551s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro551s', null=True, blank=True)
entidad_fed_destino = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro551'
class Registro552(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
vin_numero_serie = models.CharField(max_length=25, null=True, blank=True)
kilometraje_vehiculo = models.CharField(max_length=6, null=True, blank=True)
fecha_pago_real = models.DateField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro552s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro552s', null=True, blank=True)
class Meta:
db_table = 'registro552'
class Registro553(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
clave_permiso = models.CharField(max_length=3, null=True, blank=True)
firma_descargo = models.CharField(max_length=40, null=True, blank=True)
numero_permiso = models.CharField(max_length=30, null=True, blank=True)
valor_comercial_dolares = models.CharField(max_length=50, null=True, blank=True)
cantidad_mum_tarifa = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro553s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro553s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro553'
class Registro554(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
clave_caso = models.CharField(max_length=50, null=True, blank=True)
identificador_caso = models.CharField(max_length=50, null=True, blank=True)
complemento_caso = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro554s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro554s', null=True, blank=True)
class Meta:
db_table = 'registro554'
class Registro555(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
institucion_emisora = models.CharField(max_length=2, null=True, blank=True)
numero_cuenta = models.CharField(max_length=17, null=True, blank=True)
folio_constancia = models.CharField(max_length=17, null=True, blank=True)
fecha_constancia = models.DateField(null=True, blank=True)
clave_garantia = models.CharField(max_length=50, null=True, blank=True)
valor_unitario_titulo = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
total_garantia = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
cantidad_unidades_medida = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
titulos_asignados = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
fecha_pago_real = models.DateField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro555s', null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro555s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro555'
class Registro556(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
clave_contribucion = models.CharField(max_length=2, null=True, blank=True)
tasa_contribucion = models.DecimalField(max_digits=18, decimal_places=6, null=True, blank=True)
tipo_tasa = models.CharField(max_length=2, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro556s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro556s', null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro556'
class Registro557(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
clave_contribucion = models.CharField(max_length=2, null=True, blank=True)
forma_pago = models.CharField(max_length=3, null=True, blank=True)
importe_pago = models.CharField(max_length=12, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro557s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro557s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro557'
class Registro558(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
fraccion = models.CharField(max_length=8, null=True, blank=True)
secuencia_fraccion = models.CharField(max_length=50, null=True, blank=True)
secuencia_observacion = models.CharField(max_length=3, null=True, blank=True)
observaciones = models.CharField(max_length=120, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro558s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro558s', null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
class Meta:
db_table = 'registro558'
class RegistroSel(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=50, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
consecutivo_remesa = models.CharField(max_length=50, null=True, blank=True)
numero_seleccion = models.CharField(max_length=50, null=True, blank=True)
fecha_seleccion = models.DateField(null=True, blank=True)
hora_seleccion = models.TimeField(null=True, blank=True)
semaforo_fiscal = models.CharField(max_length=1, null=True, blank=True)
clave_documento = models.CharField(max_length=3, null=True, blank=True)
tipo_operacion = models.CharField(max_length=50, null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro_sel', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro_sel', null=True, blank=True)
class Meta:
db_table = 'registro_sel'
class Registro701(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=4, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=3, null=True, blank=True)
clave_documento = models.CharField(max_length=50, null=True, blank=True)
fecha_pago = models.DateField(null=True, blank=True)
pedimento_anterior = models.CharField(max_length=50, null=True, blank=True)
patente_anterior = models.CharField(max_length=50, null=True, blank=True)
seccion_aduanera_anterior = models.CharField(max_length=50, null=True, blank=True)
documento_anterior = models.CharField(max_length=50, null=True, blank=True)
fecha_operacion_anterior = models.DateField(null=True, blank=True)
pedimento_original = models.CharField(max_length=50, null=True, blank=True)
patente_aduanal_orig = models.CharField(max_length=50, null=True, blank=True)
seccion_aduanera_desp_orig = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro701s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro701s', null=True, blank=True)
class Meta:
db_table = 'registro701'
class Registro702(models.Model):
id = models.AutoField(primary_key=True)
patente = models.CharField(max_length=50, null=True, blank=True)
pedimento = models.CharField(max_length=7, null=True, blank=True)
seccion_aduanera = models.CharField(max_length=50, null=True, blank=True)
clave_contribucion = models.CharField(max_length=2, null=True, blank=True)
forma_pago = models.CharField(max_length=50, null=True, blank=True)
importe_pago = models.CharField(max_length=12, null=True, blank=True)
tipo_pedimento = models.CharField(max_length=50, null=True, blank=True)
fecha_pago_real = models.DateTimeField(null=True, blank=True)
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='registro702s', null=True, blank=True)
created_by = models.IntegerField(null=True, blank=True)
consulta = models.CharField(max_length=50, null=True, blank=True)
datastage = models.ForeignKey(DataStage, on_delete=models.CASCADE, related_name='registro702s', null=True, blank=True)
class Meta:
db_table = 'registro702'

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from .models import DataStage
from api.organization.models import Organizacion
class DataStageSerializer(serializers.ModelSerializer):
organizacion = serializers.PrimaryKeyRelatedField(required=False, allow_null=True, queryset=Organizacion.objects.all())
class Meta:
model = DataStage
fields = '__all__'
read_only_fields = ('id', 'created_at', 'updated_at')

276
api/datastage/tasks.py Normal file
View File

@@ -0,0 +1,276 @@
from celery import group
from celery import shared_task
import logging
from django.apps import apps
from django.utils import timezone
import os
import zipfile
import re
@shared_task
def procesar_datastage_task(datastage_id, user_organizacion_id=None):
import traceback
try:
logger = logging.getLogger(__name__)
from api.datastage.models import DataStage
from api.organization.models import Organizacion
from api.customs.models import Pedimento, TipoOperacion, Regimen
datastage = DataStage.objects.get(id=datastage_id)
if not datastage.archivo:
return {'detail': 'No hay archivo asociado a este DataStage.'}
file_path = datastage.archivo.path
if not os.path.exists(file_path):
return {'detail': 'El archivo no existe en el servidor.'}
if not file_path.endswith('.zip'):
return {'detail': 'El archivo no es un .zip.'}
documentos_encontrados = []
registros_cargados = {}
registros_por_archivo = {}
errores_por_archivo = {}
errores_pedimento = []
user_organizacion = None
if user_organizacion_id:
user_organizacion = Organizacion.objects.get(id=user_organizacion_id)
def to_snake_case(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
return s2.replace('__', '_').lower()
# Lanzar una subtarea por cada archivo ASC
subtasks = []
with zipfile.ZipFile(file_path, 'r') as zip_ref:
for asc_name in zip_ref.namelist():
if asc_name.endswith('.asc'):
subtasks.append(procesar_archivo_asc_task.s(datastage_id, user_organizacion_id, asc_name))
if subtasks:
job = group(subtasks).apply_async()
return {
'group_id': job.id,
'subtask_ids': [t.id for t in job.results],
'detail': 'Procesamiento lanzado. Monitorea el estado de cada subtask_id.'
}
return {'detail': 'No se encontraron archivos .asc'}
except Exception as e:
import traceback
return {'error': str(e), 'traceback': traceback.format_exc()}
@shared_task
def procesar_archivo_asc_task(datastage_id, user_organizacion_id, asc_name):
import traceback
try:
logger = logging.getLogger(__name__)
from api.datastage.models import DataStage
from api.organization.models import Organizacion
from api.customs.models import Pedimento, TipoOperacion, Regimen
from django.apps import apps
import zipfile
import re
datastage = DataStage.objects.get(id=datastage_id)
user_organizacion = None
if user_organizacion_id:
user_organizacion = Organizacion.objects.get(id=user_organizacion_id)
file_path = datastage.archivo.path
def to_snake_case(name):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
return s2.replace('__', '_').lower()
with zipfile.ZipFile(file_path, 'r') as zip_ref:
if asc_name not in zip_ref.namelist():
return {'errores': [f'{asc_name} no encontrado en el zip']}
match = re.match(r'.*_(\d+)\.asc$', asc_name)
if match:
registro_key = match.group(1)
model_name = f'Registro{registro_key}'
else:
match2 = re.match(r'.*_([A-Za-z]+)\.asc$', asc_name)
if match2:
registro_key = match2.group(1).capitalize()
model_name = f'Registro{registro_key}'
else:
return {'errores': ["No se pudo determinar el modelo"]}
try:
Model = apps.get_model('datastage', model_name)
except LookupError:
return {'errores': [f"No existe el modelo para {model_name}"]}
with zip_ref.open(asc_name) as asc_file:
first = True
field_names = []
field_names_snake = []
objects_to_create = []
errores_pedimento = []
for line in asc_file:
line_decoded = None
try:
line_decoded = line.decode('utf-8').strip()
except UnicodeDecodeError:
try:
line_decoded = line.decode('latin-1').strip()
except Exception as e:
continue
except Exception as e:
continue
if not line_decoded:
continue
if first:
field_names = [f for f in line_decoded.split('|')]
field_names_snake = [to_snake_case(f) for f in field_names]
first = False
continue
values = line_decoded.split('|')
while values and values[-1] == '':
values.pop()
if len(values) == len(field_names_snake) + 1 and values[-1] == '':
values = values[:-1]
if len(values) < len(field_names_snake):
values += [None] * (len(field_names_snake) - len(values))
if len(values) != len(field_names_snake):
continue
data = dict(zip(field_names_snake, values))
if hasattr(Model, 'organizacion_id'):
data['organizacion_id'] = user_organizacion.id if user_organizacion else None
if hasattr(Model, 'datastage_id'):
data['datastage_id'] = datastage.id
# Limpiar campos de fecha vacíos ('') a None
for field in Model._meta.get_fields():
if hasattr(field, 'get_internal_type') and field.get_internal_type() in ["DateField", "DateTimeField"]:
if data.get(field.name) == "":
data[field.name] = None
# Convertir fecha_pago_real a timezone-aware si existe
if 'fecha_pago_real' in data and data['fecha_pago_real']:
from django.utils import timezone
import datetime
fecha_val = data['fecha_pago_real']
if isinstance(fecha_val, str):
try:
dt = datetime.datetime.strptime(fecha_val, '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
dt = datetime.datetime.strptime(fecha_val, '%Y-%m-%d')
except Exception:
dt = None
if dt and timezone.is_naive(dt):
dt = timezone.make_aware(dt)
if dt:
data['fecha_pago_real'] = dt
elif isinstance(fecha_val, datetime.datetime) and timezone.is_naive(fecha_val):
data['fecha_pago_real'] = timezone.make_aware(fecha_val)
try:
obj = Model(**data)
objects_to_create.append(obj)
# Si es Registro501, crear Pedimento
if model_name == 'Registro501':
organizacion_instance = None
org_id = data.get('organizacion_id')
if org_id:
try:
organizacion_instance = Organizacion.objects.get(id=org_id)
except Exception as org_exc:
logger.warning(f"No se encontró la organización con id {org_id}: {org_exc}")
if not organizacion_instance:
organizacion_instance = user_organizacion
fecha_pago_raw = data.get('fecha_pago_real')
fecha_pago = None
if fecha_pago_raw:
if isinstance(fecha_pago_raw, str):
fecha_pago = fecha_pago_raw.split(' ')[0]
elif hasattr(fecha_pago_raw, 'date'):
fecha_pago = fecha_pago_raw.date()
else:
fecha_pago = fecha_pago_raw
aduana = data.get('seccion_aduanera')
patente = data.get('patente')
pedimento_num = data.get('pedimento')
pedimento_app = ""
try:
if fecha_pago and aduana and patente and pedimento_num:
if isinstance(fecha_pago, str):
year = fecha_pago[:4]
else:
year = str(fecha_pago.year)
pedimento_app = f"{year[-2:]}-{str(aduana).zfill(2)[-2:]}-{str(patente).zfill(4)[-4:]}-{str(pedimento_num).zfill(7)[-7:]}"
except Exception as ped_app_exc:
logger.warning(f"No se pudo generar pedimento_app: {ped_app_exc}")
tipo_operacion_val = data.get('tipo_operacion')
tipo_operacion = TipoOperacion.objects.filter(id=int(tipo_operacion_val)).first() if tipo_operacion_val else None
regimen = Regimen.objects.filter(claveped=data.get('clave_documento', '').strip(), tipo=tipo_operacion.id if tipo_operacion else None).first() if tipo_operacion else None
# Buscar o crear Importador para el RFC
importador_instance = None
rfc = data.get('rfc')
if rfc:
from api.customs.models import Importador
importador_instance = Importador.objects.filter(rfc=rfc).first()
if not importador_instance and organizacion_instance:
importador_instance = Importador.objects.create(rfc=rfc, organizacion=organizacion_instance)
pedimento_data = {
'pedimento': pedimento_num,
'patente': patente,
'aduana': aduana,
'regimen': regimen.regimenped if regimen else None,
'clave_pedimento': data.get('clave_documento'),
'pedimento_app': pedimento_app,
'organizacion': organizacion_instance,
'patente': patente,
'fecha_pago': fecha_pago,
'alerta': False,
'contribuyente': importador_instance,
'agente_aduanal': data.get('curp_agente_a'),
"tipo_operacion": tipo_operacion,
"numero_partidas": data.get('numero_partidas', 0),
"importe_total": data.get('importe_total', 0.0),
"saldo_disponible": data.get('saldo_disponible', 0.0),
"importe_pedimento": data.get('importe_pedimento', 0.0),
"existe_expediente": data.get('existe_expediente', False),
"remesas": data.get('remesas', False),
}
try:
Pedimento.objects.create(**pedimento_data)
except Exception as ped_exc:
pass
except Exception as e:
continue
if objects_to_create:
try:
Model.objects.bulk_create(objects_to_create, batch_size=1000)
except Exception as e:
return {'archivo': asc_name, 'error': str(e), 'traceback': traceback.format_exc()}
return {
'archivo': asc_name,
'insertados': len(objects_to_create)
}
except Exception as e:
import traceback
return {'archivo': asc_name, 'error': str(e), 'traceback': traceback.format_exc()}
detalles = {}
for key in ['502', '503', '504']:
model_name = f'Registro{key}'
asc_file = None
encabezado = None
errores = []
for asc_name in registros_por_archivo:
if asc_name.endswith(f'_{key}.asc'):
asc_file = asc_name
break
if asc_file:
try:
with zipfile.ZipFile(file_path, 'r') as zip_ref:
with zip_ref.open(asc_file) as f:
for line in f:
try:
encabezado = line.decode('utf-8').strip()
except UnicodeDecodeError:
encabezado = line.decode('latin-1').strip()
break
except Exception as e:
encabezado = f'Error leyendo encabezado: {e}'
errores = errores_por_archivo.get(asc_file, [])
detalles[model_name] = {
'archivo': asc_file,
'encabezado': encabezado,
'errores': errores
}
return {'registros_cargados': registros_cargados, 'errores_pedimento': errores_pedimento}

85
api/datastage/tests.py Normal file
View File

@@ -0,0 +1,85 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from api.organization.models import Organizacion
from .models import DataStage
from io import BytesIO
from django.core.files.uploadedfile import SimpleUploadedFile
User = get_user_model()
class DataStageViewSetTests(APITestCase):
def setUp(self):
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
self.org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
self.admin = User.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
self.admin.groups.create(name="admin")
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.importador = User.objects.create_user(username="importador", password="importpass", organizacion=self.org2, is_importador=True, rfc="RFC123456789")
self.importador.groups.create(name="importador")
self.client = APIClient()
def test_admin_sees_only_own_org(self):
ds1 = DataStage.objects.create(nombre="DS1", almacenamiento=10, organizacion=self.org, archivo=SimpleUploadedFile("a.txt", b"a"))
ds2 = DataStage.objects.create(nombre="DS2", almacenamiento=20, organizacion=self.org2, archivo=SimpleUploadedFile("b.txt", b"b"))
self.client.force_authenticate(user=self.admin)
url = reverse('datastage-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
nombres = [ds['nombre'] for ds in response.data]
self.assertIn("DS1", nombres)
self.assertNotIn("DS2", nombres)
def test_superuser_sees_all(self):
ds1 = DataStage.objects.create(nombre="DS1", almacenamiento=10, organizacion=self.org, archivo=SimpleUploadedFile("a.txt", b"a"))
ds2 = DataStage.objects.create(nombre="DS2", almacenamiento=20, organizacion=self.org2, archivo=SimpleUploadedFile("b.txt", b"b"))
self.client.force_authenticate(user=self.superuser)
url = reverse('datastage-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
nombres = [ds['nombre'] for ds in response.data]
self.assertIn("DS1", nombres)
self.assertIn("DS2", nombres)
def test_importador_cannot_create(self):
self.client.force_authenticate(user=self.importador)
url = reverse('datastage-list')
file_content = BytesIO(b"dummy data")
file = SimpleUploadedFile("test.txt", file_content.read(), content_type="text/plain")
data = {
"nombre": "DataStageImportador",
"almacenamiento": 10,
"archivo": file
}
response = self.client.post(url, data, format='multipart')
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_list_datastages(self):
url = reverse('datastage-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_datastage(self):
url = reverse('datastage-list')
file_content = BytesIO(b"dummy data")
file = SimpleUploadedFile("test.txt", file_content.read(), content_type="text/plain")
data = {
"nombre": "DataStageTest",
"almacenamiento": 10,
"archivo": file
}
response = self.client.post(url, data, format='multipart')
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_update_datastage(self):
# First create
file_content = BytesIO(b"dummy data")
file = SimpleUploadedFile("test.txt", file_content.read(), content_type="text/plain")
ds = DataStage.objects.create(nombre="DataStageTest", almacenamiento=10, organizacion=self.org, archivo=file)
url = reverse('datastage-detail', args=[ds.id])
data = {"almacenamiento": 20}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['almacenamiento'], 20)

12
api/datastage/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DataStageViewSet
# Create a router and register our viewset with it.
router = DefaultRouter()
router.register(r'datastages', DataStageViewSet, basename='datastage')
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]

140
api/datastage/views.py Normal file
View File

@@ -0,0 +1,140 @@
from rest_framework.pagination import PageNumberPagination
from api.customs.models import Pedimento, TipoOperacion, Regimen
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.response import Response
from django.http import FileResponse, Http404
import os
from .models import DataStage
from .serializer import DataStageSerializer
from api.logger.mixins import LoggingMixin
from mixins.filtrado_organizacion import OrganizacionFiltradaMixin
from core.permissions import (
IsSameOrganization,
IsSameOrganizationDeveloper,
IsSameOrganizationAndAdmin,
IsSuperUser
)
# Create your views here.
class DataStagePagination(PageNumberPagination):
page_size = 20 # Valor por defecto
page_size_query_param = 'page_size'
max_page_size = 1000
class DataStageViewSet(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltradaMixin):
"""
ViewSet for managing DataStage instances.
Provides CRUD operations for DataStage.
"""
serializer_class = DataStageSerializer
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
model = DataStage
my_tags = ['DataStage']
pagination_class = DataStagePagination
def get_queryset(self):
if self.request.user.is_superuser:
return DataStage.objects.all().order_by('-created_at')
if self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='Agente Aduanal').exists():
return DataStage.objects.filter(organizacion=self.request.user.organizacion).order_by('-created_at')
return self.get_queryset_filtrado_por_organizacion().order_by('-created_at')
def perform_create(self, serializer):
"""
Permite que la organización sea opcional en el request, pero si no se envía, se asigna la del usuario autenticado.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
data = serializer.validated_data
organizacion = data.get('organizacion')
if self.request.user.is_superuser:
# Permitir que el superusuario cree sin organización o la especifique
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
if not organizacion:
serializer.save(organizacion=self.request.user.organizacion)
else:
serializer.save()
return
raise ValueError("No cuentas con los permisos necesarios para crear un DataStage")
def perform_update(self, serializer):
"""
Override to ensure organization is set on update.
"""
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
raise ValueError("Usuario no autenticado o sin organización")
if self.request.user.is_superuser:
# Allow superuser to update without organization
serializer.save()
return
if (self.request.user.groups.filter(name='developer').exists() or self.request.user.groups.filter(name='admin').exists() or self.request.user.groups.filter(name='user').exists()) and self.request.user.groups.filter(name='Agente Aduanal').exists():
serializer.save(organizacion=self.request.user.organizacion)
return
raise ValueError("No cuentas con los permisos necesarios para actualizar un DataStage")
@action(detail=True, methods=['get'], url_path='download-datastage', url_name='download-datastage')
def download_datastage(self, request, pk=None):
"""
Endpoint para descargar el archivo asociado a un DataStage.
"""
try:
datastage = self.get_object()
if not datastage.archivo:
raise Http404("No hay archivo asociado a este DataStage.")
file_path = datastage.archivo.path
if not os.path.exists(file_path):
raise Http404("El archivo no existe en el servidor.")
response = FileResponse(open(file_path, 'rb'), as_attachment=True, filename=os.path.basename(file_path))
return response
except Exception as e:
return Response({'detail': str(e)}, status=404)
@action(detail=True, methods=['post'], url_path='procesar')
def procesar(self, request, pk=None):
"""
Endpoint para procesar el DataStage de forma asíncrona usando Celery.
"""
from api.datastage.tasks import procesar_datastage_task
datastage = self.get_object()
user_organizacion = getattr(self.request.user, 'organizacion', None)
user_organizacion_id = user_organizacion.id if user_organizacion else None
task = procesar_datastage_task.delay(datastage.id, user_organizacion_id)
return Response({
'task_id': task.id,
'detail': 'Procesamiento iniciado. Puede consultar el estado con el task_id.'
})
@action(detail=False, methods=['get'], url_path='task-status')
def task_status(self, request):
"""
Consulta el estado de una tarea de Celery por task_id.
"""
from celery.result import AsyncResult
task_id = request.query_params.get('task_id')
if not task_id:
return Response({'detail': 'Falta el parámetro task_id.'}, status=400)
result = AsyncResult(task_id)
return Response({
'task_id': task_id,
'status': result.status,
'result': result.result if result.successful() else None
})

0
api/licence/__init__.py Normal file
View File

10
api/licence/admin.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Licencia
# Register your models here.
class LicenciaAdmin(admin.ModelAdmin):
list_display = ('id', 'nombre')
search_fields = ('nombre',)
list_filter = ('nombre',)
admin.site.register(Licencia, LicenciaAdmin)

6
api/licence/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LicenceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.licence'

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Licencia',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('nombre', models.CharField(max_length=100)),
('descripcion', models.TextField(blank=True, null=True)),
('almacenamiento', models.PositiveIntegerField(default=0)),
],
options={
'verbose_name': 'Licencia',
'verbose_name_plural': 'Licencias',
'db_table': 'licencia',
},
),
]

View File

20
api/licence/models.py Normal file
View File

@@ -0,0 +1,20 @@
from django.db import models
# Create your models here.
class Licencia(models.Model):
# Define the Licencia model minimally for ForeignKey reference
# Add fields as needed
nombre = models.CharField(max_length=100)
descripcion = models.TextField(blank=True, null=True)
almacenamiento = models.PositiveIntegerField(default=0, blank=False, null=False) # in GB
class Meta:
verbose_name = "Licencia"
verbose_name_plural = "Licencias"
db_table = 'licencia'
def __str__(self):
return self.nombre

View File

@@ -0,0 +1,9 @@
from rest_framework import serializers
from .models import Licencia
class LicenciaSerializer(serializers.ModelSerializer):
class Meta:
model = Licencia
fields = '__all__'
read_only_fields = ('created_at', 'updated_at')

53
api/licence/tests.py Normal file
View File

@@ -0,0 +1,53 @@
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient
from rest_framework import status
from django.contrib.auth import get_user_model
from .models import Licencia
User = get_user_model()
class LicenciaViewSetTests(APITestCase):
def setUp(self):
self.superuser = User.objects.create_superuser(username="superuser", password="superpass")
self.user = User.objects.create_user(username="user", password="userpass")
self.client = APIClient()
def test_superuser_can_list_licencias(self):
Licencia.objects.create(nombre="Lic1", descripcion="desc1", almacenamiento=10)
self.client.force_authenticate(user=self.superuser)
url = reverse('licencia-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(len(response.data) >= 1)
def test_superuser_can_create_licencia(self):
self.client.force_authenticate(user=self.superuser)
url = reverse('licencia-list')
data = {"nombre": "Lic2", "descripcion": "desc2", "almacenamiento": 20}
response = self.client.post(url, data)
self.assertIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_superuser_can_update_licencia(self):
lic = Licencia.objects.create(nombre="Lic3", descripcion="desc3", almacenamiento=30)
self.client.force_authenticate(user=self.superuser)
url = reverse('licencia-detail', args=[lic.id])
data = {"descripcion": "updated"}
response = self.client.patch(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['descripcion'], "updated")
def test_user_cannot_create_licencia(self):
self.client.force_authenticate(user=self.user)
url = reverse('licencia-list')
data = {"nombre": "Lic4", "descripcion": "desc4", "almacenamiento": 40}
response = self.client.post(url, data)
self.assertNotIn(response.status_code, [status.HTTP_201_CREATED, status.HTTP_200_OK])
def test_user_cannot_update_licencia(self):
lic = Licencia.objects.create(nombre="Lic5", descripcion="desc5", almacenamiento=50)
self.client.force_authenticate(user=self.user)
url = reverse('licencia-detail', args=[lic.id])
data = {"descripcion": "updated"}
response = self.client.patch(url, data)
self.assertNotIn(response.status_code, [status.HTTP_200_OK])

22
api/licence/urls.py Normal file
View File

@@ -0,0 +1,22 @@
# This file defines the URL patterns for the customs app in a Django project.
from django.urls import path, include
from rest_framework.routers import DefaultRouter
# import necessary viewsets
# from .views import YourViewSet # Import your viewsets here
from .views import ViewSetLicencia
# Create a router and register your viewsets with it
router = DefaultRouter()
# Register your viewsets with the router here
# Example:
# from .views import MyViewSet
# router.register(r'myviewset', MyViewSet, basename='myviewset')
router.register(r'licencias', ViewSetLicencia, basename='licencia')
# Import your viewsets here
urlpatterns = [
path('', include(router.urls)),
]

61
api/licence/views.py Normal file
View File

@@ -0,0 +1,61 @@
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from core.permissions import IsSuperUser
from .models import Licencia
from .serializers import LicenciaSerializer
from api.logger.mixins import LoggingMixin
# Create your views here.
class ViewSetLicencia(LoggingMixin, viewsets.ModelViewSet):
"""
ViewSet for Licencia model.
"""
permission_classes = [IsAuthenticated, IsSuperUser]
queryset = Licencia.objects.all()
serializer_class = LicenciaSerializer
filterset_fields = ['nombre', 'descripcion', 'fecha_emision']
my_tags = ['Licencias']
def perform_create(self, serializer):
"""
Override to add custom logic for creating a Licencia.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# If superuser, allow creating without organization
if self.request.user.is_superuser:
serializer.save()
else:
raise ValueError("Solo los superusuarios pueden crear licencias sin organización asignada")
def perform_update(self, serializer):
"""
Override to add custom logic for updating a Licencia.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# If superuser, allow updating without organization
if self.request.user.is_superuser:
serializer.save()
else:
raise ValueError("Solo los superusuarios pueden actualizar licencias sin organización asignada")
def perform_destroy(self, instance):
"""
Override to add custom logic for deleting a Licencia.
"""
if not self.request.user.is_authenticated:
raise ValueError("Usuario no autenticado")
# If superuser, allow deleting without organization
if self.request.user.is_superuser:
instance.delete()
else:
raise ValueError("Solo los superusuarios pueden eliminar licencias sin organización asignada")

0
api/logger/__init__.py Normal file
View File

149
api/logger/admin.py Normal file
View File

@@ -0,0 +1,149 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .models import RequestLog, UserActivity, ErrorLog
import json
from config.settings import SITE_URL
class ReadOnlyAdminMixin:
"""Mixin para hacer que los modelos sean solo lectura en el admin."""
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(RequestLog)
class RequestLogAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'method', 'path', 'status_code',
'response_time', 'ip_address'
]
list_filter = [
'method', 'status_code', 'timestamp', 'user_agent'
]
search_fields = [
'path', 'ip_address', 'user__username', 'user__email'
]
readonly_fields = [
'timestamp', 'user', 'method', 'path', 'query_params_display',
'status_code', 'response_time', 'ip_address', 'user_agent',
'body_display', 'referer'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 50
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Anónimo"
user_display.short_description = "Usuario"
def query_params_display(self, obj):
if obj.query_params:
try:
params = json.loads(obj.query_params) if isinstance(obj.query_params, str) else obj.query_params
formatted = json.dumps(params, indent=2, ensure_ascii=False)
return format_html('<pre>{}</pre>', formatted)
except:
return obj.query_params
return "Sin parámetros"
query_params_display.short_description = "Parámetros de consulta"
def body_display(self, obj):
if obj.body:
try:
# Intentar formatear como JSON si es posible
body_data = json.loads(obj.body) if isinstance(obj.body, str) else obj.body
formatted = json.dumps(body_data, indent=2, ensure_ascii=False)
return format_html('<pre style="max-height: 200px; overflow-y: auto;">{}</pre>', formatted)
except:
# Si no es JSON válido, mostrar como texto
return format_html('<pre style="max-height: 200px; overflow-y: auto;">{}</pre>', obj.body[:1000])
return "Sin body"
body_display.short_description = "Cuerpo del request"
@admin.register(UserActivity)
class UserActivityAdmin(admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'action', 'object_type',
'object_id', 'ip_address'
]
list_filter = [
'action', 'object_type', 'timestamp'
]
search_fields = [
'user__username', 'user__email', 'action', 'object_type',
'object_id', 'ip_address', 'description'
]
readonly_fields = [
'timestamp', 'user', 'action', 'object_type', 'object_id',
'description', 'ip_address'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 50
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Sistema"
user_display.short_description = "Usuario"
@admin.register(ErrorLog)
class ErrorLogAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
list_display = [
'timestamp', 'user_display', 'level', 'message_short',
'request_path'
]
list_filter = [
'level', 'timestamp'
]
search_fields = [
'user__username', 'user__email', 'level', 'message',
'request_path', 'traceback'
]
readonly_fields = [
'timestamp', 'user', 'level', 'message', 'traceback_display',
'request_path', 'ip_address'
]
ordering = ['-timestamp']
date_hierarchy = 'timestamp'
list_per_page = 25
def user_display(self, obj):
if obj.user:
return f"{obj.user.username} ({obj.user.email})"
return "Sistema/Anónimo"
user_display.short_description = "Usuario"
def message_short(self, obj):
if obj.message and len(obj.message) > 100:
return f"{obj.message[:100]}..."
return obj.message or "Sin mensaje"
message_short.short_description = "Mensaje"
def traceback_display(self, obj):
if obj.traceback:
return format_html(
'<pre style="max-height: 400px; overflow-y: auto; background: #f8f8f8; padding: 10px; border: 1px solid #ddd;">{}</pre>',
obj.traceback
)
return "Sin traceback"
traceback_display.short_description = "Stack trace"
# Personalización del admin site
admin.site.site_header = "EFC V2 "
admin.site.site_title = "EFC V2"
admin.site.index_title = "Administración del Sistema"
admin.site.site_url = SITE_URL

6
api/logger/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class LoggerConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.logger'

91
api/logger/middleware.py Normal file
View File

@@ -0,0 +1,91 @@
import time
import json
import logging
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth.models import AnonymousUser
from .models import RequestLog, ErrorLog
logger = logging.getLogger('django')
class RequestLoggingMiddleware(MiddlewareMixin):
def process_request(self, request):
request.start_time = time.time()
return None
def process_response(self, request, response):
# Calcular tiempo de respuesta
response_time = (time.time() - getattr(request, 'start_time', 0)) * 1000
# Obtener información del usuario
user = request.user if not isinstance(request.user, AnonymousUser) else None
# Obtener IP del cliente
ip_address = self.get_client_ip(request)
# Obtener query parameters
query_params = dict(request.GET) if request.GET else {}
# Obtener body de la request (solo para POST, PUT, PATCH)
body = ""
if request.method in ['POST', 'PUT', 'PATCH']:
try:
if hasattr(request, 'body'):
body = request.body.decode('utf-8')[:1000] # Limitar a 1000 caracteres
except Exception:
body = "Could not decode body"
# Crear log de la request
try:
RequestLog.objects.create(
user=user,
ip_address=ip_address,
user_agent=request.META.get('HTTP_USER_AGENT', '')[:500],
method=request.method,
path=request.path,
query_params=json.dumps(query_params),
body=body,
status_code=response.status_code,
response_time=response_time,
referer=request.META.get('HTTP_REFERER', '')
)
except Exception as e:
logger.error(f"Error logging request: {e}")
return response
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class ErrorLoggingMiddleware(MiddlewareMixin):
def process_exception(self, request, exception):
import traceback
user = request.user if not isinstance(request.user, AnonymousUser) else None
ip_address = self.get_client_ip(request)
try:
ErrorLog.objects.create(
level='ERROR',
message=str(exception),
traceback=traceback.format_exc(),
user=user,
ip_address=ip_address,
request_path=request.path
)
except Exception as e:
logger.error(f"Error logging exception: {e}")
return None
def get_client_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip

View File

@@ -0,0 +1,73 @@
# Generated by Django 5.2.3 on 2025-07-14 16:14
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ErrorLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('level', models.CharField(choices=[('DEBUG', 'Debug'), ('INFO', 'Info'), ('WARNING', 'Warning'), ('ERROR', 'Error'), ('CRITICAL', 'Critical')], max_length=10)),
('message', models.TextField()),
('traceback', models.TextField(blank=True)),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('request_path', models.URLField(blank=True, max_length=500)),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_error_log',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='RequestLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ip_address', models.GenericIPAddressField()),
('user_agent', models.TextField(blank=True)),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PUT', 'PUT'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('OPTIONS', 'OPTIONS'), ('HEAD', 'HEAD')], max_length=10)),
('path', models.URLField(max_length=500)),
('query_params', models.TextField(blank=True)),
('body', models.TextField(blank=True)),
('status_code', models.IntegerField()),
('response_time', models.FloatField()),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('referer', models.URLField(blank=True, max_length=500)),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_request_log',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='UserActivity',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('action', models.CharField(choices=[('login', 'Login'), ('logout', 'Logout'), ('create', 'Create'), ('update', 'Update'), ('delete', 'Delete'), ('view', 'View'), ('search', 'Search'), ('export', 'Export'), ('import', 'Import')], max_length=20)),
('object_type', models.CharField(blank=True, max_length=100)),
('object_id', models.CharField(blank=True, max_length=100)),
('description', models.TextField(blank=True)),
('ip_address', models.GenericIPAddressField()),
('timestamp', models.DateTimeField(default=django.utils.timezone.now)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'logger_user_activity',
'ordering': ['-timestamp'],
},
),
]

View File

103
api/logger/mixins.py Normal file
View File

@@ -0,0 +1,103 @@
from .utils import log_user_activity
class LoggingMixin:
"""
Mixin para añadir logging automático a ViewSets
"""
log_actions = True
log_object_type = None
def get_log_object_type(self):
"""Obtiene el tipo de objeto del modelo del ViewSet"""
if self.log_object_type:
return self.log_object_type
if hasattr(self, 'queryset') and self.queryset is not None:
return self.queryset.model.__name__
if hasattr(self, 'model') and self.model is not None:
return self.model.__name__
return self.__class__.__name__.replace('ViewSet', '')
def perform_create(self, serializer):
"""Override para loggear creaciones"""
instance = serializer.save()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='create',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Creado {self.get_log_object_type()} {instance.pk}',
request=self.request
)
return instance
def perform_update(self, serializer):
"""Override para loggear actualizaciones"""
instance = serializer.save()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='update',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Actualizado {self.get_log_object_type()} {instance.pk}',
request=self.request
)
return instance
def perform_destroy(self, instance):
"""Override para loggear eliminaciones"""
object_id = instance.pk
object_type = self.get_log_object_type()
instance.delete()
if self.log_actions and self.request.user.is_authenticated:
log_user_activity(
user=self.request.user,
action='delete',
object_type=object_type,
object_id=object_id,
description=f'Eliminado {object_type} {object_id}',
request=self.request
)
def retrieve(self, request, *args, **kwargs):
"""Override para loggear visualizaciones de detalle"""
response = super().retrieve(request, *args, **kwargs)
if self.log_actions and request.user.is_authenticated:
instance = self.get_object()
log_user_activity(
user=request.user,
action='view',
object_type=self.get_log_object_type(),
object_id=instance.pk,
description=f'Visto detalle de {self.get_log_object_type()} {instance.pk}',
request=request
)
return response
def list(self, request, *args, **kwargs):
"""Override para loggear listados"""
response = super().list(request, *args, **kwargs)
if self.log_actions and request.user.is_authenticated:
log_user_activity(
user=request.user,
action='view',
object_type=self.get_log_object_type(),
object_id='',
description=f'Visto listado de {self.get_log_object_type()}',
request=request
)
return response

89
api/logger/models.py Normal file
View File

@@ -0,0 +1,89 @@
from django.db import models
from api.cuser.models import CustomUser as User # Asegúrate de que este es el modelo de usuario correcto
from django.utils import timezone
class RequestLog(models.Model):
METHODS = (
('GET', 'GET'),
('POST', 'POST'),
('PUT', 'PUT'),
('PATCH', 'PATCH'),
('DELETE', 'DELETE'),
('OPTIONS', 'OPTIONS'),
('HEAD', 'HEAD'),
)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField(blank=True)
method = models.CharField(max_length=10, choices=METHODS)
path = models.URLField(max_length=500)
query_params = models.TextField(blank=True)
body = models.TextField(blank=True)
status_code = models.IntegerField()
response_time = models.FloatField() # en milisegundos
timestamp = models.DateTimeField(default=timezone.now)
referer = models.URLField(max_length=500, blank=True)
class Meta:
db_table = 'logger_request_log'
ordering = ['-timestamp']
def __str__(self):
return f"{self.method} {self.path} - {self.status_code} ({self.timestamp})"
class UserActivity(models.Model):
ACTIONS = (
('login', 'Login'),
('logout', 'Logout'),
('create', 'Create'),
('update', 'Update'),
('delete', 'Delete'),
('view', 'View'),
('search', 'Search'),
('export', 'Export'),
('import', 'Import'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE)
action = models.CharField(max_length=20, choices=ACTIONS)
object_type = models.CharField(max_length=100, blank=True) # modelo afectado
object_id = models.CharField(max_length=100, blank=True) # ID del objeto
description = models.TextField(blank=True)
ip_address = models.GenericIPAddressField()
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
db_table = 'logger_user_activity'
ordering = ['-timestamp']
def __str__(self):
return f"{self.user.username} - {self.action} ({self.timestamp})"
class ErrorLog(models.Model):
ERROR_LEVELS = (
('DEBUG', 'Debug'),
('INFO', 'Info'),
('WARNING', 'Warning'),
('ERROR', 'Error'),
('CRITICAL', 'Critical'),
)
level = models.CharField(max_length=10, choices=ERROR_LEVELS)
message = models.TextField()
traceback = models.TextField(blank=True)
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
ip_address = models.GenericIPAddressField(null=True, blank=True)
request_path = models.URLField(max_length=500, blank=True)
timestamp = models.DateTimeField(default=timezone.now)
class Meta:
db_table = 'logger_error_log'
ordering = ['-timestamp']
def __str__(self):
return f"{self.level}: {self.message[:50]}... ({self.timestamp})"

29
api/logger/serializers.py Normal file
View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from .models import RequestLog, UserActivity, ErrorLog
from api.cuser.models import CustomUser
class UserSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ['id', 'username', 'first_name', 'last_name', 'email']
class RequestLogSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = RequestLog
fields = '__all__'
class UserActivitySerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = UserActivity
fields = '__all__'
class ErrorLogSerializer(serializers.ModelSerializer):
user = UserSimpleSerializer(read_only=True)
class Meta:
model = ErrorLog
fields = '__all__'

3
api/logger/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

12
api/logger/urls.py Normal file
View File

@@ -0,0 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RequestLogViewSet, UserActivityViewSet, ErrorLogViewSet
router = DefaultRouter()
router.register(r'requests', RequestLogViewSet)
router.register(r'activities', UserActivityViewSet)
router.register(r'errors', ErrorLogViewSet)
urlpatterns = [
path('', include(router.urls)),
]

98
api/logger/utils.py Normal file
View File

@@ -0,0 +1,98 @@
from django.contrib.auth.models import User
from .models import UserActivity, ErrorLog
import logging
def get_client_ip(request):
"""Obtiene la IP real del cliente"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def log_user_activity(user, action, object_type='', object_id='', description='', request=None):
"""
Registra actividad del usuario
Args:
user: Usuario que realiza la acción
action: Tipo de acción (login, logout, create, update, delete, view, search, export, import)
object_type: Tipo de objeto afectado (opcional)
object_id: ID del objeto afectado (opcional)
description: Descripción adicional (opcional)
request: Request object para obtener IP (opcional)
"""
ip_address = '127.0.0.1'
if request:
ip_address = get_client_ip(request)
try:
UserActivity.objects.create(
user=user,
action=action,
object_type=object_type,
object_id=str(object_id) if object_id else '',
description=description,
ip_address=ip_address
)
except Exception as e:
logging.error(f"Error logging user activity: {e}")
def log_error(level, message, traceback='', user=None, request=None):
"""
Registra errores personalizados
Args:
level: Nivel del error (DEBUG, INFO, WARNING, ERROR, CRITICAL)
message: Mensaje del error
traceback: Traceback del error (opcional)
user: Usuario relacionado (opcional)
request: Request object (opcional)
"""
ip_address = None
request_path = ''
if request:
ip_address = get_client_ip(request)
request_path = request.path
try:
ErrorLog.objects.create(
level=level,
message=message,
traceback=traceback,
user=user,
ip_address=ip_address,
request_path=request_path
)
except Exception as e:
logging.error(f"Error logging custom error: {e}")
# Decorador para loggear automáticamente acciones
def log_action(action, object_type=''):
"""
Decorador para loggear automáticamente acciones en vistas
Usage:
@log_action('create', 'Pedimento')
def create_pedimento(request):
# tu código aquí
"""
def decorator(func):
def wrapper(request, *args, **kwargs):
result = func(request, *args, **kwargs)
if hasattr(request, 'user') and request.user.is_authenticated:
object_id = kwargs.get('pk', kwargs.get('id', ''))
log_user_activity(
user=request.user,
action=action,
object_type=object_type,
object_id=object_id,
request=request
)
return result
return wrapper
return decorator

92
api/logger/views.py Normal file
View File

@@ -0,0 +1,92 @@
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from django.utils import timezone
from datetime import timedelta
from .models import RequestLog, UserActivity, ErrorLog
from .serializers import RequestLogSerializer, UserActivitySerializer, ErrorLogSerializer
from .utils import log_user_activity
from core.permissions import IsSuperUser
class RequestLogViewSet(viewsets.ReadOnlyModelViewSet):
queryset = RequestLog.objects.all()
serializer_class = RequestLogSerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['method', 'status_code', 'user']
search_fields = ['path', 'ip_address', 'user_agent']
ordering_fields = ['timestamp', 'response_time', 'status_code']
ordering = ['-timestamp']
@action(detail=False, methods=['get'])
def statistics(self, request):
"""Estadísticas de requests"""
now = timezone.now()
today = now.date()
week_ago = now - timedelta(days=7)
stats = {
'total_requests': self.queryset.count(),
'today_requests': self.queryset.filter(timestamp__date=today).count(),
'week_requests': self.queryset.filter(timestamp__gte=week_ago).count(),
'methods': self.queryset.values('method').annotate(count=Count('method')),
'status_codes': self.queryset.values('status_code').annotate(count=Count('status_code')),
'top_endpoints': self.queryset.values('path').annotate(count=Count('path')).order_by('-count')[:10],
'avg_response_time': self.queryset.aggregate(avg_time=Count('response_time'))['avg_time']
}
return Response(stats)
class UserActivityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = UserActivity.objects.all()
serializer_class = UserActivitySerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['action', 'user', 'object_type']
search_fields = ['description', 'object_id']
ordering_fields = ['timestamp']
ordering = ['-timestamp']
def get_queryset(self):
# Check if user is authenticated first
if not self.request.user.is_authenticated:
return UserActivity.objects.none()
# Los usuarios normales solo ven su propia actividad
if self.request.user.is_staff:
return UserActivity.objects.all()
return UserActivity.objects.filter(user=self.request.user)
@action(detail=False, methods=['get'])
def my_activity(self, request):
"""Actividad del usuario actual"""
if not request.user.is_authenticated:
return Response({"error": "Usuario no autenticado"}, status=401)
activities = UserActivity.objects.filter(user=request.user)[:20]
serializer = self.get_serializer(activities, many=True)
return Response(serializer.data)
class ErrorLogViewSet(viewsets.ReadOnlyModelViewSet):
queryset = ErrorLog.objects.all()
serializer_class = ErrorLogSerializer
permission_classes = [IsAuthenticated, IsSuperUser]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['level', 'user']
search_fields = ['message', 'request_path']
ordering_fields = ['timestamp']
ordering = ['-timestamp']
@action(detail=False, methods=['get'])
def recent_errors(self, request):
"""Errores recientes (últimas 24 horas)"""
yesterday = timezone.now() - timedelta(days=1)
recent_errors = self.queryset.filter(timestamp__gte=yesterday)
serializer = self.get_serializer(recent_errors, many=True)
return Response(serializer.data)

View File

Some files were not shown because too many files have changed in this diff Show More