Partidas BOM y partes para SCAI
This commit is contained in:
0
backend/api/v1/modules/a24/inv/bom/__init__.py
Normal file
0
backend/api/v1/modules/a24/inv/bom/__init__.py
Normal file
56
backend/api/v1/modules/a24/inv/bom/models.py
Normal file
56
backend/api/v1/modules/a24/inv/bom/models.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""
|
||||
Modelo ORM para la lista de materiales (BOM) de una parte - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from decimal import Decimal
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
Numeric,
|
||||
String,
|
||||
PrimaryKeyConstraint,
|
||||
ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
|
||||
class BillOfMaterial(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla inv_bom: Lista de materiales para una parte.
|
||||
"""
|
||||
__tablename__ = "inv_bom"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_bom_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["parent_part_id"], ["a76.parts.id"], name="fk_inv_bom_parent"
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["component_part_id"], ["a76.parts.id"], name="fk_inv_bom_component"
|
||||
),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
parent_part_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
component_part_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
quantity: Mapped[Decimal] = mapped_column(Numeric(19, 8), default=Decimal('1.0'))
|
||||
|
||||
# Nuevos campos Legacy
|
||||
uom_code: Mapped[str] = mapped_column(String(5), nullable=False)
|
||||
procedure_type: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) # TEMPORAL, DEFINITIVA, CTM
|
||||
is_percentage: Mapped[bool] = mapped_column(default=True)
|
||||
raw_material: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
|
||||
waste: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
|
||||
merma: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True)
|
||||
|
||||
# Relaciones
|
||||
parent_part: Mapped["Part"] = relationship("Part", foreign_keys=[parent_part_id], back_populates="bom_items")
|
||||
component_part: Mapped["Part"] = relationship("Part", foreign_keys=[component_part_id])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<BillOfMaterial(id={self.id}, parent={self.parent_part_id}, component={self.component_part_id}, uom={self.uom_code})>"
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy import (
|
||||
Boolean,
|
||||
ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -100,9 +101,60 @@ class InvPart(Base, TenantScopedMixin, TimestampMixin):
|
||||
dtb: Mapped[Optional[str]] = mapped_column(String(19)) # DTB
|
||||
dtg: Mapped[Optional[str]] = mapped_column(String(19)) # DTG
|
||||
|
||||
# --- CAMPOS ADICIONALES FRONTEND ---
|
||||
substitute_part: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
complementary_part: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
preference_part: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
use_alternate_quantity: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
||||
un_number: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
shipping_name: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
hazard_notes: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
repair_unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
repair_added_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
fraction_9801: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
immex_type: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
disable_movements: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
||||
pga_program_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
usmca_fraction: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
scrap_part_number: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
waste_part_number: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
scrap_description_en: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
scrap_description_es: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
scrap_export_fraction: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
scrap_us_fraction: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
equivalent_uom_2: Mapped[Optional[str]] = mapped_column(String(5))
|
||||
conversion_factor_2: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8))
|
||||
has_auxiliary: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
||||
auxiliary_uom: Mapped[Optional[str]] = mapped_column(String(5))
|
||||
auxiliary_conversion: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8))
|
||||
auxiliary_unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
mex_packing: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
sales_order: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
use_rule_8: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
||||
sector: Mapped[Optional[str]] = mapped_column(String(150))
|
||||
origin_country: Mapped[Optional[str]] = mapped_column(String(3), default='MEX')
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
# JSONB para almacenar clientes con restricción de no descarga
|
||||
non_discharge_clients: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True, default=[])
|
||||
|
||||
# --- RELACIÓN ---
|
||||
# Usamos string "Part" para evitar problemas de carga
|
||||
master_info: Mapped["Part"] = relationship("Part", back_populates="inv_data")
|
||||
|
||||
@property
|
||||
def bom_items(self):
|
||||
"""Exponer los BOM items de la parte maestra para Pydantic"""
|
||||
if self.master_info:
|
||||
return self.master_info.bom_items
|
||||
return []
|
||||
|
||||
@property
|
||||
def countries(self):
|
||||
"""Exponer los países de la parte maestra para Pydantic"""
|
||||
if self.master_info:
|
||||
return self.master_info.inv_countries
|
||||
return []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<InvPart(id={self.id}, part_type='{self.part_type}')>"
|
||||
81
backend/api/v1/modules/a24/inv/part_countries/dto.py
Normal file
81
backend/api/v1/modules/a24/inv/part_countries/dto.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
DTOs (Data Transfer Objects) para relación entre partes y países - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PartCountryCreateDTO(BaseModel):
|
||||
"""DTO para crear una relación entre parte y país"""
|
||||
|
||||
part_id: int = Field(..., description="Part ID")
|
||||
country_code: str = Field(..., max_length=3, description="Country code (ISO 3166-1 alpha-3)")
|
||||
fraction: Optional[str] = Field(None, max_length=20, description="Tariff fraction")
|
||||
preference: str = Field(default="GENERAL", description="Trade preference: GENERAL, PROSEC, ALADI, TLCS")
|
||||
has_certificate: bool = Field(default=False, description="Has certificate of origin")
|
||||
certificate_number: Optional[str] = Field(None, max_length=50, description="Certificate of origin number")
|
||||
end_date: Optional[datetime] = Field(None, description="Certificate expiration date")
|
||||
previous_fractions_7m: bool = Field(default=False, description="Has fractions older than 7 months")
|
||||
omission_import: bool = Field(default=False, description="Import omission flag")
|
||||
omission_export: bool = Field(default=False, description="Export omission flag")
|
||||
import_percentage: Optional[Decimal] = Field(None, description="Import tariff percentage")
|
||||
export_percentage: Optional[Decimal] = Field(None, description="Export tariff percentage")
|
||||
sector: Optional[str] = Field(None, max_length=10, description="Sector reference")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartCountryUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una relación entre parte y país"""
|
||||
|
||||
country_code: Optional[str] = Field(None, max_length=3, description="Country code")
|
||||
fraction: Optional[str] = Field(None, max_length=20, description="Tariff fraction")
|
||||
preference: Optional[str] = Field(None, description="Trade preference")
|
||||
has_certificate: Optional[bool] = Field(None, description="Has certificate of origin")
|
||||
certificate_number: Optional[str] = Field(None, max_length=50, description="Certificate number")
|
||||
end_date: Optional[datetime] = Field(None, description="Certificate expiration date")
|
||||
previous_fractions_7m: Optional[bool] = Field(None, description="Previous fractions flag")
|
||||
omission_import: Optional[bool] = Field(None, description="Import omission flag")
|
||||
omission_export: Optional[bool] = Field(None, description="Export omission flag")
|
||||
import_percentage: Optional[Decimal] = Field(None, description="Import percentage")
|
||||
export_percentage: Optional[Decimal] = Field(None, description="Export percentage")
|
||||
sector: Optional[str] = Field(None, max_length=10, description="Sector reference")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartCountryResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de relación entre parte y país"""
|
||||
|
||||
id: int
|
||||
part_id: int
|
||||
country_code: str
|
||||
fraction: Optional[str] = None
|
||||
preference: str
|
||||
has_certificate: bool
|
||||
certificate_number: Optional[str] = None
|
||||
end_date: Optional[datetime] = None
|
||||
previous_fractions_7m: bool
|
||||
omission_import: bool
|
||||
omission_export: bool
|
||||
import_percentage: Optional[Decimal] = None
|
||||
export_percentage: Optional[Decimal] = None
|
||||
sector: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartCountriesBulkResponseDTO(BaseModel):
|
||||
"""DTO para responder con lista de relaciones entre partes y países"""
|
||||
|
||||
data: list[PartCountryResponseDTO]
|
||||
total: int
|
||||
skip: int
|
||||
limit: int
|
||||
73
backend/api/v1/modules/a24/inv/part_countries/models.py
Normal file
73
backend/api/v1/modules/a24/inv/part_countries/models.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Modelo ORM para la relación entre partes y países - Anexo 24
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from decimal import Decimal
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
Boolean,
|
||||
ForeignKeyConstraint,
|
||||
DateTime,
|
||||
Numeric,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
|
||||
class PartCountry(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla inv_parte_paises: Relación entre partes y países.
|
||||
Almacena información de países de origen, preferencias, certificados y omisiones.
|
||||
"""
|
||||
__tablename__ = "inv_parte_paises"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_parte_paises_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["part_id"], ["a76.parts.id"], name="fk_inv_parte_paises_part"
|
||||
),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
part_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
country_code: Mapped[str] = mapped_column(String(3), nullable=False)
|
||||
|
||||
# Información de fracciones
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
is_origin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Preferencia comercial
|
||||
preference: Mapped[str] = mapped_column(String(15), default="GENERAL") # GENERAL, PROSEC, ALADI, TLCS
|
||||
|
||||
# Certificado de origen
|
||||
has_certificate: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
certificate_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
|
||||
# Fracciones anteriores a 7 meses
|
||||
previous_fractions_7m: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Omisiones (importación/exportación)
|
||||
omission_import: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
omission_export: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Porcentajes
|
||||
import_percentage: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2))
|
||||
export_percentage: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2))
|
||||
|
||||
# Sector (para referencia)
|
||||
sector: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
# Relación
|
||||
part: Mapped["Part"] = relationship("Part", back_populates="inv_countries")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PartCountry(id={self.id}, part={self.part_id}, country='{self.country_code}', preference='{self.preference}')>"
|
||||
162
backend/api/v1/modules/a24/inv/part_countries/routes.py
Normal file
162
backend/api/v1/modules/a24/inv/part_countries/routes.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Rutas para gestión de relaciones entre partes y países - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from .dto import (
|
||||
PartCountryCreateDTO,
|
||||
PartCountryResponseDTO,
|
||||
PartCountryUpdateDTO,
|
||||
PartCountriesBulkResponseDTO,
|
||||
)
|
||||
from .models import PartCountry
|
||||
from .service import PartCountryService
|
||||
|
||||
router = APIRouter(prefix="/part-countries", tags=["part-countries"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=dict,
|
||||
summary="Get all part-country relationships",
|
||||
)
|
||||
async def get_all_part_countries(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
part_id: int = Query(None),
|
||||
country_code: str = Query(None),
|
||||
preference: str = Query(None),
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Get all part-country relationships with optional filtering and pagination"""
|
||||
filters = {}
|
||||
if part_id:
|
||||
filters["part_id"] = part_id
|
||||
if country_code:
|
||||
filters["country_code"] = country_code
|
||||
if preference:
|
||||
filters["preference"] = preference
|
||||
|
||||
part_countries, total = PartCountryService.get_all(db, skip, limit, filters)
|
||||
|
||||
return {
|
||||
"data": [PartCountryResponseDTO.model_validate(pc) for pc in part_countries],
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit,
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{part_country_id}",
|
||||
response_model=PartCountryResponseDTO,
|
||||
summary="Get part-country relationship by ID",
|
||||
)
|
||||
async def get_part_country(
|
||||
part_country_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Get a part-country relationship by its ID"""
|
||||
part_country = PartCountryService.get_by_id(db, part_country_id)
|
||||
if not part_country:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Part-country relationship not found",
|
||||
)
|
||||
return PartCountryResponseDTO.model_validate(part_country)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/part/{part_id}",
|
||||
response_model=dict,
|
||||
summary="Get all countries for a part",
|
||||
)
|
||||
async def get_part_countries(
|
||||
part_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Get all countries associated with a specific part"""
|
||||
part_countries = PartCountryService.get_by_part_id(db, part_id)
|
||||
|
||||
return {
|
||||
"data": [PartCountryResponseDTO.model_validate(pc) for pc in part_countries],
|
||||
"total": len(part_countries),
|
||||
}
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=PartCountryResponseDTO,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create part-country relationship",
|
||||
)
|
||||
async def create_part_country(
|
||||
part_country_data: PartCountryCreateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Create a new part-country relationship"""
|
||||
part_country = PartCountryService.create(db, part_country_data)
|
||||
return PartCountryResponseDTO.model_validate(part_country)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/part/{part_id}/bulk",
|
||||
response_model=dict,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Bulk create/update part-country relationships",
|
||||
)
|
||||
async def bulk_create_part_countries(
|
||||
part_id: int,
|
||||
countries_data: List[PartCountryCreateDTO],
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Create or replace all part-country relationships for a part"""
|
||||
part_countries = PartCountryService.bulk_create(db, part_id, countries_data)
|
||||
|
||||
return {
|
||||
"data": [PartCountryResponseDTO.model_validate(pc) for pc in part_countries],
|
||||
"total": len(part_countries),
|
||||
}
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{part_country_id}",
|
||||
response_model=PartCountryResponseDTO,
|
||||
summary="Update part-country relationship",
|
||||
)
|
||||
async def update_part_country(
|
||||
part_country_id: int,
|
||||
part_country_data: PartCountryUpdateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Update a part-country relationship"""
|
||||
part_country = PartCountryService.update(db, part_country_id, part_country_data)
|
||||
if not part_country:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Part-country relationship not found",
|
||||
)
|
||||
return PartCountryResponseDTO.model_validate(part_country)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{part_country_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete part-country relationship",
|
||||
)
|
||||
async def delete_part_country(
|
||||
part_country_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Delete a part-country relationship"""
|
||||
success = PartCountryService.delete(db, part_country_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Part-country relationship not found",
|
||||
)
|
||||
186
backend/api/v1/modules/a24/inv/part_countries/service.py
Normal file
186
backend/api/v1/modules/a24/inv/part_countries/service.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Capa de servicio para lógica de negocio de relación entre partes y países
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import PartCountryCreateDTO, PartCountryResponseDTO, PartCountryUpdateDTO
|
||||
from .models import PartCountry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PartCountryService:
|
||||
"""Servicio para gestión de relaciones entre partes y países"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[PartCountry], int]:
|
||||
"""Get all part-country relationships with pagination"""
|
||||
query = db.query(PartCountry)
|
||||
|
||||
if filters:
|
||||
if filters.get("part_id"):
|
||||
query = query.filter(PartCountry.part_id == filters["part_id"])
|
||||
if filters.get("country_code"):
|
||||
query = query.filter(
|
||||
PartCountry.country_code.ilike(f"%{filters['country_code']}%")
|
||||
)
|
||||
if filters.get("preference"):
|
||||
query = query.filter(PartCountry.preference == filters["preference"])
|
||||
|
||||
total = query.count()
|
||||
part_countries = query.offset(skip).limit(limit).all()
|
||||
|
||||
return part_countries, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, part_country_id: int) -> Optional[PartCountry]:
|
||||
"""Get part-country relationship by ID"""
|
||||
return db.query(PartCountry).filter(PartCountry.id == part_country_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_by_part_id(db: Session, part_id: int) -> List[PartCountry]:
|
||||
"""Get all countries for a specific part"""
|
||||
return db.query(PartCountry).filter(PartCountry.part_id == part_id).all()
|
||||
|
||||
@staticmethod
|
||||
def get_by_part_and_country(db: Session, part_id: int, country_code: str) -> Optional[PartCountry]:
|
||||
"""Get specific part-country relationship"""
|
||||
return db.query(PartCountry).filter(
|
||||
PartCountry.part_id == part_id,
|
||||
PartCountry.country_code == country_code
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, part_country_data: PartCountryCreateDTO) -> PartCountry:
|
||||
"""Create a new part-country relationship"""
|
||||
try:
|
||||
db_part_country = PartCountry(
|
||||
**part_country_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
|
||||
db.add(db_part_country)
|
||||
db.commit()
|
||||
db.refresh(db_part_country)
|
||||
|
||||
return db_part_country
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError creating part-country relationship: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Relationship already exists or invalid foreign key",
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating part-country relationship: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error creating relationship: {str(e)}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session, part_country_id: int, part_country_data: PartCountryUpdateDTO
|
||||
) -> Optional[PartCountry]:
|
||||
"""Update part-country relationship"""
|
||||
try:
|
||||
db_part_country = db.query(PartCountry).filter(
|
||||
PartCountry.id == part_country_id
|
||||
).first()
|
||||
|
||||
if not db_part_country:
|
||||
return None
|
||||
|
||||
update_data = part_country_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_part_country, field, value)
|
||||
|
||||
db.add(db_part_country)
|
||||
db.commit()
|
||||
db.refresh(db_part_country)
|
||||
|
||||
return db_part_country
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError updating part-country relationship: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot update: constraint violation",
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating part-country relationship: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error updating relationship: {str(e)}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, part_country_id: int) -> bool:
|
||||
"""Delete part-country relationship"""
|
||||
try:
|
||||
db_part_country = db.query(PartCountry).filter(
|
||||
PartCountry.id == part_country_id
|
||||
).first()
|
||||
|
||||
if not db_part_country:
|
||||
return False
|
||||
|
||||
db.delete(db_part_country)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting part-country relationship: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error deleting relationship: {str(e)}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def bulk_create(db: Session, part_id: int, countries_data: List[PartCountryCreateDTO]) -> List[PartCountry]:
|
||||
"""Create multiple part-country relationships for a part"""
|
||||
try:
|
||||
# Delete existing relationships for this part
|
||||
db.query(PartCountry).filter(PartCountry.part_id == part_id).delete()
|
||||
db.commit()
|
||||
|
||||
# Create new relationships
|
||||
db_part_countries = []
|
||||
for country_data in countries_data:
|
||||
country_data.part_id = part_id
|
||||
db_part_country = PartCountry(
|
||||
**country_data.model_dump(exclude_unset=True)
|
||||
)
|
||||
db_part_countries.append(db_part_country)
|
||||
|
||||
db.add_all(db_part_countries)
|
||||
db.commit()
|
||||
|
||||
for pc in db_part_countries:
|
||||
db.refresh(pc)
|
||||
|
||||
return db_part_countries
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error bulk creating part-country relationships: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error creating relationships: {str(e)}",
|
||||
)
|
||||
Reference in New Issue
Block a user