feature/seed-digitalizacion
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
"""seed document_types_digitization
|
||||||
|
|
||||||
|
Revision ID: d2e3f4a5b6c7
|
||||||
|
Revises: f1a2b3c4d5e6
|
||||||
|
Create Date: 2026-05-25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from api.v1.modules.a76.doc_types_dig.seed import seed as doc_types_dig_seed
|
||||||
|
|
||||||
|
revision: str = "d2e3f4a5b6c7"
|
||||||
|
down_revision: Union[str, None] = "f1a2b3c4d5e6"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Quitar columna active (no se usa en ningún flujo)
|
||||||
|
op.drop_column('document_types_digitization', 'active', schema='a76')
|
||||||
|
|
||||||
|
def format_value(val):
|
||||||
|
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||||
|
return "NULL"
|
||||||
|
return f"'{str(val).replace(chr(39), chr(39)*2)}'"
|
||||||
|
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = Session(bind=bind)
|
||||||
|
companies = session.execute(
|
||||||
|
text("SELECT tenant_id, id FROM a76.company WHERE deleted_at IS NULL")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for tenant_id, company_id in companies:
|
||||||
|
values = ", ".join([
|
||||||
|
f"({format_value(code)}, {format_value(description)}, {tenant_id}, {company_id})"
|
||||||
|
for code, description in doc_types_dig_seed
|
||||||
|
])
|
||||||
|
if values:
|
||||||
|
session.execute(text(f"""
|
||||||
|
INSERT INTO a76.document_types_digitization (code, description, tenant_id, company_id)
|
||||||
|
VALUES {values}
|
||||||
|
ON CONFLICT (tenant_id, company_id, code) DO NOTHING;
|
||||||
|
"""))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'document_types_digitization',
|
||||||
|
sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.text('true')),
|
||||||
|
schema='a76',
|
||||||
|
)
|
||||||
@@ -6,7 +6,6 @@ class DocumentTypeDigitizationBase(BaseModel):
|
|||||||
|
|
||||||
code: str = Field(..., max_length=10, description="Código del tipo de documento")
|
code: str = Field(..., max_length=10, description="Código del tipo de documento")
|
||||||
description: str = Field(..., description="Descripción del tipo de documento")
|
description: str = Field(..., description="Descripción del tipo de documento")
|
||||||
active: bool = Field(default=True, description="Indica si el tipo está activo")
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeDigitizationCreate(DocumentTypeDigitizationBase):
|
class DocumentTypeDigitizationCreate(DocumentTypeDigitizationBase):
|
||||||
@@ -20,7 +19,6 @@ class DocumentTypeDigitizationUpdate(BaseModel):
|
|||||||
|
|
||||||
code: str | None = Field(None, max_length=10)
|
code: str | None = Field(None, max_length=10)
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
active: bool | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentTypeDigitizationResponse(DocumentTypeDigitizationBase):
|
class DocumentTypeDigitizationResponse(DocumentTypeDigitizationBase):
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||||
from core.database import Base
|
from core.database import Base
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Boolean,
|
|
||||||
Integer,
|
Integer,
|
||||||
PrimaryKeyConstraint,
|
PrimaryKeyConstraint,
|
||||||
String,
|
String,
|
||||||
@@ -29,4 +28,3 @@ class DocumentTypeDigitization(Base, TenantScopedMixin, TimestampMixin):
|
|||||||
id: Mapped[int] = mapped_column(Integer)
|
id: Mapped[int] = mapped_column(Integer)
|
||||||
code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||||
active: Mapped[bool] = mapped_column(Boolean, default=True, server_default="true")
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ async def list_document_types(
|
|||||||
page: int = Query(1, ge=1, description='Page number'),
|
page: int = Query(1, ge=1, description='Page number'),
|
||||||
page_size: int = Query(50, ge=1, le=2000, description='Page size'),
|
page_size: int = Query(50, ge=1, le=2000, description='Page size'),
|
||||||
search: Optional[str] = Query(None, description='Search by code or description'),
|
search: Optional[str] = Query(None, description='Search by code or description'),
|
||||||
active_only: bool = Query(False, description='Only active records'),
|
|
||||||
sort_by: Optional[str] = Query('code', description='Column to sort by'),
|
sort_by: Optional[str] = Query('code', description='Column to sort by'),
|
||||||
sort_order: str = Query('asc', pattern='^(asc|desc)$', description='Sort order'),
|
sort_order: str = Query('asc', pattern='^(asc|desc)$', description='Sort order'),
|
||||||
db: Session = Depends(get_core_db),
|
db: Session = Depends(get_core_db),
|
||||||
@@ -31,8 +30,6 @@ async def list_document_types(
|
|||||||
|
|
||||||
if search:
|
if search:
|
||||||
filters['search'] = search
|
filters['search'] = search
|
||||||
if active_only:
|
|
||||||
filters['active_only'] = True
|
|
||||||
|
|
||||||
items, total = DocumentTypeDigitizationService.get_all(
|
items, total = DocumentTypeDigitizationService.get_all(
|
||||||
db,
|
db,
|
||||||
|
|||||||
40
backend/api/v1/modules/a76/doc_types_dig/seed.py
Normal file
40
backend/api/v1/modules/a76/doc_types_dig/seed.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Catálogo SAT — tipos de documentos para digitalización (Ventanilla Única)
|
||||||
|
seed = [
|
||||||
|
('168', 'Calca o fotografía digital del NIV del vehículo.'),
|
||||||
|
('169', 'Aviso.'),
|
||||||
|
('170', 'Factura.'),
|
||||||
|
('171', 'Documento con el que se acredite la propiedad de la mercancía.'),
|
||||||
|
('172', 'Contratos.'),
|
||||||
|
('176', 'Documentación relacionada con la garantía otorgada en términos de los artículos 84.'),
|
||||||
|
('177', 'Identificación Oficial.'),
|
||||||
|
('179', 'Comprobante de domicilio.'),
|
||||||
|
('184', 'Documento que ampara el avaluó de las mercancías.'),
|
||||||
|
('185', 'Documentos de adjudicación judicial de las mercancías.'),
|
||||||
|
('187', 'Solicitud de retiro de mercancías que causaron abandono.'),
|
||||||
|
('189', 'Actas.'),
|
||||||
|
('192', 'Escritos.'),
|
||||||
|
('420', 'Certificado de peso o volumen.'),
|
||||||
|
('421', 'Comprobante de la importación temporal de la embarcación debidamente formalizado.'),
|
||||||
|
('422', 'Comprobante expedido por donataria.'),
|
||||||
|
('423', 'Consulta en la que conste que el vehículo no se encuentra reportado como robado,'),
|
||||||
|
('424', 'Clave Unica del Registro de Población.'),
|
||||||
|
('425', 'Declaración de internación o extracción de cantidades en efectivo y/o documentos p'),
|
||||||
|
('426', 'Declaración de operaciones que no confieren origen en países no parte de acuerdo'),
|
||||||
|
('427', 'Declaración en la que se señalen los motivos por los que efectúa la devolución de m'),
|
||||||
|
('428', 'Documentación con información que permita la identificación, análisis y control en tér'),
|
||||||
|
('429', 'Documentación que acredite que acepta y subsana la irregularidad.'),
|
||||||
|
('430', 'Documentación que ampara la importación temporal del vehículo de que se trate.'),
|
||||||
|
('431', 'Documentación que compruebe que la adquisición de las mercancías fue efectuada '),
|
||||||
|
('433', 'Documento con base en el cual se determine la procedencia y el origen de las merca'),
|
||||||
|
('434', 'Documento con que se acredite el reintegro del IVA, en caso de que el contribuyente '),
|
||||||
|
('435', 'Documentos previstos en la regla 8.7., fracciones I a IV de la Resolución del TLCAN.'),
|
||||||
|
('436', 'El Documento que compruebe el cumplimiento de las regulaciones y restricciones no '),
|
||||||
|
('438', 'Guía aérea, conocimiento de embarque o carta de porte.'),
|
||||||
|
('439', 'Hoja con los datos de la matrícula y nombre del barco, el lugar donde se localiza y se '),
|
||||||
|
('440', 'Manifiesto de carga.'),
|
||||||
|
('441', 'Oficios emitidos por autoridad.'),
|
||||||
|
('442', 'Pedimentos.'),
|
||||||
|
('443', 'Programa IMMEX.'),
|
||||||
|
('444', 'Relación de candados.'),
|
||||||
|
('445', 'Relación de certificados de origen.'),
|
||||||
|
]
|
||||||
@@ -18,14 +18,6 @@ class DocumentTypeDigitizationService:
|
|||||||
def _normalize_description(description: str) -> str:
|
def _normalize_description(description: str) -> str:
|
||||||
return (description or '').strip()
|
return (description or '').strip()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_truthy_filter(value: Any, default: bool = False) -> bool:
|
|
||||||
if value is None:
|
|
||||||
return default
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return value
|
|
||||||
return str(value).strip().lower() in {'1', 'true', 'yes', 'si'}
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_all(
|
def get_all(
|
||||||
db: Session,
|
db: Session,
|
||||||
@@ -45,15 +37,8 @@ class DocumentTypeDigitizationService:
|
|||||||
query = query.filter(DocumentTypeDigitization.company_id == company_id)
|
query = query.filter(DocumentTypeDigitization.company_id == company_id)
|
||||||
|
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
active_only = DocumentTypeDigitizationService._is_truthy_filter(
|
|
||||||
filters.get('active_only'),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
search = (filters.get('search') or '').strip()
|
search = (filters.get('search') or '').strip()
|
||||||
|
|
||||||
if active_only:
|
|
||||||
query = query.filter(DocumentTypeDigitization.active.is_(True))
|
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
like = f'%{search}%'
|
like = f'%{search}%'
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
@@ -69,7 +54,6 @@ class DocumentTypeDigitizationService:
|
|||||||
'id': DocumentTypeDigitization.id,
|
'id': DocumentTypeDigitization.id,
|
||||||
'code': DocumentTypeDigitization.code,
|
'code': DocumentTypeDigitization.code,
|
||||||
'description': DocumentTypeDigitization.description,
|
'description': DocumentTypeDigitization.description,
|
||||||
'active': DocumentTypeDigitization.active,
|
|
||||||
}.get(sort_by or 'code', DocumentTypeDigitization.code)
|
}.get(sort_by or 'code', DocumentTypeDigitization.code)
|
||||||
|
|
||||||
if sort_order == 'desc':
|
if sort_order == 'desc':
|
||||||
@@ -195,17 +179,3 @@ class DocumentTypeDigitizationService:
|
|||||||
db.refresh(db_obj)
|
db.refresh(db_obj)
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete(
|
|
||||||
db: Session,
|
|
||||||
id: int,
|
|
||||||
tenant_id: int,
|
|
||||||
company_id: int,
|
|
||||||
) -> bool:
|
|
||||||
db_obj = DocumentTypeDigitizationService.get_by_id(db, id, tenant_id, company_id)
|
|
||||||
if not db_obj:
|
|
||||||
return False
|
|
||||||
|
|
||||||
db_obj.active = False
|
|
||||||
db.commit()
|
|
||||||
return True
|
|
||||||
@@ -21,6 +21,7 @@ from ..units_of_measure.seed import seed as units_of_measure_seed
|
|||||||
from ..fractions.historical_tariff_fractions.seed import seed as historical_tariff_fractions_seed
|
from ..fractions.historical_tariff_fractions.seed import seed as historical_tariff_fractions_seed
|
||||||
from ..fractions.warning_fractions.seed import seed as warning_fractions_seed
|
from ..fractions.warning_fractions.seed import seed as warning_fractions_seed
|
||||||
from ..sectors.seed import seed as sectors_seed
|
from ..sectors.seed import seed as sectors_seed
|
||||||
|
from ...doc_types_dig.seed import seed as doc_types_dig_seed
|
||||||
from core.context import get_user_context
|
from core.context import get_user_context
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
@@ -812,6 +813,20 @@ class CompanyService:
|
|||||||
ON CONFLICT (key, tenant_id, company_id) DO NOTHING;
|
ON CONFLICT (key, tenant_id, company_id) DO NOTHING;
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
# 5. Document Types for Digitization
|
||||||
|
values_doc_types = ", ".join(
|
||||||
|
[
|
||||||
|
f"({format_value(code)}, {format_value(description)}, {tenant_id}, {company_id})"
|
||||||
|
for code, description in doc_types_dig_seed
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if values_doc_types:
|
||||||
|
db.execute(text(f"""
|
||||||
|
INSERT INTO a76.document_types_digitization (code, description, tenant_id, company_id)
|
||||||
|
VALUES {values_doc_types}
|
||||||
|
ON CONFLICT (tenant_id, company_id, code) DO NOTHING;
|
||||||
|
"""))
|
||||||
|
|
||||||
def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:
|
def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:
|
||||||
"""Get all companies for a tenant"""
|
"""Get all companies for a tenant"""
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export interface DocumentTypeDigitization {
|
|||||||
id: number;
|
id: number;
|
||||||
code: string;
|
code: string;
|
||||||
description: string;
|
description: string;
|
||||||
active: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentTypeDigitizationListResponse {
|
export interface DocumentTypeDigitizationListResponse {
|
||||||
@@ -25,7 +24,6 @@ export const documentTypesDigitizationApi = {
|
|||||||
pageSize = 50,
|
pageSize = 50,
|
||||||
companyId: number,
|
companyId: number,
|
||||||
search?: string,
|
search?: string,
|
||||||
activeOnly = false
|
|
||||||
) => {
|
) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: page.toString(),
|
page: page.toString(),
|
||||||
@@ -37,15 +35,11 @@ export const documentTypesDigitizationApi = {
|
|||||||
params.append('search', search);
|
params.append('search', search);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeOnly) {
|
|
||||||
params.append('active_only', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.get<DocumentTypeDigitizationListResponse>(`${BASE_URL}/?${params.toString()}`);
|
return api.get<DocumentTypeDigitizationListResponse>(`${BASE_URL}/?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
getAll: (companyId: number, activeOnly = true, search?: string) => {
|
getAll: (companyId: number, search?: string) => {
|
||||||
return documentTypesDigitizationApi.list(1, 2000, companyId, search, activeOnly);
|
return documentTypesDigitizationApi.list(1, 2000, companyId, search);
|
||||||
},
|
},
|
||||||
|
|
||||||
getById: (id: number, companyId: number) => {
|
getById: (id: number, companyId: number) => {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
if (docTypes.length === 0) {
|
if (docTypes.length === 0) {
|
||||||
docTypesLoading = true;
|
docTypesLoading = true;
|
||||||
documentTypesDigitizationApi
|
documentTypesDigitizationApi
|
||||||
.getAll(companyId, true)
|
.getAll(companyId)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
docTypes = res.data?.items || [];
|
docTypes = res.data?.items || [];
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
tiposDocumentos = [];
|
tiposDocumentos = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await documentTypesDigitizationApi.getAll(companyId, true);
|
const response = await documentTypesDigitizationApi.getAll(companyId);
|
||||||
tiposDocumentos = response.data?.items || [];
|
tiposDocumentos = response.data?.items || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al cargar tipos de documentos:', error);
|
console.error('Error al cargar tipos de documentos:', error);
|
||||||
|
|||||||
@@ -34,25 +34,6 @@ export function createColumns(): ColumnDef<DocumentTypeDigitization>[] {
|
|||||||
return renderSnippet(descriptionSnippet, { description: row.original.description });
|
return renderSnippet(descriptionSnippet, { description: row.original.description });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
accessorKey: 'active',
|
|
||||||
header: 'Estatus',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const activeSnippet = createRawSnippet<[{ active: boolean }]>((getActive) => {
|
|
||||||
const { active } = getActive();
|
|
||||||
const className = active
|
|
||||||
? 'border-emerald-200 bg-emerald-50 text-emerald-700'
|
|
||||||
: 'border-slate-200 bg-slate-50 text-slate-600';
|
|
||||||
const label = active ? 'Activo' : 'Inactivo';
|
|
||||||
return {
|
|
||||||
render: () =>
|
|
||||||
`<span class="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium ${className}">${label}</span>`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return renderSnippet(activeSnippet, { active: row.original.active });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user