Se agrego un modal provicional para el fomrulario y creacion de los registros en la base de datos. Aun faltan 3 catalogos para la integracion con la base de datos

This commit is contained in:
2025-12-19 17:25:12 -06:00
parent 827c5319bb
commit 153fca9ad7
71 changed files with 3316 additions and 1121 deletions

View File

@@ -14,5 +14,6 @@ class ClassificationConcept(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
classification: Mapped[str] = mapped_column(
String(30), nullable=False) # CLASIFICACION

View File

@@ -2,7 +2,7 @@
Rutas para gestión de empresa
"""
from typing import List
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
@@ -17,7 +17,29 @@ from .service import CompanyService
# Main router that includes base CRUD
router = APIRouter(prefix="/company")
# Custom endpoints
@router.post(
"", # Se suma al prefix, queda POST /api/v1/a76/company
response_model=CompanyResponseDTO,
status_code=status.HTTP_201_CREATED,
summary="Create a new company",
)
async def create_company(
data: CompanyCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
service = CompanyService(db)
return service.create_company_manually(data, tenant_id=tenant_id)
@router.get(
"/my-companies",
response_model=List[CompanyResponseDTO],
@@ -166,11 +188,10 @@ async def get_program_info(
"prosec": company.prosec,
"prosec_authorization": company.prosec_authorization,
}
# Base CRUD routes using TenantCRUDRoutes
base_router = TenantCRUDRoutes(
service=CompanyService,
create_schema=CompanyCreateDTO,
create_schema=None,
update_schema=CompanyUpdateDTO,
response_schema=CompanyResponseDTO,
prefix="",
@@ -179,4 +200,5 @@ base_router = TenantCRUDRoutes(
enable_list=True,
enable_filters=True,
).router
router.include_router(base_router)

View File

@@ -64,6 +64,7 @@ class CompanyService:
.first()
)
# ESTE ES EL MÉTODO VIEJO QUE CAUSABA PROBLEMAS (Lo dejamos por si acaso)
@staticmethod
def create(
db: Session,
@@ -71,7 +72,7 @@ class CompanyService:
tenant_id: int,
company_id: int,
) -> Company:
"""Create a new company"""
"""Create a new company (MÉTODO GENÉRICO - NO USAR PARA CREACIÓN MANUAL)"""
try:
db_company = Company(
**company_data.model_dump(exclude_unset=True),
@@ -159,3 +160,31 @@ class CompanyService:
.first()
is not None
)
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
try:
# 1. Preparar datos
obj_data = data.model_dump(exclude_unset=True)
# 2. Crear objeto SQLAlchemy
db_obj = Company(**obj_data, tenant_id=tenant_id)
# 3. Guardar
self.db.add(db_obj)
self.db.commit()
self.db.refresh(db_obj)
return db_obj
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company manually: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error de integridad: Es posible que esta empresa ya exista.",
)
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company manually: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")

View File

@@ -4,6 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict
class ConceptBase(BaseModel):
code: str = Field(..., max_length=15, description="Concept Code (CLAVE)")
company_id: int = Field(..., description="Company ID")
description: Optional[str] = Field(
None, max_length=120, description="Description")
description_en: Optional[str] = Field(
@@ -18,7 +19,6 @@ class ConceptBase(BaseModel):
section: Optional[int] = Field(None, description="Section")
classification: Optional[str] = Field(
None, max_length=30, description="Classification")
company_id: int = Field(..., description="Company ID")
class ConceptCreate(ConceptBase):
@@ -32,7 +32,7 @@ class ConceptUpdate(BaseModel):
detailed_description: Optional[str] = Field(None, max_length=1000)
priority: Optional[int] = None
priority_ame: Optional[int] = None
first_total: Optional[bool] = None
first_total: Optional[bool] = None
type: Optional[str] = Field(None, max_length=9)
is_printed: Optional[bool] = None
section: Optional[int] = None

View File

@@ -18,25 +18,39 @@ class Concept(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
company_id: Mapped[int] = mapped_column(
Integer, nullable=False) # IDEMPRESA
code: Mapped[str] = mapped_column(String(15), nullable=False) # CLAVE
description: Mapped[Optional[str]] = mapped_column(
String(120), nullable=True) # DESCRIPCION
description_en: Mapped[Optional[str]] = mapped_column(
String(120), nullable=True) # DESCRIPCIONINGLES
detailed_description: Mapped[Optional[str]] = mapped_column(
String(1000), nullable=True) # DESCRIPCIONDETALLADA
priority: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True) # PRIORIDAD
priority_ame: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True) # PRIORIDADAME
first_total: Mapped[Optional[bool]] = mapped_column(
Boolean, nullable=True) # PRIMERTOTAL
type: Mapped[Optional[str]] = mapped_column(
String(9), nullable=True) # TIPO
is_printed: Mapped[Optional[bool]] = mapped_column(
Boolean, nullable=True) # SEIMPRIME
section: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True) # SECCION
classification: Mapped[Optional[str]] = mapped_column(String(30), ForeignKey(
"a76.classification_concepts.classification"), nullable=True) # CLASIFICACION

View File

@@ -4,8 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict
class CustomsBrokerConceptBase(BaseModel):
broker_key: str = Field(..., max_length=5,
description="Customs Broker Key (CLAVEAA)")
broker_key: str = Field(..., max_length=5, description="Customs Broker Key (CLAVEAA)")
concept: str = Field(..., max_length=15, description="Concept")
amount: Optional[Decimal] = Field(None, description="Amount")
priority: Optional[int] = Field(None, description="Priority")

View File

@@ -9,17 +9,23 @@ from core.database import Base
class CustomsBrokerConcept(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "customs_broker_concepts"
__table_args__ = (
UniqueConstraint("broker_key", "concept", name="uq_broker_concept"),
UniqueConstraint("broker_key", "concept", "company_id", name="uq_broker_concept"),
{"schema": "a76"}
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
company_id: Mapped[int] = mapped_column(Integer, nullable=False)
broker_key: Mapped[str] = mapped_column(
String(5), nullable=False) # CLAVEAA
concept: Mapped[str] = mapped_column(
String(15), nullable=False) # CONCEPTO
amount: Mapped[Optional[Decimal]] = mapped_column(
Numeric(11, 2), nullable=True) # IMPORTE
priority: Mapped[Optional[int]] = mapped_column(
Integer, nullable=True) # PRIORIDAD

View File

@@ -7,7 +7,7 @@ router = TenantCRUDRoutes(
create_schema=CustomsBrokerConceptCreate,
update_schema=CustomsBrokerConceptUpdate,
response_schema=CustomsBrokerConceptResponse,
prefix="/customs-broker-concepts",
prefix="/customs-broker-concepts",
tags=["a76.general_catalogs.customs_broker_concepts"],
resource_name="Customs Broker Concept",
enable_list=True,

View File

@@ -39,11 +39,15 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
equivalency_id: Mapped[int] = mapped_column(
Integer, ForeignKey("a76.equivalencies.id"), nullable=False)
original_field: Mapped[str] = mapped_column(
String(100), nullable=False) # Relation to Unit of Measure
external_field: Mapped[str] = mapped_column(String(100), nullable=False)
equivalency: Mapped["Equivalency"] = relationship(back_populates="items")
unit_of_measure: Mapped["UnitOfMeasure"] = relationship()

View File

@@ -24,7 +24,8 @@ class ErrorClassification(Base, TenantScopedMixin, TimestampMixin):
# Primary key
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
Integer, primary_key=True, autoincrement=True
)
# Classification code (unique)
code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
@@ -60,7 +61,8 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin):
# Primary key
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
Integer, primary_key=True, autoincrement=True
)
# Error code (unique)
code: Mapped[str] = mapped_column(String(15), nullable=False, unique=True)
@@ -77,4 +79,4 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin):
)
def __repr__(self):
return f"<ErrorCatalog(id={self.id}, code={self.code}, description={self.description}, classification_id={self.classification_id})>"
return f"<ErrorCatalog(id={self.id}, code={self.code}, description={self.description}, classification_id={self.classification_id})>"

View File

@@ -30,6 +30,9 @@ class ExchangeRate(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
date: Mapped[datetime] = mapped_column(DateTime)
value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6))
local_currency: Mapped[Optional[str]] = mapped_column(String(7))
foreign_currency: Mapped[Optional[str]] = mapped_column(String(7))

View File

@@ -1,9 +1,6 @@
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# Identifier DTOs
class IdentifierBase(BaseModel):
code: str = Field(..., max_length=2, description="Identifier Code (CLAVE)")
description: Optional[str] = Field(
@@ -11,28 +8,23 @@ class IdentifierBase(BaseModel):
level: Optional[str] = Field(None, max_length=1, description="Level")
complement: Optional[str] = Field(
None, max_length=5000, description="Complement")
company_id: int = Field(..., description="Company ID")
class IdentifierCreate(IdentifierBase):
pass
class IdentifierUpdate(BaseModel):
code: Optional[str] = Field(None, max_length=2)
description: Optional[str] = Field(None, max_length=1000)
level: Optional[str] = Field(None, max_length=1)
complement: Optional[str] = Field(None, max_length=5000)
class IdentifierResponse(IdentifierBase):
id: int
tenant_id: int
company_id: int
model_config = ConfigDict(from_attributes=True)
# Identifier Detail DTOs
class IdentifierDetailBase(BaseModel):
invoice_consecutive: Optional[int] = Field(
@@ -47,13 +39,10 @@ class IdentifierDetailBase(BaseModel):
None, max_length=51, description="Complement 2")
complement3: Optional[str] = Field(
None, max_length=50, description="Complement 3")
company_id: int = Field(..., description="Company ID")
class IdentifierDetailCreate(IdentifierDetailBase):
pass
class IdentifierDetailUpdate(BaseModel):
invoice_consecutive: Optional[int] = None
part_line: Optional[int] = None
@@ -63,9 +52,9 @@ class IdentifierDetailUpdate(BaseModel):
complement2: Optional[str] = Field(None, max_length=51)
complement3: Optional[str] = Field(None, max_length=50)
class IdentifierDetailResponse(IdentifierDetailBase):
id: int
tenant_id: int
company_id: int
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)

View File

@@ -14,11 +14,15 @@ class Identifier(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(2), nullable=False) # CLAVE
description: Mapped[Optional[str]] = mapped_column(
String(1000), nullable=True) # DESCRIPCION
level: Mapped[Optional[str]] = mapped_column(
String(1), nullable=True) # NIVEL
complement: Mapped[Optional[str]] = mapped_column(
String(5000), nullable=True) # COMPLEMENTO

View File

@@ -2,7 +2,6 @@ from typing import Optional
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
class INPCBase(BaseModel):
year: str = Field(..., max_length=4, description="Year (YYYY)")
month: str = Field(..., max_length=2, description="Month (MM)")
@@ -12,7 +11,6 @@ class INPCBase(BaseModel):
class INPCCreate(INPCBase):
pass
class INPCUpdate(BaseModel):
year: Optional[str] = Field(None, max_length=4)
month: Optional[str] = Field(None, max_length=2)
@@ -21,5 +19,7 @@ class INPCUpdate(BaseModel):
class INPCResponse(INPCBase):
id: int
tenant_id: int
company_id: int
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)

View File

@@ -16,7 +16,10 @@ class INPC(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
year: Mapped[str] = mapped_column(String(4), nullable=False) # ANIO
month: Mapped[str] = mapped_column(String(2), nullable=False) # MES
value: Mapped[Optional[Decimal]] = mapped_column(
Numeric(19, 8), nullable=True) # VALOR

View File

@@ -1,20 +1,16 @@
from fastapi import APIRouter
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .models import INPC
from .dto import INPCCreate, INPCResponse, INPCUpdate
from .service import INPCService
router = APIRouter(prefix="/inpc", tags=["a76.general_catalogs.inpc"])
inpc_crud = TenantCRUDRoutes(
# Usamos TenantCRUDRoutes directamente
router = TenantCRUDRoutes(
service=INPCService,
create_schema=INPCCreate,
update_schema=INPCUpdate,
response_schema=INPCResponse,
prefix="",
tags=["INPC"],
prefix="/inpc",
tags=["a76.general_catalogs.inpc"],
resource_name="INPC",
enable_list=True,
)
router.include_router(inpc_crud.router)
).router

View File

@@ -21,3 +21,5 @@ class LegendResponse(LegendBase):
id: int
model_config = ConfigDict(from_attributes=True)
tenant_id : int
company_id : int

View File

@@ -15,6 +15,8 @@ class Legend(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
code: Mapped[int] = mapped_column(Integer, nullable=False) # CLAVELEY
description: Mapped[Optional[str]] = mapped_column(
String(2000), nullable=True) # DESCLEYENDA

View File

@@ -11,7 +11,7 @@ legend_crud = TenantCRUDRoutes(
create_schema=LegendCreate,
update_schema=LegendUpdate,
response_schema=LegendResponse,
prefix="",
prefix="/legends",
tags=["Legends"],
resource_name="Legend",
enable_list=True,

View File

@@ -27,5 +27,7 @@ class MultiCurrencyTypeUpdate(BaseModel):
class MultiCurrencyTypeResponse(MultiCurrencyTypeBase):
id: int
company_id: int
tenant_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -18,13 +18,18 @@ class MultiCurrencyType(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
currency_type_code: Mapped[str] = mapped_column(
String(3), ForeignKey("public.currency_types.code"), nullable=False)
country_key: Mapped[Optional[str]] = mapped_column(
String(3), ForeignKey("public.countries.m3_key"), nullable=True)
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
Numeric(13, 6), nullable=True)
publication_date: Mapped[int] = mapped_column(Integer, nullable=False)
currency_type: Mapped["CurrencyType"] = relationship()
country: Mapped[Optional["Country"]] = relationship()

View File

@@ -31,4 +31,4 @@ class Package(Base, TenantScopedMixin, TimestampMixin):
plurals: Mapped[Optional[str]] = mapped_column(String(4))
plural_in: Mapped[Optional[str]] = mapped_column(String(4))
code_ace: Mapped[Optional[str]] = mapped_column(String(4))
code_aamex: Mapped[Optional[str]] = mapped_column(String(9))
code_aamex: Mapped[Optional[str]] = mapped_column(String(9))

View File

@@ -11,6 +11,7 @@ class SignatureCreateDTO(BaseModel):
"""DTO para crear una firma"""
code: str = Field(..., max_length=10, description="Signature code")
signature: Optional[str] = Field(
None, max_length=1000, description="Signature")
photo_path: Optional[str] = Field(
@@ -39,6 +40,8 @@ class SignatureResponseDTO(BaseModel):
code: str
signature: Optional[str] = None
photo_path: Optional[str] = None
intenant_id: int
company_id: int
class Config:
from_attributes = True

View File

@@ -32,6 +32,7 @@ class Signature(Base, TenantScopedMixin, TimestampMixin):
# Signature information
signature: Mapped[Optional[str]] = mapped_column(String(1000))
photo_path: Mapped[Optional[str]] = mapped_column(String(1000))
def __repr__(self):

View File

@@ -2,27 +2,23 @@ from typing import Optional
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
class UnitConversionBase(BaseModel):
from_unit_code: str = Field(..., max_length=5,
description="Source Unit Code")
to_unit_code: str = Field(..., max_length=5,
description="Target Unit Code")
conversion_factor: Optional[Decimal] = Field(
None, description="Conversion Factor")
# 👇 OJO: Son códigos (Strings), no IDs
from_unit_code: str = Field(..., max_length=5, description="Source Unit Code")
to_unit_code: str = Field(..., max_length=5, description="Target Unit Code")
conversion_factor: Optional[Decimal] = Field(None, description="Conversion Factor")
class UnitConversionCreate(UnitConversionBase):
pass
class UnitConversionUpdate(BaseModel):
from_unit_code: Optional[str] = Field(None, max_length=5)
to_unit_code: Optional[str] = Field(None, max_length=5)
conversion_factor: Optional[Decimal] = None
class UnitConversionResponse(UnitConversionBase):
id: int
tenant_id: int
company_id: int
model_config = ConfigDict(from_attributes=True)
model_config = ConfigDict(from_attributes=True)

View File

@@ -27,8 +27,11 @@ class UnitConversion(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
from_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
to_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
Numeric(13, 6), nullable=True)

View File

@@ -181,4 +181,4 @@ class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin):
String(4), nullable=True) # CLAVEACE
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(overlaps="customs_unit")