Mudanza de repo
This commit is contained in:
15
.dockerignore
Normal file
15
.dockerignore
Normal 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
20
.env.example
Normal 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
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
181
.gitignore
vendored
Normal file
181
.gitignore
vendored
Normal 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
20
Dockerfile
Normal 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
22
Dockerfile.prod
Normal 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
0
api/__init__.py
Normal file
0
api/cards/__init__.py
Normal file
0
api/cards/__init__.py
Normal file
3
api/cards/admin.py
Normal file
3
api/cards/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
api/cards/apps.py
Normal file
6
api/cards/apps.py
Normal 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
3
api/cards/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
79
api/cards/tests.py
Normal file
79
api/cards/tests.py
Normal 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
21
api/cards/urls.py
Normal 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
457
api/cards/views.py
Normal 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
0
api/cuser/__init__.py
Normal file
51
api/cuser/admin.py
Normal file
51
api/cuser/admin.py
Normal 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
6
api/cuser/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CuserConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.cuser'
|
||||
45
api/cuser/jwt_cookie_views.py
Normal file
45
api/cuser/jwt_cookie_views.py
Normal 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
|
||||
51
api/cuser/migrations/0001_initial.py
Normal file
51
api/cuser/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
api/cuser/migrations/0002_alter_customuser_rfc.py
Normal file
18
api/cuser/migrations/0002_alter_customuser_rfc.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
api/cuser/migrations/0003_alter_customuser_rfc.py
Normal file
18
api/cuser/migrations/0003_alter_customuser_rfc.py
Normal 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),
|
||||
),
|
||||
]
|
||||
0
api/cuser/migrations/__init__.py
Normal file
0
api/cuser/migrations/__init__.py
Normal file
23
api/cuser/models.py
Normal file
23
api/cuser/models.py
Normal 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']
|
||||
24
api/cuser/password_reset_utils.py
Normal file
24
api/cuser/password_reset_utils.py
Normal 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
29
api/cuser/serializers.py
Normal 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
91
api/cuser/tests.py
Normal 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
18
api/cuser/urls.py
Normal 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
28
api/cuser/utils.py
Normal 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
275
api/cuser/views.py
Normal 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
0
api/customs/__init__.py
Normal file
65
api/customs/admin.py
Normal file
65
api/customs/admin.py
Normal 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
9
api/customs/apps.py
Normal 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
|
||||
224
api/customs/migrations/0001_initial.py
Normal file
224
api/customs/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
32
api/customs/migrations/0003_cove.py
Normal file
32
api/customs/migrations/0003_cove.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
19
api/customs/migrations/0006_pedimento_pedimento_app.py
Normal file
19
api/customs/migrations/0006_pedimento_pedimento_app.py
Normal 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,
|
||||
),
|
||||
]
|
||||
27
api/customs/migrations/0007_regimen.py
Normal file
27
api/customs/migrations/0007_regimen.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
139
api/customs/migrations/0008_regimen_catalogo.py
Normal file
139
api/customs/migrations/0008_regimen_catalogo.py
Normal 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),
|
||||
]
|
||||
31
api/customs/migrations/0009_importador.py
Normal file
31
api/customs/migrations/0009_importador.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
21
api/customs/migrations/0009b_poblar_importadores.py
Normal file
21
api/customs/migrations/0009b_poblar_importadores.py
Normal 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),
|
||||
]
|
||||
19
api/customs/migrations/0010_alter_pedimento_contribuyente.py
Normal file
19
api/customs/migrations/0010_alter_pedimento_contribuyente.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
0
api/customs/migrations/__init__.py
Normal file
0
api/customs/migrations/__init__.py
Normal file
187
api/customs/models.py
Normal file
187
api/customs/models.py
Normal 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}"
|
||||
96
api/customs/serializers.py
Normal file
96
api/customs/serializers.py
Normal 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')
|
||||
0
api/customs/signals/__init__.py
Normal file
0
api/customs/signals/__init__.py
Normal file
59
api/customs/signals/procesamiento.py
Normal file
59
api/customs/signals/procesamiento.py
Normal 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)])
|
||||
2
api/customs/tasks/__init__.py
Normal file
2
api/customs/tasks/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .microservice import *
|
||||
from .internal_services import *
|
||||
78
api/customs/tasks/auditoria.py
Normal file
78
api/customs/tasks/auditoria.py
Normal 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
|
||||
|
||||
220
api/customs/tasks/internal_services.py
Normal file
220
api/customs/tasks/internal_services.py
Normal 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)])
|
||||
213
api/customs/tasks/microservice.py
Normal file
213
api/customs/tasks/microservice.py
Normal 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
77
api/customs/tests.py
Normal 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
34
api/customs/urls.py
Normal 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
490
api/customs/views.py
Normal 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']
|
||||
0
api/datastage/__init__.py
Normal file
0
api/datastage/__init__.py
Normal file
44
api/datastage/admin.py
Normal file
44
api/datastage/admin.py
Normal 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
7
api/datastage/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DatastageConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.datastage'
|
||||
|
||||
33
api/datastage/migrations/0001_initial.py
Normal file
33
api/datastage/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
63
api/datastage/migrations/0005_registro701_registro702.py
Normal file
63
api/datastage/migrations/0005_registro701_registro702.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
0
api/datastage/migrations/__init__.py
Normal file
0
api/datastage/migrations/__init__.py
Normal file
571
api/datastage/models.py
Normal file
571
api/datastage/models.py
Normal 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'
|
||||
|
||||
|
||||
|
||||
|
||||
12
api/datastage/serializer.py
Normal file
12
api/datastage/serializer.py
Normal 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
276
api/datastage/tasks.py
Normal 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
85
api/datastage/tests.py
Normal 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
12
api/datastage/urls.py
Normal 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
140
api/datastage/views.py
Normal 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
0
api/licence/__init__.py
Normal file
10
api/licence/admin.py
Normal file
10
api/licence/admin.py
Normal 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
6
api/licence/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LicenceConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.licence'
|
||||
28
api/licence/migrations/0001_initial.py
Normal file
28
api/licence/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
api/licence/migrations/__init__.py
Normal file
0
api/licence/migrations/__init__.py
Normal file
20
api/licence/models.py
Normal file
20
api/licence/models.py
Normal 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
|
||||
|
||||
9
api/licence/serializers.py
Normal file
9
api/licence/serializers.py
Normal 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
53
api/licence/tests.py
Normal 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
22
api/licence/urls.py
Normal 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
61
api/licence/views.py
Normal 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
0
api/logger/__init__.py
Normal file
149
api/logger/admin.py
Normal file
149
api/logger/admin.py
Normal 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
6
api/logger/apps.py
Normal 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
91
api/logger/middleware.py
Normal 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
|
||||
73
api/logger/migrations/0001_initial.py
Normal file
73
api/logger/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
api/logger/migrations/__init__.py
Normal file
0
api/logger/migrations/__init__.py
Normal file
103
api/logger/mixins.py
Normal file
103
api/logger/mixins.py
Normal 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
89
api/logger/models.py
Normal 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
29
api/logger/serializers.py
Normal 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
3
api/logger/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
12
api/logger/urls.py
Normal file
12
api/logger/urls.py
Normal 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
98
api/logger/utils.py
Normal 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
92
api/logger/views.py
Normal 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)
|
||||
0
api/notificaciones/__init__.py
Normal file
0
api/notificaciones/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user