Merge: Resolver conflictos en client-selector-dialog.svelte

This commit is contained in:
2026-02-09 11:39:21 -06:00
195 changed files with 7001 additions and 530 deletions

View File

@@ -17,8 +17,7 @@ class PortService:
filters: Optional[Dict[str, Any]] = None
) -> Tuple[List[Port], int]:
query = db.query(Port).filter(
Port.tenant_id == tenant_id,
Port.company_id == company_id
Port.tenant_id == tenant_id
)
if filters:
@@ -38,8 +37,7 @@ class PortService:
) -> Optional[Port]:
return db.query(Port).filter(
Port.id == id,
Port.tenant_id == tenant_id,
Port.company_id == company_id
Port.tenant_id == tenant_id
).first()
@staticmethod

View File

@@ -21,7 +21,7 @@ invoice_crud = TenantCRUDRoutes(
resource_name="Invoice",
id_name="invoice_id",
id_type=int,
enable_list=True, # Enable list endpoint with pagination
enable_list=False, # Disable auto-list to override with custom filter
enable_filters=True, # Enable filters for status, operation_type, etc.
list_permissions=[],
get_permissions=[],
@@ -36,6 +36,53 @@ invoice_crud = TenantCRUDRoutes(
router.include_router(invoice_crud.router)
@router.get("/invoices/", response_model=schemas.InvoiceHeaderListResponse)
def list_invoices(
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(50, ge=1, le=200, description="Items per page"),
search: str = Query(None, description="Search by invoice number"),
status: bool = Query(None, description="Filter by status"),
operation_type: schemas.OperationType = Query(None, description="Filter by operation type"),
invoice_type: str = Query(None, description="Filter by invoice type"),
manifest_number: str = Query(None, description="Filter by manifest number"),
pedimento: str = Query(None, description="Filter by pedimento"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List invoices with optional filters, including manifest_number.
"""
print(f"DEBUG: list_invoices called with manifest_number={manifest_number}")
tenant_id = validate_access_to_resource(db, company_id, current_user)
print(f"DEBUG: tenant_id={tenant_id}, company_id={company_id}")
skip = (page - 1) * page_size
filters = {
"invoice_number": search,
"status": status,
"operation_type": operation_type,
"invoice_type": invoice_type,
"manifest_number": manifest_number,
"pedimento": pedimento,
}
# Remove None values
filters = {k: v for k, v in filters.items() if v is not None}
items, total = services.InvoiceService.get_all(
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters
)
print(f"DEBUG: InvoiceService returned {len(items)} items, total={total}")
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size
}
# Additional nested routes for child resources
# --- Logistics Routes ---

View File

@@ -426,10 +426,11 @@ class InvoiceHeaderCreate(InvoiceHeaderBase):
# --- Update Schemas ---
class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase):
"""Schema for updating Compliance MX"""
pass
is_pedimento_pending: Optional[bool] = None
class InvoiceFinancialsUpdate(InvoiceFinancialsBase):
@@ -460,6 +461,10 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase):
"""Schema for updating Invoice Header with nested relations"""
id: int
document_type: Optional[str] = None
invoice_date: Optional[date] = None
operation_type: Optional[OperationType] = None
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
financials: Optional[InvoiceFinancialsUpdate] = None
logistics: Optional[InvoiceLogisticsUpdate] = None
@@ -546,3 +551,10 @@ class InvoiceHeaderResponse(InvoiceHeaderBase):
class Config:
from_attributes = True
class InvoiceHeaderListResponse(BaseModel):
items: List[InvoiceHeaderResponse]
total: int
page: int
page_size: int

View File

@@ -73,6 +73,16 @@ class InvoiceService:
):
query = query.filter(models.InvoiceHeader.operation_type != "REPAR")
if filters.get("manifest_number"):
# Avoid duplicate joins if pedimento filter was also applied (though rare in this context)
# For safety, we can just use the relationship attribute directly if mapped,
# but explicit join is clearer given the previous pattern.
# Assuming SQLAlchemy handles the join overlap or we just accept it for now.
# To be safe and consistent with previous 'pedimento' block:
query = query.join(models.InvoiceComplianceMx).filter(
models.InvoiceComplianceMx.manifest_number.ilike(f"%{filters['manifest_number']}%")
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total

View File

@@ -0,0 +1,48 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ConceptManifestationBaseDTO(BaseModel):
value_manifestation_id: int = Field(..., description="Value Manifestation ID")
line_number: int = Field(..., description="Line number")
attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type")
number: Optional[str] = Field(None, max_length=10, description="Number")
merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider")
invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document")
amount: Optional[Decimal] = Field(None, description="Amount")
currency: Optional[str] = Field(None, max_length=3, description="Currency")
concept_load: Optional[str] = Field(None, max_length=200, description="Concept load")
class ConceptManifestationCreateDTO(ConceptManifestationBaseDTO):
pass
class ConceptManifestationUpdateDTO(BaseModel):
attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type")
number: Optional[str] = Field(None, max_length=10, description="Number")
merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider")
invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document")
amount: Optional[Decimal] = Field(None, description="Amount")
currency: Optional[str] = Field(None, max_length=3, description="Currency")
concept_load: Optional[str] = Field(None, max_length=200, description="Concept load")
class ConceptManifestationResponseDTO(ConceptManifestationBaseDTO):
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ConceptManifestationListDTO(BaseModel):
items: List[ConceptManifestationResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,45 @@
from decimal import Decimal
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
ForeignKeyConstraint,
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
if TYPE_CHECKING:
from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation
class ConceptManifestation(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "concept_manifestations"
__table_args__ = (
PrimaryKeyConstraint("value_manifestation_id", "line_number", name="concept_manifestations_pkey"),
ForeignKeyConstraint(
["value_manifestation_id"],
["a76.value_manifestations.id"],
name="fk_concept_manifestation_value_manifestation"
),
Index("idx_concept_manifestations_value_manifestation_id", "value_manifestation_id"),
{"schema": "a76"},
)
value_manifestation_id: Mapped[int] = mapped_column(Integer)
line_number: Mapped[int] = mapped_column(Integer)
attachment_type: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
number: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
merchandise_provider: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
invoice_document: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
amount: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 9), nullable=True)
currency: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
concept_load: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
value_manifestation: Mapped["ValueManifestation"] = relationship(
"ValueManifestation",
backref="concepts"
)

View File

@@ -0,0 +1,112 @@
from typing import Optional
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ConceptManifestationCreateDTO,
ConceptManifestationUpdateDTO,
ConceptManifestationResponseDTO,
ConceptManifestationListDTO,
)
from .service import ConceptManifestationService
router = APIRouter(prefix="/concept-manifestations")
@router.get("/", response_model=ConceptManifestationListDTO)
async def list_concept_manifestations(
company_id: int = Query(..., description="Company ID"),
value_manifestation_id: Optional[int] = Query(
None, description="Filter by Value Manifestation ID"
),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {}
if value_manifestation_id:
filters["value_manifestation_id"] = value_manifestation_id
items, total = ConceptManifestationService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ConceptManifestationResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ConceptManifestationResponseDTO)
async def create_concept_manifestation(
data: ConceptManifestationCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ConceptManifestationService.create(db, data, tenant_id, company_id)
@router.get(
"/{value_manifestation_id}/{line_number}",
response_model=ConceptManifestationResponseDTO,
)
async def get_concept_manifestation(
value_manifestation_id: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return item
@router.patch(
"/{value_manifestation_id}/{line_number}",
response_model=ConceptManifestationResponseDTO,
)
async def update_concept_manifestation(
value_manifestation_id: int,
line_number: int,
data: ConceptManifestationUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ConceptManifestationService.update(
db, value_manifestation_id, line_number, tenant_id, company_id, data
)
if not item:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return item
@router.delete("/{value_manifestation_id}/{line_number}", response_model=bool)
async def delete_concept_manifestation(
value_manifestation_id: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ConceptManifestationService.delete(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not success:
raise HTTPException(status_code=404, detail="Concept Manifestation not found")
return success

View File

@@ -0,0 +1,143 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ConceptManifestation
from .dtos import (
ConceptManifestationCreateDTO,
ConceptManifestationUpdateDTO,
)
logger = logging.getLogger(__name__)
class ConceptManifestationService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ConceptManifestation], int]:
query = db.query(ConceptManifestation).filter(
ConceptManifestation.tenant_id == tenant_id,
ConceptManifestation.company_id == company_id,
)
if filters:
if filters.get("value_manifestation_id"):
query = query.filter(
ConceptManifestation.value_manifestation_id == filters["value_manifestation_id"]
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
) -> Optional[ConceptManifestation]:
return (
db.query(ConceptManifestation)
.filter(
ConceptManifestation.value_manifestation_id == value_manifestation_id,
ConceptManifestation.line_number == line_number,
ConceptManifestation.tenant_id == tenant_id,
ConceptManifestation.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ConceptManifestationCreateDTO,
tenant_id: int,
company_id: int,
) -> ConceptManifestation:
try:
db_item = ConceptManifestation(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating concept manifestation: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating concept manifestation"
)
@staticmethod
def update(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
data: ConceptManifestationUpdateDTO,
) -> Optional[ConceptManifestation]:
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(
f"Error updating concept manifestation {value_manifestation_id}-{line_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error updating concept manifestation"
)
@staticmethod
def delete(
db: Session,
value_manifestation_id: int,
line_number: int,
tenant_id: int,
company_id: int,
) -> bool:
item = ConceptManifestationService.get_by_id(
db, value_manifestation_id, line_number, tenant_id, company_id
)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(
f"Error deleting concept manifestation {value_manifestation_id}-{line_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error deleting concept manifestation"
)

View File

@@ -0,0 +1,42 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ManifestDriverBaseDTO(BaseModel):
manifest_number: str = Field(..., max_length=15, description="Manifest number")
driver_name: str = Field(..., max_length=80, description="Driver name")
driver_type: Optional[str] = Field(None, max_length=1, description="Driver type")
address_1: Optional[str] = Field(None, max_length=100, description="Address line 1")
address_2: Optional[str] = Field(None, max_length=100, description="Address line 2")
city: Optional[str] = Field(None, max_length=30, description="City")
state: Optional[str] = Field(None, max_length=30, description="State")
postal_code: Optional[str] = Field(None, max_length=15, description="Postal code")
country: Optional[str] = Field(None, max_length=3, description="Country")
class ManifestDriverCreateDTO(ManifestDriverBaseDTO):
pass
class ManifestDriverUpdateDTO(ManifestDriverBaseDTO):
# Overriding to make PKs optional for update if needed, but usually PKs are in URL
manifest_number: Optional[str] = Field(None, max_length=15)
driver_name: Optional[str] = Field(None, max_length=80)
class ManifestDriverResponseDTO(ManifestDriverBaseDTO):
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ManifestDriverListDTO(BaseModel):
items: List[ManifestDriverResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,30 @@
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.orm import Mapped, mapped_column
class ManifestDriver(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "manifest_drivers"
__table_args__ = (
PrimaryKeyConstraint("manifest_number", "driver_name", name="manifest_drivers_pkey"),
Index("idx_manifest_drivers_manifest_number", "manifest_number"),
{"schema": "a76"},
)
manifest_number: Mapped[str] = mapped_column(String(15))
driver_name: Mapped[str] = mapped_column(String(80))
driver_type: Mapped[Optional[str]] = mapped_column(String(1), nullable=True)
address_1: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
address_2: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
city: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
state: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
postal_code: Mapped[Optional[str]] = mapped_column(String(15), nullable=True)
country: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)

View File

@@ -0,0 +1,98 @@
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ManifestDriverCreateDTO,
ManifestDriverUpdateDTO,
ManifestDriverResponseDTO,
ManifestDriverListDTO,
)
from .service import ManifestDriverService
router = APIRouter(prefix="/manifest-drivers")
@router.get("/", response_model=ManifestDriverListDTO)
async def list_manifest_drivers(
company_id: int = Query(..., description="Company ID"),
manifest_number: Optional[str] = Query(None, description="Filter by Manifest Number"),
search: Optional[str] = Query(None, description="Search term"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {"manifest_number": manifest_number, "search": search}
items, total = ManifestDriverService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ManifestDriverResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ManifestDriverResponseDTO)
async def create_manifest_driver(
data: ManifestDriverCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ManifestDriverService.create(db, data, tenant_id, company_id)
@router.get("/{manifest_number}/{driver_name}", response_model=ManifestDriverResponseDTO)
async def get_manifest_driver(
manifest_number: str,
driver_name: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ManifestDriverService.get_by_pk(db, manifest_number, driver_name, tenant_id, company_id)
if not item:
raise HTTPException(status_code=404, detail="Manifest Driver not found")
return item
@router.patch("/{manifest_number}/{driver_name}", response_model=ManifestDriverResponseDTO)
async def update_manifest_driver(
manifest_number: str,
driver_name: str,
data: ManifestDriverUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ManifestDriverService.update(
db, manifest_number, driver_name, tenant_id, company_id, data
)
if not item:
raise HTTPException(status_code=404, detail="Manifest Driver not found")
return item
@router.delete("/{manifest_number}/{driver_name}", response_model=bool)
async def delete_manifest_driver(
manifest_number: str,
driver_name: str,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestDriverService.delete(db, manifest_number, driver_name, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Manifest Driver not found")
return success

View File

@@ -0,0 +1,124 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ManifestDriver
from .dtos import ManifestDriverCreateDTO, ManifestDriverUpdateDTO
logger = logging.getLogger(__name__)
class ManifestDriverService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ManifestDriver], int]:
query = db.query(ManifestDriver).filter(
ManifestDriver.tenant_id == tenant_id,
ManifestDriver.company_id == company_id,
)
if filters:
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
ManifestDriver.manifest_number.ilike(search_pattern),
ManifestDriver.driver_name.ilike(search_pattern),
)
)
if filters.get("manifest_number"):
query = query.filter(ManifestDriver.manifest_number == filters["manifest_number"])
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_pk(
db: Session, manifest_number: str, driver_name: str, tenant_id: int, company_id: int
) -> Optional[ManifestDriver]:
return (
db.query(ManifestDriver)
.filter(
ManifestDriver.manifest_number == manifest_number,
ManifestDriver.driver_name == driver_name,
ManifestDriver.tenant_id == tenant_id,
ManifestDriver.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ManifestDriverCreateDTO,
tenant_id: int,
company_id: int,
) -> ManifestDriver:
try:
db_item = ManifestDriver(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating manifest driver: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating manifest driver")
@staticmethod
def update(
db: Session,
manifest_number: str,
driver_name: str,
tenant_id: int,
company_id: int,
data: ManifestDriverUpdateDTO,
) -> Optional[ManifestDriver]:
item = ManifestDriverService.get_by_pk(db, manifest_number, driver_name, tenant_id, company_id)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(f"Error updating manifest driver: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating manifest driver")
@staticmethod
def delete(
db: Session, manifest_number: str, driver_name: str, tenant_id: int, company_id: int
) -> bool:
item = ManifestDriverService.get_by_pk(db, manifest_number, driver_name, tenant_id, company_id)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting manifest driver: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting manifest driver")

View File

@@ -0,0 +1,41 @@
import sys
import os
from sqlalchemy import text, inspect
# Assume we run this from backend/ with PYTHONPATH=.
from core.database import CoreSessionLocal, core_engine as engine
def check_table():
inspector = inspect(engine)
schemas = inspector.get_schema_names()
print(f"Schemas found: {schemas}")
if 'a76' not in schemas:
print("Schema 'a76' does not exist!")
# return # Proceed anyway to check if table exists in public or other schemas?
# Check for table in a76 schema
try:
table_names = inspector.get_table_names(schema='a76')
print(f"Tables in 'a76': {table_names}")
if 'manifests' in table_names:
print("Table 'a76.manifests' exists.")
columns = [c['name'] for c in inspector.get_columns('manifests', schema='a76')]
print(f"Columns: {columns}")
# Try a simple count
try:
with CoreSessionLocal() as db:
result = db.execute(text("SELECT count(*) FROM a76.manifests"))
print(f"Count result: {result.scalar()}")
except Exception as e:
print(f"Error querying table: {e}")
else:
print("Table 'a76.manifests' DOES NOT exist in schema 'a76'.")
except Exception as e:
print(f"Error inspecting schema 'a76': {e}")
if __name__ == "__main__":
check_table()

View File

@@ -0,0 +1,66 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ManifestBaseDTO(BaseModel):
manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number")
importer_details: Optional[str] = Field(None, max_length=60, description="Importer details")
person_in_charge: Optional[str] = Field(None, max_length=60, description="Person in charge")
consigned_to: Optional[str] = Field(None, max_length=8, description="Consigned to")
sent_by: Optional[str] = Field(None, max_length=8, description="Sent by")
foreign_exit_port: Optional[str] = Field(None, max_length=6, description="Foreign exit port")
foreign_exit_port_loc: Optional[str] = Field(None, max_length=4, description="Foreign exit port location")
destination_port: Optional[str] = Field(None, max_length=6, description="Destination port")
destination_port_loc: Optional[str] = Field(None, max_length=4, description="Destination port location")
entry_port: Optional[str] = Field(None, max_length=6, description="Entry port")
entry_port_loc: Optional[str] = Field(None, max_length=4, description="Entry port location")
entry_date: Optional[int] = Field(None, description="Entry date")
net_weight: Optional[Decimal] = Field(None, description="Net weight")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
total_value: Optional[Decimal] = Field(None, description="Total value")
description: Optional[str] = Field(None, max_length=2500, description="Description")
broker_code: Optional[str] = Field(None, max_length=5, description="Broker code")
carrier_code: Optional[str] = Field(None, max_length=5, description="Carrier code")
payment_invoice_number: Optional[str] = Field(None, max_length=5, description="Payment invoice number")
seal_number: Optional[str] = Field(None, max_length=30, description="Seal number")
entry_hour: Optional[int] = Field(None, description="Entry hour")
hazardous_material: Optional[str] = Field(None, max_length=2, description="Hazardous material")
transport_mode: Optional[str] = Field(None, max_length=2, description="Transport mode")
transport_code: Optional[str] = Field(None, max_length=14, description="Transport code")
trailer_number: Optional[str] = Field(None, max_length=20, description="Trailer number")
status: Optional[str] = Field(None, max_length=14, description="Status")
status_description: Optional[str] = Field(None, max_length=1000, description="Status description")
manifest_type: Optional[str] = Field(None, max_length=3, description="Manifest type")
class ManifestCreateDTO(ManifestBaseDTO):
pass
class ManifestUpdateDTO(ManifestBaseDTO):
pass
class ManifestResponseDTO(ManifestBaseDTO):
id: int
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ManifestListDTO(BaseModel):
items: List[ManifestResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True
class InvoiceManifestUpdateDTO(BaseModel):
manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number")

View File

@@ -0,0 +1,52 @@
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
String,
Text,
)
from sqlalchemy.orm import Mapped, mapped_column
class Manifest(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "manifests"
__table_args__ = (
Index("idx_manifests_manifest_number", "manifest_number"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
manifest_number: Mapped[str] = mapped_column(String(15), nullable=True)
importer_details: Mapped[Optional[str]] = mapped_column(String(60), nullable=True)
person_in_charge: Mapped[Optional[str]] = mapped_column(String(60), nullable=True)
consigned_to: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
sent_by: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)
foreign_exit_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
foreign_exit_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
destination_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
destination_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
entry_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True)
entry_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
entry_date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
net_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
total_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
description: Mapped[Optional[str]] = mapped_column(String(2500), nullable=True)
broker_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
carrier_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
payment_invoice_number: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
seal_number: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
entry_hour: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
hazardous_material: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
transport_mode: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
transport_code: Mapped[Optional[str]] = mapped_column(String(14), nullable=True)
trailer_number: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
status: Mapped[Optional[str]] = mapped_column(String(14), nullable=True)
status_description: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
manifest_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)

View File

@@ -0,0 +1,127 @@
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ManifestCreateDTO,
ManifestUpdateDTO,
ManifestResponseDTO,
ManifestListDTO,
InvoiceManifestUpdateDTO,
)
from .service import ManifestService
router = APIRouter(prefix="/manifests")
@router.get("/", response_model=ManifestListDTO)
async def list_manifests(
company_id: int = Query(..., description="Company ID"),
search: Optional[str] = Query(None, description="Search term"),
manifest_number: Optional[str] = Query(None, description="Manifest number"),
start_date: Optional[int] = Query(None, description="Start date (YYYYMMDD)"),
end_date: Optional[int] = Query(None, description="End date (YYYYMMDD)"),
skip: int = Query(0, ge=0, description="Skip"),
limit: int = Query(50, ge=1, le=100, description="Limit"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List all manifests for a company with pagination and filters
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {
"search": search,
"manifest_number": manifest_number,
"start_date": start_date,
"end_date": end_date,
}
items, total = ManifestService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ManifestResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ManifestResponseDTO)
async def create_manifest(
manifest_data: ManifestCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ManifestService.create(db, manifest_data, tenant_id, company_id)
@router.get("/{manifest_id}", response_model=ManifestResponseDTO)
async def get_manifest(
manifest_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
raise HTTPException(status_code=404, detail="Manifest not found")
return manifest
@router.patch("/{manifest_id}", response_model=ManifestResponseDTO)
async def update_manifest(
manifest_id: int,
manifest_data: ManifestUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
manifest = ManifestService.update(
db, manifest_id, tenant_id, company_id, manifest_data
)
if not manifest:
raise HTTPException(status_code=404, detail="Manifest not found")
return manifest
@router.delete("/{manifest_id}", response_model=bool)
async def delete_manifest(
manifest_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestService.delete(db, manifest_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Manifest not found")
return success
@router.patch("/invoices/{invoice_id}/compliance", response_model=bool)
async def update_invoice_manifest(
invoice_id: int,
data: InvoiceManifestUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Dedicated endpoint to update manifest_number for an invoice.
Safely bypasses all core invoice update validations.
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestService.update_invoice_manifest(
db, invoice_id, tenant_id, company_id, data.manifest_number
)
if not success:
raise HTTPException(status_code=404, detail="Invoice not found")
return success

View File

@@ -0,0 +1,192 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import Manifest
from .dtos import ManifestCreateDTO, ManifestUpdateDTO, ManifestResponseDTO
logger = logging.getLogger(__name__)
class ManifestService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Manifest], int]:
query = db.query(Manifest).filter(
Manifest.tenant_id == tenant_id,
Manifest.company_id == company_id,
)
if filters:
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
Manifest.manifest_number.ilike(search_pattern),
Manifest.description.ilike(search_pattern),
)
)
if filters.get("manifest_number"):
query = query.filter(
Manifest.manifest_number.ilike(f"%{filters['manifest_number']}%")
)
if filters.get("start_date"):
query = query.filter(Manifest.entry_date >= filters["start_date"])
if filters.get("end_date"):
query = query.filter(Manifest.entry_date <= filters["end_date"])
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session, manifest_id: int, tenant_id: int, company_id: int
) -> Optional[Manifest]:
return (
db.query(Manifest)
.filter(
Manifest.id == manifest_id,
Manifest.tenant_id == tenant_id,
Manifest.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
manifest_data: ManifestCreateDTO,
tenant_id: int,
company_id: int,
) -> Manifest:
try:
db_manifest = Manifest(
**manifest_data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_manifest)
db.commit()
db.refresh(db_manifest)
return db_manifest
except Exception as e:
db.rollback()
logger.error(f"Error creating manifest: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating manifest")
@staticmethod
def update(
db: Session,
manifest_id: int,
tenant_id: int,
company_id: int,
manifest_data: ManifestUpdateDTO,
) -> Optional[Manifest]:
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
return None
try:
update_data = manifest_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(manifest, field, value)
db.commit()
db.refresh(manifest)
return manifest
except Exception as e:
db.rollback()
logger.error(f"Error updating manifest {manifest_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating manifest")
@staticmethod
def delete(
db: Session, manifest_id: int, tenant_id: int, company_id: int
) -> bool:
manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id)
if not manifest:
return False
try:
db.delete(manifest)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting manifest {manifest_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting manifest")
@staticmethod
def update_invoice_manifest(
db: Session,
invoice_id: int,
tenant_id: int,
company_id: int,
manifest_number: Optional[str],
) -> bool:
"""
Update ONLY the manifest_number of an invoice.
Creates compliance_mx record if it doesn't exist.
"""
# Import models here to avoid circular dependencies if any
from api.v1.modules.a76.invoices.models import (
InvoiceHeader,
InvoiceComplianceMx,
)
invoice = (
db.query(InvoiceHeader)
.filter(
InvoiceHeader.id == invoice_id,
InvoiceHeader.tenant_id == tenant_id,
InvoiceHeader.company_id == company_id,
)
.first()
)
if not invoice:
return False
try:
if invoice.compliance_mx:
invoice.compliance_mx.manifest_number = manifest_number
else:
# Create new compliance record with defaults for required fields
new_compliance = InvoiceComplianceMx(
invoice_id=invoice.id,
tenant_id=tenant_id,
company_id=company_id,
manifest_number=manifest_number,
# Required fields defaults
is_pedimento_pending=False,
is_regime_change=False,
is_owner_of_goods=False,
generate_balances=False,
is_mixed=False,
)
db.add(new_compliance)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(
f"Error linking invoice {invoice_id} to manifest {manifest_number}: {str(e)}"
)
raise HTTPException(
status_code=500, detail="Error linking invoice to manifest"
)

View File

@@ -0,0 +1,37 @@
from typing import List, Optional
from pydantic import BaseModel, Field
class ManifestAnexoBaseDTO(BaseModel):
consecutive: int = Field(..., description="Consecutive number")
line_number: int = Field(..., description="Line number")
attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type")
number: Optional[str] = Field(None, max_length=10, description="Attachment number")
attached_doc: Optional[str] = Field(None, max_length=200, description="Attached document path/name")
class ManifestAnexoCreateDTO(ManifestAnexoBaseDTO):
pass
class ManifestAnexoUpdateDTO(ManifestAnexoBaseDTO):
consecutive: Optional[int] = Field(None)
line_number: Optional[int] = Field(None)
class ManifestAnexoResponseDTO(ManifestAnexoBaseDTO):
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ManifestAnexoListDTO(BaseModel):
items: List[ManifestAnexoResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,26 @@
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.orm import Mapped, mapped_column
class ManifestAnexo(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "manifest_anexos"
__table_args__ = (
PrimaryKeyConstraint("consecutive", "line_number", name="manifest_anexos_pkey"),
Index("idx_manifest_anexos_consecutive", "consecutive"),
{"schema": "a76"},
)
consecutive: Mapped[int] = mapped_column(Integer)
line_number: Mapped[int] = mapped_column(Integer)
attachment_type: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
number: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
attached_doc: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)

View File

@@ -0,0 +1,98 @@
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from .dtos import (
ManifestAnexoCreateDTO,
ManifestAnexoUpdateDTO,
ManifestAnexoResponseDTO,
ManifestAnexoListDTO,
)
from .service import ManifestAnexoService
router = APIRouter(prefix="/manifest-anexos")
@router.get("/", response_model=ManifestAnexoListDTO)
async def list_manifest_anexos(
company_id: int = Query(..., description="Company ID"),
consecutive: Optional[int] = Query(None, description="Filter by Consecutive"),
search: Optional[str] = Query(None, description="Search term"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
filters = {"consecutive": consecutive, "search": search}
items, total = ManifestAnexoService.get_all(
db, tenant_id, company_id, skip=skip, limit=limit, filters=filters
)
return {
"items": [ManifestAnexoResponseDTO.model_validate(item) for item in items],
"total": total,
"page": (skip // limit) + 1,
"page_size": limit,
}
@router.post("/", response_model=ManifestAnexoResponseDTO)
async def create_manifest_anexo(
data: ManifestAnexoCreateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
return ManifestAnexoService.create(db, data, tenant_id, company_id)
@router.get("/{consecutive}/{line_number}", response_model=ManifestAnexoResponseDTO)
async def get_manifest_anexo(
consecutive: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ManifestAnexoService.get_by_pk(db, consecutive, line_number, tenant_id, company_id)
if not item:
raise HTTPException(status_code=404, detail="Manifest Anexo not found")
return item
@router.patch("/{consecutive}/{line_number}", response_model=ManifestAnexoResponseDTO)
async def update_manifest_anexo(
consecutive: int,
line_number: int,
data: ManifestAnexoUpdateDTO,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
item = ManifestAnexoService.update(
db, consecutive, line_number, tenant_id, company_id, data
)
if not item:
raise HTTPException(status_code=404, detail="Manifest Anexo not found")
return item
@router.delete("/{consecutive}/{line_number}", response_model=bool)
async def delete_manifest_anexo(
consecutive: int,
line_number: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
success = ManifestAnexoService.delete(db, consecutive, line_number, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Manifest Anexo not found")
return success

View File

@@ -0,0 +1,124 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ManifestAnexo
from .dtos import ManifestAnexoCreateDTO, ManifestAnexoUpdateDTO
logger = logging.getLogger(__name__)
class ManifestAnexoService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ManifestAnexo], int]:
query = db.query(ManifestAnexo).filter(
ManifestAnexo.tenant_id == tenant_id,
ManifestAnexo.company_id == company_id,
)
if filters:
if filters.get("consecutive"):
query = query.filter(ManifestAnexo.consecutive == filters["consecutive"])
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
ManifestAnexo.number.ilike(search_pattern),
ManifestAnexo.attached_doc.ilike(search_pattern),
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_pk(
db: Session, consecutive: int, line_number: int, tenant_id: int, company_id: int
) -> Optional[ManifestAnexo]:
return (
db.query(ManifestAnexo)
.filter(
ManifestAnexo.consecutive == consecutive,
ManifestAnexo.line_number == line_number,
ManifestAnexo.tenant_id == tenant_id,
ManifestAnexo.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ManifestAnexoCreateDTO,
tenant_id: int,
company_id: int,
) -> ManifestAnexo:
try:
db_item = ManifestAnexo(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating manifest anexo: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating manifest anexo")
@staticmethod
def update(
db: Session,
consecutive: int,
line_number: int,
tenant_id: int,
company_id: int,
data: ManifestAnexoUpdateDTO,
) -> Optional[ManifestAnexo]:
item = ManifestAnexoService.get_by_pk(db, consecutive, line_number, tenant_id, company_id)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(f"Error updating manifest anexo: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating manifest anexo")
@staticmethod
def delete(
db: Session, consecutive: int, line_number: int, tenant_id: int, company_id: int
) -> bool:
item = ManifestAnexoService.get_by_pk(db, consecutive, line_number, tenant_id, company_id)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting manifest anexo: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting manifest anexo")

View File

@@ -0,0 +1,75 @@
from decimal import Decimal
from typing import List, Optional
from pydantic import BaseModel, Field
class ValueManifestationBaseDTO(BaseModel):
manifestation_number: Optional[str] = Field(None, max_length=100, description="Manifestation number")
pedimento: Optional[str] = Field(None, max_length=15, description="Pedimento")
periodicity: Optional[str] = Field(None, max_length=10, description="Periodicity")
semester: Optional[int] = Field(None, description="Semester")
year: Optional[str] = Field(None, max_length=4, description="Year")
pedimento_type: Optional[str] = Field(None, max_length=3, description="Pedimento type")
aa_code: Optional[str] = Field(None, max_length=5, description="AA code")
patent: Optional[str] = Field(None, max_length=4, description="Patent")
first_name: Optional[str] = Field(None, max_length=80, description="First name")
last_name_paternal: Optional[str] = Field(None, max_length=80, description="Last name paternal")
last_name_maternal: Optional[str] = Field(None, max_length=80, description="Last name maternal")
methods_count: Optional[int] = Field(None, description="Methods count")
merchandise_value_method: Optional[str] = Field(None, max_length=10, description="Merchandise value method")
transaction_value: Optional[int] = Field(None, description="Transaction value")
identical_merchandise_value: Optional[int] = Field(None, description="Identical merchandise value")
similar_merchandise_value: Optional[int] = Field(None, description="Similar merchandise value")
unit_sale_price_value: Optional[int] = Field(None, description="Unit sale price value")
reconstructed_value: Optional[int] = Field(None, description="Reconstructed value")
article_78_value: Optional[int] = Field(None, description="Article 78 value")
provisional_value_declaration: Optional[int] = Field(None, description="Provisional value declaration")
has_attachments: Optional[int] = Field(None, description="Has attachments")
attachment_pages_number: Optional[str] = Field(None, max_length=100, description="Attachment pages number")
transaction_value_paid_price: Optional[Decimal] = Field(None, description="Transaction value paid price")
price_pre_invoice: Optional[int] = Field(None, description="Price pre invoice")
price_other_docs: Optional[int] = Field(None, description="Price other docs")
concept_article_66: Optional[int] = Field(None, description="Concept article 66")
concept_article_66_breakdown: Optional[int] = Field(None, description="Concept article 66 breakdown")
attachment_article_66: Optional[str] = Field(None, max_length=2, description="Attachment article 66")
prepaid_merchandise_article_65: Optional[str] = Field(None, max_length=2, description="Prepaid merchandise article 65")
attachment_article_65: Optional[str] = Field(None, max_length=2, description="Attachment article 65")
tax_base_no_sale: Optional[str] = Field(None, max_length=2, description="Tax base no sale")
exists_circumstances_article_67_71: Optional[str] = Field(None, max_length=2, description="Exists circumstances article 67 71")
customs_value_attachment: Optional[str] = Field(None, max_length=2, description="Customs value attachment")
provisional_value_determination: Optional[str] = Field(None, max_length=2, description="Provisional value determination")
merchandise_value_proof_attachment: Optional[str] = Field(None, max_length=2, description="Merchandise value proof attachment")
legal_rep_rfc: Optional[str] = Field(None, max_length=30, description="Legal rep RFC")
legal_representative: Optional[str] = Field(None, max_length=100, description="Legal representative")
date: Optional[int] = Field(None, description="Date")
selected_invoice: Optional[str] = Field(None, max_length=20, description="Selected invoice")
invoice_option: Optional[str] = Field(None, max_length=3, description="Invoice option")
importer_to_use: Optional[str] = Field(None, max_length=8, description="Importer to use")
class ValueManifestationCreateDTO(ValueManifestationBaseDTO):
pass
class ValueManifestationUpdateDTO(ValueManifestationBaseDTO):
pass
class ValueManifestationResponseDTO(ValueManifestationBaseDTO):
id: int
tenant_id: int
company_id: int
class Config:
from_attributes = True
class ValueManifestationListDTO(BaseModel):
items: List[ValueManifestationResponseDTO]
total: int
page: int
page_size: int
class Config:
from_attributes = True

View File

@@ -0,0 +1,66 @@
from decimal import Decimal
from typing import Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
from sqlalchemy import (
Index,
Integer,
Numeric,
PrimaryKeyConstraint,
SmallInteger,
String,
)
from sqlalchemy.orm import Mapped, mapped_column
class ValueManifestation(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "value_manifestations"
__table_args__ = (
Index("idx_value_manifestations_manifestation_number", "manifestation_number"),
{"schema": "a76"},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
manifestation_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
pedimento: Mapped[Optional[str]] = mapped_column(String(15), nullable=True)
periodicity: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
semester: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
year: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
pedimento_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
aa_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
patent: Mapped[Optional[str]] = mapped_column(String(4), nullable=True)
first_name: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
last_name_paternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
last_name_maternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True)
methods_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
merchandise_value_method: Mapped[Optional[str]] = mapped_column(String(10), nullable=True)
transaction_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
identical_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
similar_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
unit_sale_price_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
reconstructed_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
article_78_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
provisional_value_declaration: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
has_attachments: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
attachment_pages_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
transaction_value_paid_price: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
price_pre_invoice: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
price_other_docs: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
concept_article_66: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
concept_article_66_breakdown: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True)
attachment_article_66: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
prepaid_merchandise_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
attachment_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
tax_base_no_sale: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
exists_circumstances_article_67_71: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
customs_value_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
provisional_value_determination: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
merchandise_value_proof_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
legal_rep_rfc: Mapped[Optional[str]] = mapped_column(String(30), nullable=True)
legal_representative: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
selected_invoice: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
invoice_option: Mapped[Optional[str]] = mapped_column(String(3), nullable=True)
importer_to_use: Mapped[Optional[str]] = mapped_column(String(8), nullable=True)

View File

@@ -0,0 +1,136 @@
import logging
from typing import List, Optional, Dict, Any, Tuple
from fastapi import HTTPException
from sqlalchemy import or_
from sqlalchemy.orm import Session
from .models import ValueManifestation
from .dtos import (
ValueManifestationCreateDTO,
ValueManifestationUpdateDTO,
)
logger = logging.getLogger(__name__)
class ValueManifestationService:
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[ValueManifestation], int]:
query = db.query(ValueManifestation).filter(
ValueManifestation.tenant_id == tenant_id,
ValueManifestation.company_id == company_id,
)
if filters:
if filters.get("search"):
search_pattern = f"%{filters['search']}%"
query = query.filter(
or_(
ValueManifestation.manifestation_number.ilike(search_pattern),
ValueManifestation.pedimento.ilike(search_pattern),
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session,
value_manifestation_id: int,
tenant_id: int,
company_id: int,
) -> Optional[ValueManifestation]:
return (
db.query(ValueManifestation)
.filter(
ValueManifestation.id == value_manifestation_id,
ValueManifestation.tenant_id == tenant_id,
ValueManifestation.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
data: ValueManifestationCreateDTO,
tenant_id: int,
company_id: int,
) -> ValueManifestation:
try:
db_item = ValueManifestation(
**data.model_dump(exclude_unset=True),
tenant_id=tenant_id,
company_id=company_id,
)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
except Exception as e:
db.rollback()
logger.error(f"Error creating value manifestation: {str(e)}")
raise HTTPException(
status_code=500, detail="Error creating value manifestation"
)
@staticmethod
def update(
db: Session,
value_manifestation_id: int,
tenant_id: int,
company_id: int,
data: ValueManifestationUpdateDTO,
) -> Optional[ValueManifestation]:
item = ValueManifestationService.get_by_id(
db, value_manifestation_id, tenant_id, company_id
)
if not item:
return None
try:
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(item, field, value)
db.commit()
db.refresh(item)
return item
except Exception as e:
db.rollback()
logger.error(f"Error updating value manifestation {value_manifestation_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error updating value manifestation"
)
@staticmethod
def delete(
db: Session, value_manifestation_id: int, tenant_id: int, company_id: int
) -> bool:
item = ValueManifestationService.get_by_id(
db, value_manifestation_id, tenant_id, company_id
)
if not item:
return False
try:
db.delete(item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting value manifestation {value_manifestation_id}: {str(e)}")
raise HTTPException(
status_code=500, detail="Error deleting value manifestation"
)

View File

@@ -54,6 +54,9 @@ from .reports.importacion.consolidados.routes import router as consolidated_repo
from .reports.importacion.packing_list.routes import router as packing_list_router
from .reports.exportacion.aviso_consolidado.routes import router as aviso_consolidado_export_router
from .reports.exportacion.descargo.routes import router as discharge_reports_router
from .manifests.manifest.routes import router as manifests_router
from .manifests.driver.routes import router as manifest_drivers_router
from .manifests.manifiesto_anexo.routes import router as manifest_anexos_router
@@ -152,4 +155,22 @@ router.include_router(
discharge_reports_router,
prefix="/a76/reports/exportacion/descargo",
tags=["a76 / reports"]
)
router.include_router(
manifests_router,
prefix="/a76",
tags=["a76 / manifests"]
)
router.include_router(
manifest_drivers_router,
prefix="/a76",
tags=["a76 / manifests"]
)
router.include_router(
manifest_anexos_router,
prefix="/a76",
tags=["a76 / manifests"]
)