Merge branch 'development' into feature/catalog-importation
This commit is contained in:
17
backend/api/v1/modules/a24/inv/inv_aphis/dto.py
Normal file
17
backend/api/v1/modules/a24/inv/inv_aphis/dto.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .inv_aphis_general.dto import InvPartAphisGeneralDTO
|
||||
from .inv_aphis_characteristic.dto import InvPartAphisCharacteristicDTO
|
||||
from .inv_aphis_stype_pitems.dto import InvPartAphisStypePitemsDTO
|
||||
from .inv_aphis_lpcos.dto import InvPartAphisLpcosDTO
|
||||
from .inv_aphis_entities.dto import InvPartAphisEntitiesDTO
|
||||
from .inv_aphis_containers.dto import InvPartAphisContainersDTO
|
||||
from .inv_aphis_routing.dto import InvPartAphisRoutingDTO
|
||||
|
||||
__all__ = [
|
||||
"InvPartAphisGeneralDTO",
|
||||
"InvPartAphisCharacteristicDTO",
|
||||
"InvPartAphisStypePitemsDTO",
|
||||
"InvPartAphisLpcosDTO",
|
||||
"InvPartAphisEntitiesDTO",
|
||||
"InvPartAphisContainersDTO",
|
||||
"InvPartAphisRoutingDTO"
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisCharacteristicDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
item_id: Optional[str] = None
|
||||
number_from: Optional[str] = None
|
||||
number_to: Optional[str] = None
|
||||
category_type: Optional[str] = None
|
||||
commodity_qua: Optional[str] = None
|
||||
commodity_char_qua: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
category_code: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisCharacteristic(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: Item characteristic"""
|
||||
__tablename__ = "inv_aphis_characteristic"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_characteristic_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS CARACTERISTICAS ---
|
||||
item_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
number_from: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
number_to: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
category_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
commodity_qua: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
commodity_char_qua: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
description: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
category_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="characteristics")
|
||||
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisContainersDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
container_number: Optional[str] = None
|
||||
length: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisContainers(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: containers"""
|
||||
__tablename__ = "inv_aphis_containers"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_containers_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS CONTAINERS ---
|
||||
container_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
length: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="containers")
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisEntitiesDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
consignee_key: Optional[str] = None
|
||||
broker_key: Optional[str] = None
|
||||
lpco_auth_party_key: Optional[str] = None
|
||||
grower_key: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,31 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisEntities(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: entities"""
|
||||
__tablename__ = "inv_aphis_entities"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_entities_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS ENTITIES ---
|
||||
consignee_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
broker_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
lpco_auth_party_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
grower_key: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="entities")
|
||||
@@ -0,0 +1,64 @@
|
||||
from pydantic import BaseModel, field_validator, ConfigDict
|
||||
from typing import Optional, List, Any
|
||||
from datetime import date
|
||||
|
||||
from ..inv_aphis_characteristic.dto import InvPartAphisCharacteristicDTO
|
||||
from ..inv_aphis_stype_pitems.dto import InvPartAphisStypePitemsDTO
|
||||
from ..inv_aphis_lpcos.dto import InvPartAphisLpcosDTO
|
||||
from ..inv_aphis_entities.dto import InvPartAphisEntitiesDTO
|
||||
from ..inv_aphis_containers.dto import InvPartAphisContainersDTO
|
||||
from ..inv_aphis_routing.dto import InvPartAphisRoutingDTO
|
||||
|
||||
# NOTA: Importar primero los sub-dtos para resolver dependencias
|
||||
|
||||
class InvPartAphisGeneralDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
program_code: Optional[str] = None
|
||||
processing_code: Optional[str] = None
|
||||
aphis_type: Optional[str] = None
|
||||
disclaimer: Optional[str] = None
|
||||
electronic_image: Optional[str] = None
|
||||
confidential: Optional[str] = None
|
||||
global_product_id: Optional[str] = None
|
||||
intended_use_code: Optional[str] = None
|
||||
intended_use_description: Optional[str] = None
|
||||
item_type: Optional[str] = None
|
||||
product_code: Optional[str] = None
|
||||
product_code_2: Optional[str] = None
|
||||
product_code_3: Optional[str] = None
|
||||
scientific_genus_name: Optional[str] = None
|
||||
scientific_species_name: Optional[str] = None
|
||||
scientific_sub_species_name: Optional[str] = None
|
||||
common_name_specific: Optional[str] = None
|
||||
common_name_general: Optional[str] = None
|
||||
signed_doc: Optional[str] = None
|
||||
signed_doc_date: Optional[date] = None
|
||||
signed_doc_id: Optional[str] = None
|
||||
invoice_number: Optional[str] = None
|
||||
quantity_1: Optional[str] = None
|
||||
quantity_2: Optional[str] = None
|
||||
quantity_3: Optional[str] = None
|
||||
inspection: Optional[str] = None
|
||||
inspection_date: Optional[date] = None
|
||||
inspection_loc_date: Optional[date] = None
|
||||
inspection_location: Optional[str] = None
|
||||
country_production: Optional[str] = None
|
||||
country_source: Optional[str] = None
|
||||
|
||||
# Relaciones
|
||||
characteristics: List[InvPartAphisCharacteristicDTO] = []
|
||||
stype_pitems: List[InvPartAphisStypePitemsDTO] = []
|
||||
lpcos: List[InvPartAphisLpcosDTO] = []
|
||||
entities: List[InvPartAphisEntitiesDTO] = []
|
||||
containers: List[InvPartAphisContainersDTO] = []
|
||||
routing: List[InvPartAphisRoutingDTO] = []
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@field_validator('signed_doc_date', 'inspection_date', 'inspection_loc_date', mode='before')
|
||||
@classmethod
|
||||
def empty_str_to_none(cls, v: Any) -> Any:
|
||||
if v == "":
|
||||
return None
|
||||
return v
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisGeneral(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla principal de Aphis vinculada a la parte.
|
||||
Pestaña: Información general de aphis
|
||||
"""
|
||||
__tablename__ = "inv_aphis_general"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_general_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["inv_part_id"], ["a24.inv_partes.id"], name="fk_aphis_general_inv_part"
|
||||
),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
inv_part_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- 31 CAMPOS INICIALES ---
|
||||
program_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
processing_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
aphis_type: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
disclaimer: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
electronic_image: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
confidential: Mapped[Optional[str]] = mapped_column(String(1))
|
||||
global_product_id: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
intended_use_code: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
intended_use_description: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
item_type: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
product_code: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
product_code_2: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
product_code_3: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
scientific_genus_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
scientific_species_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
scientific_sub_species_name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
common_name_specific: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
common_name_general: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
signed_doc: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
signed_doc_date: Mapped[Optional[date]] = mapped_column(Date)
|
||||
signed_doc_id: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
quantity_1: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
quantity_2: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
quantity_3: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
inspection: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
inspection_date: Mapped[Optional[date]] = mapped_column(Date)
|
||||
inspection_loc_date: Mapped[Optional[date]] = mapped_column(Date)
|
||||
inspection_location: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
country_production: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
country_source: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
|
||||
# Relaciones
|
||||
inv_part: Mapped["InvPart"] = relationship("InvPart", back_populates="aphis_records")
|
||||
|
||||
characteristics: Mapped[List["InvPartAphisCharacteristic"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
stype_pitems: Mapped[List["InvPartAphisStypePitems"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
lpcos: Mapped[List["InvPartAphisLpcos"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
entities: Mapped[List["InvPartAphisEntities"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
containers: Mapped[List["InvPartAphisContainers"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
routing: Mapped[List["InvPartAphisRouting"]] = relationship(back_populates="aphis_general", cascade="all, delete-orphan")
|
||||
@@ -0,0 +1,27 @@
|
||||
from pydantic import BaseModel, field_validator, ConfigDict
|
||||
from typing import Optional, List, Any
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisLpcosDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
issuer: Optional[str] = None
|
||||
issuer_loc_qua: Optional[str] = None
|
||||
issuer_loc: Optional[str] = None
|
||||
issuer_loc_desc: Optional[str] = None
|
||||
uom: Optional[str] = None
|
||||
txn_type: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
number: Optional[str] = None
|
||||
date_qual: Optional[str] = None
|
||||
date: Optional[date] = None
|
||||
qty: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@field_validator('date', mode='before')
|
||||
@classmethod
|
||||
def empty_str_to_none(cls, v: Any) -> Any:
|
||||
if v == "":
|
||||
return None
|
||||
return v
|
||||
@@ -0,0 +1,38 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisLpcos(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: lpcos"""
|
||||
__tablename__ = "inv_aphis_lpcos"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_lpcos_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS LPCO ---
|
||||
issuer: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
issuer_loc_qua: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
issuer_loc: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
issuer_loc_desc: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
uom: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
txn_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
date_qual: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
date: Mapped[Optional[date]] = mapped_column(Date)
|
||||
qty: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="lpcos")
|
||||
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional, List
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisRoutingDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
type: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,30 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisRouting(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: routing"""
|
||||
__tablename__ = "inv_aphis_routing"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_routing_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS ROUTING ---
|
||||
type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
country: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
name: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="routing")
|
||||
@@ -0,0 +1,23 @@
|
||||
from pydantic import BaseModel, field_validator, ConfigDict
|
||||
from typing import Optional, List, Any
|
||||
from datetime import date
|
||||
|
||||
class InvPartAphisStypePitemsDTO(BaseModel):
|
||||
id: Optional[int] = None
|
||||
aphis_general_id: Optional[int] = None
|
||||
source_type_code: Optional[str] = None
|
||||
country_code: Optional[str] = None
|
||||
geo_location: Optional[str] = None
|
||||
processing_start: Optional[date] = None
|
||||
processing_end: Optional[date] = None
|
||||
processing_type: Optional[str] = None
|
||||
processing_desc: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@field_validator('processing_start', 'processing_end', mode='before')
|
||||
@classmethod
|
||||
def empty_str_to_none(cls, v: Any) -> Any:
|
||||
if v == "":
|
||||
return None
|
||||
return v
|
||||
@@ -0,0 +1,34 @@
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from datetime import date
|
||||
from sqlalchemy import (
|
||||
Integer, String, Date, Numeric, Boolean,
|
||||
PrimaryKeyConstraint, ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
class InvPartAphisStypePitems(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Módulo: stype_Pitems"""
|
||||
__tablename__ = "inv_aphis_stype_pitems"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_aphis_stype_pitems_pkey"),
|
||||
ForeignKeyConstraint(["aphis_general_id"], ["a24.inv_aphis_general.id"]),
|
||||
{"schema": "a24", "extend_existing": True},
|
||||
)
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
aphis_general_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# --- CAMPOS STYPE PITEMS ---
|
||||
source_type_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
country_code: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
geo_location: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
processing_start: Mapped[Optional[date]] = mapped_column(Date)
|
||||
processing_end: Mapped[Optional[date]] = mapped_column(Date)
|
||||
processing_type: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
processing_desc: Mapped[Optional[str]] = mapped_column(String(200))
|
||||
|
||||
aphis_general: Mapped["InvPartAphisGeneral"] = relationship(back_populates="stype_pitems")
|
||||
17
backend/api/v1/modules/a24/inv/inv_aphis/models.py
Normal file
17
backend/api/v1/modules/a24/inv/inv_aphis/models.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from .inv_aphis_general.models import InvPartAphisGeneral
|
||||
from .inv_aphis_characteristic.models import InvPartAphisCharacteristic
|
||||
from .inv_aphis_stype_pitems.models import InvPartAphisStypePitems
|
||||
from .inv_aphis_lpcos.models import InvPartAphisLpcos
|
||||
from .inv_aphis_entities.models import InvPartAphisEntities
|
||||
from .inv_aphis_containers.models import InvPartAphisContainers
|
||||
from .inv_aphis_routing.models import InvPartAphisRouting
|
||||
|
||||
__all__ = [
|
||||
"InvPartAphisGeneral",
|
||||
"InvPartAphisCharacteristic",
|
||||
"InvPartAphisStypePitems",
|
||||
"InvPartAphisLpcos",
|
||||
"InvPartAphisEntities",
|
||||
"InvPartAphisContainers",
|
||||
"InvPartAphisRouting"
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
Modelo ORM para datos específicos de Inventario y Manufactura (S-Partes) - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Optional, List
|
||||
from decimal import Decimal
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -18,10 +18,10 @@ from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
|
||||
# Importar los modelos Aphis para asegurar su registro en SQLAlchemy
|
||||
import api.v1.modules.a24.inv.inv_aphis.models as aphis_models
|
||||
class InvPart(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla inv_partes: Extensión de Anexo 24 para Inventarios (SPartes).
|
||||
@@ -135,10 +135,23 @@ class InvPart(Base, TenantScopedMixin, TimestampMixin):
|
||||
origin_country: Mapped[Optional[str]] = mapped_column(String(3), default='MEX')
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
# --- NUEVOS CAMPOS EXTENSION ---
|
||||
agency_code_definition: Mapped[Optional[str]] = mapped_column(String(50)) # Fila 7
|
||||
carta_porte: Mapped[Optional[str]] = mapped_column(String(100)) # Fila 10
|
||||
client_part_names: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True, default=[]) # Fila 5
|
||||
part_identifiers: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True, default=[]) # Fila 9
|
||||
substitute_parts: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True, default=[]) # Pestaña Continuación
|
||||
aphis_data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True, default={}) # Sección Aphis en Continuación
|
||||
|
||||
# JSONB para almacenar clientes con restricción de no descarga
|
||||
non_discharge_clients: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True, default=[])
|
||||
|
||||
# --- RELACIÓN ---
|
||||
aphis_records: Mapped[List["InvPartAphisGeneral"]] = relationship(
|
||||
"InvPartAphisGeneral",
|
||||
back_populates="inv_part",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
# Usamos string "Part" para evitar problemas de carga
|
||||
master_info: Mapped["Part"] = relationship("Part", back_populates="inv_data")
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ class ClientProviderService:
|
||||
db_address = ClientProviderAddress(
|
||||
client_id=client.id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
**client_data.address.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_address)
|
||||
@@ -199,6 +200,7 @@ class ClientProviderService:
|
||||
db_programs = ClientProviderPrograms(
|
||||
client_id=client.id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
**client_data.programs.model_dump(exclude_unset=True),
|
||||
)
|
||||
db.add(db_programs)
|
||||
|
||||
@@ -46,7 +46,7 @@ def _process_with_discharge(
|
||||
AFIJO, DONAC, SCRAP, REEXP, VEMEX.
|
||||
"""
|
||||
assign_no_discharges_series(db, lines, errors)
|
||||
review_class(db, invoice, lines, invoice.tenant_id, invoice.company_id, errors)
|
||||
review_class(db, lines, errors)
|
||||
review_exchange_rate(db, invoice, errors)
|
||||
assign_values(db, invoice, lines, invoice.tenant_id, invoice.company_id, errors)
|
||||
|
||||
@@ -201,4 +201,4 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id
|
||||
# invoice.status and totals are set inside finalize_invoice_no_discharge / termina_ac_o_lp_normal
|
||||
db.flush()
|
||||
|
||||
return {"status": "ok", "invoice_id": str(invoice.id)}
|
||||
return {"status": "success", "invoice_id": str(invoice.id)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.general_catalogs.fractions.warning_fractions.models import WarningFraction
|
||||
@@ -47,7 +47,7 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.shipped_to_id,
|
||||
)
|
||||
if not shipped_to_exists.address.country:
|
||||
if not shipped_to_exists.address or not shipped_to_exists.address.country:
|
||||
errors.add_error(
|
||||
field="compliance_mx.shipped_to_id",
|
||||
message="El Destinatario no tiene capturado el pais.",
|
||||
@@ -100,11 +100,16 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_
|
||||
)
|
||||
|
||||
# Advertencias para las fracciones y su horario
|
||||
lines = db.query(LineItem).filter(
|
||||
LineItem.invoice_id == invoice.id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
).all()
|
||||
lines = (
|
||||
db.query(LineItem)
|
||||
.options(joinedload(LineItem.fa_data))
|
||||
.filter(
|
||||
LineItem.invoice_id == invoice.id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ def compare_balances(
|
||||
|
||||
entry.quantity_used += consume
|
||||
lot.available_qty -= consume
|
||||
lot.consumed_qty += consume
|
||||
|
||||
# ── Check if the entry was fully satisfied ────────────────────────────
|
||||
if entry.quantity_used < entry.quantity:
|
||||
|
||||
@@ -21,7 +21,11 @@ class AvailableLot:
|
||||
import_item_line_id : a76.item_lines.id of the import line (the lot)
|
||||
import_invoice_id : a76.invoice_header.id of the import invoice
|
||||
part_number_id : denormalized from the import line
|
||||
available_qty : net balance available (QSaldo:Cantidad)
|
||||
available_qty : net balance available (QSaldo:Cantidad); mutated by
|
||||
compare_balances() as quantity is distributed
|
||||
consumed_qty : how much was actually taken from this lot by
|
||||
compare_balances(); used by register_discharge_ledger
|
||||
to create the exact BalanceMovement amount
|
||||
value_me : USD value of the full lot (for proportional calc)
|
||||
value_mn : MXN value of the full lot (for proportional calc)
|
||||
order_peps : PEPS ordering key — lower = older = consumed first
|
||||
@@ -33,6 +37,7 @@ class AvailableLot:
|
||||
value_me: Optional[Decimal]
|
||||
value_mn: Optional[Decimal]
|
||||
order_peps: int
|
||||
consumed_qty: Decimal = field(default_factory=Decimal)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -66,7 +66,7 @@ def collect_lines_to_discharge(
|
||||
"""
|
||||
to_discharge: List[DownloadEntry] = []
|
||||
|
||||
discharge_lines = [line for line in lines if line.discharge]
|
||||
discharge_lines = [line for line in lines if line.fa_data and line.fa_data.discharge]
|
||||
|
||||
if not discharge_lines:
|
||||
return to_discharge
|
||||
|
||||
@@ -200,7 +200,7 @@ def fill_available_balances(
|
||||
continue
|
||||
|
||||
# Status 'NA' == not processed (Clarion: Estatus = 'NA')
|
||||
if import_invoice.status == InvoiceStatus.UNPROCESSED:
|
||||
if import_invoice.status == InvoiceStatus.PENDING:
|
||||
errors.add_error(
|
||||
field=f"line[{entry.export_line}].import_invoice",
|
||||
message=f"La Factura de Importación: '{entry.import_invoice}' está Desactualizada.",
|
||||
|
||||
@@ -23,6 +23,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from .register_import_discharge import register_import_discharge
|
||||
from .register_discharge_series import register_discharge_series
|
||||
from .register_discharge_ledger import register_discharge_ledger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .discharge_types import DownloadEntry
|
||||
@@ -274,6 +275,9 @@ def finalize_invoice_with_discharge(
|
||||
generate_definitive_import(db, invoice, errors)
|
||||
|
||||
if to_discharge:
|
||||
# Write BalanceMovement (CONSUMPTION) + DischargeHeader + DischargeDetail
|
||||
register_discharge_ledger(db, invoice, to_discharge)
|
||||
# Update quantity_returned / value_returned on the import lines
|
||||
register_import_discharge(db, invoice, to_discharge)
|
||||
register_discharge_series(db, invoice, to_discharge)
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
register_discharge_ledger
|
||||
=========================
|
||||
Creates the full Annex-24 discharge record for one export invoice:
|
||||
|
||||
1. ONE DischargeHeader (one per export event)
|
||||
2. N BalanceMovement rows (type=CONSUMPTION, one per lot consumed)
|
||||
3. N DischargeDetail rows (one per export-line × import-lot pair),
|
||||
each referencing its BalanceMovement (design rule 3)
|
||||
|
||||
Design rules from a24.balance_movement (preserved here):
|
||||
1. NEVER update existing balance_movement rows — only INSERT.
|
||||
2. Balance = SUM of movements.
|
||||
3. Every DischargeDetail.movement_id MUST reference a BalanceMovement row.
|
||||
4. order_peps = movement.id (set after flush, globally monotonic).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a24.balance_movements.models import BalanceMovement, MovementType
|
||||
from api.v1.modules.a24.discharges.models import (
|
||||
DischargeDetail,
|
||||
DischargeHeader,
|
||||
DischargeStatus,
|
||||
DischargeType,
|
||||
)
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader as A76InvoiceHeader
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from .discharge_types import DownloadEntry, AvailableLot
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _discharge_type_for_invoice(invoice: InvoiceHeader) -> DischargeType:
|
||||
mapping = {
|
||||
"AFIJO": DischargeType.TEMPORARY,
|
||||
"DONAC": DischargeType.TEMPORARY,
|
||||
"SCRAP": DischargeType.WASTE_SCRAP,
|
||||
"REEXP": DischargeType.DEFINITIVE,
|
||||
"VEMEX": DischargeType.DEFINITIVE,
|
||||
}
|
||||
return mapping.get(invoice.invoice_type or "", DischargeType.TEMPORARY)
|
||||
|
||||
|
||||
def _export_date(invoice: InvoiceHeader) -> datetime.date:
|
||||
d = invoice.invoice_date
|
||||
return d.date() if hasattr(d, "date") else d
|
||||
|
||||
|
||||
def _proportional_value(
|
||||
consume: Decimal,
|
||||
lot_consumed_total: Decimal,
|
||||
lot_value: Optional[Decimal],
|
||||
) -> Optional[Decimal]:
|
||||
"""Returns the proportional value for *consume* units out of *lot_consumed_total*."""
|
||||
if not lot_value or lot_consumed_total <= 0:
|
||||
return None
|
||||
return (consume / lot_consumed_total) * lot_value
|
||||
|
||||
|
||||
def _proportional_qty(consume: Decimal, base_qty: Optional[Decimal], base_total: Optional[Decimal]) -> Optional[Decimal]:
|
||||
"""
|
||||
Proratea un valor (peso/valor) en proporción a lo consumido.
|
||||
- consume: cantidad consumida del lote
|
||||
- base_qty: valor total del lote (ej. peso neto total del lote)
|
||||
- base_total: cantidad total del lote (ej. quantity del lote)
|
||||
"""
|
||||
if base_qty is None:
|
||||
return None
|
||||
if base_total is None or base_total <= 0:
|
||||
return None
|
||||
return (consume / base_total) * base_qty
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def register_discharge_ledger(
|
||||
db: Session,
|
||||
export_invoice: InvoiceHeader,
|
||||
to_discharge: List[DownloadEntry],
|
||||
) -> Optional[DischargeHeader]:
|
||||
"""
|
||||
Persists the complete Annex-24 discharge record for *export_invoice*.
|
||||
|
||||
Expects that compare_balances() has already run and populated
|
||||
``lot.consumed_qty`` for every lot that was drawn from.
|
||||
|
||||
Returns the created DischargeHeader, or None if nothing was discharged.
|
||||
"""
|
||||
# Only process entries that actually consumed something
|
||||
active = [e for e in to_discharge if e.quantity_used > Decimal(0)]
|
||||
if not active:
|
||||
return None
|
||||
|
||||
op_date = _export_date(export_invoice)
|
||||
discharge_type = _discharge_type_for_invoice(export_invoice)
|
||||
|
||||
# Caches to avoid N+1 queries in loops
|
||||
export_line_cache: dict[int, LineItem] = {}
|
||||
import_line_cache: dict[int, LineItem] = {}
|
||||
import_invoice_number_cache: dict[int, str] = {}
|
||||
|
||||
# ── 1. DischargeHeader ────────────────────────────────────────────────
|
||||
header = DischargeHeader(
|
||||
tenant_id=export_invoice.tenant_id,
|
||||
company_id=export_invoice.company_id,
|
||||
source_invoice_id=export_invoice.id,
|
||||
discharge_type=discharge_type,
|
||||
status=DischargeStatus.APPLIED,
|
||||
discharge_date=op_date,
|
||||
)
|
||||
db.add(header)
|
||||
db.flush() # get header.id
|
||||
|
||||
total_movements = 0
|
||||
|
||||
for entry in active:
|
||||
export_line_id: Optional[int] = entry.line_item_id
|
||||
|
||||
export_line_obj: Optional[LineItem] = None
|
||||
if export_line_id:
|
||||
export_line_obj = export_line_cache.get(export_line_id)
|
||||
if export_line_obj is None:
|
||||
export_line_obj = db.get(LineItem, export_line_id)
|
||||
if export_line_obj is not None:
|
||||
export_line_cache[export_line_id] = export_line_obj
|
||||
|
||||
# Only iterate lots that were actually consumed
|
||||
consumed_lots: List[AvailableLot] = [
|
||||
lot for lot in entry.available_lots if lot.consumed_qty > Decimal(0)
|
||||
]
|
||||
|
||||
for lot in consumed_lots:
|
||||
consume = lot.consumed_qty
|
||||
|
||||
# Load import-line object for denormalized customs/weights fields
|
||||
import_line_obj = import_line_cache.get(lot.import_item_line_id)
|
||||
if import_line_obj is None:
|
||||
import_line_obj = db.get(LineItem, lot.import_item_line_id)
|
||||
if import_line_obj is not None:
|
||||
import_line_cache[lot.import_item_line_id] = import_line_obj
|
||||
|
||||
# Import invoice number (for origin_import_invoice in DischargeDetail)
|
||||
origin_import_invoice: Optional[str] = None
|
||||
if lot.import_invoice_id:
|
||||
origin_import_invoice = import_invoice_number_cache.get(lot.import_invoice_id)
|
||||
if origin_import_invoice is None:
|
||||
inv = db.get(A76InvoiceHeader, lot.import_invoice_id)
|
||||
origin_import_invoice = inv.invoice_number if inv else None
|
||||
if origin_import_invoice:
|
||||
import_invoice_number_cache[lot.import_invoice_id] = origin_import_invoice
|
||||
|
||||
# ── 2. BalanceMovement (CONSUMPTION) ──────────────────────────
|
||||
# Proportional value: consume / lot_consumed_total × lot_value
|
||||
# lot_consumed_total == consume for single-lot entries (most cases)
|
||||
value_me = _proportional_value(consume, consume, lot.value_me)
|
||||
value_mn = _proportional_value(consume, consume, lot.value_mn)
|
||||
|
||||
movement = BalanceMovement(
|
||||
tenant_id=export_invoice.tenant_id,
|
||||
company_id=export_invoice.company_id,
|
||||
import_invoice_id=lot.import_invoice_id,
|
||||
import_item_line_id=lot.import_item_line_id,
|
||||
part_number_id=lot.part_number_id,
|
||||
movement_type=MovementType.CONSUMPTION,
|
||||
quantity=consume,
|
||||
value_me=value_me,
|
||||
value_mn=value_mn,
|
||||
source_invoice_id=export_invoice.id,
|
||||
source_item_line_id=export_line_id,
|
||||
order_peps=0, # placeholder — set after flush (rule 4)
|
||||
operation_date=op_date,
|
||||
notes=(
|
||||
f"Descarga por factura de exportación "
|
||||
f"{export_invoice.invoice_number}"
|
||||
),
|
||||
)
|
||||
db.add(movement)
|
||||
db.flush() # get movement.id
|
||||
movement.order_peps = movement.id # rule 4: monotonic
|
||||
|
||||
# ── 3. DischargeDetail ─────────────────────────────────────────
|
||||
# Denormalized fields expected by reports:
|
||||
imp_cust = import_line_obj.customs if import_line_obj else None
|
||||
imp_qty = import_line_obj.quantity if import_line_obj else None
|
||||
imp_total_qty = imp_qty.quantity if imp_qty else None
|
||||
|
||||
net_weight = _proportional_qty(consume, imp_qty.net_weight if imp_qty else None, imp_total_qty)
|
||||
gross_weight = _proportional_qty(consume, imp_qty.gross_weight if imp_qty else None, imp_total_qty)
|
||||
|
||||
detail = DischargeDetail(
|
||||
tenant_id=export_invoice.tenant_id,
|
||||
company_id=export_invoice.company_id,
|
||||
discharge_header_id=header.id,
|
||||
export_item_line_id=export_line_id,
|
||||
import_item_line_id=lot.import_item_line_id,
|
||||
movement_id=movement.id,
|
||||
quantity_discharged=consume,
|
||||
unit_of_measure=entry.unit_of_measure or None,
|
||||
value_me=value_me,
|
||||
value_mn=value_mn,
|
||||
net_weight=net_weight,
|
||||
gross_weight=gross_weight,
|
||||
tariff_fraction=imp_cust.fraction if imp_cust else None,
|
||||
fraction_type=imp_cust.fraction_type if imp_cust else None,
|
||||
ad_valorem=imp_cust.advalorem if imp_cust else None,
|
||||
country_of_origin=imp_cust.origin_country if imp_cust else None,
|
||||
sector=imp_cust.sector if imp_cust else None,
|
||||
procedence=entry.origin_procedure or None,
|
||||
part_number=entry.part_number or None,
|
||||
export_part_number=(
|
||||
export_line_obj.part_info.part_number
|
||||
if export_line_obj and export_line_obj.part_info and export_line_obj.part_info.part_number
|
||||
else None
|
||||
),
|
||||
origin_import_invoice=origin_import_invoice,
|
||||
)
|
||||
db.add(detail)
|
||||
total_movements += 1
|
||||
|
||||
logger.info(
|
||||
"register_discharge_ledger: invoice=%s header_id=%s details=%d",
|
||||
export_invoice.invoice_number,
|
||||
header.id,
|
||||
total_movements,
|
||||
)
|
||||
return header
|
||||
@@ -24,7 +24,10 @@ if TYPE_CHECKING:
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
||||
|
||||
# Imported at runtime so SQLAlchemy's mapper registry can resolve the class name
|
||||
# used in the relationship string below.
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
||||
|
||||
# ============================================================================
|
||||
# CORE ENTITIES
|
||||
|
||||
@@ -3,7 +3,8 @@ API Endpoints for Items management
|
||||
Handles CRUD operations for Item with one-to-many relationships to LineItems
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -191,6 +192,39 @@ async def get_items_by_invoice(
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
@router.get("/invoice/{invoice_id}/items-with-balance", response_model=List[dict])
|
||||
async def get_items_with_balance(
|
||||
invoice_id: int = Path(..., description="Import Invoice ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
as_of_date: Optional[datetime.date] = Query(
|
||||
None,
|
||||
description=(
|
||||
"Cut-off date for balance calculation. Only consumptions on or "
|
||||
"before this date are subtracted (CALCULA_SALDO_FECHA_EXPO logic)."
|
||||
),
|
||||
),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Returns every line of the given import invoice with its available balance
|
||||
from the a24.balance_movement ledger.
|
||||
|
||||
Each item in the response includes:
|
||||
- id, line_number, part_number, class_code, unit_of_measure_code
|
||||
- quantity : original imported quantity
|
||||
- available_balance : net balance still available for export discharge
|
||||
- has_balance : true when available_balance > 0
|
||||
|
||||
Use ``as_of_date`` to restrict consumption movements to a specific date
|
||||
(pass the export invoice date so that future discharges are not counted).
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = ItemService()
|
||||
return service.get_lines_with_balance(db, invoice_id, tenant_id, company_id, as_of_date)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# STATISTICS & UTILITIES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ After refactoring: LineItem is the main entity, representing a single line item
|
||||
There is no intermediate Item entity anymore. Each LineItem belongs directly to an InvoiceHeader.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List, Tuple
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy import and_, case, func, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
@@ -41,6 +43,7 @@ from .series.models import Serie
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
||||
from api.v1.modules.a24.balance_movements.models import BalanceMovement, NEGATIVE_MOVEMENTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -372,10 +375,11 @@ class ItemService:
|
||||
errors.raise_if_errors("Error al crear el item - invoice_id es requerido")
|
||||
|
||||
invoice = invoice_exists_by_id(
|
||||
db, item_data.invoice_id, tenant_id, company_id, errors
|
||||
db, item_data.invoice_id, tenant_id, company_id, None
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
errors.add_error("invoice_id", "La factura no existe", code="NOT_FOUND", value=str(item_data.invoice_id))
|
||||
errors.raise_if_errors("Error al encontra la factura para el item")
|
||||
if not invoice_processed(db, item_data.invoice_id, tenant_id, company_id, errors):
|
||||
errors.raise_if_errors("Error al crear el item - la factura ya fue actualizada, no se pueden agregar items")
|
||||
@@ -511,9 +515,10 @@ class ItemService:
|
||||
errors = ErrorCollector()
|
||||
|
||||
invoice = invoice_exists_by_id(
|
||||
db, item_data.invoice_id, tenant_id, company_id, errors
|
||||
db, item_data.invoice_id, tenant_id, company_id, None
|
||||
)
|
||||
if not invoice:
|
||||
errors.add_error("invoice_id", "La factura no existe", code="NOT_FOUND", value=str(item_data.invoice_id))
|
||||
errors.raise_if_errors("Error al encontra la factura para el item")
|
||||
|
||||
# Lock invoice
|
||||
@@ -684,3 +689,125 @@ class ItemService:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting item: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error deleting item")
|
||||
|
||||
@staticmethod
|
||||
def get_lines_with_balance(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
as_of_date: Optional[datetime.date] = None,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Returns every line of an import invoice together with its current
|
||||
available balance calculated from the a24.balance_movement ledger.
|
||||
|
||||
Lines with balance <= 0 are included but marked as unavailable so
|
||||
the frontend can grey them out / disable them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
as_of_date : optional cut-off date. Only negative movements
|
||||
(consumptions, etc.) on or before this date are counted,
|
||||
mirroring the CALCULA_SALDO_FECHA_EXPO Clarion logic.
|
||||
If None, all movements are counted (no date restriction).
|
||||
"""
|
||||
lines: List[LineItem] = (
|
||||
db.query(LineItem)
|
||||
.filter(
|
||||
LineItem.invoice_id == invoice_id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.options(
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.description),
|
||||
joinedload(LineItem.part_info),
|
||||
joinedload(LineItem.class_info),
|
||||
joinedload(LineItem.unit_of_measure_info),
|
||||
joinedload(LineItem.fa_data),
|
||||
joinedload(LineItem.invoice),
|
||||
)
|
||||
.order_by(LineItem.line_number)
|
||||
.all()
|
||||
)
|
||||
|
||||
result = []
|
||||
for line in lines:
|
||||
available_balance = ItemService._compute_balance(db, line.id, as_of_date)
|
||||
qty = line.quantity
|
||||
desc = line.description
|
||||
fa = line.fa_data
|
||||
inv = line.invoice
|
||||
|
||||
# Count subitems (lines that reference this line as parent via subitem_number)
|
||||
subitem_count = 0
|
||||
if fa and fa.contains_subitems:
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem as FaModel
|
||||
subitem_count = (
|
||||
db.query(func.count(LineItem.id))
|
||||
.join(FaModel, FaModel.id == LineItem.id)
|
||||
.filter(
|
||||
LineItem.invoice_id == invoice_id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
FaModel.is_subitem == True,
|
||||
FaModel.subitem_number == line.line_number,
|
||||
)
|
||||
.scalar() or 0
|
||||
)
|
||||
|
||||
result.append({
|
||||
"id": line.id,
|
||||
"line_number": line.line_number,
|
||||
# Invoice info
|
||||
"invoice_number": inv.invoice_number if inv else None,
|
||||
"invoice_date": inv.invoice_date.isoformat() if inv and inv.invoice_date else None,
|
||||
"invoice_status": inv.status if inv and inv.status else None,
|
||||
# Part / class
|
||||
"part_number": line.part_info.part_number if line.part_info else None,
|
||||
"class_code": line.class_info.class_code if line.class_info else None,
|
||||
"description_spanish": desc.description_spanish if desc else None,
|
||||
"unit_of_measure_code": line.unit_of_measure_info.code if line.unit_of_measure_info else None,
|
||||
# Quantities
|
||||
"quantity": float(qty.quantity) if qty and qty.quantity is not None else None,
|
||||
"quantity_returned_temp": float(qty.quantity_returned_temp) if qty and qty.quantity_returned_temp is not None else None,
|
||||
"quantity_returned": float(qty.quantity_returned) if qty and qty.quantity_returned is not None else None,
|
||||
# Balance
|
||||
"available_balance": float(available_balance),
|
||||
"has_balance": available_balance > Decimal(0),
|
||||
# FA / subitem info
|
||||
"is_subitem": fa.is_subitem if fa else None,
|
||||
"contains_subitems": fa.contains_subitems if fa else None,
|
||||
"subitem_count": subitem_count,
|
||||
})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _compute_balance(
|
||||
db: Session,
|
||||
item_line_id: int,
|
||||
as_of_date: Optional[datetime.date],
|
||||
) -> Decimal:
|
||||
"""Net available balance for one import line from the ledger."""
|
||||
sign_expr = case(
|
||||
(BalanceMovement.movement_type.in_(NEGATIVE_MOVEMENTS), Decimal(-1)),
|
||||
else_=Decimal(1),
|
||||
)
|
||||
if as_of_date is not None:
|
||||
date_filter = case(
|
||||
(
|
||||
BalanceMovement.movement_type.in_(NEGATIVE_MOVEMENTS),
|
||||
BalanceMovement.operation_date <= as_of_date,
|
||||
),
|
||||
else_=True,
|
||||
)
|
||||
else:
|
||||
date_filter = True # type: ignore[assignment]
|
||||
|
||||
result = db.execute(
|
||||
select(func.sum(sign_expr * BalanceMovement.quantity)).where(
|
||||
BalanceMovement.import_item_line_id == item_line_id,
|
||||
date_filter,
|
||||
)
|
||||
).scalar()
|
||||
return Decimal(str(result or 0))
|
||||
|
||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Literal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from api.v1.modules.a24.inv.inv_aphis.dto import InvPartAphisGeneralDTO
|
||||
|
||||
# --- SUB-DTO: DATOS ADUANALES (FaData) ---
|
||||
class FaDataDTO(BaseModel):
|
||||
@@ -122,6 +123,16 @@ class InvDataDTO(BaseModel):
|
||||
sector: Optional[str] = None
|
||||
origin_country: Optional[str] = None
|
||||
fraction_type: Optional[str] = None
|
||||
|
||||
# Extensiones
|
||||
agency_code_definition: Optional[str] = None
|
||||
carta_porte: Optional[str] = None
|
||||
client_part_names: Optional[List[dict]] = None
|
||||
part_identifiers: Optional[List[dict]] = None
|
||||
substitute_parts: Optional[List[dict]] = None
|
||||
aphis_data: Optional[dict] = None
|
||||
aphis_records: Optional[List[InvPartAphisGeneralDTO]] = None
|
||||
|
||||
non_discharge_clients: Optional[List[dict]] = None
|
||||
|
||||
# Lista de materiales (BOM)
|
||||
|
||||
@@ -43,7 +43,8 @@ class PartService:
|
||||
"scrap_export_fraction", "scrap_us_fraction", "equivalent_uom_2", "conversion_factor_2",
|
||||
"has_auxiliary", "auxiliary_uom", "auxiliary_conversion", "auxiliary_unit_cost",
|
||||
"mex_packing", "sales_order", "use_rule_8", "sector", "origin_country", "fraction_type",
|
||||
"non_discharge_clients"
|
||||
"non_discharge_clients", "agency_code_definition", "carta_porte",
|
||||
"client_part_names", "part_identifiers", "substitute_parts", "aphis_data"
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@@ -134,8 +135,10 @@ class PartService:
|
||||
|
||||
# Extraer datos de países si existen en inv_data
|
||||
countries_data = None
|
||||
aphis_records_data = None
|
||||
if inv_dict:
|
||||
countries_data = inv_dict.pop('countries', None)
|
||||
aphis_records_data = inv_dict.pop('aphis_records', None)
|
||||
|
||||
# Inyectar IDs de contexto (Seguridad Multi-tenant)
|
||||
data['company_id'] = company_id
|
||||
@@ -213,6 +216,54 @@ class PartService:
|
||||
)
|
||||
db_part.inv_countries.append(country_obj)
|
||||
|
||||
# 7. Crear registros Aphis
|
||||
if aphis_records_data:
|
||||
from api.v1.modules.a24.inv.inv_aphis.models import (
|
||||
InvPartAphisGeneral, InvPartAphisCharacteristic, InvPartAphisStypePitems,
|
||||
InvPartAphisLpcos, InvPartAphisEntities, InvPartAphisContainers, InvPartAphisRouting
|
||||
)
|
||||
for a_data in aphis_records_data:
|
||||
a_data.pop('id', None)
|
||||
char_data = a_data.pop('characteristics', [])
|
||||
stype_data = a_data.pop('stype_pitems', [])
|
||||
lpco_data = a_data.pop('lpcos', [])
|
||||
entity_data = a_data.pop('entities', [])
|
||||
container_data = a_data.pop('containers', [])
|
||||
routing_data = a_data.pop('routing', [])
|
||||
|
||||
aphis_obj = InvPartAphisGeneral(
|
||||
**a_data,
|
||||
inv_part_id=db_part.id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
|
||||
for c_data in char_data:
|
||||
c_data.pop('id', None); c_data.pop('aphis_general_id', None)
|
||||
aphis_obj.characteristics.append(InvPartAphisCharacteristic(**c_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
for s_data in stype_data:
|
||||
s_data.pop('id', None); s_data.pop('aphis_general_id', None)
|
||||
aphis_obj.stype_pitems.append(InvPartAphisStypePitems(**s_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
for l_data in lpco_data:
|
||||
l_data.pop('id', None); l_data.pop('aphis_general_id', None)
|
||||
aphis_obj.lpcos.append(InvPartAphisLpcos(**l_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
for e_data in entity_data:
|
||||
e_data.pop('id', None); e_data.pop('aphis_general_id', None)
|
||||
aphis_obj.entities.append(InvPartAphisEntities(**e_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
for con_data in container_data:
|
||||
con_data.pop('id', None); con_data.pop('aphis_general_id', None)
|
||||
aphis_obj.containers.append(InvPartAphisContainers(**con_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
for r_data in routing_data:
|
||||
r_data.pop('id', None); r_data.pop('aphis_general_id', None)
|
||||
aphis_obj.routing.append(InvPartAphisRouting(**r_data, tenant_id=tenant_id, company_id=company_id))
|
||||
|
||||
db.add(aphis_obj)
|
||||
|
||||
try:
|
||||
db.commit() # db.add ya se hizo en el flush anterior
|
||||
db.refresh(db_part)
|
||||
@@ -263,8 +314,10 @@ class PartService:
|
||||
bom_items_data = inv_dict.pop('bom_items', None)
|
||||
|
||||
countries_data = None
|
||||
aphis_records_data = None
|
||||
if inv_dict:
|
||||
countries_data = inv_dict.pop('countries', None)
|
||||
aphis_records_data = inv_dict.pop('aphis_records', None)
|
||||
|
||||
# Actualizar campos directos
|
||||
for key, value in data.items():
|
||||
@@ -340,6 +393,81 @@ class PartService:
|
||||
)
|
||||
db_part.inv_countries.append(country_obj)
|
||||
|
||||
# Actualizar registros Aphis (Esquema relacional)
|
||||
if aphis_records_data is not None:
|
||||
from api.v1.modules.a24.inv.inv_aphis.models import (
|
||||
InvPartAphisGeneral,
|
||||
InvPartAphisCharacteristic,
|
||||
InvPartAphisStypePitems,
|
||||
InvPartAphisLpcos,
|
||||
InvPartAphisEntities,
|
||||
InvPartAphisContainers,
|
||||
InvPartAphisRouting
|
||||
)
|
||||
if db_part.inv_data:
|
||||
# Reemplazo total de registros Aphis para esta parte
|
||||
db_part.inv_data.aphis_records = []
|
||||
for a_data in aphis_records_data:
|
||||
# Limpiar IDs y sub-datos para recreación
|
||||
a_data.pop('id', None)
|
||||
|
||||
char_data = a_data.pop('characteristics', [])
|
||||
stype_data = a_data.pop('stype_pitems', [])
|
||||
lpco_data = a_data.pop('lpcos', [])
|
||||
entity_data = a_data.pop('entities', [])
|
||||
container_data = a_data.pop('containers', [])
|
||||
routing_data = a_data.pop('routing', [])
|
||||
|
||||
aphis_obj = InvPartAphisGeneral(
|
||||
**a_data,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
|
||||
for c_data in char_data:
|
||||
c_data.pop('id', None)
|
||||
c_data.pop('aphis_general_id', None)
|
||||
aphis_obj.characteristics.append(
|
||||
InvPartAphisCharacteristic(**c_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
for s_data in stype_data:
|
||||
s_data.pop('id', None)
|
||||
s_data.pop('aphis_general_id', None)
|
||||
aphis_obj.stype_pitems.append(
|
||||
InvPartAphisStypePitems(**s_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
for l_data in lpco_data:
|
||||
l_data.pop('id', None)
|
||||
l_data.pop('aphis_general_id', None)
|
||||
aphis_obj.lpcos.append(
|
||||
InvPartAphisLpcos(**l_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
for e_data in entity_data:
|
||||
e_data.pop('id', None)
|
||||
e_data.pop('aphis_general_id', None)
|
||||
aphis_obj.entities.append(
|
||||
InvPartAphisEntities(**e_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
for con_data in container_data:
|
||||
con_data.pop('id', None)
|
||||
con_data.pop('aphis_general_id', None)
|
||||
aphis_obj.containers.append(
|
||||
InvPartAphisContainers(**con_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
for r_data in routing_data:
|
||||
r_data.pop('id', None)
|
||||
r_data.pop('aphis_general_id', None)
|
||||
aphis_obj.routing.append(
|
||||
InvPartAphisRouting(**r_data, tenant_id=tenant_id, company_id=company_id)
|
||||
)
|
||||
|
||||
db_part.inv_data.aphis_records.append(aphis_obj)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_part)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class AgencyTariffCodeDTO(BaseModel):
|
||||
id: int
|
||||
tariff_flag_code: str = Field(..., max_length=10)
|
||||
agency_code: str = Field(..., max_length=10)
|
||||
requirement_level: str = Field(..., min_length=1, max_length=1)
|
||||
program_code: str = Field(..., max_length=10)
|
||||
definition: str = Field(..., max_length=500)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from core.database import Base
|
||||
from sqlalchemy import PrimaryKeyConstraint, String, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class AgencyTariffCode(Base):
|
||||
__tablename__ = "agency_tariff_codes"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="agency_tariff_codes_pkey"),
|
||||
{"schema": "public", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
tariff_flag_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
agency_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
requirement_level: Mapped[str] = mapped_column(String(1), nullable=False) # M o R
|
||||
program_code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
definition: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AgencyTariffCode(id={self.id}, flag={self.tariff_flag_code}, agency={self.agency_code})>"
|
||||
@@ -0,0 +1,104 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import AgencyTariffCodeDTO
|
||||
from .models import AgencyTariffCode
|
||||
|
||||
router = APIRouter(prefix="/agency-tariff-codes")
|
||||
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def list_agency_tariff_codes(
|
||||
page: int = Query(1, ge=1, description="Número de página"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
|
||||
agency: str = Query(None, description="Filtrar por código de agencia"),
|
||||
q: str = Query(None, description="Búsqueda general"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(AgencyTariffCode)
|
||||
|
||||
if agency:
|
||||
query = query.filter(AgencyTariffCode.agency_code == agency)
|
||||
|
||||
if q:
|
||||
search_term = f"%{q}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
AgencyTariffCode.tariff_flag_code.ilike(search_term),
|
||||
AgencyTariffCode.agency_code.ilike(search_term),
|
||||
AgencyTariffCode.program_code.ilike(search_term),
|
||||
AgencyTariffCode.definition.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
total = query.count()
|
||||
return {
|
||||
"items": [AgencyTariffCodeDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=AgencyTariffCodeDTO)
|
||||
async def get_agency_tariff_code(
|
||||
id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
obj = db.query(AgencyTariffCode).filter(AgencyTariffCode.id == id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("/", response_model=AgencyTariffCodeDTO, status_code=201)
|
||||
async def create_agency_tariff_code(
|
||||
data: AgencyTariffCodeDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = AgencyTariffCode(**data.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.put("/{id}", response_model=AgencyTariffCodeDTO)
|
||||
async def update_agency_tariff_code(
|
||||
id: int,
|
||||
data: AgencyTariffCodeDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(AgencyTariffCode).filter(AgencyTariffCode.id == id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
for field, value in data.dict().items():
|
||||
setattr(obj, field, value)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
async def delete_agency_tariff_code(
|
||||
id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(AgencyTariffCode).filter(AgencyTariffCode.id == id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return None
|
||||
@@ -0,0 +1,118 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import AgencyTariffCode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AGENCY_TARIFF_CODES_DATA = [
|
||||
{"id": 1, "tariff_flag_code": "EP1", "agency_code": "EPA", "requirement_level": "M", "program_code": "ODS", "definition": "Ozone Depleting Substances specific data may be required"},
|
||||
{"id": 2, "tariff_flag_code": "EP2", "agency_code": "EPA", "requirement_level": "R", "program_code": "ODS", "definition": "Ozone Depleting Substances specific data is required"},
|
||||
{"id": 3, "tariff_flag_code": "EP3", "agency_code": "EPA", "requirement_level": "M", "program_code": "VNE", "definition": "Vehicle and Engines specific data may be required"},
|
||||
{"id": 4, "tariff_flag_code": "EP4", "agency_code": "EPA", "requirement_level": "R", "program_code": "VNE", "definition": "Vehicle and Engines specific data is required"},
|
||||
{"id": 5, "tariff_flag_code": "EP5", "agency_code": "EPA", "requirement_level": "M", "program_code": "PS1", "definition": "Pesticides specific data may be required"},
|
||||
{"id": 6, "tariff_flag_code": "EP5", "agency_code": "EPA", "requirement_level": "M", "program_code": "PS2", "definition": "Pesticides specific data may be required"},
|
||||
{"id": 7, "tariff_flag_code": "EP5", "agency_code": "EPA", "requirement_level": "M", "program_code": "PS3", "definition": "Pesticides specific data may be required"},
|
||||
{"id": 8, "tariff_flag_code": "EP6", "agency_code": "EPA", "requirement_level": "R", "program_code": "PS1", "definition": "Pesticides specific data is required"},
|
||||
{"id": 9, "tariff_flag_code": "EP6", "agency_code": "EPA", "requirement_level": "R", "program_code": "PS2", "definition": "Pesticides specific data is required"},
|
||||
{"id": 10, "tariff_flag_code": "EP6", "agency_code": "EPA", "requirement_level": "R", "program_code": "PS3", "definition": "Pesticides specific data is required"},
|
||||
{"id": 11, "tariff_flag_code": "EP7", "agency_code": "EPA", "requirement_level": "M", "program_code": "TS1", "definition": "Toxic Substances Control Act specific data may be required"},
|
||||
{"id": 12, "tariff_flag_code": "EP7", "agency_code": "EPA", "requirement_level": "M", "program_code": "TS2", "definition": "Toxic Substances Control Act specific data may be required"},
|
||||
{"id": 13, "tariff_flag_code": "EP8", "agency_code": "EPA", "requirement_level": "R", "program_code": "TS1", "definition": "Toxic Substances Control Act specific data is required"},
|
||||
{"id": 14, "tariff_flag_code": "EP8", "agency_code": "EPA", "requirement_level": "R", "program_code": "TS2", "definition": "Toxic Substances Control Act specific data is required"},
|
||||
{"id": 15, "tariff_flag_code": "FS3", "agency_code": "FSI", "requirement_level": "M", "program_code": "FSI", "definition": "FSIS data may be required. Applicable to all FSIS programs"},
|
||||
{"id": 16, "tariff_flag_code": "FS4", "agency_code": "FSI", "requirement_level": "R", "program_code": "FSI", "definition": "FSIS data is required. Applicable to all FSIS programs"},
|
||||
{"id": 17, "tariff_flag_code": "NM1", "agency_code": "NMF", "requirement_level": "M", "program_code": "370", "definition": "370 specific data may be required"},
|
||||
{"id": 18, "tariff_flag_code": "NM2", "agency_code": "NMF", "requirement_level": "R", "program_code": "370", "definition": "370 specific data is required"},
|
||||
{"id": 19, "tariff_flag_code": "NM3", "agency_code": "NMF", "requirement_level": "M", "program_code": "AMR", "definition": "Antarctic Marine Living Resources specific data may be required"},
|
||||
{"id": 20, "tariff_flag_code": "NM4", "agency_code": "NMF", "requirement_level": "R", "program_code": "AMR", "definition": "Antarctic Marine Living Resources is required"},
|
||||
{"id": 21, "tariff_flag_code": "NM5", "agency_code": "NMF", "requirement_level": "M", "program_code": "HMS", "definition": "Highly Migratory Species specific data may be required"},
|
||||
{"id": 22, "tariff_flag_code": "NM6", "agency_code": "NMF", "requirement_level": "R", "program_code": "HMS", "definition": "Highly Migratory Species specific data is required"},
|
||||
{"id": 23, "tariff_flag_code": "DT1", "agency_code": "NHT", "requirement_level": "M", "program_code": "MV", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data may be required"},
|
||||
{"id": 24, "tariff_flag_code": "DT1", "agency_code": "NHT", "requirement_level": "M", "program_code": "REI", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data may be required"},
|
||||
{"id": 25, "tariff_flag_code": "DT1", "agency_code": "NHT", "requirement_level": "M", "program_code": "TPE", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data may be required"},
|
||||
{"id": 26, "tariff_flag_code": "DT1", "agency_code": "NHT", "requirement_level": "M", "program_code": "OEI", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data may be required"},
|
||||
{"id": 27, "tariff_flag_code": "DT1", "agency_code": "NHT", "requirement_level": "M", "program_code": "OFF", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data may be required"},
|
||||
{"id": 28, "tariff_flag_code": "DT2", "agency_code": "NHT", "requirement_level": "R", "program_code": "MVS", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data is required"},
|
||||
{"id": 29, "tariff_flag_code": "DT2", "agency_code": "NHT", "requirement_level": "R", "program_code": "REI", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data is required"},
|
||||
{"id": 30, "tariff_flag_code": "DT2", "agency_code": "NHT", "requirement_level": "R", "program_code": "TPE", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data is required"},
|
||||
{"id": 31, "tariff_flag_code": "DT2", "agency_code": "NHT", "requirement_level": "R", "program_code": "OEI", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data is required"},
|
||||
{"id": 32, "tariff_flag_code": "DT2", "agency_code": "NHT", "requirement_level": "R", "program_code": "OFF", "definition": "DOT/National Highway Traffic Safety Administration HS-7 data is required"},
|
||||
{"id": 33, "tariff_flag_code": "AL1", "agency_code": "APH", "requirement_level": "M", "program_code": "APL", "definition": "Lacey Act specific data may be required"},
|
||||
{"id": 34, "tariff_flag_code": "AL2", "agency_code": "APH", "requirement_level": "R", "program_code": "APL", "definition": "Lacey Act specific data is required"},
|
||||
{"id": 35, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "BIO", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 36, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "COS", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 37, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "DEV", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 38, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "DRU", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 39, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "FOO", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 40, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "RAD", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 41, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "TOB", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 42, "tariff_flag_code": "FD1", "agency_code": "FDA", "requirement_level": "M", "program_code": "VME", "definition": "FDA data may be required 801(a)"},
|
||||
{"id": 43, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "BIO", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 44, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "COS", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 45, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "DEV", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 46, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "DRU", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 47, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "FOO", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 48, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "RAD", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 49, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "TOB", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 50, "tariff_flag_code": "FD2", "agency_code": "FDA", "requirement_level": "R", "program_code": "VME", "definition": "FDA data Required 801(a)"},
|
||||
{"id": 51, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "BIO", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 52, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "COS", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 53, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "DEV", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 54, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "DRU", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 55, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "FOO", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 56, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "RAD", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 57, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "TOB", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 58, "tariff_flag_code": "FD3", "agency_code": "FDA", "requirement_level": "M", "program_code": "VME", "definition": "FDA Prior Notice Data may be required 801(m)"},
|
||||
{"id": 59, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "BIO", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 60, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "COS", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 61, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "DEV", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 62, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "DRU", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 63, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "FOO", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 64, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "RAD", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 65, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "TOB", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 66, "tariff_flag_code": "FD4", "agency_code": "FDA", "requirement_level": "R", "program_code": "VME", "definition": "FDA Prior Notice Data is required 801(m)"},
|
||||
{"id": 67, "tariff_flag_code": "AM1", "agency_code": "AMS", "requirement_level": "M", "program_code": "EG", "definition": "USDA/Agricultural Marketing Service Data related to egg products may be required"},
|
||||
{"id": 68, "tariff_flag_code": "AM2", "agency_code": "AMS", "requirement_level": "R", "program_code": "EG", "definition": "USDA/Agricultural Marketing Service Data Related to shell eggs is required"},
|
||||
{"id": 69, "tariff_flag_code": "AM3", "agency_code": "AMS", "requirement_level": "M", "program_code": "MO", "definition": "USDA/Agricultural Marketing Service Data Related to marketing orders may be required"},
|
||||
{"id": 70, "tariff_flag_code": "AM4", "agency_code": "AMS", "requirement_level": "R", "program_code": "MO", "definition": "USDA/Agricultural Marketing Service Data Related to marketing orders is required"},
|
||||
{"id": 71, "tariff_flag_code": "AM6", "agency_code": "AMS", "requirement_level": "R", "program_code": "PN", "definition": "USDA/Agriculture Marketing Service Data related to peanuts is required"},
|
||||
{"id": 72, "tariff_flag_code": "TB1", "agency_code": "TTB", "requirement_level": "M", "program_code": "BER", "definition": "TTB data may be required. Applicable to all TTB programs."},
|
||||
{"id": 73, "tariff_flag_code": "TB1", "agency_code": "TTB", "requirement_level": "M", "program_code": "WIN", "definition": "TTB data may be required. Applicable to all TTB programs."},
|
||||
{"id": 74, "tariff_flag_code": "TB1", "agency_code": "TTB", "requirement_level": "M", "program_code": "DSP", "definition": "TTB data may be required. Applicable to all TTB programs."},
|
||||
{"id": 75, "tariff_flag_code": "TB1", "agency_code": "TTB", "requirement_level": "M", "program_code": "TOB", "definition": "TTB data may be required. Applicable to all TTB programs."},
|
||||
{"id": 76, "tariff_flag_code": "TB2", "agency_code": "TTB", "requirement_level": "R", "program_code": "BER", "definition": "TTB data is required. Applicable to all TTB programs."},
|
||||
{"id": 77, "tariff_flag_code": "TB2", "agency_code": "TTB", "requirement_level": "R", "program_code": "WIN", "definition": "TTB data is required. Applicable to all TTB programs."},
|
||||
{"id": 78, "tariff_flag_code": "TB2", "agency_code": "TTB", "requirement_level": "R", "program_code": "DSP", "definition": "TTB data is required. Applicable to all TTB programs."},
|
||||
{"id": 79, "tariff_flag_code": "TB2", "agency_code": "TTB", "requirement_level": "R", "program_code": "TOB", "definition": "TTB data is required. Applicable to all TTB programs."},
|
||||
{"id": 80, "tariff_flag_code": "AQ1", "agency_code": "APH", "requirement_level": "M", "program_code": "AAC", "definition": "APHIS data may be required"},
|
||||
{"id": 81, "tariff_flag_code": "AQ1", "agency_code": "APH", "requirement_level": "M", "program_code": "APQ", "definition": "APHIS data may be required"},
|
||||
{"id": 82, "tariff_flag_code": "AQ1", "agency_code": "APH", "requirement_level": "M", "program_code": "AVS", "definition": "APHIS data may be required"},
|
||||
{"id": 83, "tariff_flag_code": "AQ1", "agency_code": "APH", "requirement_level": "M", "program_code": "ABS", "definition": "APHIS data may be required"},
|
||||
{"id": 84, "tariff_flag_code": "AQ2", "agency_code": "APH", "requirement_level": "R", "program_code": "AAC", "definition": "APHIS data is required"},
|
||||
{"id": 85, "tariff_flag_code": "AQ2", "agency_code": "APH", "requirement_level": "R", "program_code": "APQ", "definition": "APHIS data is required"},
|
||||
{"id": 86, "tariff_flag_code": "AQ2", "agency_code": "APH", "requirement_level": "R", "program_code": "AVS", "definition": "APHIS data is required"},
|
||||
{"id": 87, "tariff_flag_code": "AQ2", "agency_code": "APH", "requirement_level": "R", "program_code": "ABS", "definition": "APHIS data is required"},
|
||||
{"id": 88, "tariff_flag_code": "OM1", "agency_code": "OMC", "requirement_level": "M", "program_code": "OMC", "definition": "Office of Marine Conservation data may be required"},
|
||||
{"id": 89, "tariff_flag_code": "OM2", "agency_code": "OMC", "requirement_level": "R", "program_code": "OMC", "definition": "Office of Marine Conservation data is required"},
|
||||
{"id": 90, "tariff_flag_code": "FW2", "agency_code": "FWS", "requirement_level": "R", "program_code": "FWS", "definition": "U.S. Fish and Wildlife Service data is required"},
|
||||
{"id": 91, "tariff_flag_code": "DE1", "agency_code": "DEA", "requirement_level": "M", "program_code": "DEA", "definition": "U.S. Drug Enforcement Administration data may be required"},
|
||||
]
|
||||
|
||||
def seed_agency_tariff_codes(db: Session):
|
||||
"""Seed Agency Tariff Codes catalog data"""
|
||||
logger.info("Seeding Agency Tariff Codes...")
|
||||
for item in AGENCY_TARIFF_CODES_DATA:
|
||||
db_item = db.query(AgencyTariffCode).filter(AgencyTariffCode.id == item["id"]).first()
|
||||
if not db_item:
|
||||
logger.info(f"Adding agency tariff code: {item['id']}")
|
||||
new_item = AgencyTariffCode(**item)
|
||||
db.add(new_item)
|
||||
else:
|
||||
# Update fields if changed
|
||||
db_item.tariff_flag_code = item["tariff_flag_code"]
|
||||
db_item.agency_code = item["agency_code"]
|
||||
db_item.requirement_level = item["requirement_level"]
|
||||
db_item.program_code = item["program_code"]
|
||||
db_item.definition = item["definition"]
|
||||
db.commit()
|
||||
logger.info("Agency Tariff Codes seeding completed.")
|
||||
28695
backend/api/v1/modules/public/reference_data/carta_porte/cartaPorte.csv
Normal file
28695
backend/api/v1/modules/public/reference_data/carta_porte/cartaPorte.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CartaPorteDTO(BaseModel):
|
||||
id: int
|
||||
code: str = Field(..., max_length=20)
|
||||
description: str = Field(..., max_length=2000)
|
||||
similar_words: Optional[str] = Field(None, max_length=2000)
|
||||
is_hazardous: int = 0
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,21 @@
|
||||
from core.database import Base
|
||||
from sqlalchemy import String, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class CartaPorte(Base):
|
||||
__tablename__ = "carta_porte"
|
||||
__table_args__ = (
|
||||
{"schema": "public", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
code: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
description: Mapped[str] = mapped_column(String(2000), nullable=False)
|
||||
similar_words: Mapped[str] = mapped_column(String(2000), nullable=True)
|
||||
is_hazardous: Mapped[int] = mapped_column(Integer, default=0)
|
||||
start_date: Mapped[str] = mapped_column(String(20), nullable=True)
|
||||
end_date: Mapped[str] = mapped_column(String(20), nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CartaPorte(code={self.code}, description={self.description[:30]}...)>"
|
||||
@@ -0,0 +1,57 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import CartaPorteDTO
|
||||
from .models import CartaPorte
|
||||
|
||||
router = APIRouter(prefix="/carta-porte")
|
||||
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def list_carta_porte(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=100),
|
||||
q: str = Query(None, description="Buscar por código o descripción"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(CartaPorte)
|
||||
|
||||
if q:
|
||||
search_filter = f"%{q}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
CartaPorte.code.ilike(search_filter),
|
||||
CartaPorte.description.ilike(search_filter),
|
||||
CartaPorte.similar_words.ilike(search_filter)
|
||||
)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
return {
|
||||
"items": [CartaPorteDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=CartaPorteDTO)
|
||||
async def get_carta_porte(
|
||||
id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
obj = db.query(CartaPorte).filter(CartaPorte.id == id).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
@@ -0,0 +1,48 @@
|
||||
import csv
|
||||
import os
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import CartaPorte
|
||||
|
||||
def seed_carta_porte(db: Session):
|
||||
csv_path = os.path.join(os.path.dirname(__file__), "cartaPorte.csv")
|
||||
if not os.path.exists(csv_path):
|
||||
print(f"File not found: {csv_path}")
|
||||
return
|
||||
|
||||
# Check if already seeded to avoid duplicates
|
||||
if db.query(CartaPorte).first():
|
||||
print("Carta Porte catalog already has data, skipping...")
|
||||
return
|
||||
|
||||
print("Seeding Carta Porte catalog (this might take a while)...")
|
||||
|
||||
with open(csv_path, mode='r', encoding='utf-8-sig') as f:
|
||||
# User provided comma-separated data
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
batch_size = 1000
|
||||
batch = []
|
||||
|
||||
for row in reader:
|
||||
# SYSID CODIGO DESCRIPCION PALABRASSIMILARES MATPELIGROSO FECHAINICIOVIGENCIA FECHAFINVIGENCIA
|
||||
obj = CartaPorte(
|
||||
id=int(row['SYSID']),
|
||||
code=row['CODIGO'],
|
||||
description=row['DESCRIPCION'],
|
||||
similar_words=row.get('PALABRASSIMILARES'),
|
||||
is_hazardous=int(row['MATPELIGROSO']) if row['MATPELIGROSO'].isdigit() else 0,
|
||||
start_date=row.get('FECHAINICIOVIGENCIA'),
|
||||
end_date=row.get('FECHAFINVIGENCIA')
|
||||
)
|
||||
batch.append(obj)
|
||||
|
||||
if len(batch) >= batch_size:
|
||||
db.bulk_save_objects(batch)
|
||||
db.commit()
|
||||
batch = []
|
||||
print(f"Inserted {batch_size} records...")
|
||||
|
||||
if batch:
|
||||
db.bulk_save_objects(batch)
|
||||
db.commit()
|
||||
print(f"Finished seeding with {len(batch)} remaining records.")
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class IdentifierDTO(BaseModel):
|
||||
key: str = Field(..., max_length=10)
|
||||
description: str = Field(..., max_length=2000)
|
||||
level: str = Field(..., max_length=1)
|
||||
complement: str = Field(..., max_length=5000)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,26 @@
|
||||
from core.database import Base
|
||||
from sqlalchemy import PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class IdentifierCatalog(Base):
|
||||
__tablename__ = "identifiers"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("key", name="identifiers_pkey"),
|
||||
{"schema": "public", "extend_existing": True},
|
||||
)
|
||||
|
||||
key: Mapped[str] = mapped_column(
|
||||
String(10), primary_key=True, nullable=False) # clave del identificador
|
||||
description: Mapped[str] = mapped_column(
|
||||
String(2000), nullable=False
|
||||
) # descripción
|
||||
level: Mapped[str] = mapped_column(
|
||||
String(1), nullable=False
|
||||
) # nivel (G, P, etc)
|
||||
complement: Mapped[str] = mapped_column(
|
||||
String(5000), nullable=False
|
||||
) # complemento / instrucciones
|
||||
|
||||
def __repr__(self):
|
||||
return f"<IdentifierCatalog(key={self.key}, level={self.level})>"
|
||||
@@ -0,0 +1,108 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import IdentifierDTO
|
||||
from .models import IdentifierCatalog
|
||||
|
||||
router = APIRouter(prefix="/identifiers")
|
||||
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def list_identifiers(
|
||||
page: int = Query(1, ge=1, description="Número de página"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
|
||||
level: str = Query(None, description="Filtrar por nivel (G o P)"),
|
||||
q: str = Query(None, description="Búsqueda general"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(IdentifierCatalog)
|
||||
|
||||
if level:
|
||||
query = query.filter(IdentifierCatalog.nivel == level)
|
||||
|
||||
if q:
|
||||
search_term = f"%{q}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
IdentifierCatalog.key.ilike(search_term),
|
||||
IdentifierCatalog.description.ilike(search_term),
|
||||
IdentifierCatalog.complement.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
total = query.count()
|
||||
return {
|
||||
"items": [IdentifierDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=IdentifierDTO)
|
||||
async def get_identifier(
|
||||
key: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("/", response_model=IdentifierDTO, status_code=201)
|
||||
async def create_identifier(
|
||||
data: IdentifierDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
# Check if already exists
|
||||
existing = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == data.key).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Identifier with this key already exists")
|
||||
|
||||
obj = IdentifierCatalog(**data.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.put("/{key}", response_model=IdentifierDTO)
|
||||
async def update_identifier(
|
||||
key: str,
|
||||
data: IdentifierDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
for field, value in data.dict().items():
|
||||
setattr(obj, field, value)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{key}", status_code=204)
|
||||
async def delete_identifier(
|
||||
key: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return None
|
||||
186
backend/api/v1/modules/public/reference_data/identifiers/seed.py
Normal file
186
backend/api/v1/modules/public/reference_data/identifiers/seed.py
Normal file
@@ -0,0 +1,186 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import IdentifierCatalog
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IDENTIFIERS_DATA = [
|
||||
{"key": "A3", "description": "REGULARIZACION DE MERCANCIAS (IMPORTACION DEFINITIVA).", "level": "G", "complement": "Identificar conforme a los supuestos de la clave de documento A3 del apendice 2."},
|
||||
{"key": "AC", "description": "ALMACEN GENERAL DE DEPOSITO CERTIFICADO.", "level": "G", "complement": "Identificar a un almacen general de deposito certificado."},
|
||||
{"key": "AE", "description": "EMPRESA DE COMERCIO EXTERIOR.", "level": "G", "complement": "Declarar la autorizacion de empresa de comercio exterior."},
|
||||
{"key": "AF", "description": "ACTIVO FIJO.", "level": "G", "complement": "Identificar el activo fijo, Unicamente cuando la clave de documento no sea exclusiva para dicha mercancia."},
|
||||
{"key": "AG", "description": "ALMACEN GENERAL DE DEPOSITO FISCAL.", "level": "G", "complement": "Identificar a un almacen general de deposito."},
|
||||
{"key": "AI", "description": "OPERACIONES DE COMERCIO EXTERIOR CON AMPARO.", "level": "G", "complement": "Declarar operaciones de comercio exterior que se realizan con amparo."},
|
||||
{"key": "AL", "description": "MERCANCIA ORIGINARIA IMPORTADA AL AMPARO DE ALADI.", "level": "P", "complement": "Declarar preferencia arancelaria de la ALADI."},
|
||||
{"key": "AP", "description": "APLICA PAGO VIRTUAL", "level": "G", "complement": "Declarar cuando en el pedimento se señalen exclusivamente las siguientes formas de pago: 5, 6, 8, 9, 13, 14, 16, 18, 21 y 22 de conformidad con el Apéndice 13."},
|
||||
{"key": "AR", "description": "CONSULTA ARANCELARIA.", "level": "P", "complement": "Declarar consulta sobre clasificacion arancelaria a la autoridad competente."},
|
||||
{"key": "AT", "description": "AVISO DE TRANSITO.", "level": "G", "complement": "Avisar sobre el transito interno a la exportacion."},
|
||||
{"key": "AV", "description": "AVISO ELECTRONICO DE IMPORTACION Y EXPORTACION.", "level": "G", "complement": "Indicar en los previos de consolidado el uso del aviso electronico de importacion y exportacion por cada remesa presentada ante el modulo de seleccion automatizado."},
|
||||
{"key": "B2", "description": "BIENES DEL ARTÍCULO 2 DE LA LEY DEL IEPS.", "level": "P", "complement": "Identificar las mercancías conforme al artículo 2, fracción I, incisos D) y H), de la Ley del IEPS."},
|
||||
{"key": "BB", "description": "EXPORTACION DEFINITIVA Y RETORNO VIRTUAL.", "level": "G", "complement": "Identificar la exportacion definitiva virtual de mercancia (productos terminados) que enajenen residentes en el pais a recinto fiscalizado para la elaboracion, transformacion o reparacion."},
|
||||
{"key": "BR", "description": "EXPORTACION TEMPORAL DE MERCANCIAS FUNGIBLES Y SU RETORNO.", "level": "G", "complement": "Identificar mercancias listadas en el Anexo 12."},
|
||||
{"key": "C5", "description": "DEPOSITO FISCAL PARA LA INDUSTRIA AUTOMOTRIZ.", "level": "G", "complement": "Identificar a la industria automotriz terminal autorizada."},
|
||||
{"key": "C9", "description": "CERTIFICADO DE USO FINAL.", "level": "P", "complement": "Indicar que la mercancia sujeta a cuota compensatoria cumple con la excepcion que establece el decreto publicado en el DOF el dia 2 de agosto de 1994, relativo a aceros planos recubiertos y placas en hoja."},
|
||||
{"key": "CC", "description": "CARTA DE CUPO.", "level": "G", "complement": "Identificar las mercancias que se almacenaran en deposito fiscal."},
|
||||
{"key": "CD", "description": "CERTIFICADO CON DISPENSA TEMPORAL.", "level": "P", "complement": "Declarar la informacion relativa a los certificados con dispensa de acuerdo al Tratado de Libre Comercio correspondiente."},
|
||||
{"key": "CE", "description": "CERTIFICADO DE ELEGIBILIDAD.", "level": "P", "complement": "Declarar el certificado de elegibilidad de mercancias no originarias importadas bajo TLC."},
|
||||
{"key": "CF", "description": "REGISTRO ANTE LA SECRETARIA DE ECONOMIA DE EMPRESAS UBICADAS EN LA FRANJA O REGION FRONTERIZA./PREFERENCIA ARANCELARIA PARA EMPRESAS UBICADAS EN LA FRANJA O REGION FRONTERIZA.", "level": "G", "complement": "Identificar a la empresa que cuente con registro ante la SE de conformidad con el Decreto por el que se establece el impuesto general de importacion para la region fronteriza y la franja fronteriza norte./Declarar tasas preferenciales conforme al Decreto por el que se establece el impuesto general de importacion para la region fronteriza y la franja fronteriza norte."},
|
||||
{"key": "CI", "description": "CERTIFICACION EN MATERIA DE IVA E IEPS.", "level": "G", "complement": "Identificar las operaciones de las empresas que hayan obtenido la certificacionn en materia de IVA e IEPS."},
|
||||
{"key": "CO", "description": "CONDONACION DE CREDITOS FISCALES.", "level": "G", "complement": "Condonacion emitida de conformidad con el Transitorio Tercero de la Ley de Ingresos de la Federacion para el ejercicio fiscal de 2013, publicada en el DOF el 17 de diciembre de 2012."},
|
||||
{"key": "CR", "description": "RECINTO FISCALIZADO.", "level": "G", "complement": "Identificar el recinto en el que se encuentre la mercancia en deposito ante la aduana o para su introduccion al mismo."},
|
||||
{"key": "CS", "description": "COPIA SIMPLE.", "level": "G", "complement": "Declarar uso copia simple en el despacho de las mercancias al amparo de la R.G. 3.1.18., segundo parrafo, fracc. II."},
|
||||
{"key": "DA", "description": "DESPACHO ANTICIPADO", "level": "G", "complement": "Indicar que se trata de una operación de comercio exterior que se sujeta a despacho anticipado."},
|
||||
{"key": "DC", "description": "CLASIFICACION DEL CUPO.", "level": "P", "complement": "Identificar el tipo de cupo utilizado."},
|
||||
{"key": "DD", "description": "DESPACHO A DOMICILIO A LA EXPORTACION.", "level": "G", "complement": "Declarar que se cuenta con autorizacion para el despacho de las mercancias por lugar distinto o en dia u hora inhabil."},
|
||||
{"key": "DE", "description": "DESPERDICIOS.", "level": "G", "complement": "Indicar que se trata de desperdicios derivados de los procesos productivos de mercancias que se hubieran importado temporalmente por empresas con Programa IMMEX."},
|
||||
{"key": "DH", "description": "DATOS DE IMPORTACIÓN DE\u00a0HIDROCARBUROS", "level": "P", "complement": "Identificar el medio de transporte y, en su caso, el medidor con el que cuenta. Tratándose de importación por medio de ductos deberán declararse los complementos 1 y 2. Tratándose de importación por medios distintos de ductos deberá declararse el complemento 2. No obstante lo anterior, podrán declararse ambos complementos cuando se requieran."},
|
||||
{"key": "DI", "description": "DOCUMENTO DE INCREMENTABLE (CFDI)", "level": "G", "complement": "Declarar el folio del CFDI correspondiente al incrementable de la contratación del servicio de la importación de un vehículo usado, conforme a la Regla 3. 5.10."},
|
||||
{"key": "DN", "description": "DONACION POR PARTE DE LAS EMPRESAS CON PROGRAMA IMMEX.", "level": "G", "complement": "Indicar que se trata de la donacion de desperdicios, maquinaria y/o equipos obsoletos."},
|
||||
{"key": "DP", "description": "INTRODUCCION Y EXTRACCION DE DEPOSITO FISCAL PARA EXPOSICION Y VENTA DE ARTICULOS PROMOCIONALES.", "level": "P", "complement": "Identificar a los articulos promocionales de conformidad con la R.G. 4.5.27."},
|
||||
{"key": "DR", "description": "RECTIFICACION POR DISCREPANCIA DOCUMENTAL.", "level": "P", "complement": "Rectificar los datos asentados en el pedimento, de conformidad con la R.G. 4.5.7."},
|
||||
{"key": "DS", "description": "DESTRUCCION DE MERCANCIAS EN DEPOSITO FISCAL PARA LA EXPOSICION Y VENTA.", "level": "P", "complement": "Indicar la destrucciOn de mercancias extranjeras y nacionales conforme a la R.G. 4.5.22."},
|
||||
{"key": "DT", "description": "OPERACIONES SUJETAS AL ART. 303 DEL TLCAN.", "level": "P", "complement": "Senalar supuesto de aplicacion para la determinacion y pago del IGI de los insumos no originarios de la region del TLCAN."},
|
||||
{"key": "DU", "description": "OPERACIONES SUJETAS A LOS ARTS. 14DE LA DECISION O 15DE EL TLCAELC.", "level": "P", "complement": "Senalar el supuesto de aplicacion para la determinacion y pago del IGI de los insumos no originarios conforme TLCUE o TLCAELC."},
|
||||
{"key": "DV", "description": "VENTA DE MERCANCIAS A MISIONES DIPLOMATICAS Y CONSULARES CUANDO CUENTE CON FRANQUICIA DIPLOMATICA.", "level": "P", "complement": "Declarar autorizacion para la venta de mercancias a misiones diplomaticas y consulares o a los organismos internacionales, regla 4.5.25."},
|
||||
{"key": "EA", "description": "EXCEPCION DE AVISO AUTOMATICO DE IMPORTACION / EXPORTACION.", "level": "P", "complement": "Exceptuar la presentacion del aviso automatico a que se refiere Acuerdo que establece la clasificacion y codificacion de mercancias cuya importacion esta sujeta al requisito de permiso previo por parte de la Secretaria de Economia, publicado en el DOF el 6 de junio de 2007."},
|
||||
{"key": "EB", "description": "ENVASES Y EMPAQUES.", "level": "P", "complement": "Identificar los envases y empaques reutilizables de empresas con Programa IMMEX y a los que se refiere la R.G. 4.3.2."},
|
||||
{"key": "EC", "description": "EXCEPCION DE PAGO DE CUOTA COMPENSATORIA.", "level": "P", "complement": "Indicar que la mercancia no se encuentra sujeta al pago de cuota compensatoria."},
|
||||
{"key": "ED", "description": "DOCUMENTO DIGITALIZADO.", "level": "G", "complement": "Identificar a un documento digitalizado anexo al pedimento."},
|
||||
{"key": "EF", "description": "ESTIMULO FISCAL.", "level": "P", "complement": "Senalar cuando apliquen el Decreto por el que se establece un estImulo fiscal a la importacion o enajenacion de los productos que se indican."},
|
||||
{"key": "EI", "description": "AUTORIZACION DE DEPOSITO FISCAL TEMPORAL PARA EXPOSICIONES INTERNACIONALES DE MERCANCIAS.", "level": "G", "complement": "Identificar el local autorizado de deposito fiscal de conformidad con la R.G. 4.5.29."},
|
||||
{"key": "EM", "description": "EMPRESA DE MENSAJERIA Y PAQUETERIA.", "level": "G", "complement": "Identificar a las empresas de mensajeria y paqueterIa."},
|
||||
{"key": "EN", "description": "NO APLICACION DE LA NORMA OFICIAL MEXICANA.", "level": "P", "complement": "Identificar que la mercancía no está sujeta al cumplimiento de la NOM de conformidad con: El Anexo 2.4.1 del Acuerdo por el que la Secretaría de Economía emite reglas y criterios de carácter general en materia de comercio exterior. Políticas y procedimientos para la evaluación de la conformidad. Procedimientos de certificación y verificación de productos sujetos al cumplimiento de NOM's, competencia de la SE."},
|
||||
{"key": "EP", "description": "DECLARACION DE CURP./EXCEPCION DE INSCRIPCION AL PADRON DE IMPORTADORES.", "level": "G", "complement": "Identificar el tipo de excepcion para no declarar el RFC./Identificar el tipo de excepcion de conformidad con lo establecido en la R.G. 1.3.1."},
|
||||
{"key": "ES", "description": "ESTADO DE LA MERCANCIA.", "level": "P", "complement": "\u00danicamente para determinar la aplicación de regulaciones o restricciones no arancelarias, de conformidad con el estado de la mercancía., \u00danicamente para determinar la aplicación de regulaciones o restricciones no arancelarias, de conformidad con el estado de la mercancía; o para el caso de mercancías remanufacturadas importadas con tratamiento arancelario preferencial del TIPAT."},
|
||||
{"key": "EX", "description": "EXENCION DE CUENTA ADUANERA DE GARANTIA.", "level": "P", "complement": "Indicar excepcion de la presentacion de la cuenta aduanera de garantia de mercancias sujetas a precio estimado."},
|
||||
{"key": "F8", "description": "DEPOSITO FISCAL PARA EXPOSICION Y VENTA (MERCANCIAS NACIONALES O NACIONALIZADAS).", "level": "G", "complement": "1) Introduccion de mercancia nacional o nacionalizada a deposito fiscal para exposiciOn y venta, conforme a la R.G. 4.5.19., fracc. II. 2) Exportacion definitiva virtual de mercancia nacional o nacionalizada conforme a la R.G. 4.5.19., fracc. II.3) ExtracciOn de deposito fiscal de mercancias por devoluciOn para reincorporarse al mercado nacional, conforme a la R.G. 4.5.23., fracc. II. 4) Desistimiento del rEgimen de exportacion definitiva de las mercancias a deposito fiscal por devoluciOn, conforme a la R.G. 4.5.23., fracc. II."},
|
||||
{"key": "FC", "description": "FRACCI\u00d3N CORRELACIONADA", "level": "P", "complement": "Se deberán declarar las claves de acuerdo con el supuesto que corresponda: 1. Cuando la fracción arancelaria declarada en el complemento 2 pertenezca a la tarifa anterior.; 2. Cuando la fracción arancelaria declarada en el complemento 2 corresponda a la nueva tarifa."},
|
||||
{"key": "FI", "description": "FACTOR DE ACTUALIZACION CON INDICE NACIONAL DE PRECIOS AL CONSUMIDOR.", "level": "G", "complement": "Actualizar las contribuciones aplicando factor de actualizaciOn con base en el INPC."},
|
||||
{"key": "FR", "description": "FECHA QUE RIGE.", "level": "G", "complement": "Declarar cuando la fecha de entrada es igual a la fecha de pago, en aduanas con deposito ante la aduana."},
|
||||
{"key": "FT", "description": "FOLIO DE TRAMITE GENERADO POR LA VENTANILLA DIGITAL", "level": "G", "complement": "Operaciones en las que se requiera presentar anexo al pedimento una constancia, aviso o solicitud de autorización a que se refiere el segundo párrafo de la regla 2.4.11."},
|
||||
{"key": "FV", "description": "FACTOR DE ACTUALIZACION CON VARIACION CAMBIARIA.", "level": "G", "complement": "Actualizar las contribuciones aplicando factor de actualizaciOn con base en el tipo de cambio."},
|
||||
{"key": "G9", "description": "IMPORTACI\u00d3N DEFINITIVA DE RESIDENTES EN TERRITORIO NACIONAL DE MERCANC\u00cdAS QUE SE RETIRAN DE UN RECINTO FISCALIZADO ESTRAT\u00c9GICO.", "level": "G", "complement": "Indicar que la operación se realiza conforme a la regla 4.8.6., fracción III, segundo párrafo."},
|
||||
{"key": "GA", "description": "CUENTA ADUANERA DE GARANTIA.", "level": "P", "complement": "Indicar la presentacion de una cuenta aduanera de garantia."},
|
||||
{"key": "GS", "description": "EXPORTACION TEMPORAL Y RETORNO DE DISPOSITIVOS ELECTRONICOS QUE ESTABLECE LA REGLA 3.7.34.", "level": "P", "complement": "Identificar mercancias acompanadas por un dispositivo electronico, o de radiofrecuencia de localizacion, distinto de los integrados en los medios de transporte, de conformidad con la regla 3.7.34."},
|
||||
{"key": "HC", "description": "OPERACIONES SECTOR HIDROCARBUROS.", "level": "G", "complement": "Indicar que se tratan de operaciones conforme a lo establecido en la regla 3.7.33."},
|
||||
{"key": "Hl", "description": "TIPO DE GASOLINA.", "level": "P", "complement": "Declarar dependiendo el índice de octanaje."},
|
||||
{"key": "IA", "description": "CERTIFICADO DE APROBACION PARA PRODUCCION DE PARTES AERONAUTICAS.", "level": "P", "complement": "Identificar a las empresas inscritas en el Padron de Produccion Aeroespacial que cuentan con el Certificado de Aprobacion para Produccion emitido por la SCT."},
|
||||
{"key": "IC", "description": "EMPRESA CERTIFICADA.", "level": "G", "complement": "Senalar que se trata de una empresa certificada."},
|
||||
{"key": "ID", "description": "IMPORTACION DEFINITIVA DE VEHICULOS O EN FRANQUICIA DIPLOMATICA CON AUTORIZACION DE LA ADMINISTRACION GENERAL JURIDICA.", "level": "G", "complement": "Importacion definitiva de vehiculos con autorizacion \u00a0Importacion definitiva de vehiculos en franquicia diplomatica con autorizacion."},
|
||||
{"key": "II", "description": "INVENTARIO INICIAL DE EMPRESAS DENOMINADAS DUTY FREE.", "level": "P", "complement": "Declarar descargo del inventario inicial."},
|
||||
{"key": "IM", "description": "EMPRESAS CON PROGRAMA IMMEX.", "level": "G", "complement": "Indicar el numero de autorizacion de empresa IMMEX proporcionado por la Secretaria de Economia, incluso RFE que cuenten con dicho programa."},
|
||||
{"key": "IN", "description": "INCIDENCIA.", "level": "P", "complement": "Indicar el supuesto en que se realiza la rectificacion."},
|
||||
{"key": "IR", "description": "RECINTO FISCALIZADO ESTRATEGICO.", "level": "G", "complement": "Declarar la clave del RFE del inmueble habilitado."},
|
||||
{"key": "IS", "description": "MERCANCIAS EXENTAS DE IMPUESTOS AL COMERCIO EXTERIOR.", "level": "P", "complement": "Identificar mercancias por las que no se pagan los impuestos al comercio exterior al amparo del articulo 61 de la Ley."},
|
||||
{"key": "J4", "description": "RETORNO DE MERCANCIA DE PROCEDENCIA EXTRANJERA.", "level": "G", "complement": "Retorno de mercancia extranjera de recinto fiscalizado estrategico."},
|
||||
{"key": "LD", "description": "DESPACHO POR LUGAR DISTINTO.", "level": "G", "complement": "Declarar autorizacion para the despacho aduanero por lugar distinto, conforme a lo establecido en el articulo 10 de la Ley."},
|
||||
{"key": "LP", "description": "LISTA DE ESCASO ABASTO", "level": "P", "complement": "Identificar cuando la mercancía haya sido producida con materiales de escaso abasto listados en el Apéndice 1 del Anexo 4-A del TIPAT. Declarar el número de producto que le corresponda al material de escaso abasto listado en la primera columna del Apéndice 1 del Anexo 4-A del TIPAT. Declarar el número de producto que le corresponda al segundo material de escaso abasto, listado en la primera columna del Apéndice 1 del Anexo 4-A del TIPAT."},
|
||||
{"key": "LR", "description": "IMPORTACION POR PEQUENOS CONTRIBUYENTES.", "level": "G", "complement": "Senalar que se trata de importacion de mercancias mediante pedimento simplificado."},
|
||||
{"key": "M7", "description": "OPINION FAVORABLE DE LA SE.", "level": "G", "complement": "Declarar opinion favorable de la SE, para las mercancias del Anexo 12, conforme al ArtIculo 116 de la Ley."},
|
||||
{"key": "MA", "description": "EMBALAJES DE MADERA.", "level": "P", "complement": "Señalar que se trata de embalajes de madera que cumplen con la Norma Oficial Mexicana NOM-144-SEMARNAT-2017."},
|
||||
{"key": "MB", "description": "MARBETES /O PRECINTOS.", "level": "P", "complement": "Declarar marbetes y/o precintos que se coloquen en envases que contengan bebidas alcoholicas, de conformidad con la R.G. 5.1.8. de la RMF."},
|
||||
{"key": "MC", "description": "MARCA.", "level": "P", "complement": "MARCA NOMINATIVA, INNOMINADA, TRIDIMENCIONAL O MIXTA, QUE IDENTIFICA EL PRODUCTO."},
|
||||
{"key": "MD", "description": "MENAJE DE DIPLOMATICOS.", "level": "G", "complement": "Senalar para diplomaticos acreditados conforme al articulo 61 fracc. I de la Ley, articulo 90 y 91 del Reglamento y la R.G. 3.2.6."},
|
||||
{"key": "ME", "description": "MATERIAL DE ENSAMBLE.", "level": "P", "complement": "Indicar que la mercancia es material de ensamble."},
|
||||
{"key": "MI", "description": "IMPORTACION DEFINITIVA DE MUESTRAS AMPARADAS BAJO UN PROTOCOLO DE INVESTIGACION.", "level": "G", "complement": "Indicar para muestras amparadas bajo un protocolo de investigacion en humanos, conforme a la R.G. 3.1.4."},
|
||||
{"key": "MJ", "description": "OPERACIONES DE EMPRESAS DE MENSAJERIA Y PAQUETERIA DE MERCANCIAS NO SUJETAS AL PAGO DEL IGIE E IVA.", "level": "G", "complement": "Indicar para operaciones realizadas conforme a la regla 3.7.5., en relación con la regla 3.7.36., fracción I, cuando el valor en aduana de las mercancías no es mayor a 50 dólares."},
|
||||
{"key": "MM", "description": "IMPORTACION DEFINITIVA DE MUESTRAS Y MUESTRARIOS.", "level": "P", "complement": "Indicar para mercancias destinadas a demostracion o levantamiento de pedidos de conformidad con la R.G. 3.1.2."},
|
||||
{"key": "MP", "description": "PEDIMENTO SIMPLIFICADO.", "level": "G", "complement": "Indicar para operaciones por empresas de mensajeria de mexicanos residentes en el extranjero de conformidad con la R.G. 3.7.3., fracc. VI."},
|
||||
{"key": "MR", "description": "REGISTRO PARA LA TOMA DE MUESTRAS, PELIGROSAS O PARA QUE LAS QUE SE REQUIERA DE INSTALACIONES O EQUIPOS ESPECIALES PARA LA TOMA DE LAS MISMAS.", "level": "P", "complement": "Indicar que se trata de mercancias esteriles, radiactivas, peligrosas o para las que se requiera de instalaciones o equipos especiales para la toma de muestras, conforme a la R.G. 3.1.3. y el Anexo 23."},
|
||||
{"key": "MS", "description": "MODALIDAD DE SERVICIOS DE EMPRESAS CON PROGRAMA IMMEX.", "level": "G", "complement": "Indicar la actividad de servicios que corresponda a la empresa con Programa IMMEX."},
|
||||
{"key": "MT", "description": "MONTO TOTAL DEL VALOR EN DOLARES A EJERCER POR MERCANCIA TEXTIL.", "level": "G", "complement": "Declarar el importe estimado en dolares por mercancia textil (Anexo III, Decreto para el fomento de la industria manufacturera, maquiladora y de servicios de exportacion) de empresas IMMEX del Sector Textil y Confeccion 8\u00e2\u20ac\u009d."},
|
||||
{"key": "MV", "description": "ANO MODELO DEL VEHICULO.", "level": "P", "complement": "Indicar el ano y modelo del vehIculo a importar y, en su caso, el precio estimado que corresponda."},
|
||||
{"key": "NA", "description": "MERCANCIAS CON PREFERENCIA ARANCELARIA ALADI SENALADAS EN EL ACUERDO.", "level": "P", "complement": "Indicar las mercancias senaladas en el acuerdo ALADI correspondiente."},
|
||||
{"key": "NE", "description": "EXCEPCION DE CUMPLIR CON EL ANEXO 21.", "level": "P", "complement": "Identificar que la mercancia no corresponde a las listadas en el Anexo 21 (aduanas autorizadas para tramitar el despacho aduanero)."},
|
||||
{"key": "NR", "description": "OPERACION EN LA QUE LAS MERCANCIAS NO INGRESAN A RECINTO FISCALIZADO.", "level": "G", "complement": "Identificar las operaciones realizadas por empresas certificadas, de mercancías que no ingresaron a Recinto Fiscalizado, de conformidad con los lineamientos que para tal efecto emita la AGA, mismos que se darán a conocer en el Portal del SAT."},
|
||||
{"key": "NS", "description": "EXCEPCION DE INSCRIPCION EN LOS PADRONES DE IMPORTADORES EXPORTADORES SECTORIALES.", "level": "P", "complement": "Identificar las mercancias exceptuadas del Anexo 10, apartado A, conforme al: Acuerdo que establece la clasificacion y codificacion de los productos quimicos esenciales cuya importacion o exportacion esta sujeta a la presentacion de un aviso previo ante la Secretaria de Salud, publicado en el DOF el 30 de junio de 2007, modificado el 1 de junio de 2010. Acuerdo que establece la clasificacion y codificacion de mercancias y productos cuya importacion, exportacion, internacion o salida esta sujeta a regulacion sanitaria por parte de la Secretaria de Salud, publicado en el DOF el 27 de septiembre de 2007 modificado mediante acuerdos publicados en el mismo Organo informativo el 23 de enero, 30 de julio de 2009, 1 de junio y 09 de diciembre de 2010. Acuerdo que establece la clasificacion y codificacion de mercancias cuya importacion y exportacion esta sujeta a autorizacion por parte de la Secretaria de EnergIa, publicado en el DOF el 2 de marzo de 2012 y modificado mediante acuerdo publicado en el mismo Organo informativo el 18 de junio de 2012. Lo establecido en la Regla 3.1.2. de las RGCE. Las claves 401, 501, 601, 701 y 801 serAn aplicables cuando no se trate de mercancia contemplada en el Acuerdo que establece la clasificacion y codificacion de las mercancias cuya importacion o exportacion estan sujetas a regulacion por parte de la Secretaria de la Defensa Nacional\u00e2\u20ac\u009d, publicado en el DOF el 30 de junio de 2007. Identificar las mercancias exceptuadas del Anexo 10 apartado B, conforme al: Acuerdo por el que la Secretaria de Economia emite reglas y criterios de caracter general en materia de Comercio Exterior\u00e2\u20ac\u009d, publicado en el DOF el 6 de Julio de 2007 y su posterior modificacion, publicada en el mismo Organo informativo el 18 de marzo de 2011."},
|
||||
{"key": "NT", "description": "NOTA DE TRATADO.", "level": "P", "complement": "Identificar la mercancia con preferencia arancelaria prevista en el Decreto por el que se establezca la tasa aplicable del impuesto general de importacion para las mercancias originarias de conformidad con los tratados de libre comercio que mexico tenga suscritos."},
|
||||
{"key": "NZ", "description": "MERCANCIA QUE NO SE HA BENEFICIADO DEL SUGAR REEXPORT PROGRAM\u00e2\u20ac\u009d DE LOS ESTADOS UNIDOS DE AMERICA.", "level": "P", "complement": "Senalar que se presenta declaracion escrita del exportador en la que manifieste que la mercancia no se ha beneficiado del programa."},
|
||||
{"key": "OC", "description": "OPERACI\u00d3N TRAMITADA EN FASE DE CONTINGENCIA.", "level": "G", "complement": "Identificar los pedimentos tramitados durante la fase de contingencia de la Ventanilla Digital o del SAAI para la validación del pedimento."},
|
||||
{"key": "OE", "description": "OPERADOR ECONOMICO AUTORIZADO.", "level": "G", "complement": "Identificar a los Proveedores internacionales, que cuenten con una certificacion vigente del Operador EconOmico Autorizado en su pais, y se haya firmado un Acuerdo de Reconocimiento Mutuo con mexico."},
|
||||
{"key": "OM", "description": "MERCANCIA ORIGINARIA DE MEXICO.", "level": "P", "complement": "Declarar que la mercancia es originaria de mexico, de conformidad con las reglas de la Secretaria de Economia 3.2.7. y 3.4.14."},
|
||||
{"key": "OV", "description": "OPERACION VULNERABLE.", "level": "P", "complement": "Unicamente para las mercancias cuya clasificacion arancelaria se encuentre listada en el Anexo A de la Resolucion por la que se Expiden los Formatos Oficiales de los Avisos e Informes que deben presentar Quienes Realicen Actividades Vulnerables, publicada en el DOF el 30 de agosto de 2013."},
|
||||
{"key": "PA", "description": "CUMPLIMIENTO DE LA NORMA OFICIAL MEXICANA, PARA VERIFICARSE EN UN ALMACEN GENERAL DE DEPOSITO AUTORIZADO.", "level": "P", "complement": "Indicar que la NOM se cumplirá conforme al Artículo 6, fracción II del Acuerdo de NOM's vigente."},
|
||||
{"key": "PB", "description": "CUMPLIMIENTO DE NORMA OFICIAL MEXICANA PARA SU VERIFICACION DENTRO DEL TERRITORIO NACIONAL, EN UN DOMICILIO PARTICULAR.", "level": "P", "complement": "Indicar que la NOM se cumplirá conforme al Artículo 6, fracción III del Acuerdo de NOM's vigente."},
|
||||
{"key": "PC", "description": "PEDIMENTO CONSOLIDADO.", "level": "G", "complement": "Indicar para el cierre de un pedimento consolidado."},
|
||||
{"key": "PD", "description": "PARTE II.", "level": "G", "complement": "Senalar el despacho de mercancias con pedimentos Parte II, conforme a la R.G. 3.1.18., segundo parrafo, fracc. I."},
|
||||
{"key": "PG", "description": "MERCANCIA PELIGROSA.", "level": "P", "complement": "Indicar que se trata de mercancia peligrosa conforme a la R.G. 3.1.5. y al Apendice 19 del Anexo 22."},
|
||||
{"key": "PH", "description": "PEDIMENTO ELECTRONICO SIMPLIFICADO.", "level": "G", "complement": "Identificar a un pedimento electronico simplificado conforme a la R.G. 7.3.6., fracciones I y II."},
|
||||
{"key": "PI", "description": "INSPECCION PREVIA.", "level": "G", "complement": "Indicar que se trata de operaciones de conformidad con las R.G. 3.7.28. y 7.3.1., fraccion IX"},
|
||||
{"key": "PL", "description": "PRELIBERACION DE MERCANCIAS.", "level": "G", "complement": "Indicar que se trata de una operacion de comercio exterior que se sujeta a preliberacion."},
|
||||
{"key": "PM", "description": "PRESENTACION DE LA MERCANCIA.", "level": "P", "complement": "Indicar que se trata de las mercancias mencionadas en la R.G. 3.1.18.."},
|
||||
{"key": "PO", "description": "PROVEEDOR DE OR\u00cdGEN.", "level": "P", "complement": "Declarar los datos de las mercancías a las que se aplique una preferencia arancelaria al amparo de acuerdos y tratados comerciales suscritos por México, en operaciones de importación, por partida."},
|
||||
{"key": "PP", "description": "PROGRAMA DE PROMOCION SECTORIAL.", "level": "G", "complement": "Identificar operaciones al amparo del PROSEC."},
|
||||
{"key": "PR", "description": "PROPORCION DETERMINADA.", "level": "P", "complement": "Declarar el pago el impuesto general de importacion correspondiente a los bienes no originarios importados temporalmente, conforme a la R.G. 16.4. del TLCAN."},
|
||||
{"key": "PS", "description": "SECTOR AUTORIZADO AL AMPARO DE PROSEC.", "level": "P", "complement": "Determinar el arancel correspondiente a las mercancias importadas al amparo del Decreto por el que se establecen diversos Programas de Promocion Sectorial, pulicado en el DOF el 2 de agosto de 2002 y sus reformas."},
|
||||
{"key": "PT", "description": "EXPORTACION O RETORNO DE PRODUCTO TERMINADO.", "level": "P", "complement": "Especificar que se trata de producto terminado de mercancias elaboradas, transformadas o reparadas en recinto fiscalizado o por empresas con programa IMMEX."},
|
||||
{"key": "PV", "description": "PRUEBA DE VALOR.", "level": "P", "complement": "Indicar que el agente aduanal cuenta con la documentacion y medios de prueba necesarios para comprobar el valor declarado de conformidad con la fraccion III del articulo 59 de la Ley."},
|
||||
{"key": "PZ", "description": "AMPLIACION DEL PLAZO PARA EL RETORNO DE MERCANCIA IMPORTADA O EXPORTADA TEMPORALMENTE.", "level": "G", "complement": "Declarar que se cuenta con una prorroga para el retorno de la mercancia."},
|
||||
{"key": "RA", "description": "RETORNO DE RACKS.", "level": "P", "complement": "Indicar que se retornan racks que se introdujeron a deposito fiscal con la clave de pedimento F2."},
|
||||
{"key": "RC", "description": "CONSECUTIVOS DE FACTURAS O REMESAS.", "level": "G", "complement": "Indicar en el cierre de pedimentos consolidados para senalar el rango de remesas moduladas."},
|
||||
{"key": "RD", "description": "RETORNO A DEPOSITO FISCAL DE LA INDUSTRIA AUTOMOTRIZ DE MERCANCIA EXPORTADA EN DEFINITIVA.", "level": "G", "complement": "Retorno de mercancías extraídas para su exportación definitiva conforme a la regla 4.5.26., fracción IV., Retorno de mercancías extraídas para su exportación definitiva conforme a la regla 4.5.31., fracción IV."},
|
||||
{"key": "RE", "description": "IMPORTACION DEFINITIVA DE MERCANCIAS (REGULARIZACION).", "level": "G", "complement": "Indicar que se trata de operaciones de la R.G. 2.5.1."},
|
||||
{"key": "RF", "description": "CUOTA COMPENSATORIA BASADA EN PRECIO DE REFERENCIA.", "level": "P", "complement": "Identificar cuando se den los supuestos del pago de cuotas compensatorias basadas en precios de referencia."},
|
||||
{"key": "RL", "description": "RESPONSABLE SOLIDARIO.", "level": "G", "complement": "Identificar el responsable solidario de las mercancias que ingresan a DepOsito Fiscal las personas fIsicas o morales residentes en el extranjero."},
|
||||
{"key": "RO", "description": "REVISION EN ORIGEN POR PARTE DE EMPRESAS CERTIFICADAS.", "level": "G", "complement": "Identificar the despacho de mercancias de empresas certificadas mediante el procedimiento de revision en origen, conforme al articulo 98 de la Ley y la R.G. 7.3.3., fraccion XVIII."},
|
||||
{"key": "RP", "description": "RETORNO de residuos peligrosos generados por empresas con Programa IMMEX.", "level": "P", "complement": "Identificar que se trata de mercancía considerada como residuos peligrosos, conforme al \u201cAcuerdo que establece la clasificación y codificación de mercancías cuya importación y exportación está sujeta a regulación por parte de la Secretaría de Medio Ambiente y Recursos Naturales\u201d, publicado en el DOF el 19 de diciembre de 2012 y sus posteriores modificaciones."},
|
||||
{"key": "RQ", "description": "IMPORTACION DEFINITIVA DE REMOLQUES, SEMIRREMOLQUES Y PORTACONTENEDORES.", "level": "G", "complement": "Indicar que se trata de una importacion definitiva de remolques, semirremolques y portacontenedores."},
|
||||
{"key": "RT", "description": "REEXPEDICION POR TERCEROS.", "level": "G", "complement": "Identificar la reexpedicion de mercancias de la franja o region fronteriza por una persona distinta al importador."},
|
||||
{"key": "SB", "description": "IMPORTACION DE ORGANISMOS GENETICAMENTE MODIFICADOS.", "level": "P", "complement": "Identificar mercancias cuya importacion requiere autorizacion por parte de la SE y SAGARPA."},
|
||||
{"key": "SC", "description": "EXCEPCION DE PAGO DE MEDIDA DE TRANSICION.", "level": "P", "complement": "Indicar que la mercancia no se encuentra sujeta al pago de la medida de transicion."},
|
||||
{"key": "SF", "description": "CLAVE DE UNIDAD AUTORIZADA DEL ALMACEN GENERAL DE DEPOSITO.", "level": "G", "complement": "Identificar la unidad autorizada, conforme al Anexo 13., Identificar la unidad autorizada, conforme la autorización otorgada para prestar el servicio de almacenamiento de mercancías en depósito fiscal y colocar marbetes o precintos."},
|
||||
{"key": "SH", "description": "AUTORIZACION DEL SAT./AUTORIZACION DEL SAT.", "level": "G", "complement": "Identificar las operaciones: Autorizaciones otorgadas por el SAT. Franquicia diplomatica./Indicar que la importacion de las mercancias cuenta con una resoluciOn particular otorgada por el SAT."},
|
||||
{"key": "SM", "description": "EXCEPCION DE LA DECLARACION DE MARBETES.", "level": "P", "complement": "Indicar que por la naturaleza de las mercancias no se esta obligado a la declaracion de marbetes."},
|
||||
{"key": "SO", "description": "SOCIO COMERCIAL CERTIFICADO.", "level": "G", "complement": "Identificar a los contribuyentes que participan en el manejo, almacenaje, custodia y/o traslado de las mercancías de comercio exterior inscritos en el Registro en el Esquema de Certificación de Empresa bajo la modalidad de Socio Comercial Certificado."},
|
||||
{"key": "SP", "description": "(Se deroga)", "level": "G", "complement": "- - -"},
|
||||
{"key": "ST", "description": "OPERACIONES SUJETAS AL ART. 303 DEL TLCAN.", "level": "G", "complement": "Senalar en el pedimento el supuesto de aplicacion para la determinacion y pago del IGI de los insumos no originarios de la region del TLCAN."},
|
||||
{"key": "SU", "description": "OPERACIONES SUJETAS A LOS ARTICULOS 14 DE LA DECISION O 15 DEL TLCAELC.", "level": "G", "complement": "Senalar en el pedimento el supuesto de aplicacion para la determinacion y pago del IGI de los insumos no originarios conforme TLCUE o TLCAELC."},
|
||||
{"key": "TB", "description": "TRANSITO INTERNO POR ADUANAS Y MERCANCIAS ESPECIFICAS.", "level": "P", "complement": "Indicar que se trata de un transito conforme a lo establecido en la R.G. 4.6.4."},
|
||||
{"key": "TC", "description": "CORRELACION DE LAS FRACCIONES ARANCELARIAS.", "level": "P", "complement": "Declarar la fraccion correlacionada de acuerdo a lo establecido en los Decretos por los que se establece la tasa aplicable en los Tratados de Libre Comercio Suscritos por mexico."},
|
||||
{"key": "TD", "description": "TIPO DE DESISTIMIENTO Y RETORNO.", "level": "G", "complement": "Conforme a los supuestos de la clave de documento K1 del apendice 2."},
|
||||
{"key": "TF", "description": "TRANSMISION DE FACTURAS.", "level": "G", "complement": "Indicar que las facturas serAn transmitidas antes de presentar las remesas de un pedimento consolidado ante el modulo de seleccion automatizada."},
|
||||
{"key": "TI", "description": "TRANSITO INTERFRONTERIZO.", "level": "G", "complement": "Indicar que se trata de un transito conforme a lo establecido en la R.G. 4.6.11."},
|
||||
{"key": "TL", "description": "MERCANCIA ORIGINARIA AL AMPARO DE TRATADOS DE LIBRE COMERCIO.", "level": "P", "complement": "Declarar una preferencia arancelaria al amparo de un tratado suscrito por mexico."},
|
||||
{"key": "TM", "description": "TRANSITO INTERNACIONAL.", "level": "G", "complement": "Indicar que se trata de un transito conforme a lo establecido en las R.G. 4.6.22. y 4.6.23."},
|
||||
{"key": "TR", "description": "TRASPASO DE MERCANCIAS EN DEPOSITO FISCAL.", "level": "G", "complement": "Indicar operaciones de traspaso de mercancias conforme la R.G. 4.6.22., fraccs. III y IV."},
|
||||
{"key": "TU", "description": "TRANSFERENCIA DE MERCANCIAS (OPERACIONES VIRTUALES), CON PEDIMENTO UNICO.", "level": "G", "complement": "Se\u00f1alar en operaciones de transferencia de mercanc\u00edas que se importen temporalmente a trav\u00e9s del Pedimento \u00danico, de conformidad con las reglas 4.3.20., fracci\u00f3n I, inciso c) vigente hasta el 20 de junio de 2016 o 7.3.1., Apartado A, fracci\u00f3n XIV."},
|
||||
{"key": "TV", "description": "TOTAL DE MERCANCIA EXTRAIDA DE DEPOSITO FISCAL.", "level": "P", "complement": "Indicar que se trata de extracciones realizadas conforme a la R.G. 4.5.20."},
|
||||
{"key": "UM", "description": "USO DE LA MERCANCIA.", "level": "P", "complement": "Indicar el uso de la mercancia, asI como la exencion de impuestos."},
|
||||
{"key": "UP", "description": "UNIDADES PROTOTIPO.", "level": "G", "complement": "Identificar a las operaciones realizadas de conformidad con la R.G. 4.5.31."},
|
||||
{"key": "V1", "description": "TRANSFERENCIAS DE MERCANCIAS.", "level": "G", "complement": "Indicar conforme a los supuestos de la clave de documento V1 del apendice 2 del Anexo 22, R.G. 4.3.19. y articulo 86 de la Ley."},
|
||||
{"key": "V2", "description": "TRANSFERENCIA DE MERCANCIAS IMPORTADAS CON CUENTA ADUANERA.", "level": "G", "complement": "Senalar en el supuesto de la R.G. 1.6.30."},
|
||||
{"key": "V3", "description": "EXTRACCION DE DEPOSITO FISCAL DE BIENES PARA SU RETORNO O EXPORTACION VIRTUAL (IA).", "level": "G", "complement": "Identificar las operaciones de transferencia que realice la industria automotriz terminal o manufacturera de vehiculos de autotransporte."},
|
||||
{"key": "V4", "description": "RETORNO VIRTUAL DERIVADO DE LA CONSTANCIA DE TRANSFERENCIA DE MERCANCIAS.", "level": "G", "complement": "Indicar conforme al supuesto de la clave de documento V4 del Apendice 2 del Anexo 22 and la R.G. 4.3.11., fracc. II."},
|
||||
{"key": "V5", "description": "TRANSFERENCIAS DE MERCANCIAS DE EMPRESAS CERTIFICADAS O RFE A EMPRESAS RESIDENTES EN EL PAIS.", "level": "G", "complement": "Indicar conforme a los supuestos de la clave de documento V5 del Ap\u00e9ndice 2 del Anexo 22 y las Reglas 7.3.1., Apartado C, fracci\u00f3n VI o 7.3.3., fracci\u00f3n XIV \u00f3 4.8.6."},
|
||||
{"key": "V6", "description": "TRANSFERENCIAS DE MERCANCIAS SUJETAS A CUPO.", "level": "G", "complement": "Indicar conforme al supuesto de la clave de documento V6 del Apendice 2 del Anexo 22 and la R.G. 1.6.6."},
|
||||
{"key": "V7", "description": "TRANSFERENCIAS DEL SECTOR AZUCARERO.", "level": "G", "complement": "Indicar conforme al supuesto de la clave de documento V7del Apendice 2 del Anexo 22 and la R.G. 4.3.7."},
|
||||
{"key": "V8", "description": "TRANSFERENCIAS DE MERCANCIAS EXTRANJERAS, NACIONALES Y NACIONALIZADAS DE TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", "level": "G", "complement": "Indicar conforme al supuesto de la clave de documento V8 del Apendice 2 del Anexo 22 and la R.G. 4.5.21., fracc. II."},
|
||||
{"key": "V9", "description": "TRANSFERENCIAS DE MERCANCIAS POR DONACION.", "level": "G", "complement": "Indicar conforme al supuesto de la clave de documento V9 del Apendice 2 del Anexo 22 and la R.G. 3.3.10."},
|
||||
{"key": "VC", "description": "IMPORTACION DEFINITIVA DE VEHICULOS USADOS EN EL ESTADO DE CHIHUAHUA.", "level": "G", "complement": "Indicar la importacion definitiva de vehiculos usados, de conformidad con el Acuerdo que establece el programa para que el Estado de Chihuahua garantice contribuciones en la importacion definitiva de vehiculos automotores usados que circulan en dicha Entidad\u00e2\u20ac\u009d, publicado en el DOF el 30 de octubre de 2012."},
|
||||
{"key": "VF", "description": "IMPORTACION DEFINITIVA DE VEHICULOS USADOS A LA FRANJA O REGION FRONTERIZA NORTE.", "level": "G", "complement": "Indicar la importaci\u00f3n definitiva de veh\u00edculos usados conforme a las reglas 3.5.6, 3.5.11. y 3.5.13."},
|
||||
{"key": "VJ", "description": "FRONTERIZACION DE VEHICULOS.", "level": "G", "complement": "Indicar la importacion definitiva de vehiculos usados, de conformidad con el Acuerdo por el que se establece el Programa para que los Gobiernos Locales Garanticen Contribuciones en la Importacion Definitiva de VehIculos Automotores Usados destinados a permanecer en la Franja y RegiOn Fronteriza Norte, publicado en el DOF el 11 de abril de 2011."},
|
||||
{"key": "VN", "description": "IMPORTACION DEFINITIVA DE VEHICULOS NUEVOS.", "level": "G", "complement": "Indicar la importacion definitiva de vehiculos nuevos."},
|
||||
{"key": "VT", "description": "IMPORTACION DE AUTOBUSES, CAMIONES Y TRACTOCAMIONES USADOS PARA EL TRANSPORTE DE PERSONAS Y MERCANCIAS.", "level": "P", "complement": "Indicar el tipo y capacidad del vehIculo solo cuando se trate de las fracciones 8702.10.05 y 8704.22.07."},
|
||||
{"key": "VU", "description": "IMPORTACION DEFINITIVA DE VEHICULOS USADOS.", "level": "G", "complement": "Indicar la importacion definitiva de vehiculos usados, conforme a las R.G. 3.5.4. y 3.5.7."},
|
||||
{"key": "XL", "description": "PRESENTACION DE MERCANCIA EN TRANSPORTE SOBREDIMENSIONADO.", "level": "G", "complement": "Identificar la mercancia que se presenta en vehiculos con caracteristicas sobredimensionadas."},
|
||||
{"key": "XP", "description": "EXCEPCION AL CUMPLIMIENTO DE REGULACIONES Y RESTRICCIONES NO ARANCELARIAS.", "level": "P", "complement": "Indicar para exceptuar el cumplimiento de un permiso, excepto NOMs."},
|
||||
{"key": "XV", "description": "EXPORTACION DE VEHICULOS DE LA INDUSTRIA AUTOMOTRIZ TERMINAL OR MANUFACTURERA DE VEHICULOS DE AUTOTRANSPORTE.", "level": "G", "complement": "Exportacion de vehiculos, a los cuales se les incorporaron opciones especiales por parte de empresas con Programa IMMEX."},
|
||||
{"key": "ZC", "description": "CONTENIDO DE AZUCAR.", "level": "P", "complement": "Indicar el contenido de azucar de las mercancias cuyas fracciones arancelarias tengan un arancel mixto."},
|
||||
]
|
||||
|
||||
def seed_identifiers(db: Session):
|
||||
"""Seed Identifiers catalog data"""
|
||||
logger.info("Seeding Identifiers...")
|
||||
for item in IDENTIFIERS_DATA:
|
||||
db_item = db.query(IdentifierCatalog).filter(IdentifierCatalog.key == item["key"]).first()
|
||||
if not db_item:
|
||||
logger.info(f"Adding identifier: {item['key']}")
|
||||
new_item = IdentifierCatalog(**item)
|
||||
db.add(new_item)
|
||||
else:
|
||||
# Update fields if changed
|
||||
db_item.description = item["description"]
|
||||
db_item.level = item["level"]
|
||||
db_item.complement = item["complement"]
|
||||
db.commit()
|
||||
logger.info("Identifiers seeding completed.")
|
||||
@@ -0,0 +1,8 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class LicenseExceptionDTO(BaseModel):
|
||||
key: str = Field(..., min_length=1, max_length=10)
|
||||
description: str = Field(..., min_length=1, max_length=500)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,20 @@
|
||||
from core.database import Base
|
||||
from sqlalchemy import PrimaryKeyConstraint, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class LicenseException(Base):
|
||||
__tablename__ = "license_exceptions"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("key", name="license_exceptions_pkey"),
|
||||
{"schema": "public", "extend_existing": True},
|
||||
)
|
||||
|
||||
key: Mapped[str] = mapped_column(
|
||||
String(10), primary_key=True, nullable=False) # clave del simbolo
|
||||
description: Mapped[str] = mapped_column(
|
||||
String(500), nullable=False
|
||||
) # descripción oficial (en español)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<LicenseException(key={self.key}, description={self.description})>"
|
||||
@@ -0,0 +1,103 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import LicenseExceptionDTO
|
||||
from .models import LicenseException
|
||||
|
||||
router = APIRouter(prefix="/license-exceptions")
|
||||
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
async def list_license_exceptions(
|
||||
page: int = Query(1, ge=1, description="Número de página"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"),
|
||||
q: str = Query(None, description="Búsqueda general"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(LicenseException)
|
||||
|
||||
if q:
|
||||
search_term = f"%{q}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
LicenseException.key.ilike(search_term),
|
||||
LicenseException.description.ilike(search_term)
|
||||
)
|
||||
)
|
||||
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
total = query.count()
|
||||
return {
|
||||
"items": [LicenseExceptionDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{key}", response_model=LicenseExceptionDTO)
|
||||
async def get_license_exception(
|
||||
key: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
obj = db.query(LicenseException).filter(LicenseException.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return obj
|
||||
|
||||
|
||||
@router.post("/", response_model=LicenseExceptionDTO, status_code=201)
|
||||
async def create_license_exception(
|
||||
data: LicenseExceptionDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
# Check if already exists
|
||||
existing = db.query(LicenseException).filter(LicenseException.key == data.key).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="License exception with this key already exists")
|
||||
|
||||
obj = LicenseException(**data.dict())
|
||||
db.add(obj)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.put("/{key}", response_model=LicenseExceptionDTO)
|
||||
async def update_license_exception(
|
||||
key: str,
|
||||
data: LicenseExceptionDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(LicenseException).filter(LicenseException.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
for field, value in data.dict().items():
|
||||
setattr(obj, field, value)
|
||||
db.commit()
|
||||
db.refresh(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@router.delete("/{key}", status_code=204)
|
||||
async def delete_license_exception(
|
||||
key: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(has_role("admin")),
|
||||
):
|
||||
obj = db.query(LicenseException).filter(LicenseException.key == key).first()
|
||||
if not obj:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
db.delete(obj)
|
||||
db.commit()
|
||||
return None
|
||||
@@ -0,0 +1,41 @@
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from .models import LicenseException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LICENSE_EXCEPTIONS_DATA = [
|
||||
{"key": "NLR", "description": "Licencia no requerida (NLR)."},
|
||||
{"key": "LVS", "description": "Los envíos de valor limitado (LVS)."},
|
||||
{"key": "EGB", "description": "Los envíos a los países los países del Grupo B (EGB)."},
|
||||
{"key": "CIV", "description": "Los usuarios finales Civil (CIV)."},
|
||||
{"key": "TSR", "description": "Tecnología y software a restricciones (TSR)."},
|
||||
{"key": "APP", "description": "Informática (APP)."},
|
||||
{"key": "TMP", "description": "Temporales de las importaciones, exportaciones y reexportaciones (TMP)."},
|
||||
{"key": "RPL", "description": "Mantenimiento y sustitución de piezas y equipos (RPL)."},
|
||||
{"key": "GFT", "description": "Los gobiernos, Orgs. internacionales, las inspecciones internacionales en virtud del Convenio de Armas Químicas, y la Estación Espacial Internacional (GOB). Regalo parcelas y las donaciones humanitarias (GFT)."},
|
||||
{"key": "TSU", "description": "Tecnología y software libre (TSU)."},
|
||||
{"key": "BAG", "description": "Equipaje (BAG)."},
|
||||
{"key": "AVS", "description": "Las aeronaves y buques (AVS)."},
|
||||
{"key": "APR", "description": "Adicional reexportación permisiva (APR)."},
|
||||
{"key": "ENC", "description": "Cifrado de productos, software y tecnología (ENC)."},
|
||||
{"key": "AGR", "description": "Productos básicos agrícolas (AGR)."},
|
||||
{"key": "CCD", "description": "Dispositivos de Comunicaciones del Consumidor (CCD)."},
|
||||
]
|
||||
|
||||
def seed_license_exceptions(db: Session):
|
||||
"""Seed License Exceptions catalog data"""
|
||||
logger.info("Seeding License Exceptions...")
|
||||
for item in LICENSE_EXCEPTIONS_DATA:
|
||||
db_item = db.query(LicenseException).filter(LicenseException.key == item["key"]).first()
|
||||
if not db_item:
|
||||
logger.info(f"Adding license exception: {item['key']}")
|
||||
new_item = LicenseException(**item)
|
||||
db.add(new_item)
|
||||
else:
|
||||
# Update description if it changed
|
||||
if db_item.description != item["description"]:
|
||||
logger.info(f"Updating license exception: {item['key']}")
|
||||
db_item.description = item["description"]
|
||||
db.commit()
|
||||
logger.info("License Exceptions seeding completed.")
|
||||
@@ -5,14 +5,18 @@ Agrega todos los módulos de la aplicación
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .agency_tariff_codes.routes import router as agency_tariff_codes_router
|
||||
from .carta_porte.routes import router as carta_porte_router
|
||||
from .code_pedimento_regimens.routes import router as code_pedimento_regimens_router
|
||||
from .containers.routes import router as containers_router
|
||||
from .countries.routes import router as countries_router
|
||||
from .currency_types.routes import router as currency_types_router
|
||||
from .customs_sections.routes import router as customs_sections_router
|
||||
from .customs_warehouses.routes import router as customs_warehouses_router
|
||||
from .identifiers.routes import router as identifiers_catalog_router
|
||||
from .incoterms.routes import router as incoterms_router
|
||||
from .invoice_types.routes import router as invoice_types_router
|
||||
from .license_exceptions.routes import router as license_exceptions_router
|
||||
from .material_types.routes import router as material_types_router
|
||||
from .payment_methods.routes import router as payment_methods_router
|
||||
from .pedimento_transport_catalog.routes import router as pedimento_transport_catalog_router
|
||||
@@ -28,6 +32,11 @@ from .valuation_methods.routes import router as valuation_methods_router
|
||||
router = APIRouter()
|
||||
|
||||
# Registrar módulos
|
||||
router.include_router(
|
||||
agency_tariff_codes_router,
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / agency_tariff_codes"],
|
||||
)
|
||||
router.include_router(
|
||||
pedimento_transport_catalog_router,
|
||||
prefix="/reference_data",
|
||||
@@ -58,6 +67,11 @@ router.include_router(
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / material_types"],
|
||||
)
|
||||
router.include_router(
|
||||
identifiers_catalog_router,
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / identifiers"],
|
||||
)
|
||||
router.include_router(
|
||||
currency_types_router,
|
||||
prefix="/reference_data",
|
||||
@@ -91,6 +105,11 @@ router.include_router(
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / transport_modes"],
|
||||
)
|
||||
router.include_router(
|
||||
carta_porte_router,
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / carta_porte"],
|
||||
)
|
||||
router.include_router(
|
||||
customs_sections_router,
|
||||
prefix="/reference_data",
|
||||
@@ -116,3 +135,8 @@ router.include_router(
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / incoterms"],
|
||||
)
|
||||
router.include_router(
|
||||
license_exceptions_router,
|
||||
prefix="/reference_data",
|
||||
tags=["public / reference_data / license_exceptions"],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user