Se integro el modulo de manifestacion base
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
|
||||
import logging
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
@@ -6,6 +7,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Type variables for generic types
|
||||
ModelType = TypeVar("ModelType")
|
||||
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
||||
@@ -173,6 +176,7 @@ class TenantCRUDRoutes(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
|
||||
return {
|
||||
"items": [
|
||||
self.response_schema.model_validate(item) for item in items
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
0
backend/api/v1/modules/a76/manifests/__init__.py
Normal file
0
backend/api/v1/modules/a76/manifests/__init__.py
Normal 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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
41
backend/api/v1/modules/a76/manifests/manifest/debug_db.py
Normal file
41
backend/api/v1/modules/a76/manifests/manifest/debug_db.py
Normal 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()
|
||||
66
backend/api/v1/modules/a76/manifests/manifest/dtos.py
Normal file
66
backend/api/v1/modules/a76/manifests/manifest/dtos.py
Normal 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")
|
||||
52
backend/api/v1/modules/a76/manifests/manifest/models.py
Normal file
52
backend/api/v1/modules/a76/manifests/manifest/models.py
Normal 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)
|
||||
127
backend/api/v1/modules/a76/manifests/manifest/routes.py
Normal file
127
backend/api/v1/modules/a76/manifests/manifest/routes.py
Normal 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
|
||||
192
backend/api/v1/modules/a76/manifests/manifest/service.py
Normal file
192
backend/api/v1/modules/a76/manifests/manifest/service.py
Normal 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"
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -53,6 +53,7 @@ from .reports.importacion.facturas.routes import router as invoices_reports_rout
|
||||
from .reports.importacion.consolidados.routes import router as consolidated_reports_router
|
||||
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 .manifests.manifest.routes import router as manifests_router
|
||||
|
||||
|
||||
|
||||
@@ -145,4 +146,10 @@ router.include_router(
|
||||
aviso_consolidado_export_router,
|
||||
prefix="/a76/reports/exportacion/aviso_consolidado",
|
||||
tags=["a76 / reports"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
manifests_router,
|
||||
prefix="/a76",
|
||||
tags=["a76 / manifests"]
|
||||
)
|
||||
Reference in New Issue
Block a user