diff --git a/.env.example b/.env.example index 4f0f10d8..7ac9e1f2 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,8 @@ S3_PRESIGNED_EXPIRES_SECONDS=3600 COVE_FIEL_HASH_KEY= COVE_FIEL_HASH_IV= +COVE_API_URL=https://api.vu.aduanasoft.com +COVE_API_VERIFY_SSL=False # ----- Sitar API ----- SITAR_API_URL=http://api.sitar.aduanasoft.com diff --git a/backend/.env.example b/backend/.env.example index bfaae05d..677e007d 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -28,8 +28,14 @@ CORS_ORIGINS=http://localhost:5173,http://localhost:3000 # License Service LICENSE_CHECK_ENABLED=True +# Factura COVE / VUCEM / DODA / API Ventanilla Única +# Llave y IV AES-256-CBC para cifrar la clave FIEL. COVE_FIEL_HASH_KEY= COVE_FIEL_HASH_IV= +# URL base del API de Ventanilla Única (COVE, Expediente y DODA comparten esta variable). +COVE_API_URL=https://api.vu.aduanasoft.com +# Verificación SSL para el API de VU (False en redes internas / dev, True en producción). +COVE_API_VERIFY_SSL=False # Synchronization (Hub & Spoke) SYNC_SECRET_TOKEN=change-this-sync-token-in-production diff --git a/backend/alembic/versions/a1b2c3d4e5f6_create_doda_alta_log.py b/backend/alembic/versions/a1b2c3d4e5f6_create_doda_alta_log.py new file mode 100644 index 00000000..79050fd7 --- /dev/null +++ b/backend/alembic/versions/a1b2c3d4e5f6_create_doda_alta_log.py @@ -0,0 +1,117 @@ +"""create doda_alta_log table + +Revision ID: a1b2c3d4e5f6 +Revises: d1a2b3c4e5f6 +Create Date: 2026-04-26 10:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "a1b2c3d4e5f6" +down_revision = "d1a2b3c4e5f6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "doda_alta_log", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("doda_id", sa.Integer(), nullable=True), + sa.Column("variant", sa.String(length=10), nullable=True), + sa.Column("responsible", sa.String(length=20), nullable=True), + sa.Column("patent", sa.String(length=10), nullable=True), + sa.Column("dispatch_customs", sa.String(length=10), nullable=True), + sa.Column("operation_type", sa.String(length=5), nullable=True), + sa.Column("integration_number", sa.String(length=50), nullable=True), + sa.Column("task_id", sa.String(length=255), nullable=True), + sa.Column("status", sa.String(length=30), nullable=True), + sa.Column("message", sa.String(length=2000), nullable=True), + sa.Column("result_json", sa.Text(), nullable=True), + sa.Column("tenant_id", sa.Integer(), nullable=False), + sa.Column("company_id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.Column("updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False), + sa.Column("deleted_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + sa.ForeignKeyConstraint(["tenant_id"], ["core.tenants.id"]), + sa.PrimaryKeyConstraint("id", name="doda_alta_log_pkey"), + schema="a76", + ) + op.create_index( + op.f("ix_a76_doda_alta_log_company_id"), + "doda_alta_log", + ["company_id"], + unique=False, + schema="a76", + ) + op.create_index( + op.f("ix_a76_doda_alta_log_tenant_id"), + "doda_alta_log", + ["tenant_id"], + unique=False, + schema="a76", + ) + op.create_index( + op.f("ix_a76_doda_alta_log_doda_id"), + "doda_alta_log", + ["doda_id"], + unique=False, + schema="a76", + ) + op.create_index( + op.f("ix_a76_doda_alta_log_task_id"), + "doda_alta_log", + ["task_id"], + unique=False, + schema="a76", + ) + + # Reporte PDF almacenado (S3) + invalidación por huella de contenido + op.add_column( + "doda", + sa.Column("doda_report_pdf_path", sa.String(length=1000), nullable=True), + schema="a76", + ) + op.add_column( + "doda", + sa.Column("doda_report_pdf_generated_at", sa.DateTime(), nullable=True), + schema="a76", + ) + op.add_column( + "doda", + sa.Column("doda_report_source_fingerprint", sa.String(length=64), nullable=True), + schema="a76", + ) + + +def downgrade() -> None: + op.drop_column("doda", "doda_report_source_fingerprint", schema="a76") + op.drop_column("doda", "doda_report_pdf_generated_at", schema="a76") + op.drop_column("doda", "doda_report_pdf_path", schema="a76") + + op.drop_index( + op.f("ix_a76_doda_alta_log_task_id"), + table_name="doda_alta_log", + schema="a76", + ) + op.drop_index( + op.f("ix_a76_doda_alta_log_doda_id"), + table_name="doda_alta_log", + schema="a76", + ) + op.drop_index( + op.f("ix_a76_doda_alta_log_tenant_id"), + table_name="doda_alta_log", + schema="a76", + ) + op.drop_index( + op.f("ix_a76_doda_alta_log_company_id"), + table_name="doda_alta_log", + schema="a76", + ) + op.drop_table("doda_alta_log", schema="a76") diff --git a/backend/api/v1/modules/a76/csv_templates/registry.py b/backend/api/v1/modules/a76/csv_templates/registry.py index b14ae3a6..b17906b3 100644 --- a/backend/api/v1/modules/a76/csv_templates/registry.py +++ b/backend/api/v1/modules/a76/csv_templates/registry.py @@ -49,6 +49,168 @@ from api.v1.modules.a76.layouts_csv.transportistas.template_config import ( ) +def _normalize_header_for_match(header: str) -> str: + """Normaliza cabeceras para comparación interna (sin alterar salida).""" + if not header: + return "" + cleaned = header.strip() + if cleaned.startswith("* "): + cleaned = cleaned[2:] + return cleaned.strip().upper() + + +ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE: Dict[str, set[str]] = { + # Facturas encabezados + "imp_temp_header": { + "NUMERO FACTURA", + "FECHA FACTURA", + "REGIMEN", + "CLAVE PROVEEDOR", + "CLAVE VENDIDO A", + "CLAVE ENVIADO A", + "AGENTE ADUANAL", + "ADUANA DE CRUCE", + }, + "imp_def_header": { + "NUMERO FACTURA", + "FECHA FACTURA", + "REGIMEN", + "CLAVE PROVEEDOR", + "CLAVE VENDIDO A", + "CLAVE ENVIADO A", + "AGENTE ADUANAL", + }, + "exp_def_header": { + "NUMERO FACTURA", + "FECHA FACTURA", + "REGIMEN", + "CLAVE PROVEEDOR", + "CLAVE VENDIDO A", + "CLAVE ENVIADO A", + "AGENTE ADUANAL", + "ADUANA DE CRUCE", + }, + "cmex_header": { + "NUMERO FACTURA", + "FECHA FACTURA", + "CLAVE PROVEEDOR", + "CLAVE VENDIDO A", + "CLAVE ENVIADO A", + }, + # Facturas partidas (plantillas del registry) + "imp_temp_details": { + "NUMERO FACTURA", + "CLASE", + "CANTIDAD IMPORTADA", + "COSTO UNITARIO", + "PESO NETO", + "PAIS ORIGEN", + "PREFERENCIA ARANCELARIA", + }, + "imp_def_details": { + "NUMERO FACTURA", + "CLASE", + "CANTIDAD IMPORTADA", + "COSTO UNITARIO", + "PESO NETO", + "PAIS ORIGEN", + "PREFERENCIA ARANCELARIA", + "NUM. PARTE", + }, + "exp_def_details": { + "NUMERO FACTURA", + "CLASE", + "CANTIDAD IMPORTADA", + "COSTO UNITARIO", + "PESO NETO", + "PAIS ORIGEN", + "PREFERENCIA ARANCELARIA", + }, + "cmex_details": { + "NUMERO FACTURA", + "CLASE", + "CANTIDAD IMPORTADA", + "COSTO UNITARIO", + "PESO NETO", + "PAIS ORIGEN", + "PREFERENCIA ARANCELARIA", + "NUM. PARTE", + }, + # Facturas series (plantillas del registry) + "imp_temp_series": {"NUMERO FACTURA", "LINEA FACTURA"}, + "imp_def_series": {"NUMERO FACTURA", "LINEA FACTURA"}, + "cmex_series": {"NUMERO FACTURA", "LINEA FACTURA"}, + # Catálogos y transportes + "customs_brokers": {"TIPO", "CLAVE", "NOMBRE"}, + "clients_providers": {"PROCEDENCIA", "SHORT_NAME", "NOMBRE", "RFC"}, + "exchange_rates": {"FECHA", "VALOR"}, + "american_fractions": {"FRACCION_ARANCELARIA", "DESCRIPCION"}, + "material_classes": { + "CLAVE CLASE", + "CLASE", + "DESCRIPCION ESPAÑOL", + "DESCRIPCIONE", + "TIPO DE MATERIAL", + "CLAVEMAT", + "U.M. COMERCIAL", + "UNIMED", + "FRACCION ARANCELARIA", + "FRACCION", + }, + "part_numbers": { + "NUMERO DE PARTE", + "NUMPARTE", + "DESCRIPCION EN ESPAÑOL", + "DESCRIPCIONE", + "UNIDAD DE MEDIDA COMERCIAL", + "UNIMED", + }, + "items": { + "NUMERO DE PARTE", + "NUMPARTE", + "DESCRIPCION EN ESPAÑOL", + "DESCRIPCIONE", + "UNIDAD DE MEDIDA COMERCIAL", + "UNIMED", + }, + "boms": {"NUMPARTE_PADRE", "NUMPARTE_COMPONENTE", "CANTIDAD"}, + "pedimentos": { + "AÑO", "PATENTE", "NUMERO", "PEDIMENTO", + "TIPO_OPERACION", + "CLAVE_PEDIMENTO", + "REGIMEN", + "FECHA_INICIO", + "FECHA_FINAL", + "FECHA_PAGO", + "ADUANA_SECCION_CRUCE", + }, + "transports": {"CLAVE", "CODIGO DE ENTIDAD"}, + "drivers": {"TRANSPORTISTA", "LINEA", "CLAVE CONDUCTOR"}, + "trailers": {"NUMERO TRAILER"}, + "transporters": {"CLAVE TRANSPORTISTA", "NOMBRE"}, +} + + +def _apply_required_prefix(template_id: str, headers: List[str]) -> List[str]: + """Prefija '* ' a cabeceras siempre obligatorias para la plantilla.""" + required_headers = ALWAYS_REQUIRED_HEADERS_BY_TEMPLATE.get(template_id) + if not required_headers: + return headers + + required_norm = {_normalize_header_for_match(h) for h in required_headers} + out: List[str] = [] + for header in headers: + norm_header = _normalize_header_for_match(header) + if norm_header and norm_header in required_norm: + if header.startswith("* "): + out.append(header) + else: + out.append(f"* {header}") + else: + out.append(header) + return out + + def _canonicals_from_columns(cols: Optional[List[Dict]]) -> List[str]: """Extrae la lista de nombres canónicos en orden a partir de una lista de columnas.""" if not cols: @@ -163,7 +325,10 @@ TEMPLATE_FILENAMES: Dict[str, str] = { def get_template_headers(template_id: str) -> Optional[List[str]]: """Devuelve la lista de cabeceras canónicas para el template_id, o None si no existe.""" - return _TEMPLATE_HEADERS.get(template_id) + headers = _TEMPLATE_HEADERS.get(template_id) + if headers is None: + return None + return _apply_required_prefix(template_id, headers) def get_template_filename(template_id: str) -> str: diff --git a/backend/api/v1/modules/a76/expediente_archivos/external_service.py b/backend/api/v1/modules/a76/expediente_archivos/external_service.py index e5957bef..e55dcf6a 100644 --- a/backend/api/v1/modules/a76/expediente_archivos/external_service.py +++ b/backend/api/v1/modules/a76/expediente_archivos/external_service.py @@ -20,7 +20,8 @@ class ExpedienteExternalService: """ def __init__(self) -> None: - self.base_url = (settings.COVE_API_URL or "").strip() or "https://api.vu.aduanasoft.com" + self.base_url = settings.COVE_API_URL.strip() + self.verify_ssl = settings.COVE_API_VERIFY_SSL def digitalizar_archivo_json(self, payload: Dict[str, Any]) -> Dict[str, Any]: """ @@ -41,7 +42,7 @@ class ExpedienteExternalService: ) # connect=10s, read=120s: la subida del PDF puede tomar tiempo en el servidor VU - with httpx.Client(timeout=httpx.Timeout(120.0, connect=10.0), verify=False) as client: + with httpx.Client(timeout=httpx.Timeout(120.0, connect=10.0), verify=self.verify_ssl) as client: response = client.post(url, json=payload) response.raise_for_status() return response.json() @@ -55,7 +56,7 @@ class ExpedienteExternalService: # read=None: sin límite de lectura — VU mantiene la conexión abierta mientras procesa. # El timeout global del polling loop (300 s) actúa como cota máxima real. - with httpx.Client(timeout=httpx.Timeout(None, connect=10.0), verify=False) as client: + with httpx.Client(timeout=httpx.Timeout(None, connect=10.0), verify=self.verify_ssl) as client: response = client.get(url) response.raise_for_status() return response.json() diff --git a/backend/api/v1/modules/a76/factura_cove/external_service.py b/backend/api/v1/modules/a76/factura_cove/external_service.py index ee8af99c..f9811f6c 100644 --- a/backend/api/v1/modules/a76/factura_cove/external_service.py +++ b/backend/api/v1/modules/a76/factura_cove/external_service.py @@ -49,12 +49,8 @@ class CoveExternalService: """ def __init__(self) -> None: - """ - Inicializa el cliente usando COVE_API_URL si está definido; de lo contrario, - usa por defecto el endpoint público documentado en: - https://api.vu.aduanasoft.com/docs#/Factura%20COVE/generar_factura_cove_endpoint_api_v1_factura_cove_generar_factura_cove_post - """ - self.base_url = (settings.COVE_API_URL or "").strip() or "https://api.vu.aduanasoft.com" + self.base_url = settings.COVE_API_URL.strip() + self.verify_ssl = settings.COVE_API_VERIFY_SSL def generate_cove(self, payload: FacturaCoveRequest) -> CoveExternalResult: """ @@ -75,11 +71,7 @@ class CoveExternalService: len(configuracion_vu.get("archivo_key_base64") or ""), ) - # NOTA: verify=False desactiva la validación de certificado SSL. - # Esto es útil en entornos de desarrollo o cuando el entorno no confía - # en el certificado del endpoint externo. En producción, idealmente - # se debería habilitar la verificación SSL. - with httpx.Client(timeout=30.0, verify=False) as client: + with httpx.Client(timeout=30.0, verify=self.verify_ssl) as client: resp = client.post(url, json=json_payload) # Intentar parsear JSON siempre, incluso en errores 4xx/5xx @@ -176,8 +168,7 @@ class CoveExternalService: """ url = f"{self.base_url.rstrip('/')}/api/v1/factura-cove/status/{task_id}" - # Igual que en generate_cove, desactivamos verify solo para entornos de dev. - with httpx.Client(timeout=30.0, verify=False) as client: + with httpx.Client(timeout=30.0, verify=self.verify_ssl) as client: resp = client.get(url) try: diff --git a/backend/api/v1/modules/a76/factura_cove/schemas.py b/backend/api/v1/modules/a76/factura_cove/schemas.py index d9365630..3abb34c3 100644 --- a/backend/api/v1/modules/a76/factura_cove/schemas.py +++ b/backend/api/v1/modules/a76/factura_cove/schemas.py @@ -27,25 +27,25 @@ class ConfiguracionVU(BaseModel): class PersonaCove(BaseModel): tipo_identificador: str = Field(..., max_length=10) identificacion: str = Field(..., max_length=30) - apellido_paterno: Optional[str] = Field(None, max_length=80) - apellido_materno: Optional[str] = Field(None, max_length=80) - nombre: Optional[str] = Field(None, max_length=80) - calle: Optional[str] = Field(None, max_length=120) - numero_exterior: Optional[str] = Field(None, max_length=20) - numero_interior: Optional[str] = Field(None, max_length=20) - colonia: Optional[str] = Field(None, max_length=120) - localidad: Optional[str] = Field(None, max_length=120) - municipio: Optional[str] = Field(None, max_length=120) - entidad_federativa: Optional[str] = Field(None, max_length=120) + apellido_paterno: str = Field(default="", max_length=80) + apellido_materno: str = Field(default="", max_length=80) + nombre: str = Field(default="", max_length=80) + calle: str = Field(default="", max_length=120) + numero_exterior: str = Field(default="", max_length=20) + numero_interior: str = Field(default="", max_length=20) + colonia: str = Field(default="", max_length=120) + localidad: str = Field(default="", max_length=120) + municipio: str = Field(default="", max_length=120) + entidad_federativa: str = Field(default="", max_length=120) pais: str = Field(..., max_length=3, description="País en formato ISO o catálogo VU") - codigo_postal: Optional[str] = Field(None, max_length=15) + codigo_postal: str = Field(default="", max_length=15) class DescripcionEspecifica(BaseModel): - marca: Optional[str] = Field(None, max_length=80) - modelo: Optional[str] = Field(None, max_length=80) - submodelo: Optional[str] = Field(None, max_length=80) - numero_serie: Optional[str] = Field(None, max_length=80) + marca: str = Field(default="", max_length=80) + modelo: str = Field(default="", max_length=80) + submodelo: str = Field(default="", max_length=80) + numero_serie: str = Field(default="", max_length=80) class MercanciaCove(BaseModel): @@ -55,7 +55,7 @@ class MercanciaCove(BaseModel): cantidad: Decimal = Field(..., gt=0) valor_unitario: Decimal = Field(..., ge=0) valor_total: Decimal = Field(..., ge=0) - valor_dolares: Optional[Decimal] = Field(None, ge=0) + valor_dolares: Decimal = Field(default=Decimal("0"), ge=0) descripcion_especifica: List[DescripcionEspecifica] = Field(default_factory=list) diff --git a/backend/api/v1/modules/a76/factura_cove/service.py b/backend/api/v1/modules/a76/factura_cove/service.py index b7ad4354..c7a57df9 100644 --- a/backend/api/v1/modules/a76/factura_cove/service.py +++ b/backend/api/v1/modules/a76/factura_cove/service.py @@ -295,30 +295,28 @@ class FacturaCoveDomainService: identificacion=identificacion, apellido_paterno="", apellido_materno="", - nombre=(cp.name or cp.short_name or "").strip() or None, - calle=(addr.streets or "").strip() if addr and addr.streets else None, + nombre=(cp.name or cp.short_name or "").strip(), + calle=(addr.streets or "").strip() if addr and addr.streets else "", numero_exterior=(addr.exterior_number or "").strip() if addr and addr.exterior_number - else None, - # El API de COVE exige texto (no null) para numero_interior y municipio. - # Si no hay valor, enviamos cadena vacía. + else "", numero_interior=(addr.interior_number or "").strip() if addr else "", colonia=(addr.neighborhood or "").strip() if addr and addr.neighborhood - else None, - localidad=(addr.city or "").strip() if addr and addr.city else None, + else "", + localidad=(addr.city or "").strip() if addr and addr.city else "", municipio=(addr.municipality or "").strip() if addr else "", entidad_federativa=(addr.state or "").strip() if addr and addr.state - else None, + else "", pais=country_code, codigo_postal=(addr.postal_code or "").strip() if addr and addr.postal_code - else None, + else "", ) def _company_to_persona(self, company: Company) -> PersonaCove: @@ -343,28 +341,28 @@ class FacturaCoveDomainService: identificacion=(company.rfc or "").strip().upper(), apellido_paterno="", apellido_materno="", - nombre=(company.name or "").strip() or None, - calle=(addr.street or "").strip() if addr and addr.street else None, + nombre=(company.name or "").strip(), + calle=(addr.street or "").strip() if addr and addr.street else "", numero_exterior=(addr.exterior_number or "").strip() if addr and addr.exterior_number - else None, + else "", numero_interior=(addr.interior_number or "").strip() if addr else "", colonia=(addr.neighborhood or "").strip() if addr and addr.neighborhood - else None, - localidad=(addr.city or "").strip() if addr and addr.city else None, + else "", + localidad=(addr.city or "").strip() if addr and addr.city else "", municipio=(addr.municipality or "").strip() if addr else "", entidad_federativa=(addr.state or "").strip() if addr and addr.state - else None, + else "", pais=country_code, codigo_postal=(addr.postal_code or "").strip() if addr and addr.postal_code - else None, + else "", ) def _build_personas( diff --git a/backend/api/v1/modules/a76/general_catalogs/company/submodels/certification.py b/backend/api/v1/modules/a76/general_catalogs/company/submodels/certification.py index ee70040a..088071e5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/submodels/certification.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/submodels/certification.py @@ -4,7 +4,7 @@ Modelo de certificaciones de empresa from typing import Optional, TYPE_CHECKING from datetime import date -from sqlalchemy import Integer, String, ForeignKey, Boolean, Date +from sqlalchemy import String, ForeignKey, Boolean, Date, Integer from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base from api.v1.common.base_models import TimestampMixin diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/service.py b/backend/api/v1/modules/a76/general_catalogs/concepts/service.py index b0117771..4e50fc62 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/service.py @@ -27,6 +27,14 @@ class ConceptService: Concept.company_id == company_id ) + if filters: + if filters.get("code"): + query = query.filter(Concept.code.ilike(f"%{filters['code']}%")) + if filters.get("description"): + query = query.filter( + Concept.description.ilike(f"%{filters['description']}%") + ) + total = query.count() items = query.offset(skip).limit(limit).all() diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_dto.py b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_dto.py new file mode 100644 index 00000000..72ef6aab --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_dto.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class DodaAltaLogCreateDTO(BaseModel): + doda_id: Optional[int] = None + variant: Optional[str] = Field(None, max_length=10) + responsible: Optional[str] = Field(None, max_length=20) + patent: Optional[str] = Field(None, max_length=10) + dispatch_customs: Optional[str] = Field(None, max_length=10) + operation_type: Optional[str] = Field(None, max_length=5) + integration_number: Optional[str] = Field(None, max_length=50) + task_id: Optional[str] = Field(None, max_length=255) + status: Optional[str] = Field(None, max_length=30) + message: Optional[str] = Field(None, max_length=2000) + result_json: Optional[str] = None + + +class DodaAltaLogUpdateDTO(BaseModel): + status: Optional[str] = Field(None, max_length=30) + message: Optional[str] = Field(None, max_length=2000) + result_json: Optional[str] = None + + +class DodaAltaLogResponseDTO(BaseModel): + id: int + doda_id: Optional[int] = None + variant: Optional[str] = None + responsible: Optional[str] = None + patent: Optional[str] = None + dispatch_customs: Optional[str] = None + operation_type: Optional[str] = None + integration_number: Optional[str] = None + task_id: Optional[str] = None + status: Optional[str] = None + message: Optional[str] = None + result_json: Optional[str] = None + company_id: int + tenant_id: int + created_at: Optional[str] = None + updated_at: Optional[str] = None + + model_config = {"from_attributes": True} + + +class DodaAltaLogListResponse(BaseModel): + items: List[DodaAltaLogResponseDTO] + total: int + page: int + page_size: int diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_models.py b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_models.py new file mode 100644 index 00000000..581c5f55 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_models.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from sqlalchemy import Integer, String, Text +from sqlalchemy.orm import Mapped, mapped_column + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base + + +class DodaAltaLog(Base, TenantScopedMixin, TimestampMixin): + """ + Registro histórico de envíos de alta DODA/PITA al servicio externo. + Cada fila corresponde a un intento de alta para un DODA específico. + """ + + __tablename__ = "doda_alta_log" + __table_args__ = ({"schema": "a76"},) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + # Referencia al DODA origen + doda_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True) + + # Tipo de alta (doda / pita) + variant: Mapped[str | None] = mapped_column(String(10), nullable=True) + + # Datos copiados del DODA al momento del envío (para historial) + responsible: Mapped[str | None] = mapped_column(String(20), nullable=True) + patent: Mapped[str | None] = mapped_column(String(10), nullable=True) + dispatch_customs: Mapped[str | None] = mapped_column(String(10), nullable=True) + operation_type: Mapped[str | None] = mapped_column(String(5), nullable=True) + integration_number: Mapped[str | None] = mapped_column(String(50), nullable=True) + + # Respuesta del servicio externo + task_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) + status: Mapped[str | None] = mapped_column(String(30), nullable=True) + message: Mapped[str | None] = mapped_column(String(2000), nullable=True) + result_json: Mapped[str | None] = mapped_column(Text, nullable=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_service.py b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_service.py new file mode 100644 index 00000000..f4b83615 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/alta_log_service.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import json +import logging +from typing import Optional + +from sqlalchemy.orm import Session + +from .alta_log_dto import ( + DodaAltaLogCreateDTO, + DodaAltaLogListResponse, + DodaAltaLogResponseDTO, + DodaAltaLogUpdateDTO, +) +from .alta_log_models import DodaAltaLog +from .models import Doda + +logger = logging.getLogger(__name__) + + +class DodaAltaLogService: + + @staticmethod + def list( + db: Session, + company_id: int, + tenant_id: int, + page: int = 1, + page_size: int = 50, + doda_id: Optional[int] = None, + search: Optional[str] = None, + ) -> DodaAltaLogListResponse: + query = ( + db.query(DodaAltaLog) + .filter( + DodaAltaLog.company_id == company_id, + DodaAltaLog.tenant_id == tenant_id, + DodaAltaLog.deleted_at.is_(None), + ) + ) + if doda_id: + query = query.filter(DodaAltaLog.doda_id == doda_id) + if search: + like = f"%{search}%" + query = query.filter( + DodaAltaLog.task_id.ilike(like) + | DodaAltaLog.integration_number.ilike(like) + | DodaAltaLog.patent.ilike(like) + | DodaAltaLog.status.ilike(like) + ) + total = query.count() + items = ( + query.order_by(DodaAltaLog.id.desc()) + .offset((page - 1) * page_size) + .limit(page_size) + .all() + ) + return DodaAltaLogListResponse( + items=[DodaAltaLogResponseDTO.model_validate(r) for r in items], + total=total, + page=page, + page_size=page_size, + ) + + @staticmethod + def get( + db: Session, record_id: int, company_id: int, tenant_id: int + ) -> Optional[DodaAltaLog]: + return ( + db.query(DodaAltaLog) + .filter( + DodaAltaLog.id == record_id, + DodaAltaLog.company_id == company_id, + DodaAltaLog.tenant_id == tenant_id, + DodaAltaLog.deleted_at.is_(None), + ) + .first() + ) + + @staticmethod + def create( + db: Session, dto: DodaAltaLogCreateDTO, company_id: int, tenant_id: int + ) -> DodaAltaLog: + record = DodaAltaLog( + company_id=company_id, + tenant_id=tenant_id, + **dto.model_dump(exclude_none=False), + ) + db.add(record) + db.commit() + db.refresh(record) + return record + + @staticmethod + def update( + db: Session, record: DodaAltaLog, dto: DodaAltaLogUpdateDTO + ) -> DodaAltaLog: + for field, value in dto.model_dump(exclude_unset=True).items(): + setattr(record, field, value) + db.commit() + db.refresh(record) + return record + + @staticmethod + def delete(db: Session, record: DodaAltaLog) -> None: + from datetime import datetime + record.deleted_at = datetime.utcnow() + db.commit() + + @staticmethod + def create_from_alta_result( + db: Session, + doda: Doda, + company_id: int, + tenant_id: int, + variant: str, + ext_result: dict, + ) -> DodaAltaLog: + """ + Crea un registro de log a partir de la respuesta del servicio externo de alta. + Llamado automáticamente al completar `POST /{doda_id}/alta`. + """ + task_id = ext_result.get("task_id") or ext_result.get("id") or "" + status = ext_result.get("status") or "pending" + message = ext_result.get("message") or "" + + dto = DodaAltaLogCreateDTO( + doda_id=doda.id, + variant=variant, + responsible=doda.responsible, + patent=doda.patent, + dispatch_customs=doda.dispatch_customs, + operation_type=doda.operation_type, + integration_number=doda.integration_number, + task_id=task_id, + status=status, + message=message, + result_json=json.dumps(ext_result), + ) + return DodaAltaLogService.create(db, dto, company_id, tenant_id) diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/alta_service.py b/backend/api/v1/modules/a76/general_catalogs/doda/alta_service.py new file mode 100644 index 00000000..ffca5a57 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/alta_service.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import base64 +import logging +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from cryptography.hazmat.primitives import padding as crypto_padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from core.config import settings +from core.storage_s3 import get_object_bytes, object_exists + +from api.v1.modules.a76.customs_brokers import models as cb_models + +from .models import Doda, DodaContainer, DodaAmericanPedimento, DodaPedimento +from .payload_normalizer import ( + normalize_aduana_despacho, + normalize_aduana_seccion, + normalize_caat, + normalize_doda_pedimento_row, + normalize_fast_id, + normalize_id_transporte, + normalize_numero_gafete, + normalize_patente, + normalize_tipo_operacion, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Response schemas (inline para no añadir dependencias externas) +# --------------------------------------------------------------------------- + +@dataclass +class ElegibilidadReason: + field: str + message: str + solution: str = "" + + +@dataclass +class ElegibilidadResponse: + can_alta: bool + reasons: List[ElegibilidadReason] = field(default_factory=list) + + +# --------------------------------------------------------------------------- +# FIEL encryption (mismo esquema AES-256-CBC que COVE / expediente_archivos) +# --------------------------------------------------------------------------- + +def _encrypt_fiel(raw_fiel: str) -> str: + normalized = (raw_fiel or "").strip() + if not normalized: + return "" + key_bytes = (settings.COVE_FIEL_HASH_KEY or "").encode("utf-8") + iv_bytes = (settings.COVE_FIEL_HASH_IV or "").encode("utf-8") + if not key_bytes or not iv_bytes: + return normalized + key32 = key_bytes[:32].ljust(32, b"\0") + iv16 = iv_bytes[:16].ljust(16, b"\0") + padder = crypto_padding.PKCS7(algorithms.AES.block_size).padder() + padded = padder.update(normalized.encode("utf-8")) + padder.finalize() + cipher = Cipher(algorithms.AES(key32), modes.CBC(iv16)) + enc = cipher.encryptor() + encrypted = enc.update(padded) + enc.finalize() + return base64.b64encode(encrypted).decode("ascii") + + +# --------------------------------------------------------------------------- +# Main service +# --------------------------------------------------------------------------- + +class DodaAltaService: + """ + Servicio de dominio para construir el payload de alta DODA y verificar + elegibilidad antes de enviarlo al servicio externo. + """ + + def __init__(self, db: Session) -> None: + self.db = db + + # ------------------------------------------------------------------ + # Broker resolution + # ------------------------------------------------------------------ + + def _resolve_broker( + self, + responsible_key: Optional[str], + company_id: int, + tenant_id: int, + ) -> Optional[cb_models.CustomsBroker]: + """ + Resuelve el agente aduanal a partir de Doda.responsible (ClaveAA del legacy). + Prioridad: broker_key exacto → license exacto. + """ + normalized = (responsible_key or "").strip() + if not normalized: + return None + + brokers = ( + self.db.query(cb_models.CustomsBroker) + .filter( + or_( + cb_models.CustomsBroker.broker_key == normalized, + cb_models.CustomsBroker.license == normalized, + ), + cb_models.CustomsBroker.company_id == company_id, + cb_models.CustomsBroker.tenant_id == tenant_id, + cb_models.CustomsBroker.deleted_at.is_(None), + ) + .order_by(cb_models.CustomsBroker.id.desc()) + .all() + ) + if not brokers: + return None + + exact = next( + (b for b in brokers if (b.broker_key or "").strip() == normalized), + None, + ) + return exact or brokers[0] + + # ------------------------------------------------------------------ + # configuracion_vu DODA + # ------------------------------------------------------------------ + + def _build_configuracion_vu_doda( + self, + broker: cb_models.CustomsBroker, + errors: List[ElegibilidadReason], + ) -> Optional[Dict[str, Any]]: + """ + Construye configuracion_vu usando los campos DODA del CustomsBrokerVU: + doda_certificate_path, doda_key_path, doda_fiel_access_key. + """ + vu = broker.vu if broker else None + + if not vu: + errors.append(ElegibilidadReason( + field="vu", + message="El agente aduanal no tiene configuración VU.", + solution="Configura la sección VU/DODA del agente aduanal.", + )) + return None + + doda_cert_path = (getattr(vu, "doda_certificate_path", None) or "").strip() + doda_key_path = (getattr(vu, "doda_key_path", None) or "").strip() + doda_fiel = (getattr(vu, "doda_fiel_access_key", None) or "").strip() + + if not doda_cert_path or not doda_key_path: + errors.append(ElegibilidadReason( + field="vu.doda_certificate_path", + message="Faltan rutas de certificado o llave DODA en la configuración VU del agente.", + solution="Sube el .cer y .key DODA en la pestaña DODA del agente aduanal.", + )) + return None + + if not doda_fiel: + errors.append(ElegibilidadReason( + field="vu.doda_fiel_access_key", + message="La clave FIEL DODA no está configurada en la VU del agente.", + solution="Captura la clave FIEL DODA en la configuración VU del agente aduanal.", + )) + return None + + cer_b64: Optional[str] = None + key_b64: Optional[str] = None + try: + if not object_exists(doda_cert_path): + errors.append(ElegibilidadReason( + field="vu.doda_certificate_path", + message="El certificado DODA no existe en el almacenamiento.", + solution="Vuelve a subir el .cer DODA en la configuración VU del agente.", + )) + else: + cer_b64 = base64.b64encode(get_object_bytes(doda_cert_path)).decode("ascii") + + if not object_exists(doda_key_path): + errors.append(ElegibilidadReason( + field="vu.doda_key_path", + message="La llave DODA no existe en el almacenamiento.", + solution="Vuelve a subir el .key DODA en la configuración VU del agente.", + )) + else: + key_b64 = base64.b64encode(get_object_bytes(doda_key_path)).decode("ascii") + except Exception: + logger.exception("Error leyendo certificados DODA desde S3") + errors.append(ElegibilidadReason( + field="vu", + message="Error leyendo certificados DODA desde el almacenamiento.", + solution="Verifica la configuración de S3/MinIO y las rutas de los certificados.", + )) + return None + + if errors: + return None + + rfc_ciec = ( + (getattr(vu, "query_tax_id", None) or "").strip() + or (getattr(broker, "tax_id", None) or "").strip() + ) + + clave_fiel = _encrypt_fiel(doda_fiel) + + return { + "rfc_ciec": rfc_ciec, + "archivo_cer_base64": cer_b64 or "", + "archivo_key_base64": key_b64 or "", + "clave_fiel": clave_fiel, + } + + def _attach_user_email( + self, configuracion_vu: Dict[str, Any], user_email: str + ) -> Dict[str, Any]: + """Agrega el email del usuario autenticado al bloque configuracion_vu.""" + configuracion_vu["email"] = (user_email or "").strip() + return configuracion_vu + + # ------------------------------------------------------------------ + # Payload builders for child collections + # ------------------------------------------------------------------ + + def _build_contenedores( + self, containers: List[DodaContainer] + ) -> List[Dict[str, Any]]: + result = [] + for c in containers: + candados = [] + for seal in (c.seals_detail or []): + if seal.seal_value: + candados.append({"candado": seal.seal_value}) + # Fallback: si no hay filas en seals_detail pero hay string legado en seals + if not candados and c.seals: + for raw in c.seals.split(","): + val = raw.strip() + if val: + candados.append({"candado": val}) + val = (c.container_value or "").strip() + result.append({ + "valor_contenedor": val, + "candados": candados, + }) + return result + + def _build_pedimentos_americanos( + self, american_pedimentos: List[DodaAmericanPedimento] + ) -> List[Dict[str, Any]]: + return [ + { + "tipo_pedimento_americano": p.american_pedimento_type or "", + "valor_pedimento_americano": p.american_pedimento_value or "", + } + for p in american_pedimentos + ] + + def _build_pedimentos( + self, pedimentos_detail: List[DodaPedimento] + ) -> List[Dict[str, Any]]: + out: List[Dict[str, Any]] = [] + for p in pedimentos_detail: + row = normalize_doda_pedimento_row( + document=p.document, + authorization_patent=p.authorization_patent, + shipment=p.shipment, + cove=p.cove, + umc=p.umc, + dta_niu=p.dta_niu, + pedimento_type=p.pedimento_type, + effective=p.effective_amount_usd, + diff=p.difference_amount_usd, + ) + out.append(row) + return out + + # ------------------------------------------------------------------ + # Elegibilidad (validaciones del legacy Clarion activas) + # ------------------------------------------------------------------ + + def check_elegibilidad( + self, + doda_id: int, + tenant_id: int, + company_id: int, + variant: str = "doda", + user_email: Optional[str] = None, + ) -> ElegibilidadResponse: + """ + Verifica si el DODA cumple los requisitos para enviar el alta. + Porta las validaciones activas del código Clarion legacy. + """ + reasons: List[ElegibilidadReason] = [] + + # --- Email del usuario autenticado --- + if not (user_email or "").strip(): + reasons.append(ElegibilidadReason( + field="user_email", + message="El usuario no tiene correo electrónico registrado.", + solution="Configura un correo electrónico en tu perfil de Keycloak.", + )) + + doda = self.db.query(Doda).filter( + Doda.id == doda_id, + Doda.tenant_id == tenant_id, + Doda.company_id == company_id, + ).first() + + if not doda: + reasons.append(ElegibilidadReason( + field="doda_id", + message=f"DODA {doda_id} no encontrado.", + )) + return ElegibilidadResponse(can_alta=False, reasons=reasons) + + # --- Campos obligatorios (ramas activas del legacy) --- + if not (doda.responsible or "").strip(): + reasons.append(ElegibilidadReason( + field="responsible", + message="El campo Responsable se encuentra vacío.", + solution="Captura la clave del agente aduanal responsable.", + )) + + if not (doda.dispatch_customs or "").strip(): + reasons.append(ElegibilidadReason( + field="dispatch_customs", + message="El campo Aduana de Despacho se encuentra vacío.", + solution="Captura la clave de la aduana de despacho.", + )) + + if not (doda.customs_sections or "").strip(): + reasons.append(ElegibilidadReason( + field="customs_sections", + message="El campo Aduana Sección (E/S) se encuentra vacío.", + solution="Captura la sección aduanera.", + )) + + if not (doda.operation_type or "").strip(): + reasons.append(ElegibilidadReason( + field="operation_type", + message="El campo Tipo de Operación se encuentra vacío.", + solution="Selecciona el tipo de operación.", + )) + + if not (doda.caat or "").strip(): + reasons.append(ElegibilidadReason( + field="caat", + message="El campo CAAT se encuentra vacío.", + solution="Captura el código CAAT del transportista.", + )) + + if not (doda.transport_identification or "").strip(): + reasons.append(ElegibilidadReason( + field="transport_identification", + message="El campo Identificación de Transporte se encuentra vacío.", + solution="Captura el número de identificación del transporte.", + )) + + if not (doda.patent or "").strip(): + reasons.append(ElegibilidadReason( + field="patent", + message="El campo Patente se encuentra vacío.", + solution="La patente se llena automáticamente al seleccionar el responsable.", + )) + + # --- Gafete único: obligatorio si variant=doda --- + if variant.lower() == "doda": + if not (doda.unique_badge_number or "").strip(): + reasons.append(ElegibilidadReason( + field="unique_badge_number", + message="El campo Número de Gafete Único es obligatorio para el Alta DODA.", + solution="Captura el número de gafete único del conductor.", + )) + + # --- Contenedores máximo 4 (legacy: IF SQL2:C2 = 4 THEN MESSAGE) --- + containers = doda.containers or [] + if len(containers) > 4: + reasons.append(ElegibilidadReason( + field="containers", + message=f"El DODA tiene {len(containers)} contenedores. El máximo permitido es 4.", + solution="Elimina los contenedores sobrantes antes de enviar el alta.", + )) + + # --- Precintos: máximo 8 en todo el DODA (legacy gDoda_Contenedores_Candados) --- + seal_count = 0 + for c in containers: + details = getattr(c, "seals_detail", None) or [] + if details: + seal_count += len(details) + else: + legacy = (getattr(c, "seals", None) or "").strip() + if legacy: + seal_count += len([s for s in legacy.split(",") if s.strip()]) + if seal_count > 8: + reasons.append(ElegibilidadReason( + field="containers", + message=f"El DODA tiene {seal_count} precintos. El máximo permitido es 8.", + solution="Elimina precintos hasta quedar en 8 o menos.", + )) + + # --- Pedimentos americanos: tipo obligatorio y rango según operación (legacy, salvo PITA) --- + clearance = getattr(doda, "customs_clearance", None) + if clearance != 1: + op = (doda.operation_type or "").strip().upper() + if op in ("I", "1"): + allowed_tipo = {"1", "2", "3", "4", "5"} + elif op in ("E", "2"): + allowed_tipo = {"6", "7", "8"} + else: + allowed_tipo = set() + for idx, ap in enumerate(doda.american_pedimentos or [], 1): + tipo = (ap.american_pedimento_type or "").strip() + if not tipo: + reasons.append(ElegibilidadReason( + field="american_pedimentos", + message=f"Pedimento americano (línea {idx}): el tipo es obligatorio para este tipo de despacho.", + solution="Captura el tipo de pedimento americano (1–5 importación, 6–8 exportación).", + )) + elif allowed_tipo and tipo not in allowed_tipo: + reasons.append(ElegibilidadReason( + field="american_pedimentos", + message=( + f"Pedimento americano (línea {idx}): el tipo '{tipo}' no corresponde al tipo de operación." + ), + solution="Corrige el tipo según importación (1–5) o exportación (6–8).", + )) + + # --- Patente vs. patente del agente aduanal (legacy: DODA:Patente <> AgeAdu:Patente) --- + broker = self._resolve_broker(doda.responsible, company_id, tenant_id) + + if (doda.responsible or "").strip() and not broker: + reasons.append(ElegibilidadReason( + field="responsible", + message=f"La clave de Responsable '{doda.responsible}' no existe en el catálogo de agentes aduanales.", + solution="Selecciona un agente aduanal válido del catálogo.", + )) + elif broker and (doda.patent or "").strip(): + broker_patent = (broker.license or "").strip() + doda_patent = (doda.patent or "").strip() + if broker_patent and doda_patent and doda_patent != broker_patent: + reasons.append(ElegibilidadReason( + field="patent", + message=( + f"La patente declarada en el DODA '{doda_patent}' es distinta " + f"a la patente del responsable '{broker_patent}'." + ), + solution="Verifica o actualiza la patente del DODA para que coincida con la del agente.", + )) + + # --- Certificados DODA en VU del agente (equivalente a RutaArchivosXMLDODA) --- + if broker: + vu = broker.vu + if not vu: + reasons.append(ElegibilidadReason( + field="vu", + message="El agente aduanal no tiene configuración VU.", + solution="Configura la sección VU/DODA del agente aduanal.", + )) + else: + if not (getattr(vu, "doda_certificate_path", None) or "").strip(): + reasons.append(ElegibilidadReason( + field="vu.doda_certificate_path", + message="No hay certificado DODA (.cer) configurado en la VU del agente.", + solution="Sube el certificado .cer DODA en la pestaña DODA del agente aduanal.", + )) + if not (getattr(vu, "doda_key_path", None) or "").strip(): + reasons.append(ElegibilidadReason( + field="vu.doda_key_path", + message="No hay llave DODA (.key) configurada en la VU del agente.", + solution="Sube la llave .key DODA en la pestaña DODA del agente aduanal.", + )) + if not (getattr(vu, "doda_fiel_access_key", None) or "").strip(): + reasons.append(ElegibilidadReason( + field="vu.doda_fiel_access_key", + message="La clave FIEL DODA no está configurada en la VU del agente.", + solution="Captura la clave FIEL DODA en la configuración VU del agente.", + )) + + return ElegibilidadResponse( + can_alta=len(reasons) == 0, + reasons=reasons, + ) + + # ------------------------------------------------------------------ + # Build full payload + # ------------------------------------------------------------------ + + def build_alta_payload( + self, + doda_id: int, + tenant_id: int, + company_id: int, + variant: str = "doda", + user_email: str = "", + ) -> Dict[str, Any]: + """ + Construye el payload completo para POST /api/v1/doda/alta. + Lanza ValueError si hay problemas de configuración críticos. + """ + doda = self.db.query(Doda).filter( + Doda.id == doda_id, + Doda.tenant_id == tenant_id, + Doda.company_id == company_id, + ).first() + + if not doda: + raise ValueError(f"DODA {doda_id} no encontrado.") + + broker = self._resolve_broker(doda.responsible, company_id, tenant_id) + if not broker: + raise ValueError( + f"No se encontró el agente aduanal con clave '{doda.responsible}'." + ) + + errors: List[ElegibilidadReason] = [] + configuracion_vu = self._build_configuracion_vu_doda(broker, errors) + if errors or not configuracion_vu: + msgs = "; ".join(r.message for r in errors) + raise ValueError(f"Error en configuración VU DODA: {msgs}") + + self._attach_user_email(configuracion_vu, user_email) + + containers = doda.containers or [] + american_pedimentos = doda.american_pedimentos or [] + pedimentos_detail = doda.pedimentos_detail or [] + + # El API externo espera "1" para DODA y "2" para PITA (ver DODARequest.despacho_aduanero) + despacho_aduanero = "1" if variant.lower() == "doda" else "2" + + payload: Dict[str, Any] = { + "configuracion_vu": configuracion_vu, + "despacho_aduanero": despacho_aduanero, + "numero_gafete_unico": normalize_numero_gafete( + doda.unique_badge_number + ), + "aduana_despacho": normalize_aduana_despacho(doda.dispatch_customs), + "aduana_seccion": normalize_aduana_seccion(doda.customs_sections), + "patente": normalize_patente(doda.patent), + "caat": normalize_caat(doda.caat), + "id_transporte": normalize_id_transporte(doda.transport_identification), + "fast_id": normalize_fast_id(doda.fast_id), + "tipo_operacion": normalize_tipo_operacion(doda.operation_type), + "contenedores": self._build_contenedores(containers), + "pedimentos_americanos": self._build_pedimentos_americanos(american_pedimentos), + "cfdi_carta_porte": {"cfdi_carta_porte": ""}, + "pedimentos": self._build_pedimentos(pedimentos_detail), + } + + return payload diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/dto.py b/backend/api/v1/modules/a76/general_catalogs/doda/dto.py index 0d188199..5e7b1822 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/dto.py @@ -6,7 +6,7 @@ from datetime import datetime from decimal import Decimal from typing import Optional, List -from pydantic import BaseModel, Field +from pydantic import AliasChoices, BaseModel, Field # ============ DODA CONTAINER SEAL DTOS ============ @@ -24,7 +24,9 @@ class DodaContainerSealResponseDTO(BaseModel): """DTO para responder con datos de un candado""" id: int - doda_sys_id: int + doda_sys_id: int = Field( + validation_alias=AliasChoices("doda_sys_id", "doda_id") + ) seal_line: int seal_value: Optional[str] = None @@ -62,7 +64,9 @@ class DodaContainerResponseDTO(BaseModel): """DTO para responder con datos de un contenedor""" id: int - doda_sys_id: int + doda_sys_id: int = Field( + validation_alias=AliasChoices("doda_sys_id", "doda_id") + ) container_line: int container_value: Optional[str] = None seals: Optional[str] = None @@ -105,7 +109,9 @@ class DodaAmericanPedimentoResponseDTO(BaseModel): """DTO para responder con datos de un pedimento americano""" id: int - doda_sys_id: int + doda_sys_id: int = Field( + validation_alias=AliasChoices("doda_sys_id", "doda_id") + ) american_pedimento_line: int american_pedimento_type: Optional[str] = None american_pedimento_value: Optional[str] = None @@ -183,7 +189,9 @@ class DodaPedimentoResponseDTO(BaseModel): """DTO para responder con datos de un pedimento DODA""" id: int - doda_sys_id: int + doda_sys_id: int = Field( + validation_alias=AliasChoices("doda_sys_id", "doda_id") + ) pedimento_line: int authorization_patent: Optional[str] = None document: Optional[str] = None @@ -369,6 +377,9 @@ class DodaResponseDTO(BaseModel): sat_digital_seal: Optional[str] = None xml_doda_sent_path: Optional[str] = None xml_doda_response_path: Optional[str] = None + doda_report_pdf_path: Optional[str] = None + doda_report_pdf_generated_at: Optional[datetime] = None + doda_report_source_fingerprint: Optional[str] = None sat_original_chain: Optional[str] = None customs_clearance: Optional[int] = None unique_badge_number: Optional[str] = None @@ -412,6 +423,9 @@ class DodaDetailResponseDTO(BaseModel): sat_digital_seal: Optional[str] = None xml_doda_sent_path: Optional[str] = None xml_doda_response_path: Optional[str] = None + doda_report_pdf_path: Optional[str] = None + doda_report_pdf_generated_at: Optional[datetime] = None + doda_report_source_fingerprint: Optional[str] = None sat_original_chain: Optional[str] = None customs_clearance: Optional[int] = None unique_badge_number: Optional[str] = None diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/export_service.py b/backend/api/v1/modules/a76/general_catalogs/doda/export_service.py new file mode 100644 index 00000000..feffe1d3 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/export_service.py @@ -0,0 +1,318 @@ +""" +Exportación de listado DODA a CSV / TSV (xls) / pipe, alineada al reporte legacy GDoda. +""" + +from __future__ import annotations + +import csv +import io +from datetime import date, datetime +from enum import Enum +from typing import Any, List, Optional + +from sqlalchemy import and_ +from sqlalchemy.orm import Session + +from .models import Doda, DodaPedimento + +# Encabezados (orden legacy Clarion) +EXPORT_HEADERS: List[str] = [ + "SYSID", + "NUM INTEGRACIÓN", + "FECHA", + "HORA", + "ADUANA", + "ADUANA ES", + "PATENTE", + "PEDIMENTOS", + "CAAT", + "IDEN. TRANSPORTE", + "FAST_ID", + "TIPO OPERACIÓN", + "RESPONSABLE", + "TRANSPORTISTA", + "REMESAS", + "TIPO PEDIMENTO", + "CADENA ORIGINIAL", + "NUMERO SERIE", + "FIRMA ELECTRÓNICA", + "NO TRANSACCIÓN", + "ESTATUS", + "LINQSQTQR", + "SAT_CERTIFICADO", + "SELLO DIGITAL", + "PATH XML ENVÍO", + "PATH XML RESPUESTA", + "SAT_CADENA ORIGINAL", + "DESPACHO ADUANERO", + "GAFETE ÚNICO", + "USUARIO", +] + + +class DodaExportFormat(str, Enum): + csv = "csv" + xls = "xls" + txt = "txt" + + +def _parse_iso_date(s: str) -> date: + s = (s or "").strip() + for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%d/%m/%Y", "%d-%m-%Y"): + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + raise ValueError(f"Fecha inválida: {s!r} (use YYYY-MM-DD)") + + +def _date_to_yyyymmdd(d: date) -> int: + return d.year * 10000 + d.month * 100 + d.day + + +def _format_doda_date_formatted(doda_date: Optional[int]) -> str: + if doda_date is None: + return "" + s = str(doda_date) + if len(s) == 8 and s.isdigit(): + y, m, d = s[:4], s[4:6], s[6:8] + return f"{d}/{m}/{y}" + return s + + +def _format_doda_time(doda_time: Optional[int]) -> str: + if doda_time is None: + return "" + t = int(doda_time) + s = str(t) + if len(s) <= 2: + return s + if len(s) == 4: + return f"{s[:2]}:{s[2:4]}" + if len(s) == 6: + return f"{s[:2]}:{s[2:4]}:{s[4:6]}" + if len(s) > 6: + return s[:2] + ":" + s[2:4] + ":" + s[4:6] + return s + + +def _as_text(value: Any) -> str: + if value is None: + return "" + if isinstance(value, bool): + return "1" if value else "0" + s = str(value) + s = s.replace("\r\n", " ").replace("\n", " ").replace("\r", " ") + return s + + +def doda_row_values( + row: Doda, *, date_mode: str +) -> List[str]: + """date_mode: 'raw' | 'formatted' (legacy FechaJul branch).""" + if date_mode == "raw": + fecha = _as_text(row.doda_date) + hora = _as_text(row.doda_time) + else: + fecha = _format_doda_date_formatted(row.doda_date) + hora = _format_doda_time(row.doda_time) + + return [ + _as_text(row.id), + _as_text(row.integration_number), + fecha, + hora, + _as_text(row.dispatch_customs), + _as_text(row.customs_sections), + _as_text(row.patent), + _as_text(row.pedimentos), + _as_text(row.caat), + _as_text(row.transport_identification), + _as_text(row.fast_id), + _as_text(row.operation_type), + _as_text(row.responsible), + _as_text(row.carrier), + _as_text(row.shipments), + _as_text(row.pedimento_type), + _as_text(row.original_chain), + _as_text(row.serial_number), + _as_text(row.electronic_signature), + _as_text(row.transaction_number), + _as_text(row.status), + _as_text(row.linq_sat_qr), + _as_text(row.sat_certificate), + _as_text(row.sat_digital_seal), + _as_text(row.xml_doda_sent_path), + _as_text(row.xml_doda_response_path), + _as_text(row.sat_original_chain), + _as_text(row.customs_clearance), + _as_text(row.unique_badge_number), + _as_text(row.last_user), + ] + + +def _delimiter_for_format(fmt: DodaExportFormat) -> str: + if fmt == DodaExportFormat.csv: + return "," + if fmt == DodaExportFormat.xls: + return "\t" + if fmt == DodaExportFormat.txt: + return "|" + return "," + + +def _content_type_and_filename(fmt: DodaExportFormat) -> tuple[str, str]: + if fmt == DodaExportFormat.csv: + return "text/csv; charset=utf-8", "doda_export.csv" + if fmt == DodaExportFormat.xls: + return "application/vnd.ms-excel; charset=utf-8", "doda_export.xls" + return "text/plain; charset=utf-8", "doda_export.txt" + + +def list_dodas_in_date_range( + db: Session, + *, + tenant_id: int, + company_id: int, + date_start: int, + date_end: int, +) -> List[Doda]: + return ( + db.query(Doda) + .filter( + and_( + Doda.tenant_id == tenant_id, + Doda.company_id == company_id, + Doda.doda_date.isnot(None), + Doda.doda_date >= date_start, + Doda.doda_date <= date_end, + ) + ) + .order_by(Doda.doda_date.asc(), Doda.id.asc()) + .all() + ) + + +def build_export_text( + rows: List[Doda], + *, + export_format: DodaExportFormat, + date_mode: str = "formatted", +) -> str: + delim = _delimiter_for_format(export_format) + out = io.StringIO() + w = csv.writer( + out, + delimiter=delim, + quoting=csv.QUOTE_MINIMAL, + lineterminator="\r\n", + ) + w.writerow(EXPORT_HEADERS) + for r in rows: + w.writerow(doda_row_values(r, date_mode=date_mode)) + return out.getvalue() + + +def parse_export_params( + date_from: str, + date_to: str, + format_str: str, + date_mode: str, +) -> tuple[int, int, DodaExportFormat, str]: + d0 = _date_to_yyyymmdd(_parse_iso_date(date_from)) + d1 = _date_to_yyyymmdd(_parse_iso_date(date_to)) + if d0 > d1: + raise ValueError("date_from no puede ser posterior a date_to") + try: + fmt = DodaExportFormat(format_str.lower().strip()) + except ValueError: + raise ValueError("format debe ser csv, xls o txt") + mode = (date_mode or "formatted").lower().strip() + if mode not in ("raw", "formatted"): + raise ValueError("date_mode debe ser raw o formatted") + return d0, d1, fmt, mode + + +# --- Exportación de pedimentos (líneas) de un DODA específico (legacy / pantalla) --- + +PEDIMENTO_EXPORT_HEADERS: List[str] = [ + "PATENTE", + "DOCUMENTO", + "ACUSE_VA", + "REMESA", + "CANTIDAD", + "IMPORTE_USD", + "IMPORTE_DIF_USD", + "NIU", + "ARTICULO", +] + + +def _as_decimal_text(value: Any) -> str: + if value is None: + return "" + return _as_text(value) + + +def pedimento_row_values(row: DodaPedimento) -> List[str]: + """ + ACUSE_VA = COVE; CANTIDAD = UMC (captura típica en listado); + IMPORTE_USD / IMPORTE_DIF_USD = montos en USD; ARTICULO = art. 7 (0/1). + """ + return [ + _as_text(row.authorization_patent), + _as_text(row.document), + _as_text(row.cove), + _as_text(row.shipment), + _as_text(row.umc), + _as_decimal_text(row.effective_amount_usd), + _as_decimal_text(row.difference_amount_usd), + _as_text(row.dta_niu), + _as_text(row.article_7), + ] + + +def list_pedimentos_for_doda_export( + db: Session, + *, + tenant_id: int, + company_id: int, + doda_id: int, +) -> List[DodaPedimento]: + return ( + db.query(DodaPedimento) + .join(Doda, DodaPedimento.doda_id == Doda.id) + .filter( + Doda.id == doda_id, + Doda.tenant_id == tenant_id, + Doda.company_id == company_id, + ) + .order_by(DodaPedimento.pedimento_line.asc()) + .all() + ) + + +def build_pedimentos_export_text( + rows: List[DodaPedimento], + *, + export_format: DodaExportFormat, +) -> str: + delim = _delimiter_for_format(export_format) + out = io.StringIO() + w = csv.writer( + out, + delimiter=delim, + quoting=csv.QUOTE_MINIMAL, + lineterminator="\r\n", + ) + w.writerow(PEDIMENTO_EXPORT_HEADERS) + for r in rows: + w.writerow(pedimento_row_values(r)) + return out.getvalue() + + +def parse_pedimento_export_format(format_str: str) -> DodaExportFormat: + try: + return DodaExportFormat(format_str.lower().strip()) + except ValueError as e: + raise ValueError("format debe ser csv, xls o txt") from e diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/external_service.py b/backend/api/v1/modules/a76/general_catalogs/doda/external_service.py new file mode 100644 index 00000000..a972bb66 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/external_service.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import logging +from typing import Any, Dict + +import httpx + +from core.config import settings + +logger = logging.getLogger(__name__) + + +class DodaExternalService: + """ + Cliente HTTP para el servicio externo de alta DODA (API de Ventanilla Única). + + Endpoints: + POST {base_url}/api/v1/doda/alta + GET {base_url}/api/v1/doda/alta-status/{task_id} + + Usa COVE_API_URL como URL base (la misma variable que COVE y Expediente). + """ + + def __init__(self) -> None: + self.base_url = settings.COVE_API_URL.strip() + self.verify_ssl = settings.COVE_API_VERIFY_SSL + + def post_alta(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """ + Envía el payload de alta DODA al servicio externo. + Retorna {task_id, status, message}. + """ + url = f"{self.base_url.rstrip('/')}/api/v1/doda/alta" + + configuracion_vu = payload.get("configuracion_vu") or {} + logger.info( + "Enviando alta DODA: rfc_ciec=%s cer_len=%s key_len=%s clave_fiel_len=%s", + configuracion_vu.get("rfc_ciec"), + len(configuracion_vu.get("archivo_cer_base64") or ""), + len(configuracion_vu.get("archivo_key_base64") or ""), + len(configuracion_vu.get("clave_fiel") or ""), + ) + + with httpx.Client( + timeout=httpx.Timeout(60.0, connect=10.0), verify=self.verify_ssl + ) as client: + response = client.post(url, json=payload) + response.raise_for_status() + return response.json() + + def get_status(self, task_id: str) -> Dict[str, Any]: + """ + Consulta el estado de una tarea de alta DODA en el servicio externo. + """ + url = f"{self.base_url.rstrip('/')}/api/v1/doda/alta-status/{task_id}" + logger.debug("Consultando estado tarea DODA: task_id=%s url=%s", task_id, url) + + with httpx.Client( + timeout=httpx.Timeout(30.0, connect=10.0), verify=self.verify_ssl + ) as client: + response = client.get(url) + response.raise_for_status() + return response.json() diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/fingerprint.py b/backend/api/v1/modules/a76/general_catalogs/doda/fingerprint.py new file mode 100644 index 00000000..0687b871 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/fingerprint.py @@ -0,0 +1,132 @@ +""" +Huella de contenido (fingerprint) para invalidar el PDF de reporte DODA. +""" + +from __future__ import annotations + +import hashlib +import json +from datetime import date, datetime +from decimal import Decimal +from typing import Any, Dict + +from sqlalchemy import inspect as sa_inspect +from sqlalchemy.orm import Session + +from .models import ( + Doda, + DodaAmericanPedimento, + DodaContainer, + DodaContainerSeal, + DodaPedimento, +) + +_DODA_FINGERPRINT_EXCLUDE = frozenset( + { + "doda_report_pdf_path", + "doda_report_pdf_generated_at", + "doda_report_source_fingerprint", + "created_at", + "updated_at", + "deleted_at", + } +) + + +def _json_default(obj: Any) -> Any: + if isinstance(obj, Decimal): + return str(obj) + if isinstance(obj, (datetime, date)): + return obj.isoformat() + if isinstance(obj, (bytes, bytearray)): + return obj.hex() + raise TypeError(f"Type {type(obj)} not serializable") + + +def _instance_payload(instance: object, *, exclude: frozenset[str]) -> Dict[str, Any]: + insp = sa_inspect(instance) + d: Dict[str, Any] = {} + for col in insp.mapper.column_attrs: + name = col.key + if name in exclude or name in _DODA_FINGERPRINT_EXCLUDE: + continue + d[name] = getattr(instance, name) + return d + + +def build_doda_fingerprint(db: Session, doda_id: int) -> str: + doda = db.get(Doda, doda_id) + if doda is None: + raise ValueError("DODA no encontrado") + + doda_block = _instance_payload(doda, exclude=frozenset()) + + containers_rows = ( + db.query(DodaContainer) + .filter(DodaContainer.doda_id == doda_id) + .order_by(DodaContainer.container_line.asc()) + .all() + ) + container_blocks: list[Dict[str, Any]] = [] + for c in containers_rows: + c_block = _instance_payload( + c, + exclude=frozenset( + { + "id", + "doda_id", + } + ), + ) + seals = ( + db.query(DodaContainerSeal) + .filter(DodaContainerSeal.container_id == c.id) + .order_by(DodaContainerSeal.seal_line.asc()) + .all() + ) + c_block["seals"] = [ + _instance_payload( + s, + exclude=frozenset({"id", "container_id", "doda_id"}), + ) + for s in seals + ] + container_blocks.append(c_block) + + american = ( + db.query(DodaAmericanPedimento) + .filter(DodaAmericanPedimento.doda_id == doda_id) + .order_by(DodaAmericanPedimento.american_pedimento_line.asc()) + .all() + ) + american_blocks = [ + _instance_payload( + p, exclude=frozenset({"id", "doda_id"}) + ) + for p in american + ] + + pedimentos = ( + db.query(DodaPedimento) + .filter(DodaPedimento.doda_id == doda_id) + .order_by(DodaPedimento.pedimento_line.asc()) + .all() + ) + pedimento_blocks = [ + _instance_payload(p, exclude=frozenset({"id", "doda_id"})) for p in pedimentos + ] + + snapshot: Dict[str, Any] = { + "doda": doda_block, + "containers": container_blocks, + "american_pedimentos": american_blocks, + "pedimentos_detail": pedimento_blocks, + } + raw = json.dumps( + snapshot, + sort_keys=True, + ensure_ascii=True, + separators=(",", ":"), + default=_json_default, + ) + return hashlib.sha256(raw.encode("utf-8")).hexdigest() diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/models.py b/backend/api/v1/modules/a76/general_catalogs/doda/models.py index 810d4977..18bc0f9a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/models.py @@ -2,17 +2,18 @@ Modelos ORM para gestión de DODA (Documentos de Operación de Aduana) """ +from datetime import datetime from typing import Optional from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base from sqlalchemy import ( ForeignKeyConstraint, + DateTime, Integer, PrimaryKeyConstraint, String, Text, - LargeBinary, Numeric, Boolean, ) @@ -80,6 +81,11 @@ class Doda(Base, TenantScopedMixin, TimestampMixin): xml_doda_sent_path: Mapped[Optional[str]] = mapped_column(String(1000)) xml_doda_response_path: Mapped[Optional[str]] = mapped_column(String(1000)) + # Reporte PDF (S3) + caché por huella de contenido + doda_report_pdf_path: Mapped[Optional[str]] = mapped_column(String(1000)) + doda_report_pdf_generated_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + doda_report_source_fingerprint: Mapped[Optional[str]] = mapped_column(String(64)) + # SAT original chain sat_original_chain: Mapped[Optional[Text]] = mapped_column(Text) diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/payload_normalizer.py b/backend/api/v1/modules/a76/general_catalogs/doda/payload_normalizer.py new file mode 100644 index 00000000..88dd91d3 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/payload_normalizer.py @@ -0,0 +1,128 @@ +""" +Normaliza valores de negocio hacia el JSON del servicio DODA externo. +Solo cadenas limpias: sin mezclar claves de catálogo (broker_key, vehicle_key) como +sustituto de patente, CAAT o id de transporte cuando deban ser otros datos. +""" +from __future__ import annotations + +import re +from typing import Any, Dict, Optional + +# Aduana-Patente-Pedimento (3 segmentos; patente 4, pedimento alfanum limpio a dígitos) +_PEDIMENTO_DOC_SPLIT = re.compile(r"^[\s\-–—]*([^\s\-–—]+)[\s\-–—]+([^\s\-–—]+)[\s\-–—]+([^\s\-–—]+)[\s\-–—]*$") + + +def _digits_only(s: str, max_len: int) -> str: + d = re.sub(r"\D", "", s or "") + if max_len and len(d) > max_len: + return d[-max_len:] + return d + + +def normalize_aduana_despacho(value: Optional[str]) -> str: + s = (value or "").strip() + if not s: + return "" + return _digits_only(s, 3).zfill(3) if _digits_only(s, 3) else s[:3] + + +def normalize_aduana_seccion(value: Optional[str]) -> str: + s = (value or "").strip() + if not s: + return "" + d = _digits_only(s, 3) + if d: + return d.zfill(3) + return s[:3] + + +def normalize_patente(value: Optional[str]) -> str: + s = (re.sub(r"[\s\u00A0]+", " ", (value or "").strip())) + d = re.sub(r"\D", "", s) + if len(d) >= 4: + return d[-4:] + return s[:4] if s else "" + + +def normalize_caat(value: Optional[str]) -> str: + return (value or "").strip()[:20] + + +def normalize_id_transporte(value: Optional[str]) -> str: + return (re.sub(r"\s+", " ", (value or "").strip()))[:20] + + +def normalize_tipo_operacion(value: Optional[str]) -> str: + v = (value or "").strip().upper()[:1] + return v if v in ("I", "E") else v + + +def normalize_numero_gafete(value: Optional[str]) -> str: + return (value or "").strip()[:250] + + +def normalize_fast_id(value: Optional[str]) -> str: + return (value or "").strip()[:20] + + +def _parse_document_pedimento( + document: Optional[str], authorization_patent: Optional[str] +) -> tuple[str, str]: + """ + Retorna (patente_4, número_pedimento) a partir de documento o patente de autorización. + """ + doc = (document or "").strip() + m = _PEDIMENTO_DOC_SPLIT.match(doc) + if m: + b, c = m.group(2), m.group(3) + pat = re.sub(r"\D", "", b) + if len(pat) > 4: + pat = pat[-4:] + else: + pat = pat.zfill(4) if pat else "" + if not pat: + ap = (authorization_patent or "").strip() + pat = re.sub(r"\D", "", ap)[-4:].zfill(4) if ap else "" + ped = re.sub(r"[^\d\w]", "", c) or re.sub(r"\D", "", c) + return (pat[:4], ped) + ped = re.sub(r"[^\d]", "", doc) if doc else "" + ap = (authorization_patent or "").strip() + pat4 = re.sub(r"\D", "", ap)[:4] if ap else "" + if len(pat4) < 4 and ap and ap.isalnum(): + pat4 = (re.sub(r"\D", "", ap) + "0000")[:4] if re.sub(r"\D", "", ap) else (ap + "0")[:4] + return (pat4, ped) + + +def normalize_doda_pedimento_row( + document: Optional[str], + authorization_patent: Optional[str], + shipment: Optional[str], + cove: Optional[str], + umc: Optional[str], + dta_niu: Optional[str], + pedimento_type: Optional[str], + effective: Any, + diff: Any, +) -> Dict[str, Any]: + ap_raw = (authorization_patent or "").strip() + pat, ped = _parse_document_pedimento(document, ap_raw) + if not pat and ap_raw: + d = re.sub(r"\D", "", ap_raw) + pat = d[-4:].zfill(4) if d else ap_raw[:4] + + rem = (shipment or "").strip()[:11] if (shipment or "").strip() else "" + if not rem and ped: + rem = ped + + return { + "patente": pat, + "pedimento": ped, + "numero_remesa": rem, + "tipo_pedimento": (pedimento_type or "")[:20], + "dta_niu": (dta_niu or "")[:20], + "importe_efectivo_dolares": float(effective or 0) if effective is not None else 0.0, + "importe_diferencia_dolares": float(diff or 0) if diff is not None else 0.0, + "campo_12_apendice_17": 0, + "cove": (cove or "")[:50], + "umc": (umc or "")[:20], + } diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/print_cache.py b/backend/api/v1/modules/a76/general_catalogs/doda/print_cache.py new file mode 100644 index 00000000..7d1df83d --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/print_cache.py @@ -0,0 +1,75 @@ +""" +Caché del PDF de reporte DODA (S3 + columnas en a76.doda) e invalidación. +""" + +from __future__ import annotations + +import logging +from typing import Optional + +from sqlalchemy.orm import Session + +from core.config import settings +from core.s3_keys import doda_report_pdf_key +from core import storage_s3 + +from .models import Doda + +logger = logging.getLogger(__name__) + + +def _delete_stored_s3_key(key: Optional[str]) -> None: + if not key or not settings.use_s3_object_storage: + return + storage_s3.delete_object_if_exists(key) + + +def clear_doda_report_fields(doda: Doda) -> None: + doda.doda_report_pdf_path = None + doda.doda_report_pdf_generated_at = None + doda.doda_report_source_fingerprint = None + + +def touch_invalidate_doda_report( + db: Session, + *, + tenant_id: int, + company_id: int, + doda_id: int, + doda: Optional[Doda] = None, +) -> None: + """ + Borra el PDF previo en S3 (si aplica) y limpia columnas de caché en el DODA. + Llamar tras mutaciones de DODA o de tablas hijas. + """ + if doda is None: + doda = ( + db.query(Doda) + .filter( + Doda.id == doda_id, + Doda.tenant_id == tenant_id, + Doda.company_id == company_id, + ) + .first() + ) + if not doda: + return + + keys: set[str] = set() + if doda.doda_report_pdf_path: + keys.add(doda.doda_report_pdf_path) + try: + keys.add(doda_report_pdf_key(tenant_id, company_id, doda_id)) + except ValueError: + pass + + for k in keys: + _delete_stored_s3_key(k) + + clear_doda_report_fields(doda) + try: + db.add(doda) + db.commit() + except Exception: + db.rollback() + logger.exception("Error invalidating DODA report cache doda_id=%s", doda_id) diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/report_service.py b/backend/api/v1/modules/a76/general_catalogs/doda/report_service.py new file mode 100644 index 00000000..6aaa2696 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/report_service.py @@ -0,0 +1,205 @@ +""" +Generación de PDF de reporte DODA (Jinja2 + pdfkit / wkhtmltopdf). +""" + +from __future__ import annotations + +import json +import logging +import shutil +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, List, Optional + +import pdfkit +from jinja2 import Environment, FileSystemLoader, select_autoescape +from sqlalchemy.orm import Session + +from .fingerprint import build_doda_fingerprint +from .models import ( + Doda, + DodaAmericanPedimento, + DodaContainer, + DodaContainerSeal, + DodaPedimento, +) + +logger = logging.getLogger(__name__) + + +@dataclass +class _SealView: + seal_line: int + seal_value: str + + +@dataclass +class _ContainerView: + container_line: int + container_value: str + seals: List[_SealView] + + +@dataclass +class _AmericanView: + american_pedimento_line: int + american_pedimento_type: str + american_pedimento_value: str + + +@dataclass +class _PedimentoView: + pedimento_line: int + authorization_patent: str + document: str + shipment: str + cove: str + umc: str + pedimento_type: str + + +def _s(v: Optional[str]) -> str: + if v is None: + return "" + return str(v).strip() + + +class DodaReportPdfService: + def __init__(self) -> None: + self.template_dir = Path(__file__).parent / "templates" + self.jinja_env = Environment( + loader=FileSystemLoader(self.template_dir), + autoescape=select_autoescape(["html", "xml"]), + ) + self._tpl = self.jinja_env.get_template("doda_report.html") + + def _get_wkhtmltopdf_config(self): + for path in ( + shutil.which("wkhtmltopdf"), + "/usr/local/bin/wkhtmltopdf", + "/usr/bin/wkhtmltopdf", + ): + if path: + return pdfkit.configuration(wkhtmltopdf=path) + raise RuntimeError("wkhtmltopdf binary not found.") + + @staticmethod + def _doda_header_block(d: Doda) -> dict[str, str]: + return { + "integration_number": _s(d.integration_number), + "patent": _s(d.patent), + "dispatch_customs": _s(d.dispatch_customs), + "customs_sections": _s(d.customs_sections), + "caat": _s(d.caat), + "transport_identification": _s(d.transport_identification), + "fast_id": _s(d.fast_id), + "operation_type": _s(d.operation_type), + "status": _s(d.status), + "transaction_number": _s(d.transaction_number), + } + + def _load_children(self, db: Session, doda_id: int) -> dict[str, Any]: + containers = ( + db.query(DodaContainer) + .filter(DodaContainer.doda_id == doda_id) + .order_by(DodaContainer.container_line.asc()) + .all() + ) + cviews: list[_ContainerView] = [] + for c in containers: + seals = ( + db.query(DodaContainerSeal) + .filter(DodaContainerSeal.container_id == c.id) + .order_by(DodaContainerSeal.seal_line.asc()) + .all() + ) + cviews.append( + _ContainerView( + container_line=c.container_line, + container_value=_s(c.container_value), + seals=[ + _SealView( + seal_line=s.seal_line, seal_value=_s(s.seal_value) + ) + for s in seals + ], + ) + ) + + american = ( + db.query(DodaAmericanPedimento) + .filter(DodaAmericanPedimento.doda_id == doda_id) + .order_by(DodaAmericanPedimento.american_pedimento_line.asc()) + .all() + ) + amer_views = [ + _AmericanView( + american_pedimento_line=p.american_pedimento_line, + american_pedimento_type=_s(p.american_pedimento_type), + american_pedimento_value=_s(p.american_pedimento_value), + ) + for p in american + ] + + peds = ( + db.query(DodaPedimento) + .filter(DodaPedimento.doda_id == doda_id) + .order_by(DodaPedimento.pedimento_line.asc()) + .all() + ) + ped_views = [ + _PedimentoView( + pedimento_line=p.pedimento_line, + authorization_patent=_s(p.authorization_patent), + document=_s(p.document), + shipment=_s(p.shipment), + cove=_s(p.cove), + umc=_s(p.umc), + pedimento_type=_s(p.pedimento_type), + ) + for p in peds + ] + + return { + "containers": [asdict(x) for x in cviews], + "american_pedimentos": [asdict(x) for x in amer_views], + "pedimentos_detail": [asdict(x) for x in ped_views], + } + + def build_context(self, db: Session, doda: Doda) -> dict[str, Any]: + fp = build_doda_fingerprint(db, doda.id) + children = self._load_children(db, doda.id) + return { + "doda": self._doda_header_block(doda), + "linq_sat_qr": _s(d.linq_sat_qr), + "sat_chain_preview": _s((d.sat_original_chain or d.original_chain) or "")[:2000], + "sat_digital_seal_preview": _s(d.sat_digital_seal)[:2000], + "fingerprint_sha256": fp, + **children, + } + + def render_pdf_bytes(self, context: dict[str, Any]) -> bytes: + html = self._tpl.render(**context) + options = { + "page-size": "A4", + "encoding": "UTF-8", + "margin-top": "12mm", + "margin-bottom": "12mm", + "margin-left": "10mm", + "margin-right": "10mm", + } + return pdfkit.from_string( + html, + False, + options=options, + configuration=self._get_wkhtmltopdf_config(), + ) + + def build_pdf_for_doda(self, db: Session, doda: Doda) -> bytes: + ctx = self.build_context(db, doda) + return self.render_pdf_bytes(ctx) + + @staticmethod + def debug_json_snapshot(context: dict[str, Any]) -> str: + """Para depuración: snapshot legible (no contiene el sello completo).""" + return json.dumps(context, ensure_ascii=True, indent=2) diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py index 3b4133f5..6e5cb50e 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/routes.py @@ -2,12 +2,19 @@ Rutas para gestión de DODA (Documentos de Operación de Aduana) """ -from typing import List +import io +import logging +from datetime import datetime, timezone +from typing import Any, Dict, List from fastapi import APIRouter, Depends, HTTPException, Query, status +from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session +from core import storage_s3 +from core.config import settings from core.database import get_core_db +from core.s3_keys import doda_report_pdf_key from api.v1.common.tenant_crud_routes import TenantCRUDRoutes from .dto import ( DodaCreateDTO, @@ -17,6 +24,8 @@ from .dto import ( DodaContainerCreateDTO, DodaContainerResponseDTO, DodaContainerUpdateDTO, + DodaContainerSealCreateDTO, + DodaContainerSealResponseDTO, DodaAmericanPedimentoCreateDTO, DodaAmericanPedimentoResponseDTO, DodaAmericanPedimentoUpdateDTO, @@ -26,15 +35,132 @@ from .dto import ( ) from .models import Doda from .service import DodaService +from .alta_service import DodaAltaService +from .external_service import DodaExternalService +from .alta_log_dto import ( + DodaAltaLogCreateDTO, + DodaAltaLogListResponse, + DodaAltaLogResponseDTO, + DodaAltaLogUpdateDTO, +) +from .alta_log_service import DodaAltaLogService +from .fingerprint import build_doda_fingerprint +from .print_cache import touch_invalidate_doda_report +from .report_service import DodaReportPdfService +from .export_service import ( + build_export_text, + build_pedimentos_export_text, + list_dodas_in_date_range, + list_pedimentos_for_doda_export, + parse_export_params, + parse_pedimento_export_format, + _content_type_and_filename, +) from core.security import get_current_user, validate_access_to_resource -# Create CRUD router -crud_router = TenantCRUDRoutes( +logger = logging.getLogger(__name__) + +# Router independiente para rutas literales (deben registrarse antes que /{id}) +router = APIRouter(prefix="/doda", tags=["doda"]) + +# ============ RUTAS LITERALES (antes del CRUD /{id}) ============ + + +@router.get( + "/export", + summary="Exportar DODA por rango de fechas (CSV, TSV como XLS, o TXT con |)", +) +async def export_doda_list( + company_id: int = Query(..., description="Company ID"), + date_from: str = Query(..., description="Fecha inicio (YYYY-MM-DD)"), + date_to: str = Query(..., description="Fecha fin (YYYY-MM-DD)"), + file_format: str = Query("csv", alias="format", description="csv, xls o txt"), + date_mode: str = Query("formatted", description="raw o formatted (fechas/horas)"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Listado al estilo legacy: filtra `doda_date` (YYYYMMDD) entre inicio y fin. + """ + tenant_id = int(validate_access_to_resource(db, company_id, current_user)) + try: + d0, d1, fmt, mode = parse_export_params( + date_from, date_to, file_format, date_mode + ) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)) + + rows = list_dodas_in_date_range( + db, tenant_id=tenant_id, company_id=company_id, date_start=d0, date_end=d1 + ) + if not rows: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No existen DODA en el rango de fechas seleccionado.", + ) + + text = build_export_text(rows, export_format=fmt, date_mode=mode) + content_type, default_name = _content_type_and_filename(fmt) + data = ("\ufeff" + text).encode("utf-8") + return StreamingResponse( + io.BytesIO(data), + media_type=content_type, + headers={ + "Content-Disposition": f'attachment; filename="{default_name}"', + }, + ) + + +@router.get( + "/export/pedimentos/{doda_id}", + summary="Exportar líneas de pedimento de un DODA (CSV, TSV como XLS, TXT con |)", +) +async def export_doda_pedimentos( + doda_id: int, + company_id: int = Query(..., description="Company ID"), + file_format: str = Query("xls", alias="format", description="csv, xls o txt"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Reporte por DODA seleccionado: columnas alineadas al listado de pedimentos (PATENTE, DOCUMENTO, COVE, etc.). + Si no hay líneas, se devuelve el archivo solo con encabezados. + """ + tenant_id = int(validate_access_to_resource(db, company_id, current_user)) + try: + fmt = parse_pedimento_export_format(file_format) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e) + ) from e + + doda = DodaService.get_by_id(db, doda_id, tenant_id, company_id) + if not doda: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="DODA no encontrado." + ) + + rows = list_pedimentos_for_doda_export( + db, tenant_id=tenant_id, company_id=company_id, doda_id=doda_id + ) + text = build_pedimentos_export_text(rows, export_format=fmt) + content_type, _ = _content_type_and_filename(fmt) + fname = f"doda_pedimentos_{doda_id}.{fmt.value}" + data = ("\ufeff" + text).encode("utf-8") + return StreamingResponse( + io.BytesIO(data), + media_type=content_type, + headers={"Content-Disposition": f'attachment; filename="{fname}"'}, + ) + + +# Incluir rutas CRUD (contiene GET /{id}, POST /, PUT /{id}, DELETE /{id}). +_crud_router = TenantCRUDRoutes( service=DodaService, create_schema=DodaCreateDTO, update_schema=DodaUpdateDTO, response_schema=DodaResponseDTO, - prefix="/doda", + prefix="", tags=["doda"], resource_name="DODA", id_name="doda_id", @@ -42,8 +168,7 @@ crud_router = TenantCRUDRoutes( enable_filters=True, ).router -router = crud_router - +router.include_router(_crud_router) @router.get( @@ -67,6 +192,87 @@ async def get_doda_detail( return DodaDetailResponseDTO.model_validate(doda) +@router.get( + "/{doda_id}/print", + summary="Imprimir DODA (PDF)", + responses={422: {"description": "Validación (p. ej. falta sello digital SAT)."}}, +) +async def print_doda_pdf( + doda_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Genera o reutiliza el PDF almacenado en S3 cuando el contenido no ha cambiado + (huella SHA-256 de DODA + hijos). + """ + tenant_id = int(validate_access_to_resource(db, company_id, current_user)) + doda = DodaService.get_by_id(db, doda_id, tenant_id, company_id) + if not doda: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="DODA not found", + ) + + if not (doda.sat_digital_seal and str(doda.sat_digital_seal).strip()): + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Falta el sello digital SAT requerido para imprimir el DODA.", + ) + + content_fp = build_doda_fingerprint(db, doda_id) + expected_key = doda_report_pdf_key(tenant_id, company_id, doda_id) + + can_reuse = ( + doda.doda_report_source_fingerprint == content_fp + and doda.doda_report_pdf_path == expected_key + and bool(doda.doda_report_pdf_path) + ) + if can_reuse and settings.use_s3_object_storage and storage_s3.object_exists(expected_key): + data = storage_s3.get_object_bytes(expected_key) + return StreamingResponse( + io.BytesIO(data), + media_type="application/pdf", + headers={"Content-Disposition": f'inline; filename="doda_{doda_id}.pdf"'}, + ) + + # Re-generar: eliminar caché previa (S3 + columnas) y volver a guardar + if settings.use_s3_object_storage: + touch_invalidate_doda_report( + db, + tenant_id=tenant_id, + company_id=company_id, + doda_id=doda_id, + doda=None, + ) + + doda_fresh = DodaService.get_by_id(db, doda_id, tenant_id, company_id) + if not doda_fresh: + raise HTTPException(status_code=404, detail="DODA not found") + + service = DodaReportPdfService() + pdf = service.build_pdf_for_doda(db, doda_fresh) + if settings.use_s3_object_storage: + storage_s3.put_object_bytes(expected_key, pdf, content_type="application/pdf") + + now = datetime.now(timezone.utc) + doda_fresh.doda_report_pdf_path = expected_key if settings.use_s3_object_storage else None + doda_fresh.doda_report_pdf_generated_at = now + doda_fresh.doda_report_source_fingerprint = content_fp + db.add(doda_fresh) + db.commit() + if settings.use_s3_object_storage: + pdf = storage_s3.get_object_bytes(expected_key) + + return StreamingResponse( + io.BytesIO(pdf), + media_type="application/pdf", + headers={"Content-Disposition": f'inline; filename="doda_{doda_id}.pdf"'}, + ) + + +# ============ CONTAINERS ENDPOINTS ============ @router.get( "/{doda_id}/containers", response_model=List[DodaContainerResponseDTO], @@ -125,6 +331,81 @@ async def update_container( return DodaContainerResponseDTO.model_validate(container) +@router.delete( + "/{doda_id}/containers/{container_line}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Delete container from DODA", +) +async def delete_container( + doda_id: int, + container_line: int, + db: Session = Depends(get_core_db), +): + """ + Elimina un contenedor del DODA. + Devuelve 409 si el contenedor tiene precintos (candados) asignados. + """ + DodaService.delete_container(db, doda_id, container_line) + return None + + +def _seal_to_response(seal) -> DodaContainerSealResponseDTO: + """ORM usa doda_id; DTO expone doda_sys_id.""" + return DodaContainerSealResponseDTO( + id=seal.id, + doda_sys_id=seal.doda_id, + seal_line=seal.seal_line, + seal_value=seal.seal_value, + ) + + +# ============ CONTAINER SEALS (PRECINTOS) ============ +@router.get( + "/{doda_id}/containers/{container_line}/seals", + response_model=List[DodaContainerSealResponseDTO], + summary="Listar precintos de un contenedor", +) +async def get_container_seals( + doda_id: int, + container_line: int, + db: Session = Depends(get_core_db), +): + seals = DodaService.get_seals_for_container(db, doda_id, container_line) + return [_seal_to_response(s) for s in seals] + + +@router.post( + "/{doda_id}/containers/{container_line}/seals", + response_model=DodaContainerSealResponseDTO, + status_code=status.HTTP_201_CREATED, + summary="Agregar precinto a un contenedor", +) +async def add_container_seal( + doda_id: int, + container_line: int, + seal_data: DodaContainerSealCreateDTO, + db: Session = Depends(get_core_db), +): + seal = DodaService.add_seal(db, doda_id, container_line, seal_data) + return _seal_to_response(seal) + + +@router.delete( + "/{doda_id}/containers/{container_line}/seals/{seal_line}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Eliminar precinto de un contenedor", +) +async def delete_container_seal( + doda_id: int, + container_line: int, + seal_line: int, + db: Session = Depends(get_core_db), +): + DodaService.delete_seal(db, doda_id, container_line, seal_line) + return None + + +# ============ AMERICAN PEDIMENTOS ENDPOINTS ============ @router.get( "/{doda_id}/american-pedimentos", response_model=List[DodaAmericanPedimentoResponseDTO], @@ -160,6 +441,23 @@ async def add_american_pedimento( return DodaAmericanPedimentoResponseDTO.model_validate(pedimento) +# ============ AMERICAN PEDIMENTOS (DELETE) ============ +@router.delete( + "/{doda_id}/american-pedimentos/{pedimento_line}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Delete American pedimento from DODA", +) +async def delete_american_pedimento( + doda_id: int, + pedimento_line: int, + db: Session = Depends(get_core_db), +): + """Elimina un pedimento americano del DODA.""" + DodaService.delete_american_pedimento(db, doda_id, pedimento_line) + return None + + +# ============ PEDIMENTOS ENDPOINTS ============ @router.get( "/{doda_id}/pedimentos", response_model=List[DodaPedimentoResponseDTO], @@ -193,3 +491,279 @@ async def add_pedimento( detail="DODA not found", ) return DodaPedimentoResponseDTO.model_validate(pedimento) + + +@router.delete( + "/{doda_id}/pedimentos/{pedimento_line}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Delete pedimento from DODA", +) +async def delete_pedimento( + doda_id: int, + pedimento_line: int, + db: Session = Depends(get_core_db), +): + """Elimina un pedimento del DODA.""" + DodaService.delete_pedimento(db, doda_id, pedimento_line) + return None + + +# ============ ALTA DODA ENDPOINTS ============ + + +@router.get( + "/{doda_id}/alta/elegibilidad", + summary="Verificar elegibilidad para Alta DODA", + tags=["doda-alta"], +) +async def get_doda_elegibilidad( + doda_id: int, + company_id: int = Query(..., description="Company ID"), + variant: str = Query("doda", description="Tipo de alta: 'doda' o 'pita'"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +) -> Dict[str, Any]: + """ + Verifica si el DODA cumple los requisitos para enviar el alta al servicio externo. + Porta las validaciones del sistema legacy (campos requeridos, max 4 contenedores, + gafete si DODA, patente vs agente, certificados DODA en VU). + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + user_email = ( + current_user.get("email") + or current_user.get("preferred_username") + or "" + ) + service = DodaAltaService(db) + result = service.check_elegibilidad( + doda_id=doda_id, + tenant_id=int(tenant_id), + company_id=company_id, + variant=variant, + user_email=user_email, + ) + return { + "can_alta": result.can_alta, + "reasons": [ + {"field": r.field, "message": r.message, "solution": r.solution} + for r in result.reasons + ], + } + + +@router.post( + "/{doda_id}/alta", + summary="Enviar Alta DODA al servicio externo (asíncrono)", + tags=["doda-alta"], +) +async def post_doda_alta( + doda_id: int, + company_id: int = Query(..., description="Company ID"), + variant: str = Query("doda", description="Tipo de alta: 'doda' o 'pita'"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +) -> Dict[str, Any]: + """ + Verifica elegibilidad, construye el payload desde los datos del DODA y su VU, + y envía el alta al servicio externo. Retorna {task_id, status, message} para polling. + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + user_email = ( + current_user.get("email") + or current_user.get("preferred_username") + or "" + ) + service = DodaAltaService(db) + + elegibilidad = service.check_elegibilidad( + doda_id=doda_id, + tenant_id=int(tenant_id), + company_id=company_id, + variant=variant, + user_email=user_email, + ) + if not elegibilidad.can_alta: + reasons = [ + {"field": r.field, "message": r.message, "solution": r.solution} + for r in elegibilidad.reasons + ] + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail={"message": "El DODA no cumple los requisitos para el alta.", "reasons": reasons}, + ) + + try: + payload = service.build_alta_payload( + doda_id=doda_id, + tenant_id=int(tenant_id), + company_id=company_id, + variant=variant, + user_email=user_email, + ) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=str(exc), + ) from exc + + try: + ext = DodaExternalService() + result = ext.post_alta(payload) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(exc), + ) from exc + except Exception as exc: + logger.exception("Error al enviar alta DODA al servicio externo: doda_id=%s", doda_id) + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=f"Error al contactar el servicio DODA externo: {exc}", + ) from exc + + # Persistir el log del alta + doda_record = DodaService.get_by_id(db, doda_id, tenant_id, company_id) + if doda_record: + try: + DodaAltaLogService.create_from_alta_result( + db=db, + doda=doda_record, + company_id=company_id, + tenant_id=int(tenant_id), + variant=variant, + ext_result=result, + ) + except Exception: + logger.exception("Error persistiendo DodaAltaLog para doda_id=%s", doda_id) + + return result + + +@router.get( + "/alta-status/{task_id}", + summary="Consultar estado de tarea de Alta DODA", + tags=["doda-alta"], +) +async def get_doda_alta_status( + task_id: str, + current_user: dict = Depends(get_current_user), +) -> Any: + """ + Proxy transparente al servicio externo para consultar el estado de una tarea de alta DODA. + """ + try: + ext = DodaExternalService() + return ext.get_status(task_id) + except ValueError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(exc), + ) from exc + except Exception as exc: + logger.exception("Error consultando estado DODA task_id=%s", task_id) + raise HTTPException( + status_code=status.HTTP_502_BAD_GATEWAY, + detail=f"Error al consultar el estado de la tarea DODA: {exc}", + ) from exc + + +# ============ DODA ALTA LOG CRUD ============ + + +@router.get( + "/alta-logs", + response_model=DodaAltaLogListResponse, + summary="Listar registros de alta DODA", + tags=["doda-alta"], +) +async def list_doda_alta_logs( + company_id: int = Query(...), + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=200), + doda_id: int = Query(None), + search: str = Query(None), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + return DodaAltaLogService.list( + db, company_id, int(tenant_id), page, page_size, doda_id, search + ) + + +@router.get( + "/alta-logs/{log_id}", + response_model=DodaAltaLogResponseDTO, + summary="Obtener registro de alta DODA", + tags=["doda-alta"], +) +async def get_doda_alta_log( + log_id: int, + company_id: int = Query(...), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) + if not record: + raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") + return DodaAltaLogResponseDTO.model_validate(record) + + +@router.post( + "/alta-logs", + response_model=DodaAltaLogResponseDTO, + status_code=status.HTTP_201_CREATED, + summary="Crear registro de alta DODA manualmente", + tags=["doda-alta"], +) +async def create_doda_alta_log( + dto: DodaAltaLogCreateDTO, + company_id: int = Query(...), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + record = DodaAltaLogService.create(db, dto, company_id, int(tenant_id)) + return DodaAltaLogResponseDTO.model_validate(record) + + +@router.put( + "/alta-logs/{log_id}", + response_model=DodaAltaLogResponseDTO, + summary="Actualizar registro de alta DODA", + tags=["doda-alta"], +) +async def update_doda_alta_log( + log_id: int, + dto: DodaAltaLogUpdateDTO, + company_id: int = Query(...), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) + if not record: + raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") + record = DodaAltaLogService.update(db, record, dto) + return DodaAltaLogResponseDTO.model_validate(record) + + +@router.delete( + "/alta-logs/{log_id}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Eliminar registro de alta DODA", + tags=["doda-alta"], +) +async def delete_doda_alta_log( + log_id: int, + company_id: int = Query(...), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + record = DodaAltaLogService.get(db, log_id, company_id, int(tenant_id)) + if not record: + raise HTTPException(status_code=404, detail="Registro de alta DODA no encontrado.") + DodaAltaLogService.delete(db, record) + return None diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/service.py b/backend/api/v1/modules/a76/general_catalogs/doda/service.py index 98a0b05b..ee60e5e5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/doda/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/doda/service.py @@ -6,6 +6,7 @@ import logging from typing import Any, Dict, List, Optional, Tuple from fastapi import HTTPException +from sqlalchemy import func from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session @@ -15,6 +16,7 @@ from .dto import ( DodaUpdateDTO, DodaContainerCreateDTO, DodaContainerUpdateDTO, + DodaContainerSealCreateDTO, DodaAmericanPedimentoCreateDTO, DodaAmericanPedimentoUpdateDTO, DodaPedimentoCreateDTO, @@ -27,6 +29,7 @@ from .models import ( DodaAmericanPedimento, DodaPedimento, ) +from .print_cache import touch_invalidate_doda_report logger = logging.getLogger(__name__) @@ -34,6 +37,19 @@ logger = logging.getLogger(__name__) class DodaService: """Servicio para gestión de DODA""" + @staticmethod + def _invalidate_report_after_mutation( + db: Session, + *, + doda_id: int, + tenant_id: int, + company_id: int, + ) -> None: + """Limpia PDF de reporte y objeto S3 previo (patrón artefactos).""" + touch_invalidate_doda_report( + db, tenant_id=tenant_id, company_id=company_id, doda_id=doda_id + ) + @staticmethod def get_all( db: Session, @@ -94,6 +110,9 @@ class DodaService: db.add(db_doda) db.commit() db.refresh(db_doda) + DodaService._invalidate_report_after_mutation( + db, doda_id=db_doda.id, tenant_id=tenant_id, company_id=company_id + ) return db_doda except IntegrityError as e: db.rollback() @@ -119,6 +138,9 @@ class DodaService: db.commit() db.refresh(db_doda) + DodaService._invalidate_report_after_mutation( + db, doda_id=db_doda.id, tenant_id=tenant_id, company_id=company_id + ) return db_doda except IntegrityError as e: db.rollback() @@ -139,7 +161,16 @@ class DodaService: return False try: - db.delete(db_doda) + tid = int(db_doda.tenant_id) + cid = int(db_doda.company_id) + did = int(db_doda.id) + DodaService._invalidate_report_after_mutation( + db, doda_id=did, tenant_id=tid, company_id=cid + ) + to_delete = DodaService.get_by_id(db, id, tenant_id, company_id) + if not to_delete: + return True + db.delete(to_delete) db.commit() return True except IntegrityError as e: @@ -165,6 +196,13 @@ class DodaService: if not doda: return None + cv = (container_data.container_value or "").strip() + if not cv: + raise HTTPException( + status_code=400, + detail="El valor del contenedor no puede estar vacío.", + ) + # Get max line number max_line = ( db.query(DodaContainer) @@ -172,19 +210,30 @@ class DodaService: .count() ) + dump = container_data.model_dump(exclude_unset=True) + dump.pop("seals_detail", None) + dump["container_value"] = cv + db_container = DodaContainer( doda_id=doda_id, container_line=max_line + 1, - **{ - k: v - for k, v in container_data.model_dump(exclude_unset=True).items() - if k != "seals_detail" - }, + tenant_id=doda.tenant_id, + company_id=doda.company_id, + **{k: v for k, v in dump.items() if k not in ("doda_id", "container_line")}, ) db.add(db_container) db.commit() db.refresh(db_container) + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) return db_container + except HTTPException: + db.rollback() + raise except Exception as e: db.rollback() logger.error(f"Error adding container: {str(e)}") @@ -216,6 +265,14 @@ class DodaService: db.commit() db.refresh(db_container) + doda = db.get(Doda, doda_id) + if doda: + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) return db_container except Exception as e: db.rollback() @@ -232,6 +289,211 @@ class DodaService: .all() ) + @staticmethod + def delete_container( + db: Session, doda_id: int, container_line: int + ) -> None: + """ + Delete a container from a DODA. + Raises HTTP 409 if the container has seals assigned (Clarion: validación precintos). + Raises HTTP 404 if not found. + """ + db_container = ( + db.query(DodaContainer) + .filter( + DodaContainer.doda_id == doda_id, + DodaContainer.container_line == container_line, + ) + .first() + ) + if not db_container: + raise HTTPException(status_code=404, detail="Contenedor no encontrado.") + + has_seals = bool(db_container.seals_detail) + if not has_seals and db_container.seals: + has_seals = any(s.strip() for s in db_container.seals.split(",")) + + if has_seals: + raise HTTPException( + status_code=409, + detail=( + "El contenedor tiene uno o más precintos asignados. " + "No se puede borrar el contenedor." + ), + ) + + try: + doda = db.get(Doda, doda_id) + tid = int(doda.tenant_id) if doda else 0 + cid = int(doda.company_id) if doda else 0 + db.delete(db_container) + db.commit() + if doda: + DodaService._invalidate_report_after_mutation( + db, doda_id=doda_id, tenant_id=tid, company_id=cid, doda=None + ) + except Exception as e: + db.rollback() + logger.error(f"Error deleting container doda_id={doda_id} line={container_line}: {e}") + raise HTTPException(status_code=500, detail="Error al eliminar el contenedor.") + + # ============ CONTAINER SEALS (PRECINTOS) ============ + MAX_SEALS_PER_DODA = 8 + + @staticmethod + def get_seals_for_container( + db: Session, doda_id: int, container_line: int + ) -> List[DodaContainerSeal]: + """Precintos de un contenedor (por línea de contenedor dentro del DODA).""" + db_container = ( + db.query(DodaContainer) + .filter( + DodaContainer.doda_id == doda_id, + DodaContainer.container_line == container_line, + ) + .first() + ) + if not db_container: + return [] + return ( + db.query(DodaContainerSeal) + .filter(DodaContainerSeal.container_id == db_container.id) + .order_by(DodaContainerSeal.seal_line.asc()) + .all() + ) + + @staticmethod + def add_seal( + db: Session, + doda_id: int, + container_line: int, + seal_data: DodaContainerSealCreateDTO, + ) -> DodaContainerSeal: + """ + Añade un precinto (candado) a un contenedor. + Máximo 8 precintos en total por DODA (legacy Clarion: gDoda_Contenedores_Candados). + """ + doda = db.query(Doda).filter(Doda.id == doda_id).first() + if not doda: + raise HTTPException(status_code=404, detail="DODA no encontrado.") + + container = ( + db.query(DodaContainer) + .filter( + DodaContainer.doda_id == doda_id, + DodaContainer.container_line == container_line, + ) + .first() + ) + if not container: + raise HTTPException(status_code=404, detail="Contenedor no encontrado.") + + raw_value = (seal_data.seal_value or "").strip() + if not raw_value: + raise HTTPException( + status_code=400, + detail="El campo precinto no puede estar vacío.", + ) + + total_seals = ( + db.query(DodaContainerSeal) + .filter(DodaContainerSeal.doda_id == doda_id) + .count() + ) + if total_seals >= DodaService.MAX_SEALS_PER_DODA: + raise HTTPException( + status_code=400, + detail=( + "El DODA supera el máximo de precintos (8). " + "Revise los precintos registrados." + ), + ) + + max_line = ( + db.query(func.max(DodaContainerSeal.seal_line)) + .filter(DodaContainerSeal.container_id == container.id) + .scalar() + ) + next_line = (max_line or 0) + 1 + + try: + db_seal = DodaContainerSeal( + doda_id=doda_id, + container_id=container.id, + seal_line=next_line, + seal_value=raw_value, + tenant_id=doda.tenant_id, + company_id=doda.company_id, + ) + db.add(db_seal) + db.commit() + db.refresh(db_seal) + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) + return db_seal + except Exception as e: + db.rollback() + logger.error( + "Error adding seal doda_id=%s container_line=%s: %s", + doda_id, + container_line, + e, + ) + raise HTTPException(status_code=500, detail="Error al agregar el precinto.") + + @staticmethod + def delete_seal( + db: Session, doda_id: int, container_line: int, seal_line: int + ) -> None: + """Elimina un precinto por línea de contenedor y línea de candado.""" + container = ( + db.query(DodaContainer) + .filter( + DodaContainer.doda_id == doda_id, + DodaContainer.container_line == container_line, + ) + .first() + ) + if not container: + raise HTTPException(status_code=404, detail="Contenedor no encontrado.") + + seal = ( + db.query(DodaContainerSeal) + .filter( + DodaContainerSeal.container_id == container.id, + DodaContainerSeal.seal_line == seal_line, + ) + .first() + ) + if not seal: + raise HTTPException(status_code=404, detail="Precinto no encontrado.") + + try: + doda = db.get(Doda, doda_id) + db.delete(seal) + db.commit() + if doda: + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) + except Exception as e: + db.rollback() + logger.error( + "Error deleting seal doda_id=%s line=%s seal_line=%s: %s", + doda_id, + container_line, + seal_line, + e, + ) + raise HTTPException(status_code=500, detail="Error al eliminar el precinto.") + # ============ AMERICAN PEDIMENTOS ============ @staticmethod def add_american_pedimento( @@ -243,21 +505,62 @@ class DodaService: if not doda: return None + tipo = (pedimento_data.american_pedimento_type or "").strip() + valor = (pedimento_data.american_pedimento_value or "").strip() + # Legacy: IF DODA:DespachoAduanero = '3' (PITA) → sin validación de tipo; web usa customs_clearance=1 para PITA + clearance = getattr(doda, "customs_clearance", None) + if clearance != 1: + if not tipo: + raise HTTPException( + status_code=400, + detail="El tipo de pedimento americano es obligatorio.", + ) + op = (doda.operation_type or "").strip().upper() + if op in ("I", "1"): + allowed = {"1", "2", "3", "4", "5"} + elif op in ("E", "2"): + allowed = {"6", "7", "8"} + else: + raise HTTPException( + status_code=400, + detail="El tipo de operación del DODA no permite validar el pedimento americano.", + ) + if tipo not in allowed: + raise HTTPException( + status_code=400, + detail="El tipo de pedimento americano no es correcto para el tipo de operación.", + ) + max_line = ( db.query(DodaAmericanPedimento) .filter(DodaAmericanPedimento.doda_id == doda_id) .count() ) + dump = pedimento_data.model_dump(exclude_unset=True) + if clearance == 1: + dump["american_pedimento_type"] = tipo or None + db_pedimento = DodaAmericanPedimento( doda_id=doda_id, american_pedimento_line=max_line + 1, - **pedimento_data.model_dump(exclude_unset=True), + tenant_id=doda.tenant_id, + company_id=doda.company_id, + **dump, ) db.add(db_pedimento) db.commit() db.refresh(db_pedimento) + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) return db_pedimento + except HTTPException: + db.rollback() + raise except Exception as e: db.rollback() logger.error(f"Error adding American pedimento: {str(e)}") @@ -274,6 +577,44 @@ class DodaService: .all() ) + # ============ AMERICAN PEDIMENTOS (DELETE) ============ + @staticmethod + def delete_american_pedimento( + db: Session, doda_id: int, pedimento_line: int + ) -> None: + """Delete an American pedimento from a DODA.""" + db_pedimento = ( + db.query(DodaAmericanPedimento) + .filter( + DodaAmericanPedimento.doda_id == doda_id, + DodaAmericanPedimento.american_pedimento_line == pedimento_line, + ) + .first() + ) + if not db_pedimento: + raise HTTPException( + status_code=404, detail="Pedimento americano no encontrado." + ) + try: + doda = db.get(Doda, doda_id) + db.delete(db_pedimento) + db.commit() + if doda: + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) + except Exception as e: + db.rollback() + logger.error( + f"Error deleting american pedimento doda_id={doda_id} line={pedimento_line}: {e}" + ) + raise HTTPException( + status_code=500, detail="Error al eliminar el pedimento americano." + ) + # ============ PEDIMENTOS ============ @staticmethod def add_pedimento( @@ -294,12 +635,23 @@ class DodaService: db_pedimento = DodaPedimento( doda_id=doda_id, pedimento_line=max_line + 1, + tenant_id=doda.tenant_id, + company_id=doda.company_id, **pedimento_data.model_dump(exclude_unset=True), ) db.add(db_pedimento) db.commit() db.refresh(db_pedimento) + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) return db_pedimento + except HTTPException: + db.rollback() + raise except Exception as e: db.rollback() logger.error(f"Error adding pedimento: {str(e)}") @@ -313,3 +665,36 @@ class DodaService: db.query(DodaPedimento).filter( DodaPedimento.doda_id == doda_id).all() ) + + @staticmethod + def delete_pedimento( + db: Session, doda_id: int, pedimento_line: int + ) -> None: + """Delete a pedimento from a DODA.""" + db_pedimento = ( + db.query(DodaPedimento) + .filter( + DodaPedimento.doda_id == doda_id, + DodaPedimento.pedimento_line == pedimento_line, + ) + .first() + ) + if not db_pedimento: + raise HTTPException(status_code=404, detail="Pedimento no encontrado.") + try: + doda = db.get(Doda, doda_id) + db.delete(db_pedimento) + db.commit() + if doda: + DodaService._invalidate_report_after_mutation( + db, + doda_id=doda_id, + tenant_id=int(doda.tenant_id), + company_id=int(doda.company_id), + ) + except Exception as e: + db.rollback() + logger.error( + f"Error deleting pedimento doda_id={doda_id} line={pedimento_line}: {e}" + ) + raise HTTPException(status_code=500, detail="Error al eliminar el pedimento.") diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/templates/doda_report.html b/backend/api/v1/modules/a76/general_catalogs/doda/templates/doda_report.html new file mode 100644 index 00000000..bc92e065 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/templates/doda_report.html @@ -0,0 +1,160 @@ + + +
+ +Huella de contenido (SHA-256): {{ fingerprint_sha256 }}
+ +| Folio de integración | {{ doda.integration_number }} | +Patente | {{ doda.patent }} | +
|---|---|---|---|
| Aduana despacho | {{ doda.dispatch_customs }} | +Secciones aduaneras | {{ doda.customs_sections }} | +
| Operación | {{ doda.operation_type }} | +Estado | {{ doda.status }} | +
| Ident. transporte | {{ doda.transport_identification }} | +ID rápida | {{ doda.fast_id }} | +
| CAAT | {{ doda.caat }} | +Transacción / folio | {{ doda.transaction_number }} | +
{{ linq_sat_qr }}
+ {% endif %} + + {% if sat_chain_preview %} +{{ sat_chain_preview }}
+ {% endif %} + + {% if sat_digital_seal_preview %} +{{ sat_digital_seal_preview }}
+ {% endif %} + +Sin contenedores registrados.
+ {% else %} +| Línea | +Contenedor | +Precintos (línea / valor) | +||
|---|---|---|---|---|
| {{ c.container_line }} | +{{ c.container_value }} | +
+ {% if c.seals|length == 0 %}
+ —
+ {% else %}
+
|
+
Sin partidas de pedimentos nacionales.
+ {% else %} +| Línea | +Patente auth. | +Documento / ped. | +Embarque | +COVE | +UMC | +Tipo | +
|---|---|---|---|---|---|---|
| {{ p.pedimento_line }} | +{{ p.authorization_patent }} | +{{ p.document }} | +{{ p.shipment }} | +{{ p.cove }} | +{{ p.umc }} | +{{ p.pedimento_type }} | +
Sin pedimentos americanos.
+ {% else %} +| Línea | +Tipo | +Valor | +
|---|---|---|
| {{ a.american_pedimento_line }} | +{{ a.american_pedimento_type }} | +{{ a.american_pedimento_value }} | +
+ Documento generado automáticamente. Los extractos de cadena / sello se truncan en este reporte; + los datos de huella (SHA-256) reflejan el contenido completo persistido. +
+ + diff --git a/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py b/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py index c4ff7578..187371f4 100644 --- a/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/identifiers/service.py @@ -22,8 +22,12 @@ class IdentifierService: ) if filters: - # Add filters here if needed - pass + if filters.get("code"): + query = query.filter(Identifier.code.ilike(f"%{filters['code']}%")) + if filters.get("description"): + query = query.filter( + Identifier.description.ilike(f"%{filters['description']}%") + ) total = query.count() items = query.offset(skip).limit(limit).all() diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/service.py b/backend/api/v1/modules/a76/general_catalogs/legends/service.py index 0351f16a..e7b6c930 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/service.py @@ -27,8 +27,12 @@ class LegendService: ) if filters: - # Add filters here if needed - pass + if filters.get("code"): + query = query.filter(Legend.code.ilike(f"%{filters['code']}%")) + if filters.get("description"): + query = query.filter( + Legend.description.ilike(f"%{filters['description']}%") + ) total = query.count() items = query.offset(skip).limit(limit).all() diff --git a/backend/core/config.py b/backend/core/config.py index bcda63cc..207f8691 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -52,12 +52,12 @@ class Settings(BaseSettings): # External APIs SITAR_API_URL: str = "api.sitar.aduanasoft.com:880" - COVE_API_URL: str = "" + COVE_API_URL: str = "https://api.vu.aduanasoft.com" + COVE_API_VERIFY_SSL: bool = False COVE_FIEL_HASH_KEY: str = "" COVE_FIEL_HASH_IV: str = "" SITAR_API_USER: str = "" SITAR_API_PASSWORD: str = "" - # SMTP Email Configuration SMTP_HOST: str = "smtp.gmail.com" SMTP_PORT: int = 587 diff --git a/backend/core/s3_keys.py b/backend/core/s3_keys.py index ac3e5efe..bf5720fa 100644 --- a/backend/core/s3_keys.py +++ b/backend/core/s3_keys.py @@ -15,6 +15,7 @@ funciones de este módulo (no construir ``tenants/...`` a mano en las rutas HTTP - ``tenants/{tid}/companies/{company_id}/`` Recursos ligados a una empresa: + - ``.../doda/{doda_id}/report/doda_report.pdf`` — reporte DODA en PDF. ``doda_report_pdf_key``. - ``.../branding/{filename}`` — logo. ``company_logo_key``. - ``.../certificates/{tipo}_{timestamp}.{cer|key}`` — CER/KEY FIEL, CFDI, cancelación. ``company_certificate_key``. @@ -272,6 +273,18 @@ def company_logo_key( return f"{tenant_company_prefix(tenant_id, company_id)}branding/{fn}" +def doda_report_pdf_key( + tenant_id: Union[int, str], + company_id: int, + doda_id: int, +) -> str: + """ + Reporte DODA en PDF bajo ``.../doda/{doda_id}/report/doda_report.pdf`` (clave estable). + """ + did = _segment(doda_id, "doda_id") + return f"{tenant_company_prefix(tenant_id, company_id)}doda/{did}/report/doda_report.pdf" + + def company_certificate_key( tenant_id: Union[int, str], company_id: int, diff --git a/backend/tests/unit/general_catalogs/doda/test_doda_export.py b/backend/tests/unit/general_catalogs/doda/test_doda_export.py new file mode 100644 index 00000000..0b3bb2a5 --- /dev/null +++ b/backend/tests/unit/general_catalogs/doda/test_doda_export.py @@ -0,0 +1,72 @@ +from types import SimpleNamespace + +import pytest + +from api.v1.modules.a76.general_catalogs.doda.export_service import ( + DodaExportFormat, + build_export_text, + doda_row_values, + parse_export_params, +) + + +def test_parse_export_params_valid(): + d0, d1, fmt, mode = parse_export_params("2024-01-15", "2024-01-20", "csv", "formatted") + assert d0 == 20240115 + assert d1 == 20240120 + assert fmt == DodaExportFormat.csv + assert mode == "formatted" + + +def test_parse_export_params_rejects_inverted_range(): + with pytest.raises(ValueError, match="date_from"): + parse_export_params("2024-02-01", "2024-01-01", "csv", "formatted") + + +def _minimal_row(): + return SimpleNamespace( + id=1, + integration_number="INT1", + doda_date=20240110, + doda_time=1430, + dispatch_customs="64", + customs_sections=None, + patent="1234", + pedimentos=None, + caat=None, + transport_identification=None, + fast_id=None, + operation_type="I", + responsible=None, + carrier=None, + shipments=None, + pedimento_type=None, + original_chain=None, + serial_number=None, + electronic_signature=None, + transaction_number=None, + status="PENDIENTE", + linq_sat_qr=None, + sat_certificate=None, + sat_digital_seal=None, + xml_doda_sent_path=None, + xml_doda_response_path=None, + sat_original_chain=None, + customs_clearance=None, + unique_badge_number=None, + last_user=None, + ) + + +def test_doda_row_values_length_matches_headers(): + row = _minimal_row() + assert len(doda_row_values(row, date_mode="formatted")) == 30 + + +def test_build_export_text_includes_header_and_row(): + row = _minimal_row() + text = build_export_text([row], export_format=DodaExportFormat.csv, date_mode="formatted") + lines = text.strip().split("\r\n") + assert "SYSID" in lines[0] + assert "INT1" in lines[1] + assert lines[1].startswith("1,") diff --git a/backend/tests/unit/general_catalogs/doda/test_doda_print.py b/backend/tests/unit/general_catalogs/doda/test_doda_print.py new file mode 100644 index 00000000..e6604ea6 --- /dev/null +++ b/backend/tests/unit/general_catalogs/doda/test_doda_print.py @@ -0,0 +1,126 @@ +from unittest.mock import patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from api.v1.modules.a76.general_catalogs.doda.fingerprint import build_doda_fingerprint +from api.v1.modules.a76.general_catalogs.doda.models import Doda +from api.v1.modules.a76.general_catalogs.doda import routes as doda_routes +from core import storage_s3 +from core.config import settings +from core.database import get_core_db +from core.s3_keys import doda_report_pdf_key +from core.security import get_current_user +from tests.conftest import allocate_ephemeral_tenant_company_ids, ensure_tenant_company + + +@pytest.mark.usefixtures("db_session") +def test_doda_fingerprint_changes_when_field_changes(db_session: Session): + tid, cid = allocate_ephemeral_tenant_company_ids(db_session) + ensure_tenant_company(db_session, tenant_id=tid, company_id=cid) + d = Doda( + tenant_id=tid, + company_id=cid, + integration_number="A1", + sat_digital_seal="x", + ) + db_session.add(d) + db_session.commit() + fp1 = build_doda_fingerprint(db_session, d.id) + d.patent = "1234" + db_session.commit() + fp2 = build_doda_fingerprint(db_session, d.id) + assert fp1 != fp2 + + +def _print_client(db_session: Session, test_tenant) -> TestClient: + app = FastAPI() + app.include_router(doda_routes.router, prefix="/doda") + + def _override_get_db(): + yield db_session + + async def _user(): + return {"sub": "test", "tenant_id": test_tenant.tenant_id} + + app.dependency_overrides[get_core_db] = _override_get_db + app.dependency_overrides[get_current_user] = _user + return TestClient(app) + + +@patch.object( + doda_routes, + "validate_access_to_resource", + lambda db, company_id, current_user: int(current_user["tenant_id"]), +) +def test_print_uses_s3_cache_when_fingerprint_matches( + db_session: Session, test_tenant, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.setattr(settings, "S3_FILE_STORAGE", True, raising=False) + monkeypatch.setattr(settings, "CSV_IMPORT_STORAGE", "redis", raising=False) + + tid, cid = test_tenant.tenant_id, test_tenant.company_id + ensure_tenant_company(db_session, tenant_id=tid, company_id=cid) + d = Doda( + tenant_id=tid, + company_id=cid, + integration_number="X", + sat_digital_seal="s", + ) + db_session.add(d) + db_session.commit() + + fp = build_doda_fingerprint(db_session, d.id) + d.doda_report_source_fingerprint = fp + d.doda_report_pdf_path = doda_report_pdf_key(tid, cid, d.id) + db_session.commit() + + called = {"render": 0} + + class _NoRender: + def build_pdf_for_doda(self, db, doda): + called["render"] += 1 + raise AssertionError("should not render when S3 cache hits") + + monkeypatch.setattr(doda_routes, "DodaReportPdfService", lambda: _NoRender()) + monkeypatch.setattr(storage_s3, "object_exists", lambda key: True) + monkeypatch.setattr(storage_s3, "get_object_bytes", lambda key: b"%PDF-1.4 cached") + + client = _print_client(db_session, test_tenant) + resp = client.get(f"/doda/{d.id}/print?company_id={cid}") + assert resp.status_code == 200 + assert resp.content == b"%PDF-1.4 cached" + assert called["render"] == 0 + + +@patch.object( + doda_routes, + "validate_access_to_resource", + lambda db, company_id, current_user: int(current_user["tenant_id"]), +) +def test_print_422_without_digital_seal( + db_session: Session, test_tenant, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.setattr(settings, "S3_FILE_STORAGE", True, raising=False) + + tid, cid = test_tenant.tenant_id, test_tenant.company_id + ensure_tenant_company(db_session, tenant_id=tid, company_id=cid) + d = Doda( + tenant_id=tid, company_id=cid, integration_number="N", sat_digital_seal=" " + ) + db_session.add(d) + db_session.commit() + + put_calls = [] + + def _put(key, body, content_type="application/octet-stream"): + put_calls.append((key, body, content_type)) + + monkeypatch.setattr(storage_s3, "put_object_bytes", _put) + + client = _print_client(db_session, test_tenant) + resp = client.get(f"/doda/{d.id}/print?company_id={cid}") + assert resp.status_code == 422 + assert put_calls == [] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 90840164..0edc6fd1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -174,6 +174,8 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - COVE_API_VERIFY_SSL=${COVE_API_VERIFY_SSL:-False} - VALKEY_URL=${VALKEY_URL:-redis://valkey:6379/0} - CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""} - SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production} @@ -242,6 +244,9 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - COVE_API_VERIFY_SSL=${COVE_API_VERIFY_SSL:-False} + - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} @@ -277,6 +282,9 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - COVE_API_VERIFY_SSL=${COVE_API_VERIFY_SSL:-False} + - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} diff --git a/docker-compose.yml b/docker-compose.yml index 55c17606..49cbc088 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -179,6 +179,8 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - COVE_API_VERIFY_SSL=${COVE_API_VERIFY_SSL:-False} - VALKEY_URL=${VALKEY_URL:-redis://valkey:6379/0} - CENTRAL_SERVER_URL=${CENTRAL_SERVER_URL:-""} - SYNC_SECRET_TOKEN=${SYNC_SECRET_TOKEN:-change-this-sync-token-in-production} @@ -304,6 +306,9 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - DODA_API_BASE_URL=${DODA_API_BASE_URL} + - DODA_API_VERIFY_SSL=${DODA_API_VERIFY_SSL:-False} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} @@ -343,6 +348,9 @@ services: - SITAR_API_PASSWORD=${SITAR_API_PASSWORD} - COVE_FIEL_HASH_KEY=${COVE_FIEL_HASH_KEY} - COVE_FIEL_HASH_IV=${COVE_FIEL_HASH_IV} + - COVE_API_URL=${COVE_API_URL:-https://api.vu.aduanasoft.com} + - DODA_API_BASE_URL=${DODA_API_BASE_URL} + - DODA_API_VERIFY_SSL=${DODA_API_VERIFY_SSL:-False} - CSV_IMPORT_STORAGE=${CSV_IMPORT_STORAGE:-minio} - S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-${MINIO_ROOT_USER:-minioadmin}} diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9e8dd9a8..095efa87 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -1,1211 +1,1472 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from en!", - "sidebar": { - "dashboard": "Dashboard", - "reference_data": { - "title": "Fixed Catalogs", - "codes_pedimento_regimen": "Pedimento and Regime Codes", - "containers": "Containers", - "countries": "Countries", - "currency_types": "Currency Types", - "customs_sections": "Customs Sections", - "customs_warehouses": "Customs Warehouses", - "incoterms": "Incoterms", - "document_types_digitization": "Document types for digitization", - "invoice_types": "Invoice Types", - "material_types": "Material Types", - "payment_methods": "Payment Methods", - "pedimento_codes": "Pedimento Codes", - "pedimento_regimes": "Pedimento Regimes", - "sectors": "Sectors", - "states": "States", - "transportation_modes": "Transportation Modes", - "transportation_types": "Transportation Types", - "valuation_methods": "Valuation Methods", - "configuracion": "Settings", - "general": "General", - "licencia": "License", - "usuarios": "Users", - "ayuda": "Help" - }, - "general_catalogs": { - "title": "General Catalogs", - "company_information": "Company Information", - "packages": "Packages", - "concepts": "Concepts", - "classification": "Classification", - "identifiers": "Identifiers", - "incoterms": "Incoterms", - "inpc": "I.N.P.C", - "fixed_legends": "Fixed Legends", - "seals": "Seals", - "valuation_methods": "Valuation Methods", - "countries": "Countries", - "ports": "Ports", - "unit_measures": "Units of Measure", - "um_customs_mex": "Units of Measure - Mexican Customs", - "um_customs_ame": "Units of Measure - American Customs", - "um_ace": "Units of Measure - ACE", - "um_oma": "Units of Measure - OMA", - "conversions": "Conversions", - "equivalences": "Equivalences", - "exchange_rates": "Exchange Rates", - "currency_types": "Currency Types", - "multi_currency": "Multi Currency", - "invoice_types": "Invoice Types", - "electronic_signatures": "Electronic Signatures", - "billing_errors": "Billing Errors", - "customs_warehouses": "Customs Warehouses", - "locations": "Locations", - "doda": "DODA", - "packing_list": "Packing List", - "prevalidators": "Prevalidators", - "electronic_notices": "Electronic Notices", - "back_flush": "Back Flush", - "crossing_notice": "Crossing Notice", - "customs_broker_concepts": "Customs Broker Concepts" - }, - "fractions": { - "title": "Fractions", - "sitar": "Fraction Sitar", - "sitar_seventh_amendment": "Fraction Sitar - Seventh Amendment", - "sitar_us": "Fraction Sitar US", - "american": "Fraction US", - "canadian": "Fraction Canadian", - "historical": "Fraction Historical", - "sectors": "Sectors" - }, - "goods": { - "title": "Goods", - "classes": "Classes", - "parts": "Parts", - "fda_codes": "FDA Codes" - }, - "pedimentos": { - "title": "Pedimentos", - "pedimento_management": "Pedimento Management", - "pedimento_codes": "Pedimento Codes", - "customs_regimes": "Customs Regimes", - "payment_methods": "Payment Methods", - "customs_sections": "Customs Sections", - "anexo_22_app_31": "Anexo 22 App 3" - }, - "import_invoices": { - "title": "Import Invoices", - "temporary": "Temporary", - "definitive": "Definitive", - "mexican_purchases": "Mexican Purchases", - "regime_change": "Regime Change", - "repair": "Repair" - }, - "export_invoices": { - "title": "Export Invoices", - "exportation": "Exportation", - "repair": "Repair" - }, - "export": { - "title": "Exportation", - "catalog": "Export Catalog", - "repair": "Repair", - "manifest": "Manifest", - "proforma": "Proforma", - "reports": "Reports", - "used_materials": "Used Materials Module", - "destruction": "Destruction", - "special_processes": "Special Processes" - }, - "clients_and_providers": "Clients and Providers", - "customs_brokers": "Customs Brokers", - "audit_logs": "Audit Logs", - "audit_logs_title": "Audit Logs", - "audit_logs_description": "Audit trail of operations and background task (Celery) status.", - "audit_logs_tab_bitacora": "Audit trail", - "audit_logs_tab_tasks": "Background tasks", - "audit_logs_tab_files": "File manager", - "audit_logs_files_title": "File manager", - "audit_logs_files_root": "Files root", - "audit_logs_files_refresh": "Refresh", - "audit_logs_files_list_title": "Contents", - "audit_logs_files_error_prefix": "Error:", - "audit_logs_files_col_name": "Name", - "audit_logs_files_col_size": "Size", - "audit_logs_files_col_modified": "Modified", - "audit_logs_files_col_actions": "Actions", - "audit_logs_files_loading": "Loading files...", - "audit_logs_files_empty": "No files or folders found in this location.", - "audit_logs_files_download": "Download", - "digitalizacion": { - "title": "Digitization", - "subtitle": "Digitized Documents Catalog", - "new": "New", - "refresh": "Refresh", - "table_title": "Digitized documents", - "col_consecutivo": "Consecutive", - "col_tipo_documento": "Document Type", - "col_e_document": "E-Document", - "col_fecha": "Date", - "col_num_operacion_vu": "VU Operation No.", - "col_actions": "Actions", - "form_e_document": "E-Document", - "form_num_operacion": "Operation No.", - "form_tipo_documento": "Document Type", - "form_archivo_digitalizado_en": "Digitized in", - "form_fecha": "Date", - "form_agente_aduanal": "Customs Broker", - "form_pedimento": "Entry", - "form_nombre_archivo": "File name", - "digitalizar_title": "Digitize Document", - "digitalizar_subtitle": "Send document to Ventanilla Única", - "digitalizar_file_label": "File", - "digitalizar_rfc_consulta": "RFC Query", - "digitalizar_clave_documento": "Document Key", - "progress_title": "Digitalizing document...", - "progress_step": "Step", - "progress_success": "Digitalization completed successfully.", - "progress_download_acuse": "Download Receipt", - "action_digitalizar": "Digitalize", - "action_download_zip": "Download ZIP", - "action_acuse": "Receipt", - "action_envio_xml": "Envío XML", - "action_respuesta_xml": "Respuesta XML", - "action_consulta_envio_xml": "Consulta Envío XML", - "action_consulta_respuesta_xml": "Consulta Respuesta XML", - "action_edit": "Edit", - "action_delete": "Delete", - "empty": "No digitized documents", - "loading": "Loading...", - "search_placeholder": "Search:", - "confirm_delete": "Are you sure you want to delete this document?" - }, - "client_provider_type": { - "client_indicator": "C", - "provider_indicator": "P", - "both_indicator": "B" - }, - "nav_user": { - "profile": "Profile", - "settings": "Settings", - "logout": "Logout" - }, - "transports": { - "title": "Transportation", - "transporters": "Carriers", - "drivers": "Drivers", - "trailers": "Trailers", - "vehicles": "Vehicles" - }, - "reports": { - "title": "Reports", - "invoices": "Impo/Expo Invoices", - "downloaded_parts": "Downloaded Parts", - "expiration": "Expiration Report" - }, - "settings": { - "general": "General" - } - }, - "invoice_list": { - "skip_to_actions": "Go to invoice actions", - "header": { - "title": "Invoices", - "description": "Manage system invoices" - }, - "titles": { - "base": "INVOICE CATALOG", - "import": "IMPORT", - "export": "EXPORT", - "import_temporal": "TEMPORARY IMPORT", - "import_definitive": "DEFINITIVE IMPORT", - "import_mexican": "MEXICAN PURCHASES", - "import_regime_change": "REGIME CHANGE AND REGULARIZATION", - "import_repair": "IMPORT REPAIR", - "export_definitive": "DEFINITIVE EXIT", - "export_repair": "REPAIR" - }, - "filters": { - "operation_label": "Operation Type", - "operation_all_option": "Operation: All", - "invoice_type_label": "Invoice Type", - "invoice_type_all_option": "Invoice: All", - "invoice_number_placeholder": "Invoice No.", - "year_start_placeholder": "Start year", - "year_end_placeholder": "End year", - "active_filters": "Active filters" - }, - "actions": { - "parameters": "Settings", - "new_invoice": "New Invoice", - "refresh": "Refresh", - "reports": "Reports", - "more_actions": "More Actions", - "downloads": "Downloads", - "other_actions": "Other Actions", - "cancel": "Cancel", - "continue": "Continue", - "generate_cove": "Generate COVE", - "close": "Close" - }, - "card": { - "invoice_list_title": "Invoice List" - }, - "summary": { - "showing": "Showing", - "of": "of", - "records": "records" - }, - "operation_types": { - "all": "All", - "import": "Import", - "export": "Export" - }, - "cove_dialog": { - "title": "Generate COVE", - "description_prefix": "Select the recipient email for invoice", - "recipient_label": "Recipient email", - "destination": "COVE destination", - "select_email": "Select an email", - "fallback_email": "It will be sent to the email of the user who generated the invoice", - "search_email": "Search email...", - "loading_emails": "Loading available emails...", - "no_emails": "No emails available for COVE.", - "selected_badge": "Selected" - }, - "progress": { - "title_pdf": "Generating Invoice PDF", - "title_consolidated": "Generating Consolidated Report", - "title_descargo": "Generating FIFO Report", - "title_packing_list": "Generating Packing List", - "title_winsaai": "Generating WINSAAI Report", - "title_process_invoice": "Processing invoice", - "title_revert_invoice": "Reverting invoice", - "title_validate_cove": "Validating data for COVE", - "complete_processed": "Invoice processed successfully", - "complete_reverted": "Invoice reverted successfully", - "complete_cove_validation": "COVE validation completed", - "complete_default": "Process completed" - }, - "steps": { - "load_invoice": "Loading invoice", - "validate_invoice_data": "Validating invoice data", - "review_classes_exchange_rate": "Reviewing classes and exchange rate", - "calculate_item_values": "Calculating item values", - "validate_items": "Validating items", - "validate_rule8_quotas": "Validating Rule Eight quotas", - "update_totals": "Updating totals", - "validate_invoice_status": "Validating invoice status", - "verify_item_balances": "Verifying item balances", - "confirm_changes": "Confirming changes" - }, - "dialogs": { - "revert_title_export": "Revert Export Invoice", - "revert_title_import": "Revert Import Invoice", - "revert_description_intro": "The invoice", - "revert_description_warning": "This operation will revert the balance/discharge records generated when processing the invoice.", - "revert_description_question": "Do you want to continue?", - "winsaai_title": "Customs and Inventory Control System", - "winsaai_description_intro": "Invoice", - "winsaai_of_type": "of type", - "winsaai_description_process": "has been assigned to WINSAAI File Generation.", - "winsaai_description_question": "Do you want to Continue or Cancel?" - }, - "footer": { - "toolbar_aria": "Invoice actions", - "invoice_pdf": "Invoice PDF", - "invoice_csv": "Invoice CSV", - "consolidated": "Consolidated", - "consolidated_notice": "Consolidated Notice", - "packing_list": "Packing List", - "four_copies_rem": "4 REM Copies", - "descargo_peps": "FIFO Discharge", - "transferencia_electronica": "Electronic Transfer", - "interface_vu": "VU Interface", - "vu_options_keyboard": "VU options (keyboard)", - "vu_consult": "Consult", - "vu_addenda": "Addenda", - "vu_cove_receipt": "COVE Receipt", - "vu_massive_cove": "Mass COVE", - "cons_sed": "SED Consult", - "encomienda": "Commission", - "fact_mex_cons": "Mex Invoice Cons", - "fact_mex_ord_cat": "Mex Invoice Ord Cat", - "export_sia": "Export SIA", - "interface": "Interface", - "process_update": "Update", - "unprocess": "Revert", - "view_details": "View Details", - "customs_broker_interface": "Customs Broker Interface", - "edit": "Edit", - "delete": "Delete" - }, - "submenu": { - "consult_soon": "VU Consult - Coming soon", - "addenda_soon": "VU Addenda - Coming soon", - "massive_cove_soon": "Mass COVE - Coming soon", - "generate_invoice_csv_soon": "Generate Invoice CSV - Coming soon", - "four_copies_soon": "4 REM Copies - Coming soon", - "cons_sed_soon": "SED Consult - Coming soon", - "encomienda_soon": "Commission - Coming soon", - "fact_mex_cons_soon": "Mex Consolidated Invoice - Coming soon", - "fact_mex_ord_cat_soon": "Mex Invoice Capture Order - Coming soon", - "export_sia_soon": "Export SIA - Coming soon", - "interface_soon": "Interface - Coming soon" - }, - "recipients": { - "company_vu_email": "Company VU email", - "company_main_email": "Company main email", - "company_industrial_1": "Industrial email 1", - "company_industrial_2": "Industrial email 2", - "company_description": "Company {name}", - "single_window_email": "Single window email", - "main_email": "Main email", - "company_user_email": "Company user - {email}", - "my_email": "My email", - "authenticated_user": "Authenticated user - {email}", - "load_error": "Could not load available emails for COVE", - "no_configured": "No emails configured for COVE" - }, - "toasts": { - "select_invoice_for_cove": "Select an invoice to generate COVE", - "no_company_selected": "No company selected", - "session_expired_reloading": "Session expired. Reloading page...", - "load_more_error": "Error loading more data", - "apply_filters_error": "Error applying filters", - "reload_data_error": "Error reloading data", - "download_start_error": "Could not start download", - "consolidated_download_start_error": "Could not start consolidated download", - "calculating_peps": "Calculating FIFO assignment...", - "peps_calculation_error_prefix": "Error calculating FIFO: {error}", - "peps_calculation_completed": "FIFO calculation completed", - "peps_report_start_error": "Could not start FIFO report download", - "aviso_consolidado_start_error": "Could not start Consolidated Notice download", - "packing_list_start_error": "Could not start Packing List download", - "fast_interface_import_only": "Quick interface is only available for Import invoices", - "customs_broker_interface_start_error": "Could not start Customs Broker Interface generation", - "pdf_download_success": "PDF downloaded successfully", - "invoice_processed_success": "Invoice processed successfully", - "worker_error_prefix": "Worker reported an error: {error}", - "task_result_process_error": "Error processing task result", - "select_invoice_to_edit": "Select an invoice to edit", - "no_table_rows": "No rows in the table", - "select_invoice_for_reports": "Select an invoice for reports", - "select_invoice_for_more_actions": "Select an invoice for more actions", - "select_invoice_to_revert": "Select an invoice to revert", - "select_invoice_for_details": "Select an invoice to view details", - "select_invoice": "Select an invoice", - "select_at_least_one_invoice_to_delete": "Select at least one invoice to delete", - "select_invoice_for_pdf": "Select an invoice to download PDF", - "select_invoice_for_consolidated": "Select an invoice to download consolidated report", - "select_invoice_to_change_status": "Select an invoice to change status", - "update_status_error_prefix": "Error trying to {action} invoice: {error}", - "status_action_update": "update", - "status_action_revert": "revert", - "status_updated_success": "Invoice updated successfully", - "status_reverted_success": "Invoice reverted successfully", - "update_status_unexpected_error": "Unexpected error while changing status", - "select_invoice_to_process": "Select an invoice to process", - "process_start_error_prefix": "Error starting process: {error}", - "process_start_error": "Could not start process", - "revert_start_error_prefix": "Error starting revert: {error}", - "revert_start_error": "Could not start revert", - "select_recipient_email_for_cove": "Select an email to send COVE", - "cove_eligibility_error_prefix": "Could not validate COVE eligibility: {error}", - "cove_requirements_not_met": "Invoice does not meet COVE generation requirements", - "cove_verification_error": "Could not verify whether invoice can generate COVE", - "cove_start_error_prefix": "Error starting COVE generation: {error}", - "cove_start_error": "Could not start COVE generation", - "validation_extra_more": "\n...and {count} more", - "validation_error_count": "{count} validation error(s):\n{preview}{extra}", - "cove_external_queued_default": "COVE invoice started in Single Window. Use task_id to check status." - } - }, - "invoice_table": { - "no_results": "No results.", - "loading_more": "Loading more...", - "scroll_to_load_more": "Scroll to load more", - "processed": "Processed", - "pending": "Pending", - "operation": "Operation", - "operation_import": "Import", - "operation_export": "Export", - "invoice_type": "Invoice Type", - "invoice_number": "Invoice No.", - "pedimento_18": "Pedimento 18", - "remesa": "Remesa", - "invoice_date": "Invoice Date", - "pedimento_code": "Pedimento Code", - "document_type": "Doc Type", - "total_items": "Total Items", - "currency": "Currency", - "currency_type": "Currency Type", - "weight_type": "Weight Type", - "mixed": "Mixed", - "related_doc": "Related Doc", - "yes": "Yes", - "no": "No", - "not_available_short": "N/A" - }, - "invoice_selectors": { - "identifier_catalog": { - "title": "Select Identifier", - "description": "Search and select an identifier from catalog (Appendix 8).", - "search_placeholder": "Search by code or description...", - "column_code": "Code", - "column_description": "Description", - "column_level": "Level", - "empty": "No identifiers found." - }, - "valuation_method": { - "title": "Select Valuation Method", - "description": "Search and select a valuation method from the list.", - "search_placeholder": "Search by code or description...", - "column_code": "Code", - "column_description": "Description", - "empty": "No valuation methods found." - }, - "location": { - "title": "Location catalog (machinery and equipment)", - "no_company_selected": "No company selected", - "load_error": "Error loading locations", - "required_key": "Key is required", - "save_error": "Error saving", - "key_label": "Key *", - "key_placeholder": "Location key", - "location_label": "Location", - "location_placeholder": "Name or description", - "department_label": "Department", - "responsible_label": "Responsible", - "observations_label": "Observations", - "optional_placeholder": "Optional", - "back_to_list": "Back to list", - "save": "Save", - "search_placeholder": "Search by key or location...", - "register_new": "Register new location", - "column_key": "Key", - "column_location": "Location", - "no_results": "No results found", - "cancel": "Cancel" - }, - "tariff_fraction": { - "title": "SITAR FRACTIONS CATALOG - SCAII", - "search_label": "Searching:", - "search_placeholder": "Search by fraction, description, NICO...", - "column_key": "Key", - "column_fraction": "Fraction", - "column_nico": "NICO", - "column_description": "Description", - "column_umt": "U.M.T", - "column_adv_impo": "Adv. Impo", - "column_adv_expo": "Adv. Expo", - "column_dof": "DOF", - "column_aplica_ieps": "Applies IEPS", - "loading": "Loading fractions...", - "empty": "No fractions available", - "cancel": "Cancel" - }, - "us_tariff_fraction": { - "no_company_selected": "No company selected", - "load_error_prefix": "Error: {error}", - "no_records_info": "No registered US tariff fractions were found", - "connection_error_prefix": "Connection error: {error}", - "title": "Select US Tariff Fraction", - "description": "Select tariff fraction (HTS) from catalog.", - "search_placeholder": "Search by code or description...", - "loading_catalog": "Loading catalog...", - "no_results": "No fractions found.", - "column_code": "Code (HTS)", - "column_description": "Description", - "records_found": "{count} records found", - "cancel": "Cancel" - }, - "invoice_selector_modal": { - "no_active_company": "No active company has been selected", - "search_error": "Error searching invoices", - "title_export": "Export Invoices", - "title_import": "Import Invoices ({regimen})", - "description_export": "Select an invoice from catalog to link it to the item.", - "description_import": "Select a processed import invoice for regimen {regimen}.", - "search_placeholder": "Search by invoice number...", - "searching_button": "Searching...", - "search_button": "Search", - "searching_available": "Searching available invoices...", - "no_invoices": "No invoices found", - "try_other_filter": "Try another invoice number or filter", - "processed_badge": "Processed", - "pedimento_label": "Pedimento", - "no_date": "No date", - "not_available_short": "N/A", - "select": "Select", - "total_found": "Total: {count} invoices found", - "close": "Close" - }, - "port_selector": { - "title": "Select Port (Customs/Section)", - "description": "Search and select a customs section from the list.", - "search_placeholder": "Search by code or name...", - "column_code": "Code", - "column_name": "Name / Section", - "loading": "Loading customs sections...", - "empty": "No results found", - "cancel": "Cancel" - }, - "manifest_selector": { - "title": "Select Manifest", - "description": "Search and select an export manifest to link to this invoice.", - "search_placeholder": "Search by number...", - "search_button": "Search", - "searching": "Searching manifests...", - "column_number": "Manifest Number", - "column_description": "Description", - "empty": "No results found" - } - }, - "invoice_edit": { - "new_title": "New Invoice", - "edit_title": "Edit Invoice", - "new_description": "Enter the new invoice data", - "edit_description": "Modify the invoice data", - "draft_badge": "Draft", - "saved_success": "All changes were saved successfully", - "invoice_number_prefix": "Number:", - "edit_details": "Edit the invoice details", - "page_invoice_prefix": "Invoice #", - "page_default_values_loaded_prefix": "Default values loaded for {invoiceType}", - "page_save_error_prefix": "Error saving the invoice", - "page_save_changes_error": "Error saving changes", - "page_console_hint": "Check the console for more details", - "page_session_expired": "Session expired. Reloading page...", - "tabs": { - "general": "General", - "compliance": "Compliance", - "financials": "Financials", - "observations": "Observations", - "items": "Items", - "others": "Others", - "continuation": "Cont." - }, - "form": { - "operation_type_label": "Operation Type *", - "operation_type_placeholder": "Select type", - "operation_type_import": "Import", - "operation_type_export": "Export", - "invoice_number_label": "Invoice Number", - "invoice_number_placeholder": "Invoice number", - "invoice_type_label": "Invoice Type", - "invoice_type_placeholder": "Invoice type", - "no_company_selected": "No company selected", - "exchange_rate_required": "Exchange rate is required (Financials tab)", - "exchange_rate_positive": "Exchange rate must be greater than 0 (Financials tab)", - "save_error": "Error saving", - "loading_defaults_prefix": "Default values loaded for", - "pedimento_pending": "Pedimento pending?", - "pedimento_label": "Pedimento", - "pedimento_placeholder": "Select pedimento...", - "remesa_label": "Remesa", - "invoice_number_label_short": "Invoice No.", - "invoice_date_label_exp": "Date", - "invoice_date_label_mex": "Entry date", - "invoice_date_label_default": "Invoice date", - "emission_date_label": "Emission date", - "iva_factor_label": "IVA factor", - "alternate_invoice_label": "Alternate invoice", - "project_number_label": "Project Number", - "project_number_placeholder": "Project number", - "purchase_order_label": "Purchase Order", - "purchase_order_placeholder": "Purchase order", - "invoice_date_label": "Invoice date", - "validation": { - "trailer_required": "Trailer is required when Transport Type is different from None.", - "missing_fields": "The following fields are required:", - "check_transport_data": "Check transport and logistics data", - "save_error": "Error saving changes" - }, - "traffic_light_status_label": "Traffic light", - "traffic_light_status_placeholder": "Traffic light status", - "observation_es_label": "Observations (Spanish)", - "observation_es_placeholder": "Observations in Spanish", - "observation_en_label": "Observations (English)", - "observation_en_placeholder": "Observations in English", - "remesa_placeholder": "Remesa number", - "aduana_label": "Customs", - "aduana_placeholder": "Customs code", - "customs_broker_label": "Customs broker", - "customs_broker_placeholder": "Customs broker ID", - "provider_label": "Provider", - "provider_placeholder": "Provider ID", - "edocument_label": "E-Document", - "edocument_placeholder": "E-document number", - "is_mixed_label": "Mixed operation", - "currency_placeholder": "MXN, USD, etc.", - "exchange_rate_placeholder": "Exchange rate", - "value_mn_label": "MN value", - "value_mn_placeholder": "Value in local currency", - "value_me_label": "ME value", - "value_me_placeholder": "Value in foreign currency", - "customs_value_mn_label": "Customs value MN", - "customs_value_mn_placeholder": "Customs value in MN", - "freight_label": "Freight", - "freight_placeholder": "Freight cost", - "insurance_label": "Insurance", - "insurance_placeholder": "Insurance cost", - "iva_mn_label": "IVA MN", - "iva_mn_placeholder": "IVA in MN", - "total_quantity_label": "Total quantity", - "total_quantity_placeholder": "Total quantity", - "gross_weight_label": "Gross weight", - "gross_weight_placeholder": "Gross weight", - "net_weight_label": "Net weight", - "net_weight_placeholder": "Net weight", - "bundle_count_label": "Bundle count", - "bundle_count_placeholder": "Bundle count", - "update_button": "Update", - "create_button": "Create" - }, - "general": { - "pedimento_section": "Pedimento data", - "pedimento_date_from": "Date from:", - "pedimento_date_to": "Date to:", - "pedimento_code": "Code:", - "pedimento_regimen": "Regime:", - "clients_suppliers_broker": "Clients - Suppliers - Customs Broker", - "provider_header_supplier": "Supplier", - "provider_header_exporter": "Exporter", - "sold_to_header_consignado": "Consigned to", - "sold_to_header_vendido": "Sold to", - "sold_to_header_exportado": "Exported to", - "sold_to_header_importador": "Importer", - "shipped_to_header_enviado": "Sent to", - "shipped_to_header_transferido": "Transferred to", - "shipped_to_header_donado": "Donated to", - "shipped_to_header_importador": "Importer", - "shipped_by_header_enviado_por": "Sent by", - "shipped_by_header_destinatario": "Recipient", - "shipped_by_header_vendido_por": "Sold by", - "shipped_by_header_notificar": "Notify to", - "select_header_placeholder": "Select header...", - "select_placeholder": "Select...", - "select_broker_placeholder": "Select...", - "broker_mex_label": "Mex. Customs Broker:", - "broker_usa_label": "US Customs Broker:", - "currency_weight_section": "Currency Type - Net and Gross Weights", - "exchange_rate": "Exchange rate:", - "currency_foreign": "Foreign (USD)", - "currency_local": "Local (MXN)", - "currency_manual": "Manual entry", - "currency_label": "Currency:", - "weight_type_label": "Weight type:", - "weight_type_kgs": "Kilograms (kg)", - "weight_type_lbs": "Pounds (lb)", - "manifest_number_label": "Manifest no.:", - "manifest_placeholder": "Manifest...", - "transport_section": "Transporter", - "transport_label": "Transporter:", - "transport_key_label": "Transport key:", - "transport_type_label": "Transport type:", - "trailer_label": "Trailer:", - "driver_label": "Driver:", - "iva_label": "VAT:", - "customs_label": "Customs and dispatch section:", - "document_type_label": "Customs regime code:", - "select_transporter_placeholder": "Select transporter...", - "select_vehicle_placeholder": "Select vehicle...", - "select_driver_placeholder": "Select driver...", - "select_trailer_placeholder": "Select trailer...", - "select_customs_placeholder": "Select customs office...", - "select_regimen_placeholder": "Select regime...", - "choose_transporter_first": "Choose transporter first...", - "no_data": "No data", - "no_drivers_for_transporter": "No drivers for this transporter", - "no_regimens_for_operation": "No regimes for type", - "choose_operation_first": "Select operation type first", - "transport_none": "None", - "transport_type_transport": "Transport", - "transport_type_box": "Box", - "transport_type_licence_plates": "Plates", - "transport_type_truck": "Truck", - "transport_type_vessel": "Vessel", - "transport_type_rail_barge": "Rail barge", - "transport_type_container": "Container", - "transport_type_airplane": "Airplane", - "transport_type_gondola": "Gondola", - "transport_type_flatbed": "Flatbed", - "signature_label": "Electronic signature:", - "general_info": "General information" - }, - "page": { - "saving_all_changes": "Saving all changes...", - "save_all_changes": "Save All Changes", - "cancel": "Cancel" - }, - "observations": { - "mexican_observation": "Mexican invoice observations:", - "bilingual_observation": "Mexican and bilingual invoice observations:", - "textarea_placeholder": "Write your observations here.", - "fixed_legend": "Fixed legend:", - "selected_legend_prefix": "Key", - "select_legend_placeholder": "Select legend...", - "add_to_observations": "Add to observations", - "american_observation": "US invoice observations:", - "identifiers_title": "Identifiers", - "first_label": "First:", - "second_label": "Second:", - "key_placeholder": "Key...", - "complements_title": "Complements", - "one_label": "1:", - "two_label": "2:", - "office_label": "Office:", - "incrementables_title": "Incrementables:", - "freight_label": "Freight:", - "insurance_label": "Insurance:", - "packaging_label": "Packaging:", - "other_increments_label": "Other incr.:", - "other_deductibles_label": "Other deduct.:", - "seal_number_label": "Seal Number:", - "movement_type_label": "Movement Type:", - "alternate_invoice_label": "Alternate Invoice:", - "proforma_number_label": "Proforma Number:", - "subdivision_label": "Subdivision:", - "yes": "Yes", - "no": "No", - "acts_as_cd_label": "Acts as CD:", - "incoterm_label": "Incoterm:", - "select_placeholder": "Select...", - "valuation_method_label": "Valuation Method:", - "mixed_label": "Mixed?", - "seal_count_label": "Seal Count:", - "delivery_title": "Delivery Data", - "delivered_label": "Delivered", - "received_by_label": "Received by:", - "delivery_date_label": "Delivery Date:", - "rule_parties_label": "Rule 3.1.21 Parties II", - "status_comment_label": "Status Comment:", - "status_comment_placeholder": "Status comment", - "related_docs_label": "Docs Relation ID:", - "electronic_signature_label": "Electronic Signature:", - "authorized_person_label": "Attorney/Authorized Person:", - "contingency_mode_label": "Contingency Mode", - "cove_label": "COVE:", - "operation_number_label": "Operation No.:", - "adendas_label": "Addenda(s):", - "vu_observations_label": "VU Observations:", - "load_info": "Load Info.", - "entry_exit_date_label": "Entry/Exit Date:", - "payment_date_label": "Payment Date:", - "certificate_number_label": "Certificate Number:", - "enclosure_label": "Enclosure:", - "alternate_flags_title": "Alternate Invoice & Flags", - "valuation_method_placeholder": "Select...", - "mixed_label_short": "Mixed?", - "errors_title": "Billing Errors", - "line": "Line", - "key": "Key", - "description": "Description", - "no_errors": "No errors registered", - "insert": "Insert", - "edit": "Edit", - "delete": "Delete" - }, - "others": { - "transport_mode_label": "Transport Mode:", - "select_mode_placeholder": "Select mode", - "print_stamp_label": "Print stamp for value less than 2500 USD", - "mixed_label": "Mixed?", - "yes": "Yes", - "no": "No", - "master_bol_label": "Master BOL Number:", - "guide_number_label": "Guide Number:", - "shipment_number_label": "Shipment Number:", - "option_iv18_label": "IV 18 Option:", - "select_option_placeholder": "Select option", - "delivery_title": "Delivery Data", - "delivered_label": "Delivered", - "received_by_label": "Received by:", - "delivery_date_label": "Delivery Date:", - "rule_3121_label": "Rule 3.1.21 Parties II", - "status_comment_label": "Status Comment:", - "status_comment_placeholder": "Status comment", - "related_docs_label": "Docs Relation ID:", - "electronic_signature_label": "Electronic Signature:", - "authorized_person_label": "Attorney/Authorized Person:", - "contingency_mode_label": "Contingency Mode", - "cove_label": "COVE:", - "operation_number_label": "Operation No.:", - "adendas_label": "Addenda(s):", - "vu_observations_label": "VU Observations:", - "load_info": "Load Info.", - "entry_exit_date_label": "Entry/Exit Date:", - "payment_date_label": "Payment Date:", - "certificate_number_label": "Certificate Number:", - "electronic_signature_2_label": "Electronic Signature:", - "errors_title": "Billing Errors", - "line": "Line", - "key": "Key", - "description": "Description", - "no_errors": "No errors registered", - "insert": "Insert", - "edit": "Edit", - "delete": "Delete" - }, - "items": { - "unsaved_invoice_title": "Invoice not saved", - "unsaved_invoice_description": "You must save the invoice before adding items.", - "loaded_more_items": "Loading more items...", - "deleted": "Item deleted", - "delete_failed": "Could not delete the item", - "no_data_to_save": "No data to save", - "required_fields": "Fill in the required fields (Class or Description)", - "no_active_company": "There is no active company ID. Make sure you have a company selected.", - "no_invoice_id": "There is no invoice ID. The invoice must be saved before adding items.", - "update_failed": "Could not update the item", - "updated": "Item updated", - "create_failed": "Could not create the item", - "created": "Item created", - "save_error": "Error saving", - "saved_to_template": "Item saved to template", - "save_invoice_first": "Save the invoice first to use templates.", - "use_template_description": "Select a predefined template to load its items.", - "refresh": "Refresh", - "search_templates_placeholder": "Search templates...", - "loading": "Loading...", - "template_applied": "Template applied", - "apply_template_error": "Error applying template", - "template_saved": "Template saved", - "save_template_error": "Error saving template", - "title": "Invoice Items", - "subtitle": "Load items, create templates, or apply them without leaving this view.", - "use_template": "Use template", - "create_template": "Create template", - "add_items": "Add items", - "cancel": "Cancel", - "applying": "Applying...", - "apply_template": "Apply Template", - "create_template_dialog_title": "Create template", - "create_template_dialog_description": "Save the current items as a reusable template to inject into other items.", - "template_name_label": "Template Name", - "template_name_placeholder": "E.g. Standard parts package", - "template_description_label": "Description", - "template_description_placeholder": "Describe what this template is for...", - "template_items_count": "items/lines", - "template_items_title": "Template items", - "add_item_line": "Add Item/Line", - "template_table_hash": "#", - "template_table_description": "Description", - "template_table_quantity": "Qty.", - "template_table_actions": "Actions", - "template_empty": "Use the \"Add Item/Line\" button to define the template contents.", - "no_description": "No description", - "no_description_short": "No description available.", - "no_description_available": "No description available.", - "no_templates_found": "No templates found", - "select_template_to_view": "Select a template to view its details", - "created_label": "Created", - "item_description": "Item Description", - "quantity_short": "Qty.", - "quantities": "Quantities:", - "template_empty_items": "This template does not contain items.", - "imported_quantity": "Imported Qty.", - "reference": "Ref:", - "saving": "Saving...", - "save_template": "Save template", - "column_line": "Line", - "column_impo_invoice": "Impo Invoice", - "column_ps": "P/S", - "column_class": "Class", - "column_part_number": "Part Number", - "column_description": "Description", - "column_has_subitem": "Contains Sub-item", - "column_main_item": "Main Item", - "column_class_description": "Class Description", - "column_um": "U.M.", - "column_preference": "Preference", - "column_quantity": "Quantity", - "column_actions": "Actions", - "no_items_available": "No items available", - "showing_lines": "Showing {displayed} of {total} lines", - "spanish_description_label": "Description in Spanish:", - "select_row_to_view_description": "Select a row to view the description.", - "bultos": "Bundles:", - "imported": "Imported:", - "net_weight": "Net weight:", - "gross_weight": "Gross weight:", - "import_values_title": "Import values:", - "dollars": "Dollars:", - "pesos": "Pesos:", - "capture_value": "Capture Value:", - "customs_value_short": "Customs:" - } - }, - "invoice_item_fa": { - "item_sheet": { - "tab_general": "General", - "tab_identifiers": "Identifiers", - "not_available_short": "N/A" - }, - "repair": { - "generate_discharge": "Generate Discharge?", - "export_invoice_label": "Expo Invoice", - "export_line_label": "Expo Line", - "type_search_label": "Search Type", - "import_type_label": "Import Type:", - "import_invoice_label": "Import Invoice", - "line_label": "Line", - "loading_line": "Loading...", - "search_placeholder": "Select...", - "temporal": "TEM (Temporary)", - "definitive": "DEF (Definitive)", - "loading_item_data": "Loading item data...", - "close": "Close", - "cancel": "Cancel", - "select_line_title": "Select line", - "import_title": "Import items", - "import_description": "Select a line with available balance to perform the discharge.", - "loading_invoice_items": "Loading invoice items...", - "no_balance": "No balance available", - "no_balance_description": "There are no lines with balance in this invoice to discharge.", - "no_description": "No description" - }, - "main_data": { - "legend": "Main Data", - "quantity": "Quantity", - "unit_cost": "Unit Cost", - "total_value": "Total Value", - "tariff_type": "Tariff Type" - }, - "packages": { - "legend": "PACKAGES", - "quantity": "Quantity", - "package_code": "Package Code", - "weight": "Weight", - "description": "Description", - "weights": "WEIGHTS", - "net": "Net", - "gross": "Gross", - "space": "Space", - "permit_number": "Permit No.", - "page_region": "Page/Region", - "american_fraction": "US Fraction", - "brand": "Brand", - "model": "Model", - "purchase_order": "Purchase Order" - }, - "summary": { - "general_data": "GENERAL DATA", - "return_quantity_subitems": "RETURN QUANTITY SUB-ITEMS", - "temporary": "Temporary", - "replacement_or_change": "Replacement or Change", - "definitive": "Definitive", - "returned_values": "Returned Values", - "weights_kilos": "WEIGHTS (KILOS)", - "weights_pounds": "WEIGHTS (POUNDS)", - "net": "Net", - "gross": "Gross", - "costs_values": "COSTS AND VALUES", - "dollars": "(Dollars)", - "pesos": "(Pesos)", - "cost": "Cost", - "value": "Value", - "customs_value": "Customs Value", - "capture_cost": "Capture Cost", - "capture_value": "Capture Value" - }, - "continuation": { - "tax_paid": "TAX PAID", - "yes": "Yes", - "no": "No", - "general_info": "General information", - "transport_number_type": "Transport number/type:", - "vehicle_data": "Vehicle data:", - "is_rail": "Is rail?", - "bill_number": "Bill of lading no.:", - "guide_count": "Shipping guide count (BL):", - "destination_origin": "Destination/Origin:", - "destination_origin_placeholder": "FRANJA FRONT.", - "is_mixed": "Mixed?", - "entry_port": "Entry port:", - "export_reason": "Export reason:", - "reason_sold": "Sold", - "reason_not_sold": "Not sold", - "reason_other": "Other", - "payment_terms": "Payment terms:", - "handling_fees": "Handling fees:", - "reviewed_equipment": "Equipment reviewed", - "subdivision": "Subdivision", - "acts_as_cd": "Acts as CD", - "pedimento_arrived": "Pedimento arrived", - "billing_errors": "Billing errors", - "error_line": "Line", - "error_key": "Key", - "error_description": "Description", - "no_errors": "No errors registered", - "insert": "Insert", - "edit": "Edit", - "delete": "Delete", - "traffic_light": "Traffic light", - "green_mx": "Green MX", - "green_usa": "Green USA", - "red_mx": "Red MX", - "red_usa": "Red USA", - "cfdi_data_title": "CFDI DATA", - "cfdi_uuid_label": "CFDI UUId:", - "cfdi_pdf_label": "CFDI Path PDF:", - "cfdi_xml_label": "CFDI Path XML:", - "payment_method": "Payment Method", - "igi_amount": "IGI Amount", - "dollars": "DOLLARS", - "igi_payment_method": "IGI Payment Method", - "has_fda_code": "Has FDA Code", - "has_certificate_of_origin": "Has Certificate of Origin?", - "certificate_number": "Certificate of Origin No.", - "end_date": "End Date", - "machinery_equipment_location": "Machinery and equipment location", - "location_variable": "Location variable", - "military_equipment_enable": "Enable if Item Contains Military Equipment", - "own_equipment": "Own Equipment", - "omit_annex31": "Omit Annex 31", - "lot": "Lot", - "entry_number": "Entry No.", - "eighth_rule_permit": "Eighth Rule Permit", - "eighth_rule_fraction": "Eighth Rule Fraction", - "line": "Line", - "consider_a31": "Consider in A31", - "extra_description_spanish": "Extra Description in Spanish" - }, - "configuration": { - "is": "Is", - "item": "Item", - "subitem": "Subitem", - "contains_subitems": "Contains Sub-Items", - "yes": "Yes", - "main_item_number": "Main Item Number", - "main_item_number_placeholder": "Enter main item number", - "description_spanish": "Description in Spanish", - "description_english": "Description in English" - }, - "labeling": { - "legend": "Labeling & Valuation", - "label_number": "Label Number", - "label_type": "Label Type", - "observations": "Observations", - "observations_placeholder": "Labeling observations...", - "assets_series": "Assets / Series", - "asset_number_short": "Asset Num", - "actions_short": "Act.", - "asset_number": "Asset Number", - "cancel": "Cancel", - "save": "Save" - }, - "identifiers": { - "asset_number": "Asset Number", - "asset_tag_title": "Asset Tag" - }, - "dialogs": { - "countries_load_error": "Error loading countries", - "states_load_error": "Error loading states", - "packages_load_error": "Error loading packages", - "units_load_error": "Error loading units of measure", - "payment_methods_load_error": "Error loading payment methods" - }, - "invoice_item_inv": { - "edit_title": "Edit Item", - "add_title": "Add New Item", - "edit_description": "Modify inventory fields and save changes.", - "add_description": "Fill in the new inventory item information.", - "line_prefix": "Line", - "required_fields_hint": "Fields marked with * are required.", - "tab_general": "General", - "tab_classification": "Classification", - "tab_quantities": "Quantities", - "tab_other": "Other", - "invoice_info_title": "Invoice Information", - "invoice_unsaved_warning": "This invoice has not been saved yet. Items will be associated when you save the invoice.", - "invoice_id": "Invoice ID:", - "operation_type": "Operation Type:", - "invoice_number": "Invoice Number:", - "system": "System:", - "class_label": "Class", - "select_class_placeholder": "Select a class", - "quantity_label": "Quantity", - "unit_label": "U.M.", - "select_unit_placeholder": "Select U.M.", - "unit_cost_label": "Unit Cost", - "country_label": "Country of Origin", - "select_country_placeholder": "Select country", - "fraction_label": "Fraction", - "select_fraction_placeholder": "Select fraction", - "tariff_type_label": "Tariff Type", - "reference_number_label": "Reference Number", - "purchase_order_label": "Purchase/Sales Order", - "warehouse_label": "Warehouse", - "location_label": "Location", - "description_es_label": "Description (Spanish)", - "description_es_placeholder": "Description in Spanish", - "description_en_label": "Description (English)", - "description_en_placeholder": "Description in English", - "sku_label": "SKU", - "sku_placeholder": "Product SKU code", - "batch_label": "Batch", - "batch_placeholder": "Batch number", - "classification_fraction_label": "Tariff Fraction", - "fraction_digits_placeholder": "8 digits", - "product_type_label": "Product Type", - "product_type_placeholder": "Raw material, finished product, etc.", - "material_type_label": "Material Type", - "material_type_placeholder": "Metal, plastic, etc.", - "product_code_label": "Product Code", - "product_code_placeholder": "Internal code", - "country_origin_label": "Country of Origin", - "country_code_placeholder": "Country code", - "merchandise_category_label": "Merchandise Category", - "merchandise_category_placeholder": "Category", - "quantity_tab_label": "Quantity", - "unit_of_measure_label": "Unit of Measure", - "unit_of_measure_placeholder": "PCS, KG, M, etc.", - "zero_placeholder": "0", - "decimal_placeholder": "0.00", - "net_weight_label": "Net Weight (KG)", - "gross_weight_label": "Gross Weight (KG)", - "unit_cost_usd_label": "Unit Cost (USD)", - "total_value_label": "Total Value (USD)", - "packages_label": "Number of Packages", - "package_type_label": "Package Type", - "package_type_placeholder": "Box, pallet, etc.", - "imported_quantity_label": "Imported Quantity", - "remaining_quantity_label": "Remaining Quantity", - "brand_label": "Brand", - "brand_placeholder": "Product brand", - "expiration_date_label": "Expiration Date", - "production_date_label": "Production Date", - "min_stock_label": "Minimum Stock", - "max_stock_label": "Maximum Stock", - "observations_label": "Observations", - "observations_placeholder": "Additional inventory notes...", - "loading_item_data": "Loading item data...", - "loading_more_items": "Loading more items...", - "invoice_line_info": "Invoice information ({systemLabel})", - "select_line": "Select line", - "import_title": "Import items", - "import_description": "Select a line with available balance to perform the discharge.", - "loading_invoice_items": "Loading invoice items...", - "no_balance": "No balance available", - "no_balance_description": "There are no lines with balance in this invoice to discharge.", - "balance_required": "Available balance line", - "cancel": "Cancel", - "close": "Close", - "saving": "Saving...", - "update": "Update", - "create": "Create" - }, - "prerequisites": { - "title": "Notice", - "message_both": "There are no Customs brokers or Clients registered. You must register them to work in this module.", - "message_agents": "There are no Customs brokers registered. You must register them to work in this module.", - "message_clients": "There are no Clients registered. You must register them to work in this module.", - "register_hint": "You can register them in", - "agents_link": "Customs Brokers", - "clients_link": "Clients and Providers", - "and": "and", - "cancel": "Cancel", - "accept": "Accept" - } - } -} \ No newline at end of file + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from en!", + "sidebar": { + "dashboard": "Dashboard", + "reference_data": { + "title": "Fixed Catalogs", + "codes_pedimento_regimen": "Pedimento and Regime Codes", + "containers": "Containers", + "countries": "Countries", + "currency_types": "Currency Types", + "customs_sections": "Customs Sections", + "customs_warehouses": "Customs Warehouses", + "incoterms": "Incoterms", + "document_types_digitization": "Document types for digitization", + "invoice_types": "Invoice Types", + "material_types": "Material Types", + "payment_methods": "Payment Methods", + "pedimento_codes": "Pedimento Codes", + "pedimento_regimes": "Pedimento Regimes", + "sectors": "Sectors", + "states": "States", + "transportation_modes": "Transportation Modes", + "transportation_types": "Transportation Types", + "valuation_methods": "Valuation Methods", + "configuracion": "Settings", + "general": "General", + "licencia": "License", + "usuarios": "Users", + "ayuda": "Help" + }, + "general_catalogs": { + "title": "General Catalogs", + "company_information": "Company Information", + "packages": "Packages", + "concepts": "Concepts", + "classification": "Classification", + "identifiers": "Identifiers", + "incoterms": "Incoterms", + "inpc": "I.N.P.C", + "fixed_legends": "Fixed Legends", + "seals": "Seals", + "valuation_methods": "Valuation Methods", + "countries": "Countries", + "ports": "Ports", + "unit_measures": "Units of Measure", + "um_customs_mex": "Units of Measure - Mexican Customs", + "um_customs_ame": "Units of Measure - American Customs", + "um_ace": "Units of Measure - ACE", + "um_oma": "Units of Measure - OMA", + "conversions": "Conversions", + "equivalences": "Equivalences", + "exchange_rates": "Exchange Rates", + "currency_types": "Currency Types", + "multi_currency": "Multi Currency", + "invoice_types": "Invoice Types", + "electronic_signatures": "Electronic Signatures", + "billing_errors": "Billing Errors", + "customs_warehouses": "Customs Warehouses", + "locations": "Locations", + "doda": "DODA", + "packing_list": "Packing List", + "prevalidators": "Prevalidators", + "electronic_notices": "Electronic Notices", + "back_flush": "Back Flush", + "crossing_notice": "Crossing Notice", + "customs_broker_concepts": "Customs Broker Concepts" + }, + "fractions": { + "title": "Fractions", + "sitar": "Fraction Sitar", + "sitar_seventh_amendment": "Fraction Sitar - Seventh Amendment", + "sitar_us": "Fraction Sitar US", + "american": "Fraction US", + "canadian": "Fraction Canadian", + "historical": "Fraction Historical", + "sectors": "Sectors" + }, + "goods": { + "title": "Goods", + "classes": "Classes", + "parts": "Parts", + "fda_codes": "FDA Codes" + }, + "pedimentos": { + "title": "Pedimentos", + "pedimento_management": "Pedimento Management", + "pedimento_codes": "Pedimento Codes", + "customs_regimes": "Customs Regimes", + "payment_methods": "Payment Methods", + "customs_sections": "Customs Sections", + "anexo_22_app_31": "Anexo 22 App 3" + }, + "import_invoices": { + "title": "Import Invoices", + "temporary": "Temporary", + "definitive": "Definitive", + "mexican_purchases": "Mexican Purchases", + "regime_change": "Regime Change", + "repair": "Repair" + }, + "export_invoices": { + "title": "Export Invoices", + "exportation": "Exportation", + "repair": "Repair" + }, + "export": { + "title": "Exportation", + "catalog": "Export Catalog", + "repair": "Repair", + "manifest": "Manifest", + "proforma": "Proforma", + "reports": "Reports", + "used_materials": "Used Materials Module", + "destruction": "Destruction", + "special_processes": "Special Processes" + }, + "clients_and_providers": "Clients and Providers", + "customs_brokers": "Customs Brokers", + "audit_logs": "Audit Logs", + "audit_logs_title": "Audit Logs", + "audit_logs_description": "Audit trail of operations and background task (Celery) status.", + "audit_logs_tab_bitacora": "Audit trail", + "audit_logs_tab_tasks": "Background tasks", + "audit_logs_tab_files": "File manager", + "audit_logs_files_title": "File manager", + "audit_logs_files_root": "Files root", + "audit_logs_files_refresh": "Refresh", + "audit_logs_files_list_title": "Contents", + "audit_logs_files_error_prefix": "Error:", + "audit_logs_files_col_name": "Name", + "audit_logs_files_col_size": "Size", + "audit_logs_files_col_modified": "Modified", + "audit_logs_files_col_actions": "Actions", + "audit_logs_files_loading": "Loading files...", + "audit_logs_files_empty": "No files or folders found in this location.", + "audit_logs_files_download": "Download", + "despacho": { + "title": "Dispatch", + "digitalizacion": "Digitization", + "doda": "DODA" + }, + "doda_alta": { + "title": "DODA", + "subtitle": "Customs Clearance Declaration", + "new": "New", + "refresh": "Refresh", + "table_title": "DODAs", + "col_integration_number": "Integration No.", + "col_patent": "Patent", + "col_status": "Status", + "col_dispatch_customs": "Dispatch Customs", + "col_operation_type": "Operation Type", + "col_actions": "Actions", + "action_alta_doda": "DODA Filing", + "action_alta_pita": "PITA Filing", + "action_edit": "Edit", + "action_delete": "Delete", + "action_new": "New DODA", + "progress_title": "Processing DODA filing...", + "progress_success": "DODA filing completed successfully.", + "progress_error": "Error in DODA filing.", + "eligibility_error": "DODA does not meet the requirements for filing.", + "eligibility_checking": "Checking eligibility...", + "empty": "No DODAs", + "loading": "Loading...", + "search_placeholder": "Search:", + "confirm_delete": "Are you sure you want to delete this DODA?", + "delete_success": "DODA deleted successfully", + "delete_error": "Error deleting DODA", + "delete_missing_company": "Select a company", + "delete_select_one": "Select exactly one DODA from the list", + "delete_not_found": "Could not locate the DODA. Select the row again and retry", + "filter_integration_number": "Integration No.", + "filter_patent": "Patent", + "filter_status": "Status", + "filter_operation_type": "Operation Type", + "action_generar": "Submit", + "action_export_excel": "Report by date range", + "action_export_pedimentos": "DODA report", + "export_pedimentos_success": "DODA report generated.", + "export_pedimentos_error": "Could not generate the DODA report.", + "export_excel_title": "Export DODA list", + "export_excel_subtitle": "Filter by DODA date (stored as YYYYMMDD).", + "export_excel_badge": "DODA CATALOG", + "export_report_heading": "General report by date range", + "export_fecha_inicio": "Start date", + "export_fecha_final": "End date", + "export_julian_label": "Use Julian (numeric) date in Excel file.", + "export_report_generar": "Generate", + "export_date_from": "From", + "export_date_to": "To", + "export_format": "File format", + "export_date_mode": "Date/time in file", + "export_date_mode_formatted": "Formatted (DD/MM/YYYY and time)", + "export_date_mode_raw": "Numeric (raw YYYYMMDD)", + "export_download": "Download", + "export_cancel": "Close", + "export_excel_success": "File generated.", + "export_excel_error": "Could not generate the file.", + "export_no_data": "No DODAs in the selected date range. Widen the range or try other dates.", + "export_excel_invalid_dates": "Enter from and to dates." + }, + "digitalizacion": { + "title": "Digitization", + "subtitle": "Digitized Documents Catalog", + "new": "New", + "refresh": "Refresh", + "table_title": "Digitized documents", + "col_consecutivo": "Consecutive", + "col_tipo_documento": "Document Type", + "col_e_document": "E-Document", + "col_fecha": "Date", + "col_num_operacion_vu": "VU Operation No.", + "col_actions": "Actions", + "form_e_document": "E-Document", + "form_num_operacion": "Operation No.", + "form_tipo_documento": "Document Type", + "form_archivo_digitalizado_en": "Digitized in", + "form_fecha": "Date", + "form_agente_aduanal": "Customs Broker", + "form_pedimento": "Entry", + "form_nombre_archivo": "File name", + "digitalizar_title": "Digitize Document", + "digitalizar_subtitle": "Send document to Ventanilla Única", + "digitalizar_file_label": "File", + "digitalizar_rfc_consulta": "RFC Query", + "digitalizar_clave_documento": "Document Key", + "progress_title": "Digitalizing document...", + "progress_step": "Step", + "progress_success": "Digitalization completed successfully.", + "progress_download_acuse": "Download Receipt", + "action_digitalizar": "Digitalize", + "action_download_zip": "Download ZIP", + "action_acuse": "Receipt", + "action_envio_xml": "Envío XML", + "action_respuesta_xml": "Respuesta XML", + "action_consulta_envio_xml": "Consulta Envío XML", + "action_consulta_respuesta_xml": "Consulta Respuesta XML", + "action_edit": "Edit", + "action_delete": "Delete", + "empty": "No digitized documents", + "loading": "Loading...", + "search_placeholder": "Search:", + "confirm_delete": "Are you sure you want to delete this document?" + }, + "client_provider_type": { + "client_indicator": "C", + "provider_indicator": "P", + "both_indicator": "B" + }, + "nav_user": { + "profile": "Profile", + "settings": "Settings", + "logout": "Logout" + }, + "transports": { + "title": "Transportation", + "transporters": "Carriers", + "drivers": "Drivers", + "trailers": "Trailers", + "vehicles": "Vehicles" + }, + "reports": { + "title": "Reports", + "invoices": "Impo/Expo Invoices", + "downloaded_parts": "Downloaded Parts", + "expiration": "Expiration Report" + }, + "settings": { + "general": "General" + }, + "doda_form": { + "shortcuts_scope": "DODA form", + "title_new": "New DODA", + "title_edit": "Edit DODA", + "description_catalog": "Catalogs · DODA", + "tab_general": "General", + "tab_seals_sat": "Seals and SAT", + "shortcuts_hint": "Alt+1/2 · Ctrl+S save · Esc cancel", + "btn_cancel": "Cancel", + "btn_save": "Save", + "btn_saving": "Saving...", + "btn_save_changes": "Save changes", + "btn_create_doda": "Create DODA", + "btn_accept": "OK", + "card_broker_customs": "Customs agent and office", + "card_transport": "Transport", + "card_control": "Control and dispatch", + "card_sat_chain": "Original chain and signatures (SAT)", + "label_responsible": "Broker", + "label_patent": "Patent", + "label_dispatch": "Dispatch office", + "label_section_es": "E/S section", + "label_operation_type": "Operation type", + "label_transporter": "Carrier", + "label_transport_id": "Transport ID", + "label_caat": "CAAT", + "label_doda_date": "DODA date", + "label_status": "Status", + "label_dispatch_type": "Dispatch type", + "label_unique_badge": "Unique badge", + "label_integration_num": "Integration No.", + "label_transaction_num": "Transaction No.", + "label_fast_id": "Fast ID", + "label_last_user": "Last user", + "label_original_chain": "Original chain", + "label_serial_cert": "Serial (certificate)", + "label_uuid_cp": "Carta porte UUID", + "label_electronic_sig": "Electronic signature", + "label_sat_cert": "SAT certificate", + "label_sat_chain": "Original SAT chain", + "ph_aga": "AGA key", + "ph_0000": "0000", + "ph_000": "000", + "ph_select": "Select", + "ph_plate": "Plate / vehicle ID", + "ph_dash": "—", + "ph_yyyymmdd": "YYYYMMDD", + "ph_badge_pita": "N/A — PITA", + "ph_badge_num": "Badge no.", + "ph_example_container": "E.g. 53056", + "op_import": "I — Import", + "op_export": "E — Export", + "type_pita": "PITA", + "type_doda": "DODA", + "vu_checking": "Verifying agent VU DODA…", + "vu_incomplete": "VU DODA incomplete: agent needs .cer, .key, and DODA FIEL password.", + "vu_complete": "VU DODA complete for API submission.", + "badge_required_hint": "Required for DODA filing API.", + "pedimentos": "Pedimentos", + "lines": "lines", + "containers": "Containers", + "american_pedimentos": "U.S. pedimentos", + "seals_block_title": "Seals — total in DODA: {n} / 8", + "seals_help": "Select a container. Maximum 8 seals per DODA (SCAII).", + "seals_select_container": "Select a container in the table to view or edit its seals.", + "container_no_id_warning": "Container not saved on server. Enter value, press Save; new containers are sent and reloaded with id for seals.", + "container_line_info": "Container:", + "seal_on_line": "seal(s) on this line", + "line_word": "Line", + "btn_add_seal": "Add seal", + "btn_seal_delete": "Delete", + "seals_empty_line": "No seals on this container.", + "col_line": "Line", + "col_auth_patent": "Auth. patent", + "col_document": "Document", + "col_remesa": "Shipment", + "col_cove": "COVE", + "col_umc": "UMC", + "col_cash_usd": "Cash USD", + "col_diff_usd": "Difference USD", + "col_dta_niu": "DTA NIU", + "col_art7": "Art. 7", + "col_container": "Container", + "col_seals": "Seals", + "col_seal_value": "Seal", + "col_american_type": "Type", + "col_american_ped": "U.S. pedimento", + "col_pedimento_only": "U.S. pedimento", + "yes": "Yes", + "no": "No", + "child_empty": "No rows. “New” to add.", + "child_new": "New", + "child_edit": "Edit", + "child_delete": "Delete", + "modal_container_new": "New container", + "modal_container_edit": "Edit container", + "modal_container_desc": "Enter the container value for the DODA declaration.", + "label_container_value": "Container value", + "modal_seals_in_container": "Seals in container", + "seal_modal_title": "Containers > Seal", + "seal_modal_desc": "Enter the seal value for the selected container.", + "label_seal": "Seal", + "ph_seal": "Seal value", + "american_modal_title": "U.S. pedimento", + "american_modal_desc": "Enter type and value of the U.S. pedimento.", + "label_american_type_short": "U.S. type", + "label_american_value": "U.S. pedimento", + "ph_american_value": "U.S. pedimento value", + "line_label": "Line:", + "select_type": "Select type", + "american_cat_6": "AMERICAN PEDIMENTO", + "american_cat_7": "SELF-DECLARATION", + "american_cat_8": "NOT PRESENT", + "err_american_tipo_required": "U.S. pedimento type is required.", + "err_american_tipo_import": "U.S. pedimento type is not valid for import (must be 1, 2, 3, 4, or 5).", + "err_american_tipo_export": "U.S. pedimento type is not valid for export (must be 6, 7, or 8).", + "err_american_op_undefined": "Set operation type (I/E) before validating the U.S. pedimento.", + "err_company": "Select a company", + "err_responsible": "Broker is required", + "err_patent": "Patent is required", + "err_transport": "Transport ID is required. Select a vehicle.", + "err_badge": "Unique badge number is required for DODA filing.", + "err_vu_wait": "Wait for agent VU DODA check to finish, then try again.", + "err_vu_config": "The customs agent does not have full VU DODA config (.cer, .key, DODA FIEL password).", + "err_min_containers": "Add at least one container for API submission.", + "err_american_new_lines": "Enter the U.S. pedimento value for each new line.", + "err_save": "Error saving", + "toast_saved": "Changes saved successfully.", + "toast_created": "DODA created successfully.", + "load_error": "Could not load DODA", + "warn_vu_incomplete": "This DODA’s agent does not have full VU DODA (.cer, .key, DODA FIEL password).", + "warn_vu_fetch": "Could not validate the agent’s VU settings.", + "warn_broker_select": "Selected agent has incomplete VU DODA. Configure in Customs agents before generating.", + "seal_save_first": "Save the DODA before managing seals.", + "seal_pick_container": "Select a container in the table.", + "seal_not_persisted": "This container is not on the server yet. Save the DODA and reload.", + "seal_empty": "Seal cannot be empty.", + "seal_max": "DODA already has the maximum 8 seals.", + "seal_add_err": "Error adding seal", + "seal_delete_err": "Error removing seal", + "pedimento_remove_blocked": "Cannot remove pedimentos already saved on the server here.", + "container_delete_err": "Error deleting container", + "american_delete_err": "Error deleting U.S. pedimento", + "container_update_err": "Error updating container", + "american_cannot_edit_persisted": "To change saved U.S. pedimentos, remove and add again.", + "err_american_value": "Enter the U.S. pedimento value.", + "err_american_type_or_value": "Enter type and/or U.S. pedimento value.", + "err_containers_max": "A DODA can have at most 4 containers.", + "err_container_empty": "Container value cannot be empty.", + "err_container_not_found": "Container to edit not found.", + "pedimento_selector_title": "Containers > Seal", + "list_page_subtitle": "Manage your Customs Operation Documents (DODA)", + "list_btn_new": "New DODA", + "list_card_title": "DODA list", + "list_ph_folio": "Folio", + "list_ph_patent": "Patent", + "list_filter_status_ph": "Status", + "list_filter_status_all": "All", + "list_filter_op_import": "Import", + "list_filter_op_export": "Export", + "list_filter_op": "Operation", + "list_filter_op_all": "All", + "list_btn_clear": "Clear", + "list_showing": "Showing {a} of {b} records", + "list_active_filters": "Active filters: {n}", + "list_btn_edit": "Edit", + "list_btn_print": "Print", + "list_toast_reload_error": "Error reloading data", + "list_elig_error_prefix": "Error checking eligibility: ", + "list_elig_not_meet": "This DODA does not meet the filing requirements.", + "list_alta_error_prefix": "Error sending DODA filing: ", + "list_print_error": "Error generating DODA PDF", + "list_alta_complete": "DODA filing completed successfully", + "list_shortcuts_scope": "DODA list", + "list_col_folio": "Folio", + "list_col_doda_date": "DODA date", + "list_col_desp": "Cstm.", + "list_col_patent": "Patent", + "list_col_pedimentos": "Pedimento(s)", + "list_col_remesas": "Shipment(s)", + "list_col_integracion": "Integration", + "list_col_trans": "Trans. no.", + "list_col_id_transport": "Transport ID", + "list_col_caat": "CAAT", + "list_col_user": "User", + "list_col_status": "Status", + "list_loading_more": "Loading more...", + "list_scroll_for_more": "Scroll to load more", + "list_confirm_delete": "Are you sure you want to delete this DODA record?", + "list_toast_delete_ok": "DODA deleted successfully", + "list_toast_delete_err": "Error deleting DODA", + "list_filter_i": "I — Import", + "list_filter_e": "E — Export", + "list_no_results": "No results." + } + }, + "invoice_list": { + "skip_to_actions": "Go to invoice actions", + "header": { + "title": "Invoices", + "description": "Manage system invoices" + }, + "titles": { + "base": "INVOICE CATALOG", + "import": "IMPORT", + "export": "EXPORT", + "import_temporal": "TEMPORARY IMPORT", + "import_definitive": "DEFINITIVE IMPORT", + "import_mexican": "MEXICAN PURCHASES", + "import_regime_change": "REGIME CHANGE AND REGULARIZATION", + "import_repair": "IMPORT REPAIR", + "export_definitive": "DEFINITIVE EXIT", + "export_repair": "REPAIR" + }, + "filters": { + "operation_label": "Operation Type", + "operation_all_option": "Operation: All", + "invoice_type_label": "Invoice Type", + "invoice_type_all_option": "Invoice: All", + "invoice_number_placeholder": "Invoice No.", + "year_start_placeholder": "Start year", + "year_end_placeholder": "End year", + "active_filters": "Active filters" + }, + "actions": { + "parameters": "Settings", + "new_invoice": "New Invoice", + "refresh": "Refresh", + "reports": "Reports", + "more_actions": "More Actions", + "downloads": "Downloads", + "other_actions": "Other Actions", + "cancel": "Cancel", + "continue": "Continue", + "generate_cove": "Generate COVE", + "close": "Close" + }, + "card": { + "invoice_list_title": "Invoice List" + }, + "summary": { + "showing": "Showing", + "of": "of", + "records": "records" + }, + "operation_types": { + "all": "All", + "import": "Import", + "export": "Export" + }, + "cove_dialog": { + "title": "Generate COVE", + "description_prefix": "Select the recipient email for invoice", + "recipient_label": "Recipient email", + "destination": "COVE destination", + "select_email": "Select an email", + "fallback_email": "It will be sent to the email of the user who generated the invoice", + "search_email": "Search email...", + "loading_emails": "Loading available emails...", + "no_emails": "No emails available for COVE.", + "selected_badge": "Selected" + }, + "progress": { + "title_pdf": "Generating Invoice PDF", + "title_consolidated": "Generating Consolidated Report", + "title_descargo": "Generating FIFO Report", + "title_packing_list": "Generating Packing List", + "title_winsaai": "Generating WINSAAI Report", + "title_process_invoice": "Processing invoice", + "title_revert_invoice": "Reverting invoice", + "title_validate_cove": "Validating data for COVE", + "complete_processed": "Invoice processed successfully", + "complete_reverted": "Invoice reverted successfully", + "complete_cove_validation": "COVE validation completed", + "complete_default": "Process completed" + }, + "steps": { + "load_invoice": "Loading invoice", + "validate_invoice_data": "Validating invoice data", + "review_classes_exchange_rate": "Reviewing classes and exchange rate", + "calculate_item_values": "Calculating item values", + "validate_items": "Validating items", + "validate_rule8_quotas": "Validating Rule Eight quotas", + "update_totals": "Updating totals", + "validate_invoice_status": "Validating invoice status", + "verify_item_balances": "Verifying item balances", + "confirm_changes": "Confirming changes" + }, + "dialogs": { + "revert_title_export": "Revert Export Invoice", + "revert_title_import": "Revert Import Invoice", + "revert_description_intro": "The invoice", + "revert_description_warning": "This operation will revert the balance/discharge records generated when processing the invoice.", + "revert_description_question": "Do you want to continue?", + "winsaai_title": "Customs and Inventory Control System", + "winsaai_description_intro": "Invoice", + "winsaai_of_type": "of type", + "winsaai_description_process": "has been assigned to WINSAAI File Generation.", + "winsaai_description_question": "Do you want to Continue or Cancel?" + }, + "footer": { + "toolbar_aria": "Invoice actions", + "invoice_pdf": "Invoice PDF", + "invoice_csv": "Invoice CSV", + "consolidated": "Consolidated", + "consolidated_notice": "Consolidated Notice", + "packing_list": "Packing List", + "four_copies_rem": "4 REM Copies", + "descargo_peps": "FIFO Discharge", + "transferencia_electronica": "Electronic Transfer", + "interface_vu": "VU Interface", + "vu_options_keyboard": "VU options (keyboard)", + "vu_consult": "Consult", + "vu_addenda": "Addenda", + "vu_cove_receipt": "COVE Receipt", + "vu_massive_cove": "Mass COVE", + "cons_sed": "SED Consult", + "encomienda": "Commission", + "fact_mex_cons": "Mex Invoice Cons", + "fact_mex_ord_cat": "Mex Invoice Ord Cat", + "export_sia": "Export SIA", + "interface": "Interface", + "process_update": "Update", + "unprocess": "Revert", + "view_details": "View Details", + "customs_broker_interface": "Customs Broker Interface", + "edit": "Edit", + "delete": "Delete" + }, + "submenu": { + "consult_soon": "VU Consult - Coming soon", + "addenda_soon": "VU Addenda - Coming soon", + "massive_cove_soon": "Mass COVE - Coming soon", + "generate_invoice_csv_soon": "Generate Invoice CSV - Coming soon", + "four_copies_soon": "4 REM Copies - Coming soon", + "cons_sed_soon": "SED Consult - Coming soon", + "encomienda_soon": "Commission - Coming soon", + "fact_mex_cons_soon": "Mex Consolidated Invoice - Coming soon", + "fact_mex_ord_cat_soon": "Mex Invoice Capture Order - Coming soon", + "export_sia_soon": "Export SIA - Coming soon", + "interface_soon": "Interface - Coming soon" + }, + "recipients": { + "company_vu_email": "Company VU email", + "company_main_email": "Company main email", + "company_industrial_1": "Industrial email 1", + "company_industrial_2": "Industrial email 2", + "company_description": "Company {name}", + "single_window_email": "Single window email", + "main_email": "Main email", + "company_user_email": "Company user - {email}", + "my_email": "My email", + "authenticated_user": "Authenticated user - {email}", + "load_error": "Could not load available emails for COVE", + "no_configured": "No emails configured for COVE" + }, + "toasts": { + "select_invoice_for_cove": "Select an invoice to generate COVE", + "no_company_selected": "No company selected", + "session_expired_reloading": "Session expired. Reloading page...", + "load_more_error": "Error loading more data", + "apply_filters_error": "Error applying filters", + "reload_data_error": "Error reloading data", + "download_start_error": "Could not start download", + "consolidated_download_start_error": "Could not start consolidated download", + "calculating_peps": "Calculating FIFO assignment...", + "peps_calculation_error_prefix": "Error calculating FIFO: {error}", + "peps_calculation_completed": "FIFO calculation completed", + "peps_report_start_error": "Could not start FIFO report download", + "aviso_consolidado_start_error": "Could not start Consolidated Notice download", + "packing_list_start_error": "Could not start Packing List download", + "fast_interface_import_only": "Quick interface is only available for Import invoices", + "customs_broker_interface_start_error": "Could not start Customs Broker Interface generation", + "pdf_download_success": "PDF downloaded successfully", + "invoice_processed_success": "Invoice processed successfully", + "worker_error_prefix": "Worker reported an error: {error}", + "task_result_process_error": "Error processing task result", + "select_invoice_to_edit": "Select an invoice to edit", + "no_table_rows": "No rows in the table", + "select_invoice_for_reports": "Select an invoice for reports", + "select_invoice_for_more_actions": "Select an invoice for more actions", + "select_invoice_to_revert": "Select an invoice to revert", + "select_invoice_for_details": "Select an invoice to view details", + "select_invoice": "Select an invoice", + "select_at_least_one_invoice_to_delete": "Select at least one invoice to delete", + "select_invoice_for_pdf": "Select an invoice to download PDF", + "select_invoice_for_consolidated": "Select an invoice to download consolidated report", + "select_invoice_to_change_status": "Select an invoice to change status", + "update_status_error_prefix": "Error trying to {action} invoice: {error}", + "status_action_update": "update", + "status_action_revert": "revert", + "status_updated_success": "Invoice updated successfully", + "status_reverted_success": "Invoice reverted successfully", + "update_status_unexpected_error": "Unexpected error while changing status", + "select_invoice_to_process": "Select an invoice to process", + "process_start_error_prefix": "Error starting process: {error}", + "process_start_error": "Could not start process", + "revert_start_error_prefix": "Error starting revert: {error}", + "revert_start_error": "Could not start revert", + "select_recipient_email_for_cove": "Select an email to send COVE", + "cove_eligibility_error_prefix": "Could not validate COVE eligibility: {error}", + "cove_requirements_not_met": "Invoice does not meet COVE generation requirements", + "cove_verification_error": "Could not verify whether invoice can generate COVE", + "cove_start_error_prefix": "Error starting COVE generation: {error}", + "cove_start_error": "Could not start COVE generation", + "validation_extra_more": "\n...and {count} more", + "validation_error_count": "{count} validation error(s):\n{preview}{extra}", + "cove_external_queued_default": "COVE invoice started in Single Window. Use task_id to check status." + } + }, + "invoice_table": { + "no_results": "No results.", + "loading_more": "Loading more...", + "scroll_to_load_more": "Scroll to load more", + "processed": "Processed", + "pending": "Pending", + "operation": "Operation", + "operation_import": "Import", + "operation_export": "Export", + "invoice_type": "Invoice Type", + "invoice_number": "Invoice No.", + "pedimento_18": "Pedimento 18", + "remesa": "Remesa", + "invoice_date": "Invoice Date", + "pedimento_code": "Pedimento Code", + "document_type": "Doc Type", + "total_items": "Total Items", + "currency": "Currency", + "currency_type": "Currency Type", + "weight_type": "Weight Type", + "mixed": "Mixed", + "related_doc": "Related Doc", + "yes": "Yes", + "no": "No", + "not_available_short": "N/A" + }, + "invoice_selectors": { + "identifier_catalog": { + "title": "Select Identifier", + "description": "Search and select an identifier from catalog (Appendix 8).", + "search_placeholder": "Search by code or description...", + "column_code": "Code", + "column_description": "Description", + "column_level": "Level", + "empty": "No identifiers found." + }, + "valuation_method": { + "title": "Select Valuation Method", + "description": "Search and select a valuation method from the list.", + "search_placeholder": "Search by code or description...", + "column_code": "Code", + "column_description": "Description", + "empty": "No valuation methods found." + }, + "location": { + "title": "Location catalog (machinery and equipment)", + "no_company_selected": "No company selected", + "load_error": "Error loading locations", + "required_key": "Key is required", + "save_error": "Error saving", + "key_label": "Key *", + "key_placeholder": "Location key", + "location_label": "Location", + "location_placeholder": "Name or description", + "department_label": "Department", + "responsible_label": "Responsible", + "observations_label": "Observations", + "optional_placeholder": "Optional", + "back_to_list": "Back to list", + "save": "Save", + "search_placeholder": "Search by key or location...", + "register_new": "Register new location", + "column_key": "Key", + "column_location": "Location", + "no_results": "No results found", + "cancel": "Cancel" + }, + "tariff_fraction": { + "title": "SITAR FRACTIONS CATALOG - SCAII", + "search_label": "Searching:", + "search_placeholder": "Search by fraction, description, NICO...", + "column_key": "Key", + "column_fraction": "Fraction", + "column_nico": "NICO", + "column_description": "Description", + "column_umt": "U.M.T", + "column_adv_impo": "Adv. Impo", + "column_adv_expo": "Adv. Expo", + "column_dof": "DOF", + "column_aplica_ieps": "Applies IEPS", + "loading": "Loading fractions...", + "empty": "No fractions available", + "cancel": "Cancel" + }, + "us_tariff_fraction": { + "no_company_selected": "No company selected", + "load_error_prefix": "Error: {error}", + "no_records_info": "No registered US tariff fractions were found", + "connection_error_prefix": "Connection error: {error}", + "title": "Select US Tariff Fraction", + "description": "Select tariff fraction (HTS) from catalog.", + "search_placeholder": "Search by code or description...", + "loading_catalog": "Loading catalog...", + "no_results": "No fractions found.", + "column_code": "Code (HTS)", + "column_description": "Description", + "records_found": "{count} records found", + "cancel": "Cancel" + }, + "invoice_selector_modal": { + "no_active_company": "No active company has been selected", + "search_error": "Error searching invoices", + "title_export": "Export Invoices", + "title_import": "Import Invoices ({regimen})", + "description_export": "Select an invoice from catalog to link it to the item.", + "description_import": "Select a processed import invoice for regimen {regimen}.", + "search_placeholder": "Search by invoice number...", + "searching_button": "Searching...", + "search_button": "Search", + "searching_available": "Searching available invoices...", + "no_invoices": "No invoices found", + "try_other_filter": "Try another invoice number or filter", + "processed_badge": "Processed", + "pedimento_label": "Pedimento", + "no_date": "No date", + "not_available_short": "N/A", + "select": "Select", + "total_found": "Total: {count} invoices found", + "close": "Close" + }, + "port_selector": { + "title": "Select Port (Customs/Section)", + "description": "Search and select a customs section from the list.", + "search_placeholder": "Search by code or name...", + "column_code": "Code", + "column_name": "Name / Section", + "loading": "Loading customs sections...", + "empty": "No results found", + "cancel": "Cancel" + }, + "manifest_selector": { + "title": "Select Manifest", + "description": "Search and select an export manifest to link to this invoice.", + "search_placeholder": "Search by number...", + "search_button": "Search", + "searching": "Searching manifests...", + "column_number": "Manifest Number", + "column_description": "Description", + "empty": "No results found" + } + }, + "invoice_edit": { + "new_title": "New Invoice", + "edit_title": "Edit Invoice", + "new_description": "Enter the new invoice data", + "edit_description": "Modify the invoice data", + "draft_badge": "Draft", + "saved_success": "All changes were saved successfully", + "invoice_number_prefix": "Number:", + "edit_details": "Edit the invoice details", + "page_invoice_prefix": "Invoice #", + "page_default_values_loaded_prefix": "Default values loaded for {invoiceType}", + "page_save_error_prefix": "Error saving the invoice", + "page_save_changes_error": "Error saving changes", + "page_console_hint": "Check the console for more details", + "page_session_expired": "Session expired. Reloading page...", + "tabs": { + "general": "General", + "compliance": "Compliance", + "financials": "Financials", + "observations": "Observations", + "items": "Items", + "others": "Others", + "continuation": "Cont." + }, + "form": { + "operation_type_label": "Operation Type *", + "operation_type_placeholder": "Select type", + "operation_type_import": "Import", + "operation_type_export": "Export", + "invoice_number_label": "Invoice Number", + "invoice_number_placeholder": "Invoice number", + "invoice_type_label": "Invoice Type", + "invoice_type_placeholder": "Invoice type", + "no_company_selected": "No company selected", + "exchange_rate_required": "Exchange rate is required (Financials tab)", + "exchange_rate_positive": "Exchange rate must be greater than 0 (Financials tab)", + "save_error": "Error saving", + "loading_defaults_prefix": "Default values loaded for", + "pedimento_pending": "Pedimento pending?", + "pedimento_label": "Pedimento", + "pedimento_placeholder": "Select pedimento...", + "remesa_label": "Remesa", + "invoice_number_label_short": "Invoice No.", + "invoice_date_label_exp": "Date", + "invoice_date_label_mex": "Entry date", + "invoice_date_label_default": "Invoice date", + "emission_date_label": "Emission date", + "iva_factor_label": "IVA factor", + "alternate_invoice_label": "Alternate invoice", + "project_number_label": "Project Number", + "project_number_placeholder": "Project number", + "purchase_order_label": "Purchase Order", + "purchase_order_placeholder": "Purchase order", + "invoice_date_label": "Invoice date", + "validation": { + "trailer_required": "Trailer is required when Transport Type is different from None.", + "missing_fields": "The following fields are required:", + "check_transport_data": "Check transport and logistics data", + "save_error": "Error saving changes" + }, + "traffic_light_status_label": "Traffic light", + "traffic_light_status_placeholder": "Traffic light status", + "observation_es_label": "Observations (Spanish)", + "observation_es_placeholder": "Observations in Spanish", + "observation_en_label": "Observations (English)", + "observation_en_placeholder": "Observations in English", + "remesa_placeholder": "Remesa number", + "aduana_label": "Customs", + "aduana_placeholder": "Customs code", + "customs_broker_label": "Customs broker", + "customs_broker_placeholder": "Customs broker ID", + "provider_label": "Provider", + "provider_placeholder": "Provider ID", + "edocument_label": "E-Document", + "edocument_placeholder": "E-document number", + "is_mixed_label": "Mixed operation", + "currency_placeholder": "MXN, USD, etc.", + "exchange_rate_placeholder": "Exchange rate", + "value_mn_label": "MN value", + "value_mn_placeholder": "Value in local currency", + "value_me_label": "ME value", + "value_me_placeholder": "Value in foreign currency", + "customs_value_mn_label": "Customs value MN", + "customs_value_mn_placeholder": "Customs value in MN", + "freight_label": "Freight", + "freight_placeholder": "Freight cost", + "insurance_label": "Insurance", + "insurance_placeholder": "Insurance cost", + "iva_mn_label": "IVA MN", + "iva_mn_placeholder": "IVA in MN", + "total_quantity_label": "Total quantity", + "total_quantity_placeholder": "Total quantity", + "gross_weight_label": "Gross weight", + "gross_weight_placeholder": "Gross weight", + "net_weight_label": "Net weight", + "net_weight_placeholder": "Net weight", + "bundle_count_label": "Bundle count", + "bundle_count_placeholder": "Bundle count", + "update_button": "Update", + "create_button": "Create" + }, + "general": { + "pedimento_section": "Pedimento data", + "pedimento_date_from": "Date from:", + "pedimento_date_to": "Date to:", + "pedimento_code": "Code:", + "pedimento_regimen": "Regime:", + "clients_suppliers_broker": "Clients - Suppliers - Customs Broker", + "provider_header_supplier": "Supplier", + "provider_header_exporter": "Exporter", + "sold_to_header_consignado": "Consigned to", + "sold_to_header_vendido": "Sold to", + "sold_to_header_exportado": "Exported to", + "sold_to_header_importador": "Importer", + "shipped_to_header_enviado": "Sent to", + "shipped_to_header_transferido": "Transferred to", + "shipped_to_header_donado": "Donated to", + "shipped_to_header_importador": "Importer", + "shipped_by_header_enviado_por": "Sent by", + "shipped_by_header_destinatario": "Recipient", + "shipped_by_header_vendido_por": "Sold by", + "shipped_by_header_notificar": "Notify to", + "select_header_placeholder": "Select header...", + "select_placeholder": "Select...", + "select_broker_placeholder": "Select...", + "broker_mex_label": "Mex. Customs Broker:", + "broker_usa_label": "US Customs Broker:", + "currency_weight_section": "Currency Type - Net and Gross Weights", + "exchange_rate": "Exchange rate:", + "currency_foreign": "Foreign (USD)", + "currency_local": "Local (MXN)", + "currency_manual": "Manual entry", + "currency_label": "Currency:", + "weight_type_label": "Weight type:", + "weight_type_kgs": "Kilograms (kg)", + "weight_type_lbs": "Pounds (lb)", + "manifest_number_label": "Manifest no.:", + "manifest_placeholder": "Manifest...", + "transport_section": "Transporter", + "transport_label": "Transporter:", + "transport_key_label": "Transport key:", + "transport_type_label": "Transport type:", + "trailer_label": "Trailer:", + "driver_label": "Driver:", + "iva_label": "VAT:", + "customs_label": "Customs and dispatch section:", + "document_type_label": "Customs regime code:", + "select_transporter_placeholder": "Select transporter...", + "select_vehicle_placeholder": "Select vehicle...", + "select_driver_placeholder": "Select driver...", + "select_trailer_placeholder": "Select trailer...", + "select_customs_placeholder": "Select customs office...", + "select_regimen_placeholder": "Select regime...", + "choose_transporter_first": "Choose transporter first...", + "no_data": "No data", + "no_drivers_for_transporter": "No drivers for this transporter", + "no_regimens_for_operation": "No regimes for type", + "choose_operation_first": "Select operation type first", + "transport_none": "None", + "transport_type_transport": "Transport", + "transport_type_box": "Box", + "transport_type_licence_plates": "Plates", + "transport_type_truck": "Truck", + "transport_type_vessel": "Vessel", + "transport_type_rail_barge": "Rail barge", + "transport_type_container": "Container", + "transport_type_airplane": "Airplane", + "transport_type_gondola": "Gondola", + "transport_type_flatbed": "Flatbed", + "signature_label": "Electronic signature:", + "general_info": "General information" + }, + "page": { + "saving_all_changes": "Saving all changes...", + "save_all_changes": "Save All Changes", + "cancel": "Cancel" + }, + "observations": { + "mexican_observation": "Mexican invoice observations:", + "bilingual_observation": "Mexican and bilingual invoice observations:", + "textarea_placeholder": "Write your observations here.", + "fixed_legend": "Fixed legend:", + "selected_legend_prefix": "Key", + "select_legend_placeholder": "Select legend...", + "add_to_observations": "Add to observations", + "american_observation": "US invoice observations:", + "identifiers_title": "Identifiers", + "first_label": "First:", + "second_label": "Second:", + "key_placeholder": "Key...", + "complements_title": "Complements", + "one_label": "1:", + "two_label": "2:", + "office_label": "Office:", + "incrementables_title": "Incrementables:", + "freight_label": "Freight:", + "insurance_label": "Insurance:", + "packaging_label": "Packaging:", + "other_increments_label": "Other incr.:", + "other_deductibles_label": "Other deduct.:", + "seal_number_label": "Seal Number:", + "movement_type_label": "Movement Type:", + "alternate_invoice_label": "Alternate Invoice:", + "proforma_number_label": "Proforma Number:", + "subdivision_label": "Subdivision:", + "yes": "Yes", + "no": "No", + "acts_as_cd_label": "Acts as CD:", + "incoterm_label": "Incoterm:", + "select_placeholder": "Select...", + "valuation_method_label": "Valuation Method:", + "mixed_label": "Mixed?", + "seal_count_label": "Seal Count:", + "delivery_title": "Delivery Data", + "delivered_label": "Delivered", + "received_by_label": "Received by:", + "delivery_date_label": "Delivery Date:", + "rule_parties_label": "Rule 3.1.21 Parties II", + "status_comment_label": "Status Comment:", + "status_comment_placeholder": "Status comment", + "related_docs_label": "Docs Relation ID:", + "electronic_signature_label": "Electronic Signature:", + "authorized_person_label": "Attorney/Authorized Person:", + "contingency_mode_label": "Contingency Mode", + "cove_label": "COVE:", + "operation_number_label": "Operation No.:", + "adendas_label": "Addenda(s):", + "vu_observations_label": "VU Observations:", + "load_info": "Load Info.", + "entry_exit_date_label": "Entry/Exit Date:", + "payment_date_label": "Payment Date:", + "certificate_number_label": "Certificate Number:", + "enclosure_label": "Enclosure:", + "alternate_flags_title": "Alternate Invoice & Flags", + "valuation_method_placeholder": "Select...", + "mixed_label_short": "Mixed?", + "errors_title": "Billing Errors", + "line": "Line", + "key": "Key", + "description": "Description", + "no_errors": "No errors registered", + "insert": "Insert", + "edit": "Edit", + "delete": "Delete" + }, + "others": { + "transport_mode_label": "Transport Mode:", + "select_mode_placeholder": "Select mode", + "print_stamp_label": "Print stamp for value less than 2500 USD", + "mixed_label": "Mixed?", + "yes": "Yes", + "no": "No", + "master_bol_label": "Master BOL Number:", + "guide_number_label": "Guide Number:", + "shipment_number_label": "Shipment Number:", + "option_iv18_label": "IV 18 Option:", + "select_option_placeholder": "Select option", + "delivery_title": "Delivery Data", + "delivered_label": "Delivered", + "received_by_label": "Received by:", + "delivery_date_label": "Delivery Date:", + "rule_3121_label": "Rule 3.1.21 Parties II", + "status_comment_label": "Status Comment:", + "status_comment_placeholder": "Status comment", + "related_docs_label": "Docs Relation ID:", + "electronic_signature_label": "Electronic Signature:", + "authorized_person_label": "Attorney/Authorized Person:", + "contingency_mode_label": "Contingency Mode", + "cove_label": "COVE:", + "operation_number_label": "Operation No.:", + "adendas_label": "Addenda(s):", + "vu_observations_label": "VU Observations:", + "load_info": "Load Info.", + "entry_exit_date_label": "Entry/Exit Date:", + "payment_date_label": "Payment Date:", + "certificate_number_label": "Certificate Number:", + "electronic_signature_2_label": "Electronic Signature:", + "errors_title": "Billing Errors", + "line": "Line", + "key": "Key", + "description": "Description", + "no_errors": "No errors registered", + "insert": "Insert", + "edit": "Edit", + "delete": "Delete" + }, + "items": { + "unsaved_invoice_title": "Invoice not saved", + "unsaved_invoice_description": "You must save the invoice before adding items.", + "loaded_more_items": "Loading more items...", + "deleted": "Item deleted", + "delete_failed": "Could not delete the item", + "no_data_to_save": "No data to save", + "required_fields": "Fill in the required fields (Class or Description)", + "no_active_company": "There is no active company ID. Make sure you have a company selected.", + "no_invoice_id": "There is no invoice ID. The invoice must be saved before adding items.", + "update_failed": "Could not update the item", + "updated": "Item updated", + "create_failed": "Could not create the item", + "created": "Item created", + "save_error": "Error saving", + "saved_to_template": "Item saved to template", + "save_invoice_first": "Save the invoice first to use templates.", + "use_template_description": "Select a predefined template to load its items.", + "refresh": "Refresh", + "search_templates_placeholder": "Search templates...", + "loading": "Loading...", + "template_applied": "Template applied", + "apply_template_error": "Error applying template", + "template_saved": "Template saved", + "save_template_error": "Error saving template", + "title": "Invoice Items", + "subtitle": "Load items, create templates, or apply them without leaving this view.", + "use_template": "Use template", + "create_template": "Create template", + "add_items": "Add items", + "cancel": "Cancel", + "applying": "Applying...", + "apply_template": "Apply Template", + "create_template_dialog_title": "Create template", + "create_template_dialog_description": "Save the current items as a reusable template to inject into other items.", + "template_name_label": "Template Name", + "template_name_placeholder": "E.g. Standard parts package", + "template_description_label": "Description", + "template_description_placeholder": "Describe what this template is for...", + "template_items_count": "items/lines", + "template_items_title": "Template items", + "add_item_line": "Add Item/Line", + "template_table_hash": "#", + "template_table_description": "Description", + "template_table_quantity": "Qty.", + "template_table_actions": "Actions", + "template_empty": "Use the \"Add Item/Line\" button to define the template contents.", + "no_description": "No description", + "no_description_short": "No description available.", + "no_description_available": "No description available.", + "no_templates_found": "No templates found", + "select_template_to_view": "Select a template to view its details", + "created_label": "Created", + "item_description": "Item Description", + "quantity_short": "Qty.", + "quantities": "Quantities:", + "template_empty_items": "This template does not contain items.", + "imported_quantity": "Imported Qty.", + "reference": "Ref:", + "saving": "Saving...", + "save_template": "Save template", + "column_line": "Line", + "column_impo_invoice": "Impo Invoice", + "column_ps": "P/S", + "column_class": "Class", + "column_part_number": "Part Number", + "column_description": "Description", + "column_has_subitem": "Contains Sub-item", + "column_main_item": "Main Item", + "column_class_description": "Class Description", + "column_um": "U.M.", + "column_preference": "Preference", + "column_quantity": "Quantity", + "column_actions": "Actions", + "no_items_available": "No items available", + "showing_lines": "Showing {displayed} of {total} lines", + "spanish_description_label": "Description in Spanish:", + "select_row_to_view_description": "Select a row to view the description.", + "bultos": "Bundles:", + "imported": "Imported:", + "net_weight": "Net weight:", + "gross_weight": "Gross weight:", + "import_values_title": "Import values:", + "dollars": "Dollars:", + "pesos": "Pesos:", + "capture_value": "Capture Value:", + "customs_value_short": "Customs:" + } + }, + "invoice_item_fa": { + "item_sheet": { + "tab_general": "General", + "tab_identifiers": "Identifiers", + "not_available_short": "N/A" + }, + "repair": { + "generate_discharge": "Generate Discharge?", + "export_invoice_label": "Expo Invoice", + "export_line_label": "Expo Line", + "type_search_label": "Search Type", + "import_type_label": "Import Type:", + "import_invoice_label": "Import Invoice", + "line_label": "Line", + "loading_line": "Loading...", + "search_placeholder": "Select...", + "temporal": "TEM (Temporary)", + "definitive": "DEF (Definitive)", + "loading_item_data": "Loading item data...", + "close": "Close", + "cancel": "Cancel", + "select_line_title": "Select line", + "import_title": "Import items", + "import_description": "Select a line with available balance to perform the discharge.", + "loading_invoice_items": "Loading invoice items...", + "no_balance": "No balance available", + "no_balance_description": "There are no lines with balance in this invoice to discharge.", + "no_description": "No description" + }, + "main_data": { + "legend": "Main Data", + "quantity": "Quantity", + "unit_cost": "Unit Cost", + "total_value": "Total Value", + "tariff_type": "Tariff Type" + }, + "packages": { + "legend": "PACKAGES", + "quantity": "Quantity", + "package_code": "Package Code", + "weight": "Weight", + "description": "Description", + "weights": "WEIGHTS", + "net": "Net", + "gross": "Gross", + "space": "Space", + "permit_number": "Permit No.", + "page_region": "Page/Region", + "american_fraction": "US Fraction", + "brand": "Brand", + "model": "Model", + "purchase_order": "Purchase Order" + }, + "summary": { + "general_data": "GENERAL DATA", + "return_quantity_subitems": "RETURN QUANTITY SUB-ITEMS", + "temporary": "Temporary", + "replacement_or_change": "Replacement or Change", + "definitive": "Definitive", + "returned_values": "Returned Values", + "weights_kilos": "WEIGHTS (KILOS)", + "weights_pounds": "WEIGHTS (POUNDS)", + "net": "Net", + "gross": "Gross", + "costs_values": "COSTS AND VALUES", + "dollars": "(Dollars)", + "pesos": "(Pesos)", + "cost": "Cost", + "value": "Value", + "customs_value": "Customs Value", + "capture_cost": "Capture Cost", + "capture_value": "Capture Value" + }, + "continuation": { + "tax_paid": "TAX PAID", + "yes": "Yes", + "no": "No", + "general_info": "General information", + "transport_number_type": "Transport number/type:", + "vehicle_data": "Vehicle data:", + "is_rail": "Is rail?", + "bill_number": "Bill of lading no.:", + "guide_count": "Shipping guide count (BL):", + "destination_origin": "Destination/Origin:", + "destination_origin_placeholder": "FRANJA FRONT.", + "is_mixed": "Mixed?", + "entry_port": "Entry port:", + "export_reason": "Export reason:", + "reason_sold": "Sold", + "reason_not_sold": "Not sold", + "reason_other": "Other", + "payment_terms": "Payment terms:", + "handling_fees": "Handling fees:", + "reviewed_equipment": "Equipment reviewed", + "subdivision": "Subdivision", + "acts_as_cd": "Acts as CD", + "pedimento_arrived": "Pedimento arrived", + "billing_errors": "Billing errors", + "error_line": "Line", + "error_key": "Key", + "error_description": "Description", + "no_errors": "No errors registered", + "insert": "Insert", + "edit": "Edit", + "delete": "Delete", + "traffic_light": "Traffic light", + "green_mx": "Green MX", + "green_usa": "Green USA", + "red_mx": "Red MX", + "red_usa": "Red USA", + "cfdi_data_title": "CFDI DATA", + "cfdi_uuid_label": "CFDI UUId:", + "cfdi_pdf_label": "CFDI Path PDF:", + "cfdi_xml_label": "CFDI Path XML:", + "payment_method": "Payment Method", + "igi_amount": "IGI Amount", + "dollars": "DOLLARS", + "igi_payment_method": "IGI Payment Method", + "has_fda_code": "Has FDA Code", + "has_certificate_of_origin": "Has Certificate of Origin?", + "certificate_number": "Certificate of Origin No.", + "end_date": "End Date", + "machinery_equipment_location": "Machinery and equipment location", + "location_variable": "Location variable", + "military_equipment_enable": "Enable if Item Contains Military Equipment", + "own_equipment": "Own Equipment", + "omit_annex31": "Omit Annex 31", + "lot": "Lot", + "entry_number": "Entry No.", + "eighth_rule_permit": "Eighth Rule Permit", + "eighth_rule_fraction": "Eighth Rule Fraction", + "line": "Line", + "consider_a31": "Consider in A31", + "extra_description_spanish": "Extra Description in Spanish" + }, + "configuration": { + "is": "Is", + "item": "Item", + "subitem": "Subitem", + "contains_subitems": "Contains Sub-Items", + "yes": "Yes", + "main_item_number": "Main Item Number", + "main_item_number_placeholder": "Enter main item number", + "description_spanish": "Description in Spanish", + "description_english": "Description in English" + }, + "labeling": { + "legend": "Labeling & Valuation", + "label_number": "Label Number", + "label_type": "Label Type", + "observations": "Observations", + "observations_placeholder": "Labeling observations...", + "assets_series": "Assets / Series", + "asset_number_short": "Asset Num", + "actions_short": "Act.", + "asset_number": "Asset Number", + "cancel": "Cancel", + "save": "Save" + }, + "identifiers": { + "asset_number": "Asset Number", + "asset_tag_title": "Asset Tag" + }, + "dialogs": { + "countries_load_error": "Error loading countries", + "states_load_error": "Error loading states", + "packages_load_error": "Error loading packages", + "units_load_error": "Error loading units of measure", + "payment_methods_load_error": "Error loading payment methods" + }, + "invoice_item_inv": { + "edit_title": "Edit Item", + "add_title": "Add New Item", + "edit_description": "Modify inventory fields and save changes.", + "add_description": "Fill in the new inventory item information.", + "line_prefix": "Line", + "required_fields_hint": "Fields marked with * are required.", + "tab_general": "General", + "tab_classification": "Classification", + "tab_quantities": "Quantities", + "tab_other": "Other", + "invoice_info_title": "Invoice Information", + "invoice_unsaved_warning": "This invoice has not been saved yet. Items will be associated when you save the invoice.", + "invoice_id": "Invoice ID:", + "operation_type": "Operation Type:", + "invoice_number": "Invoice Number:", + "system": "System:", + "class_label": "Class", + "select_class_placeholder": "Select a class", + "quantity_label": "Quantity", + "unit_label": "U.M.", + "select_unit_placeholder": "Select U.M.", + "unit_cost_label": "Unit Cost", + "country_label": "Country of Origin", + "select_country_placeholder": "Select country", + "fraction_label": "Fraction", + "select_fraction_placeholder": "Select fraction", + "tariff_type_label": "Tariff Type", + "reference_number_label": "Reference Number", + "purchase_order_label": "Purchase/Sales Order", + "warehouse_label": "Warehouse", + "location_label": "Location", + "description_es_label": "Description (Spanish)", + "description_es_placeholder": "Description in Spanish", + "description_en_label": "Description (English)", + "description_en_placeholder": "Description in English", + "sku_label": "SKU", + "sku_placeholder": "Product SKU code", + "batch_label": "Batch", + "batch_placeholder": "Batch number", + "classification_fraction_label": "Tariff Fraction", + "fraction_digits_placeholder": "8 digits", + "product_type_label": "Product Type", + "product_type_placeholder": "Raw material, finished product, etc.", + "material_type_label": "Material Type", + "material_type_placeholder": "Metal, plastic, etc.", + "product_code_label": "Product Code", + "product_code_placeholder": "Internal code", + "country_origin_label": "Country of Origin", + "country_code_placeholder": "Country code", + "merchandise_category_label": "Merchandise Category", + "merchandise_category_placeholder": "Category", + "quantity_tab_label": "Quantity", + "unit_of_measure_label": "Unit of Measure", + "unit_of_measure_placeholder": "PCS, KG, M, etc.", + "zero_placeholder": "0", + "decimal_placeholder": "0.00", + "net_weight_label": "Net Weight (KG)", + "gross_weight_label": "Gross Weight (KG)", + "unit_cost_usd_label": "Unit Cost (USD)", + "total_value_label": "Total Value (USD)", + "packages_label": "Number of Packages", + "package_type_label": "Package Type", + "package_type_placeholder": "Box, pallet, etc.", + "imported_quantity_label": "Imported Quantity", + "remaining_quantity_label": "Remaining Quantity", + "brand_label": "Brand", + "brand_placeholder": "Product brand", + "expiration_date_label": "Expiration Date", + "production_date_label": "Production Date", + "min_stock_label": "Minimum Stock", + "max_stock_label": "Maximum Stock", + "observations_label": "Observations", + "observations_placeholder": "Additional inventory notes...", + "loading_item_data": "Loading item data...", + "loading_more_items": "Loading more items...", + "invoice_line_info": "Invoice information ({systemLabel})", + "select_line": "Select line", + "import_title": "Import items", + "import_description": "Select a line with available balance to perform the discharge.", + "loading_invoice_items": "Loading invoice items...", + "no_balance": "No balance available", + "no_balance_description": "There are no lines with balance in this invoice to discharge.", + "balance_required": "Available balance line", + "cancel": "Cancel", + "close": "Close", + "saving": "Saving...", + "update": "Update", + "create": "Create" + }, + "prerequisites": { + "title": "Notice", + "message_both": "There are no Customs brokers or Clients registered. You must register them to work in this module.", + "message_agents": "There are no Customs brokers registered. You must register them to work in this module.", + "message_clients": "There are no Clients registered. You must register them to work in this module.", + "register_hint": "You can register them in", + "agents_link": "Customs Brokers", + "clients_link": "Clients and Providers", + "and": "and", + "cancel": "Cancel", + "accept": "Accept" + } + } +} diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 9e913d89..06351ad7 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -1,1211 +1,1472 @@ { - "$schema": "https://inlang.com/schema/inlang-message-format", - "hello_world": "Hello, {name} from es!", - "sidebar": { - "dashboard": "Dashboard", - "reference_data": { - "title": "Catálogos Fijos", - "codes_pedimento_regimen": "Códigos de Pedimento y Régimen", - "containers": "Contenedores", - "countries": "Países", - "currency_types": "Tipos de moneda", - "customs_sections": "Secciones de aduanas", - "customs_warehouses": "Recintos", - "incoterms": "Incoterms", - "document_types_digitization": "Tipos de documento para digitalización", - "invoice_types": "Tipos de factura", - "material_types": "Tipos de material", - "payment_methods": "Métodos de pago", - "pedimento_codes": "Códigos de pedimento", - "pedimento_regimes": "Regímenes de pedimentos", - "sectors": "Sectores", - "states": "Estados", - "transportation_modes": "Métodos de transporte", - "transportation_types": "Tipos de transporte", - "valuation_methods": "Métodos de valoración", - "configuracion": "Configuración", - "general": "General", - "licencia": "Licencia", - "usuarios": "Usuarios", - "ayuda": "Ayuda" - }, - "general_catalogs": { - "title": "Catalogos Generales", - "company_information": "Información de la empresa", - "packages": "Bultos", - "concepts": "Conceptos", - "classification": "Clasificación", - "identifiers": "Identificadores", - "incoterms": "Incoterms", - "inpc": "I.N.P.C", - "fixed_legends": "Leyendas fijas", - "seals": "Precintos", - "valuation_methods": "Metódos de valoración", - "countries": "Países", - "ports": "Puertos", - "unit_measures": "Unidades de medida", - "um_customs_mex": "UM Aduanas MX", - "um_customs_ame": "UM Aduanas USA", - "um_ace": "UM ACE", - "um_oma": "UM OMA", - "conversions": "Conversiones", - "equivalences": "Equivalencias", - "exchange_rates": "Tipos de cambio", - "currency_types": "Tipos de moneda", - "multi_currency": "Multi Moneda", - "invoice_types": "Tipos de factura", - "electronic_signatures": "Firmas electrónicas", - "billing_errors": "Errores de facturación", - "customs_warehouses": "Recintos", - "locations": "Localizaciones", - "doda": "DODA", - "packing_list": "Packing List", - "prevalidators": "Prevalidadores", - "electronic_notices": "Avisos electrónicos", - "back_flush": "Back Flush", - "crossing_notice": "Aviso de cruce", - "customs_broker_concepts": "Conceptos de Agente Aduanal" - }, - "fractions": { - "title": "Fracciones", - "sitar": "Fracciones Sitar", - "sitar_seventh_amendment": "Fracciones Sitar - 7ma enmienda", - "sitar_us": "Fracciones Sitar US", - "american": "Fracciones US", - "canadian": "Fracciones Canadiense", - "historical": "Fracciones Historicas", - "sectors": "Sectores" - }, - "goods": { - "title": "Mercancías", - "classes": "Clases", - "parts": "Partes", - "fda_codes": "Códigos F.D.A." - }, - "pedimentos": { - "title": "Pedimentos", - "pedimento_management": "Gestión de Pedimentos", - "pedimento_codes": "Claves de Pedimento", - "customs_regimes": "Regímenes Aduaneros", - "payment_methods": "Formas de Pago", - "customs_sections": "Secciones Aduaneras", - "anexo_22_app_31": "Anexo 22 App 3" - }, - "import_invoices": { - "title": "Facturas de importación", - "temporary": "Temporal", - "definitive": "Definitiva", - "mexican_purchases": "Compras mexicanas", - "regime_change": "Cambio de régimen", - "repair": "Reparación" - }, - "export_invoices": { - "title": "Facturas de exportación", - "exportation": "Exportación", - "repair": "Reparación" - }, - "export": { - "title": "Exportación", - "catalog": "Catálogo de exportación", - "repair": "Reparación", - "manifest": "Manifiesto", - "proforma": "Proforma", - "reports": "Reportes", - "used_materials": "Módulo de materiales utilizados", - "destruction": "Destrucción", - "special_processes": "Procesos Especiales" - }, - "clients_and_providers": "Clientes y Proveedores", - "customs_brokers": "Agentes Aduanales", - "audit_logs": "Bitácora", - "audit_logs_title": "Bitácora de Movimientos", - "audit_logs_description": "Auditoría de operaciones y seguimiento de tareas en segundo plano (Celery).", - "audit_logs_tab_bitacora": "Bitácora", - "audit_logs_tab_tasks": "Tareas en segundo plano", - "audit_logs_tab_files": "Gestor de archivos", - "audit_logs_files_title": "Gestor de archivos", - "audit_logs_files_root": "Raíz de archivos", - "audit_logs_files_refresh": "Actualizar", - "audit_logs_files_list_title": "Contenido", - "audit_logs_files_error_prefix": "Error:", - "audit_logs_files_col_name": "Nombre", - "audit_logs_files_col_size": "Tamaño", - "audit_logs_files_col_modified": "Modificado", - "audit_logs_files_col_actions": "Acciones", - "audit_logs_files_loading": "Cargando archivos...", - "audit_logs_files_empty": "No hay archivos o carpetas en esta ubicación.", - "audit_logs_files_download": "Descargar", - "digitalizacion": { - "title": "Digitalización", - "subtitle": "Catálogo de Documentos Digitalizados", - "new": "Nuevo", - "refresh": "Actualizar", - "table_title": "Documentos digitalizados", - "col_consecutivo": "Consecutivo", - "col_tipo_documento": "Tipo Documento", - "col_e_document": "E-Document", - "col_fecha": "Fecha", - "col_num_operacion_vu": "Núm. Operación VU", - "col_actions": "Acciones", - "form_e_document": "E-Document", - "form_num_operacion": "Núm. Operación", - "form_tipo_documento": "Tipo Documento", - "form_archivo_digitalizado_en": "Archivo Digitalizado en", - "form_fecha": "Fecha", - "form_agente_aduanal": "Agente Aduanal", - "form_pedimento": "Pedimento", - "form_nombre_archivo": "Nombre del archivo", - "digitalizar_title": "Digitalizar Documento", - "digitalizar_subtitle": "Enviar documento a Ventanilla Única", - "digitalizar_file_label": "Archivo", - "digitalizar_rfc_consulta": "RFC Consulta", - "digitalizar_clave_documento": "Clave Documento", - "progress_title": "Digitalizando documento...", - "progress_step": "Paso", - "progress_success": "Digitalización completada exitosamente.", - "progress_download_acuse": "Descargar Acuse", - "action_digitalizar": "Digitalizar", - "action_download_zip": "Descargar ZIP", - "action_acuse": "Acuse", - "action_envio_xml": "Envío XML", - "action_respuesta_xml": "Respuesta XML", - "action_consulta_envio_xml": "Consulta Envío XML", - "action_consulta_respuesta_xml": "Consulta Respuesta XML", - "action_edit": "Editar", - "action_delete": "Borrar", - "empty": "Sin documentos digitalizados", - "loading": "Cargando...", - "search_placeholder": "Buscando:", - "confirm_delete": "¿Está seguro de eliminar este documento?" - }, - "client_provider_type": { - "client_indicator": "C", - "provider_indicator": "P", - "both_indicator": "A" - }, - "nav_user": { - "profile": "Perfil", - "settings": "Configuración" - }, - "transports": { - "title": "Transportes", - "transporters": "Transportistas", - "drivers": "Conductores", - "trailers": "Trailers", - "vehicles": "Vehículos" - }, - "reports": { - "title": "Reportes", - "invoices": "Facturas Impo/Expo", - "downloaded_parts": "Partes descargadas", - "expiration": "Reporte de Vencimiento" - }, - "settings": { - "general": "General" - } - }, - "invoice_list": { - "skip_to_actions": "Ir a acciones de factura", - "header": { - "title": "Facturas", - "description": "Gestiona las facturas del sistema" - }, - "titles": { - "base": "CATALOGO DE FACTURAS", - "import": "DE IMPORTACION", - "export": "DE EXPORTACION", - "import_temporal": "DE IMPORTACION TEMPORAL", - "import_definitive": "DE IMPORTACION DEFINITIVA", - "import_mexican": "DE COMPRAS MEXICANAS", - "import_regime_change": "DE CAMBIO DE REGIMEN Y REGULARIZACION", - "import_repair": "DE IMPO. DE REPARACION", - "export_definitive": "DE SALIDA DEFINITIVA", - "export_repair": "DE REPARACION" - }, - "filters": { - "operation_label": "Tipo de Operacion", - "operation_all_option": "Operacion: Todas", - "invoice_type_label": "Tipo de Factura", - "invoice_type_all_option": "Factura: Todas", - "invoice_number_placeholder": "No. Factura", - "year_start_placeholder": "Ano inicio", - "year_end_placeholder": "Ano fin", - "active_filters": "Filtros activos" - }, - "actions": { - "parameters": "Parametros", - "new_invoice": "Nueva Factura", - "refresh": "Actualizar", - "reports": "Reportes", - "more_actions": "Mas Acciones", - "downloads": "Descargas", - "other_actions": "Otras Acciones", - "cancel": "Cancelar", - "continue": "Continuar", - "generate_cove": "Generar COVE", - "close": "Cerrar" - }, - "card": { - "invoice_list_title": "Listado de Facturas" - }, - "summary": { - "showing": "Mostrando", - "of": "de", - "records": "registros" - }, - "operation_types": { - "all": "Todas", - "import": "Importacion", - "export": "Exportacion" - }, - "cove_dialog": { - "title": "Generar COVE", - "description_prefix": "Selecciona el correo destinatario para la factura", - "recipient_label": "Correo destinatario", - "destination": "Destino COVE", - "select_email": "Selecciona un correo", - "fallback_email": "Se enviara al correo del usuario que genero la factura", - "search_email": "Buscar correo...", - "loading_emails": "Cargando correos disponibles...", - "no_emails": "No hay correos disponibles para COVE.", - "selected_badge": "Seleccionado" - }, - "progress": { - "title_pdf": "Generando PDF de Factura", - "title_consolidated": "Generando Consolidado", - "title_descargo": "Generando Reporte PEPS", - "title_packing_list": "Generando Packing List", - "title_winsaai": "Generando Reporte WINSAAI", - "title_process_invoice": "Procesando factura", - "title_revert_invoice": "Des-actualizando factura", - "title_validate_cove": "Validando datos para COVE", - "complete_processed": "Factura procesada correctamente", - "complete_reverted": "Factura des-actualizada correctamente", - "complete_cove_validation": "Validacion de COVE completada", - "complete_default": "Proceso completado" - }, - "steps": { - "load_invoice": "Cargando factura", - "validate_invoice_data": "Validando datos de la factura", - "review_classes_exchange_rate": "Revisando clases y tipo de cambio", - "calculate_item_values": "Calculando valores por partida", - "validate_items": "Validando partidas", - "validate_rule8_quotas": "Validando cupos de Regla Octava", - "update_totals": "Actualizando totales", - "validate_invoice_status": "Validando estatus de la factura", - "verify_item_balances": "Verificando saldos de partidas", - "confirm_changes": "Confirmando cambios" - }, - "dialogs": { - "revert_title_export": "Des-actualizar Factura de Exportacion", - "revert_title_import": "Des-actualizar Factura de Importacion", - "revert_description_intro": "Se va a des-actualizar la factura", - "revert_description_warning": "Esta operacion revertira los registros de saldos/descargos generados al procesar la factura.", - "revert_description_question": "Desea continuar?", - "winsaai_title": "Sistema de Control de Aduanas e Inventarios", - "winsaai_description_intro": "A la Factura", - "winsaai_of_type": "de tipo", - "winsaai_description_process": "se le ha asignado el proceso Generacion del Archivo WINSAAI.", - "winsaai_description_question": "Desea Continuar o Cancelar?" - }, - "footer": { - "toolbar_aria": "Acciones de factura", - "invoice_pdf": "Factura PDF", - "invoice_csv": "Factura CSV", - "consolidated": "Consolidado", - "consolidated_notice": "Aviso Consolidado", - "packing_list": "Packing List", - "four_copies_rem": "4 Copias Rem", - "descargo_peps": "Descargo PEPS", - "transferencia_electronica": "Transferencia Electronica", - "interface_vu": "Interface VU", - "vu_options_keyboard": "Opciones VU (teclado)", - "vu_consult": "Consulta", - "vu_addenda": "Adenda", - "vu_cove_receipt": "Acuse de COVE", - "vu_massive_cove": "COVE Masivos", - "cons_sed": "Cons SED", - "encomienda": "Encomienda", - "fact_mex_cons": "Fact Mex Cons", - "fact_mex_ord_cat": "Fact Mex Ord Cat", - "export_sia": "Export SIA", - "interface": "Interface", - "process_update": "Actualizar", - "unprocess": "Desactualizar", - "view_details": "Ver Detalles", - "customs_broker_interface": "Interface Agente Aduanal", - "edit": "Editar", - "delete": "Eliminar" - }, - "submenu": { - "consult_soon": "Consulta VU - Proximamente", - "addenda_soon": "Adenda VU - Proximamente", - "massive_cove_soon": "COVE Masivos - Proximamente", - "generate_invoice_csv_soon": "Generar Factura CSV - Proximamente", - "four_copies_soon": "4 Copias Rem - Proximamente", - "cons_sed_soon": "Cons SED - Proximamente", - "encomienda_soon": "Encomienda - Proximamente", - "fact_mex_cons_soon": "Factura Mex Consolidada - Proximamente", - "fact_mex_ord_cat_soon": "Factura Mex Orden Captura - Proximamente", - "export_sia_soon": "Export SIA - Proximamente", - "interface_soon": "Interface - Proximamente" - }, - "recipients": { - "company_vu_email": "Correo VU de la empresa", - "company_main_email": "Correo principal de la empresa", - "company_industrial_1": "Correo industrial 1", - "company_industrial_2": "Correo industrial 2", - "company_description": "Empresa {name}", - "single_window_email": "Correo de ventanilla unica", - "main_email": "Correo principal", - "company_user_email": "Usuario de la empresa - {email}", - "my_email": "Mi correo", - "authenticated_user": "Usuario autenticado - {email}", - "load_error": "No se pudieron cargar los correos disponibles para COVE", - "no_configured": "No hay correos configurados para COVE" - }, - "toasts": { - "select_invoice_for_cove": "Selecciona una factura para generar COVE", - "no_company_selected": "No hay empresa seleccionada", - "session_expired_reloading": "Sesión expirada. Recargando página...", - "load_more_error": "Error cargando más datos", - "apply_filters_error": "Error aplicando filtros", - "reload_data_error": "Error recargando datos", - "download_start_error": "No se pudo iniciar la descarga", - "consolidated_download_start_error": "No se pudo iniciar la descarga del consolidado", - "calculating_peps": "Calculando asignacion PEPS...", - "peps_calculation_error_prefix": "Error al calcular PEPS: {error}", - "peps_calculation_completed": "Calculo PEPS completado", - "peps_report_start_error": "No se pudo iniciar la descarga del reporte PEPS", - "aviso_consolidado_start_error": "No se pudo iniciar la descarga del Aviso Consolidado", - "packing_list_start_error": "No se pudo iniciar la descarga del Packing List", - "fast_interface_import_only": "La interfaz rapida solo esta disponible para facturas de Importacion", - "customs_broker_interface_start_error": "No se pudo iniciar la generacion de Interface Agente Aduanal", - "pdf_download_success": "PDF descargado exitosamente", - "invoice_processed_success": "Factura procesada correctamente", - "worker_error_prefix": "El worker reporto un error: {error}", - "task_result_process_error": "Error al procesar el resultado de la tarea", - "select_invoice_to_edit": "Seleccione una factura para editar", - "no_table_rows": "No hay filas en la tabla", - "select_invoice_for_reports": "Seleccione una factura para reportes", - "select_invoice_for_more_actions": "Seleccione una factura para mas acciones", - "select_invoice_to_revert": "Seleccione una factura para desactualizar", - "select_invoice_for_details": "Seleccione una factura para ver detalles", - "select_invoice": "Seleccione una factura", - "select_at_least_one_invoice_to_delete": "Seleccione al menos una factura para eliminar", - "select_invoice_for_pdf": "Seleccione una factura para descargar PDF", - "select_invoice_for_consolidated": "Seleccione una factura para descargar consolidado", - "select_invoice_to_change_status": "Seleccione una factura para cambiar su estatus", - "update_status_error_prefix": "Error al {action} factura: {error}", - "status_action_update": "actualizar", - "status_action_revert": "desactualizar", - "status_updated_success": "Factura actualizada correctamente", - "status_reverted_success": "Factura desactualizada correctamente", - "update_status_unexpected_error": "Error inesperado al cambiar el estatus", - "select_invoice_to_process": "Selecciona una factura para procesar", - "process_start_error_prefix": "Error al iniciar el proceso: {error}", - "process_start_error": "No se pudo iniciar el proceso", - "revert_start_error_prefix": "Error al iniciar la des-actualizacion: {error}", - "revert_start_error": "No se pudo iniciar la des-actualizacion", - "select_recipient_email_for_cove": "Selecciona un correo para enviar el COVE", - "cove_eligibility_error_prefix": "No se pudo validar elegibilidad COVE: {error}", - "cove_requirements_not_met": "La factura no cumple los requisitos para generar COVE", - "cove_verification_error": "No se pudo verificar si la factura puede generar COVE", - "cove_start_error_prefix": "Error al iniciar generacion de COVE: {error}", - "cove_start_error": "No se pudo iniciar la generacion de COVE", - "validation_extra_more": "\n...y {count} mas", - "validation_error_count": "{count} error(es) de validacion:\n{preview}{extra}", - "cove_external_queued_default": "Factura COVE iniciada en Ventanilla Unica. Use el task_id para consultar el estado." - } - }, - "invoice_table": { - "no_results": "No hay resultados.", - "loading_more": "Cargando mas...", - "scroll_to_load_more": "Desplazate para cargar mas", - "processed": "Procesada", - "pending": "Pendiente", - "operation": "Operacion", - "operation_import": "Importacion", - "operation_export": "Exportacion", - "invoice_type": "Tipo Factura", - "invoice_number": "Num. Factura", - "pedimento_18": "Pedimento 18", - "remesa": "Remesa", - "invoice_date": "Fecha Factura", - "pedimento_code": "Clave Ped.", - "document_type": "Tipo Doc.", - "total_items": "Total Partidas", - "currency": "Moneda", - "currency_type": "Tipo Moneda", - "weight_type": "Tipo Peso", - "mixed": "Mixto", - "related_doc": "Doc. Relacionado", - "yes": "Si", - "no": "No", - "not_available_short": "N/D" - }, - "invoice_selectors": { - "identifier_catalog": { - "title": "Seleccionar Identificador", - "description": "Busca y selecciona un identificador del catalogo (Apendice 8).", - "search_placeholder": "Buscar por clave o descripcion...", - "column_code": "Clave", - "column_description": "Descripcion", - "column_level": "Nivel", - "empty": "No se encontraron identificadores." - }, - "valuation_method": { - "title": "Seleccionar Metodo de Valoracion", - "description": "Busca y selecciona un metodo de valoracion de la lista.", - "search_placeholder": "Buscar por clave o descripcion...", - "column_code": "Clave", - "column_description": "Descripcion", - "empty": "No se encontraron metodos de valoracion." - }, - "location": { - "title": "Catalogo de ubicaciones (maquinaria y equipo)", - "no_company_selected": "No hay compania seleccionada", - "load_error": "Error al cargar ubicaciones", - "required_key": "La clave es requerida", - "save_error": "Error al guardar", - "key_label": "Clave *", - "key_placeholder": "Clave de localizacion", - "location_label": "Localizacion", - "location_placeholder": "Nombre o descripcion", - "department_label": "Departamento", - "responsible_label": "Responsable", - "observations_label": "Observaciones", - "optional_placeholder": "Opcional", - "back_to_list": "Volver al listado", - "save": "Guardar", - "search_placeholder": "Buscar por clave o localizacion...", - "register_new": "Registrar nueva ubicacion", - "column_key": "Clave", - "column_location": "Localizacion", - "no_results": "No se encontraron resultados", - "cancel": "Cancelar" - }, - "tariff_fraction": { - "title": "CATALOGO DE FRACCIONES SITAR - SCAII", - "search_label": "Buscando:", - "search_placeholder": "Buscar por fraccion, descripcion, NICO...", - "column_key": "Clave", - "column_fraction": "Fraccion", - "column_nico": "NICO", - "column_description": "Descripcion", - "column_umt": "U.M.T", - "column_adv_impo": "Adv. Impo", - "column_adv_expo": "Adv. Expo", - "column_dof": "DOF", - "column_aplica_ieps": "Aplica IEPS", - "loading": "Cargando fracciones...", - "empty": "No hay fracciones disponibles", - "cancel": "Cancelar" - }, - "us_tariff_fraction": { - "no_company_selected": "No hay empresa seleccionada", - "load_error_prefix": "Error: {error}", - "no_records_info": "No se encontraron fracciones US registradas", - "connection_error_prefix": "Error de conexion: {error}", - "title": "Seleccionar Fracción US", - "description": "Seleccione la fraccion arancelaria (HTS) del catalogo.", - "search_placeholder": "Buscar por codigo o descripcion...", - "loading_catalog": "Cargando catalogo...", - "no_results": "No se encontraron fracciones.", - "column_code": "Codigo (HTS)", - "column_description": "Descripcion", - "records_found": "{count} registros encontrados", - "cancel": "Cancelar" - }, - "invoice_selector_modal": { - "no_active_company": "No se ha seleccionado una empresa activa", - "search_error": "Error al buscar facturas", - "title_export": "Facturas de Exportacion", - "title_import": "Facturas de Importacion ({regimen})", - "description_export": "Selecciona una factura del catalogo para vincularla a la partida.", - "description_import": "Selecciona una factura de importacion procesada para el regimen {regimen}.", - "search_placeholder": "Buscar por numero de factura...", - "searching_button": "Buscando...", - "search_button": "Buscar", - "searching_available": "Buscando facturas disponibles...", - "no_invoices": "No se encontraron facturas", - "try_other_filter": "Intenta con otro numero de factura o filtro", - "processed_badge": "Procesada", - "pedimento_label": "Pedimento", - "no_date": "Sin fecha", - "not_available_short": "N/D", - "select": "Seleccionar", - "total_found": "Total: {count} facturas encontradas", - "close": "Cerrar" - }, - "port_selector": { - "title": "Seleccionar Puerto (Aduana/Sección)", - "description": "Busca y selecciona una sección aduanera de la lista.", - "search_placeholder": "Buscar por código o nombre...", - "column_code": "Código", - "column_name": "Nombre / Sección", - "loading": "Cargando secciones aduaneras...", - "empty": "No se encontraron resultados", - "cancel": "Cancelar" - }, - "manifest_selector": { - "title": "Seleccionar Manifiesto", - "description": "Busca y selecciona un manifiesto del catálogo de exportación para vincular a esta factura.", - "search_placeholder": "Buscar por número...", - "search_button": "Buscar", - "searching": "Buscando manifiestos...", - "column_number": "Número de Manifiesto", - "column_description": "Descripción", - "empty": "No se encontraron resultados" - } - }, - "invoice_edit": { - "new_title": "Nueva Factura", - "edit_title": "Editar Factura", - "new_description": "Ingresa los datos de la nueva factura", - "edit_description": "Modifica los datos de la factura", - "draft_badge": "Borrador", - "saved_success": "Todos los cambios se guardaron correctamente", - "invoice_number_prefix": "Número:", - "edit_details": "Edita los detalles de la factura", - "page_invoice_prefix": "Factura #", - "page_default_values_loaded_prefix": "Valores predeterminados cargados para {invoiceType}", - "page_save_error_prefix": "Error al guardar la factura", - "page_save_changes_error": "Error al guardar los cambios", - "page_console_hint": "Revisa la consola para más detalles", - "page_session_expired": "Sesión expirada. Recargando página...", - "tabs": { - "general": "General", - "compliance": "Cumplimiento", - "financials": "Financieros", - "observations": "Observaciones", - "items": "Partidas", - "others": "Otros", - "continuation": "Cont." - }, - "form": { - "operation_type_label": "Tipo de Operación *", - "operation_type_placeholder": "Seleccionar tipo", - "operation_type_import": "Importación", - "operation_type_export": "Exportación", - "invoice_number_label": "Número de Factura", - "invoice_number_placeholder": "Número de factura", - "invoice_type_label": "Tipo de Factura", - "invoice_type_placeholder": "Tipo de factura", - "no_company_selected": "No hay compañía seleccionada", - "exchange_rate_required": "El tipo de cambio es requerido (pestaña Financieros)", - "exchange_rate_positive": "El tipo de cambio debe ser mayor a 0 (pestaña Financieros)", - "save_error": "Error al guardar", - "loading_defaults_prefix": "Valores predeterminados cargados para", - "pedimento_pending": "¿Pedimento pendiente?", - "pedimento_label": "Pedimento", - "pedimento_placeholder": "Selecciona pedimento...", - "remesa_label": "Remesa", - "invoice_number_label_short": "Núm. Factura", - "invoice_date_label_exp": "Fecha", - "invoice_date_label_mex": "Fecha de Entrada", - "invoice_date_label_default": "Fecha Factura", - "emission_date_label": "Fecha Emisión", - "iva_factor_label": "Factor IVA", - "alternate_invoice_label": "Factura Alterna", - "project_number_label": "Número de Proyecto", - "project_number_placeholder": "Número de proyecto", - "purchase_order_label": "Orden de Compra", - "purchase_order_placeholder": "Orden de compra", - "invoice_date_label": "Fecha de Factura", - "validation": { - "trailer_required": "El Remolque es obligatorio cuando el Tipo de Transporte es distinto de Ninguno.", - "missing_fields": "Los siguientes campos son obligatorios:", - "check_transport_data": "Revisa los datos de transporte y logística", - "save_error": "Error al guardar los cambios" - }, - "traffic_light_status_label": "Semáforo", - "traffic_light_status_placeholder": "Estado del semáforo", - "observation_es_label": "Observaciones (Español)", - "observation_es_placeholder": "Observaciones en español", - "observation_en_label": "Observaciones (Inglés)", - "observation_en_placeholder": "Observaciones en inglés", - "remesa_placeholder": "Número de remesa", - "aduana_label": "Aduana", - "aduana_placeholder": "Código de aduana", - "customs_broker_label": "Agente Aduanal", - "customs_broker_placeholder": "ID del agente aduanal", - "provider_label": "Proveedor", - "provider_placeholder": "ID del proveedor", - "edocument_label": "E-Document", - "edocument_placeholder": "Número de e-document", - "is_mixed_label": "Operación Mixta", - "currency_placeholder": "MXN, USD, etc.", - "exchange_rate_placeholder": "Tipo de cambio", - "value_mn_label": "Valor MN", - "value_mn_placeholder": "Valor en moneda nacional", - "value_me_label": "Valor ME", - "value_me_placeholder": "Valor en moneda extranjera", - "customs_value_mn_label": "Valor Aduana MN", - "customs_value_mn_placeholder": "Valor de aduana en MN", - "freight_label": "Flete", - "freight_placeholder": "Costo de flete", - "insurance_label": "Seguro", - "insurance_placeholder": "Costo de seguro", - "iva_mn_label": "IVA MN", - "iva_mn_placeholder": "IVA en MN", - "total_quantity_label": "Cantidad Total", - "total_quantity_placeholder": "Cantidad total", - "gross_weight_label": "Peso Bruto", - "gross_weight_placeholder": "Peso bruto", - "net_weight_label": "Peso Neto", - "net_weight_placeholder": "Peso neto", - "bundle_count_label": "Número de Bultos", - "bundle_count_placeholder": "Número de bultos", - "update_button": "Actualizar", - "create_button": "Crear" - }, - "general": { - "pedimento_section": "Datos del pedimento", - "pedimento_date_from": "Fecha del:", - "pedimento_date_to": "Fecha al:", - "pedimento_code": "Clave:", - "pedimento_regimen": "Régimen:", - "clients_suppliers_broker": "Clientes - Proveedores - Agente Aduanal", - "provider_header_supplier": "Proveedor", - "provider_header_exporter": "Exportador", - "sold_to_header_consignado": "Consignado a", - "sold_to_header_vendido": "Vendido a", - "sold_to_header_exportado": "Exportado a", - "sold_to_header_importador": "Importador", - "shipped_to_header_enviado": "Enviado a", - "shipped_to_header_transferido": "Transferido a", - "shipped_to_header_donado": "Donado a", - "shipped_to_header_importador": "Importador", - "shipped_by_header_enviado_por": "Enviado Por", - "shipped_by_header_destinatario": "Destinatario", - "shipped_by_header_vendido_por": "Vendido Por", - "shipped_by_header_notificar": "Notificar a", - "select_header_placeholder": "Selecciona encabezado...", - "select_placeholder": "Selecciona...", - "select_broker_placeholder": "Selecciona...", - "broker_mex_label": "Agente Aduanal Mex:", - "broker_usa_label": "Agente Aduanal US:", - "currency_weight_section": "Tipo de Moneda - Pesos Netos y Brutos", - "exchange_rate": "Tipo de cambio:", - "currency_foreign": "Extranjera (Dlls)", - "currency_local": "Nacional (Pesos)", - "currency_manual": "De Captura", - "currency_label": "Moneda:", - "weight_type_label": "Tipo Peso:", - "weight_type_kgs": "Kilogramos (kg)", - "weight_type_lbs": "Libras (lb)", - "manifest_number_label": "Num. de Manifiesto:", - "manifest_placeholder": "Manifiesto...", - "transport_section": "Transportista", - "transport_label": "Transportista:", - "transport_key_label": "Clave Transporte:", - "transport_type_label": "Tipo Transporte:", - "trailer_label": "Remolque:", - "driver_label": "Conductor:", - "iva_label": "IVA:", - "customs_label": "Aduana y Sección de Despacho:", - "document_type_label": "Clave de Régimen Aduanero:", - "select_transporter_placeholder": "Selecciona transportista...", - "select_vehicle_placeholder": "Selecciona vehículo...", - "select_driver_placeholder": "Selecciona conductor...", - "select_trailer_placeholder": "Selecciona remolque...", - "select_customs_placeholder": "Selecciona aduana...", - "select_regimen_placeholder": "Selecciona régimen...", - "choose_transporter_first": "Primero elige transportista...", - "no_data": "Sin datos", - "no_drivers_for_transporter": "Sin conductores para este transportista", - "no_regimens_for_operation": "Sin regímenes para tipo", - "choose_operation_first": "Selecciona tipo de operación primero", - "transport_none": "Ninguno", - "transport_type_transport": "Transporte", - "transport_type_box": "Caja", - "transport_type_licence_plates": "Placas", - "transport_type_truck": "Camión", - "transport_type_vessel": "Buque", - "transport_type_rail_barge": "Ferrobarcaza", - "transport_type_container": "Contenedor", - "transport_type_airplane": "Avión", - "transport_type_gondola": "Góndola", - "transport_type_flatbed": "Plataforma", - "signature_label": "Firma Electrónica:", - "general_info": "Información General" - }, - "page": { - "saving_all_changes": "Guardando todos los cambios...", - "save_all_changes": "Guardar Todos los Cambios", - "cancel": "Cancelar" - }, - "observations": { - "mexican_observation": "Observaciones de la factura mexicana:", - "bilingual_observation": "Observación de la factura mexicana y bilingüe:", - "textarea_placeholder": "Escribe tus observaciones aquí.", - "fixed_legend": "Leyenda fija:", - "selected_legend_prefix": "Clave", - "select_legend_placeholder": "Selecciona leyenda...", - "add_to_observations": "Agregar a observaciones", - "american_observation": "Observaciones de la factura US:", - "identifiers_title": "Identificadores", - "first_label": "Primero:", - "second_label": "Segundo:", - "key_placeholder": "Clave...", - "complements_title": "Complementos", - "one_label": "1:", - "two_label": "2:", - "office_label": "Oficio:", - "incrementables_title": "Incrementables:", - "freight_label": "Flete:", - "insurance_label": "Seguros:", - "packaging_label": "Embalajes:", - "other_increments_label": "Otros increm.:", - "other_deductibles_label": "Otros deduc.:", - "seal_number_label": "Número de Precinto:", - "movement_type_label": "Tipo Movimiento:", - "alternate_invoice_label": "Factura Alterna:", - "proforma_number_label": "Número de Proforma:", - "subdivision_label": "Sub División:", - "yes": "Sí", - "no": "No", - "acts_as_cd_label": "Funge como CD:", - "incoterm_label": "Incoterm:", - "select_placeholder": "Selecciona...", - "valuation_method_label": "Método de Valoración:", - "mixed_label": "¿Es mixto?", - "seal_count_label": "Num Precintos:", - "delivery_title": "Datos Entrega", - "delivered_label": "Entregado", - "received_by_label": "Recibido por:", - "delivery_date_label": "Fecha Entrega:", - "rule_parties_label": "Regla 3.1.21 Partes II", - "status_comment_label": "Comentario Estatus:", - "status_comment_placeholder": "Comentario estatus", - "related_docs_label": "ID Relación Docs:", - "electronic_signature_label": "Firma Electrónica:", - "authorized_person_label": "Mandatario/Persona Autorizada:", - "contingency_mode_label": "Modo Contingencia", - "cove_label": "COVE:", - "operation_number_label": "Núm Operación:", - "adendas_label": "Adenda(s):", - "vu_observations_label": "Observaciones VU:", - "load_info": "Cargar Info.", - "entry_exit_date_label": "Fecha Entrada/Salida:", - "payment_date_label": "Fecha Pago:", - "certificate_number_label": "Número Certificado:", - "enclosure_label": "Recinto:", - "alternate_flags_title": "Factura Alterna & Flags", - "valuation_method_placeholder": "Selecciona...", - "mixed_label_short": "Es mixto?", - "errors_title": "Errores de Facturación", - "line": "Línea", - "key": "Clave", - "description": "Descripción", - "no_errors": "Sin errores registrados", - "insert": "Insertar", - "edit": "Editar", - "delete": "Borrar" - }, - "others": { - "transport_mode_label": "Modo de Transporte:", - "select_mode_placeholder": "Seleccionar modo", - "print_stamp_label": "Imprimir el Sello por Valor menor a 2500 dlls", - "mixed_label": "Es Mixto?", - "yes": "Sí", - "no": "No", - "master_bol_label": "Número Master BOL:", - "guide_number_label": "Número Guía:", - "shipment_number_label": "Número Embarque:", - "option_iv18_label": "Opción IV 18:", - "select_option_placeholder": "Seleccionar opción", - "delivery_title": "Datos Entrega", - "delivered_label": "Entregado", - "received_by_label": "Recibido por:", - "delivery_date_label": "Fecha Entrega:", - "rule_3121_label": "Regla 3.1.21 Partes II", - "status_comment_label": "Comentario Estatus:", - "status_comment_placeholder": "Comentario estatus", - "related_docs_label": "ID Relación Docs:", - "electronic_signature_label": "Firma Electrónica:", - "authorized_person_label": "Mandatario/Persona Autorizada:", - "contingency_mode_label": "Modo Contingencia", - "cove_label": "COVE:", - "operation_number_label": "Núm Operación:", - "adendas_label": "Adenda(s):", - "vu_observations_label": "Observaciones VU:", - "load_info": "Cargar Info.", - "entry_exit_date_label": "Fecha Entrada/Salida:", - "payment_date_label": "Fecha Pago:", - "certificate_number_label": "Número Certificado:", - "electronic_signature_2_label": "Firma Electrónica:", - "errors_title": "Errores de Facturación", - "line": "Línea", - "key": "Clave", - "description": "Descripción", - "no_errors": "Sin errores registrados", - "insert": "Insertar", - "edit": "Editar", - "delete": "Borrar" - }, - "items": { - "unsaved_invoice_title": "Factura no guardada", - "unsaved_invoice_description": "Debes guardar la factura primero antes de agregar partidas.", - "loaded_more_items": "Cargando más items...", - "deleted": "Partida eliminada", - "delete_failed": "No se pudo eliminar la partida", - "no_data_to_save": "No hay datos para guardar", - "required_fields": "Completa los campos necesarios (Clase o Descripción)", - "no_active_company": "No hay ID de empresa activo. Asegúrate de tener una empresa seleccionada.", - "no_invoice_id": "No hay ID de factura. La factura debe ser guardada antes de agregar partidas.", - "update_failed": "No se pudo actualizar la partida", - "updated": "Partida actualizada", - "create_failed": "No se pudo crear la partida", - "created": "Partida creada", - "save_error": "Error al guardar", - "saved_to_template": "Partida guardada en plantilla", - "save_invoice_first": "Primero guarda la factura para usar plantillas.", - "use_template_description": "Selecciona una plantilla predefinida para cargar sus partidas.", - "refresh": "Actualizar", - "search_templates_placeholder": "Buscar plantillas...", - "loading": "Cargando...", - "template_applied": "Plantilla aplicada", - "apply_template_error": "Error al aplicar plantilla", - "template_saved": "Plantilla guardada", - "save_template_error": "Error al guardar plantilla", - "title": "Items de la Factura", - "subtitle": "Carga partidas, crea o aplica plantillas sin salir de esta vista.", - "use_template": "Usar plantilla", - "create_template": "Crear plantilla", - "add_items": "Agregar Partidas", - "cancel": "Cancelar", - "applying": "Aplicando...", - "apply_template": "Aplicar Plantilla", - "create_template_dialog_title": "Crear plantilla", - "create_template_dialog_description": "Guarda los elementos actuales como una plantilla reutilizable para inyectar en otras partidas.", - "template_name_label": "Nombre de la Plantilla", - "template_name_placeholder": "Ej. Paquete estándar de refacciones", - "template_description_label": "Descripción", - "template_description_placeholder": "Indica para qué sirve esta plantilla...", - "template_items_count": "items/líneas", - "template_items_title": "Items de la plantilla", - "add_item_line": "Agregar Item/Línea", - "template_table_hash": "#", - "template_table_description": "Descripción", - "template_table_quantity": "Cant.", - "template_table_actions": "Acciones", - "template_empty": "Usa el botón \"Agregar Item/Línea\" para definir el contenido de la plantilla.", - "no_description": "Sin descripción", - "no_description_short": "Sin descripción disponible.", - "no_description_available": "Sin descripción disponible.", - "no_templates_found": "No se encontraron plantillas", - "select_template_to_view": "Selecciona una plantilla para ver sus detalles", - "created_label": "Creada", - "item_description": "Descripción del Item", - "quantity_short": "Cant.", - "quantities": "Cantidades:", - "template_empty_items": "Esta plantilla no contiene items.", - "imported_quantity": "Cant. Importada", - "reference": "Ref:", - "saving": "Guardando...", - "save_template": "Guardar plantilla", - "column_line": "Línea", - "column_impo_invoice": "Factura Impo", - "column_ps": "P/S", - "column_class": "Clase", - "column_part_number": "Número Parte", - "column_description": "Descripción", - "column_has_subitem": "Contiene Subpartida", - "column_main_item": "Partida Principal", - "column_class_description": "Descripción Clase", - "column_um": "U.M.", - "column_preference": "Preferencia", - "column_quantity": "Cantidad", - "column_actions": "Acciones", - "no_items_available": "No hay items disponibles", - "showing_lines": "Mostrando {displayed} de {total} líneas", - "spanish_description_label": "Descripción en español:", - "select_row_to_view_description": "Selecciona una fila para ver la descripción.", - "bultos": "Bultos:", - "imported": "Importada:", - "net_weight": "Peso neto:", - "gross_weight": "Peso bruto:", - "import_values_title": "Valores de importación:", - "dollars": "Dólares:", - "pesos": "Pesos:", - "capture_value": "De Captura:", - "customs_value_short": "Aduana:" - } - }, - "invoice_item_fa": { - "item_sheet": { - "tab_general": "Generales", - "tab_identifiers": "Identificadores", - "not_available_short": "N/D" - }, - "repair": { - "generate_discharge": "Genera Descarga?", - "export_invoice_label": "Factura de Expo", - "export_line_label": "Línea de Expo", - "type_search_label": "Tipo Búsqueda", - "import_type_label": "Tipo Importación:", - "import_invoice_label": "Factura Impo", - "line_label": "Línea", - "loading_line": "Cargando...", - "search_placeholder": "Seleccionar...", - "temporal": "TEM (Temporal)", - "definitive": "DEF (Definitiva)", - "loading_item_data": "Cargando datos de la partida...", - "close": "Cerrar", - "cancel": "Cancelar", - "save": "Guardar", - "select_line_title": "Seleccionar línea", - "import_title": "Partidas de Importación", - "import_description": "Selecciona una línea con saldo disponible para realizar la descarga.", - "loading_invoice_items": "Cargando partidas de la factura...", - "no_balance": "Sin saldo disponible", - "no_balance_description": "No hay líneas con saldo en esta factura para descargar.", - "no_description": "Sin descripción" - }, - "main_data": { - "legend": "Datos principales", - "quantity": "Cantidad", - "unit_cost": "Costo unitario", - "total_value": "Valor total", - "tariff_type": "Tipo arancelario" - }, - "packages": { - "legend": "Bultos", - "quantity": "Cantidad", - "package_code": "Clave bulto", - "weight": "Peso", - "description": "Descripcion", - "weights": "Pesos", - "net": "Neto", - "gross": "Bruto", - "space": "Espacio", - "permit_number": "Num. permiso", - "page_region": "Pag/Region", - "american_fraction": "Fracción US", - "brand": "Marca", - "model": "Modelo", - "purchase_order": "Orden de compra" - }, - "summary": { - "general_data": "DATOS GENERALES", - "return_quantity_subitems": "CANTIDAD DE RETORNO SUBPARTIDAS", - "temporary": "Temporal", - "replacement_or_change": "Reemplazo o cambio", - "definitive": "Definitiva", - "returned_values": "Valores retornados", - "weights_kilos": "PESOS (KILOS)", - "weights_pounds": "PESOS (LIBRAS)", - "net": "Neto", - "gross": "Bruto", - "costs_values": "COSTOS Y VALORES", - "dollars": "(Dolares)", - "pesos": "(Pesos)", - "cost": "Costo", - "value": "Valor", - "customs_value": "Valor aduana", - "capture_cost": "Costo captura", - "capture_value": "Valor captura" - }, - "continuation": { - "tax_paid": "IMPUESTO PAGADO", - "yes": "Si", - "no": "No", - "general_info": "Información General", - "transport_number_type": "Número/Tipo de Transporte:", - "vehicle_data": "Datos Vehículo:", - "is_rail": "Es Ferrocarril?", - "bill_number": "Número BL:", - "guide_count": "Cantidad de Guías de Embarque (BL):", - "destination_origin": "Destino/Origen:", - "destination_origin_placeholder": "FRANJA FRONT.", - "is_mixed": "Es Mixto?", - "entry_port": "Puerto Entrada:", - "export_reason": "Razón de exportación:", - "reason_sold": "Vendido", - "reason_not_sold": "No Vendido", - "reason_other": "Otro", - "payment_terms": "Términos de Pago:", - "handling_fees": "Maniobras (Handlings):", - "reviewed_equipment": "Fue Revisado el Equipo", - "subdivision": "Sub División", - "acts_as_cd": "Funge Como CD", - "pedimento_arrived": "Llegó el Pedimento", - "billing_errors": "Errores de Facturación", - "error_line": "Línea", - "error_key": "Clave", - "error_description": "Descripción", - "no_errors": "Sin errores registrados", - "insert": "Insertar", - "edit": "Editar", - "delete": "Borrar", - "traffic_light": "Semáforo", - "green_mx": "Verde MX", - "green_usa": "Verde USA", - "red_mx": "Rojo MX", - "red_usa": "Rojo USA", - "cfdi_data_title": "DATOS CFDI", - "cfdi_uuid_label": "CFDI UUId:", - "cfdi_pdf_label": "CFDI Path PDF:", - "cfdi_xml_label": "CFDI Path XML:", - "payment_method": "Forma de pago", - "igi_amount": "Monto IGI", - "dollars": "DOLARES", - "igi_payment_method": "Forma de pago IGI", - "has_fda_code": "Tiene clave FDA", - "has_certificate_of_origin": "Tiene certificado de origen?", - "certificate_number": "Num. certificado de origen", - "end_date": "Fecha fin", - "machinery_equipment_location": "Ubicacion de maquinaria y equipo", - "location_variable": "Variable de ubicacion", - "military_equipment_enable": "Habilitar si la partida contiene equipo militar", - "own_equipment": "Equipo propio", - "omit_annex31": "Omitir anexo 31", - "lot": "Lote", - "entry_number": "Num. entrada", - "eighth_rule_permit": "Permiso regla octava", - "eighth_rule_fraction": "Fraccion regla octava", - "line": "Linea", - "consider_a31": "Considerar en A31", - "extra_description_spanish": "Descripcion adicional en espanol" - }, - "configuration": { - "is": "Es", - "item": "Partida", - "subitem": "Subpartida", - "contains_subitems": "Contiene subpartidas", - "yes": "Si", - "main_item_number": "Numero de partida principal", - "main_item_number_placeholder": "Captura numero de partida principal", - "description_spanish": "Descripcion en espanol", - "description_english": "Descripcion en ingles" - }, - "labeling": { - "legend": "Etiquetado y Valoracion", - "label_number": "Numero de etiqueta", - "label_type": "Tipo de etiqueta", - "observations": "Observaciones", - "observations_placeholder": "Observaciones de etiquetado...", - "assets_series": "Activos / Series", - "asset_number_short": "Num. activo", - "actions_short": "Acc.", - "asset_number": "Numero de activo", - "cancel": "Cancelar", - "save": "Guardar" - }, - "identifiers": { - "asset_number": "Numero de activo", - "asset_tag_title": "Etiqueta de activo" - }, - "dialogs": { - "countries_load_error": "Error al cargar paises", - "states_load_error": "Error al cargar estados", - "packages_load_error": "Error al cargar bultos", - "units_load_error": "Error al cargar unidades de medida", - "payment_methods_load_error": "Error al cargar formas de pago" - }, - "invoice_item_inv": { - "edit_title": "Editar Item", - "add_title": "Agregar Nuevo Item", - "edit_description": "Modifica los campos del inventario y guarda los cambios.", - "add_description": "Completa la información del nuevo item de inventario.", - "line_prefix": "Línea", - "required_fields_hint": "Los campos marcados con * son obligatorios.", - "tab_general": "General", - "tab_classification": "Clasificación", - "tab_quantities": "Cantidades", - "tab_other": "Otros", - "invoice_info_title": "Información de la Factura", - "invoice_unsaved_warning": "Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la factura.", - "invoice_id": "ID Factura:", - "operation_type": "Tipo Operación:", - "invoice_number": "Número de Factura:", - "system": "Sistema:", - "class_label": "Clase", - "select_class_placeholder": "Selecciona una clase", - "quantity_label": "Cantidad", - "unit_label": "U.M.", - "select_unit_placeholder": "Selecciona U.M.", - "unit_cost_label": "Costo Unitario", - "country_label": "País de Origen", - "select_country_placeholder": "Selecciona país", - "fraction_label": "Fracción", - "select_fraction_placeholder": "Selecciona fracción", - "tariff_type_label": "Tipo de Tarifa", - "reference_number_label": "Número de Referencia", - "purchase_order_label": "Orden de Compra/Venta", - "warehouse_label": "Almacén", - "location_label": "Ubicación", - "description_es_label": "Descripción (Español)", - "description_es_placeholder": "Descripción en español", - "description_en_label": "Descripción (Inglés)", - "description_en_placeholder": "Description in English", - "sku_label": "SKU", - "sku_placeholder": "Código SKU del producto", - "batch_label": "Lote", - "batch_placeholder": "Número de lote", - "classification_fraction_label": "Fracción Arancelaria", - "fraction_digits_placeholder": "8 dígitos", - "product_type_label": "Tipo de Producto", - "product_type_placeholder": "Materia prima, producto terminado, etc.", - "material_type_label": "Tipo de Material", - "material_type_placeholder": "Metal, plástico, etc.", - "product_code_label": "Código de Producto", - "product_code_placeholder": "Código interno", - "country_origin_label": "País de Origen", - "country_code_placeholder": "Código del país", - "merchandise_category_label": "Categoría de Mercancía", - "merchandise_category_placeholder": "Categoría", - "quantity_tab_label": "Cantidad", - "unit_of_measure_label": "Unidad de Medida", - "unit_of_measure_placeholder": "PZA, KG, M, etc.", - "zero_placeholder": "0", - "decimal_placeholder": "0.00", - "net_weight_label": "Peso Neto (KG)", - "gross_weight_label": "Peso Bruto (KG)", - "unit_cost_usd_label": "Costo Unitario (USD)", - "total_value_label": "Valor Total (USD)", - "packages_label": "Número de Bultos", - "package_type_label": "Tipo de Empaque", - "package_type_placeholder": "Caja, pallet, etc.", - "imported_quantity_label": "Cantidad Importada", - "remaining_quantity_label": "Cantidad Remanente", - "brand_label": "Marca", - "brand_placeholder": "Marca del producto", - "expiration_date_label": "Fecha de Caducidad", - "production_date_label": "Fecha de Producción", - "min_stock_label": "Stock Mínimo", - "max_stock_label": "Stock Máximo", - "observations_label": "Observaciones", - "observations_placeholder": "Notas adicionales sobre el inventario...", - "loading_item_data": "Cargando datos de la partida...", - "loading_more_items": "Cargando más items...", - "invoice_line_info": "Información de la factura ({systemLabel})", - "select_line": "Seleccionar línea", - "import_title": "Partidas de Importación", - "import_description": "Selecciona una línea con saldo disponible para realizar la descarga.", - "loading_invoice_items": "Cargando partidas de la factura...", - "no_balance": "Sin saldo disponible", - "no_balance_description": "No hay líneas con saldo en esta factura para descargar.", - "balance_required": "Línea con saldo disponible", - "cancel": "Cancelar", - "close": "Cerrar", - "saving": "Guardando...", - "update": "Guardar", - "create": "Guardar" - }, - "prerequisites": { - "title": "Aviso", - "message_both": "No hay Agentes aduanales ni Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.", - "message_agents": "No hay Agentes aduanales registrados. Debes darlos de alta para poder trabajar en este módulo.", - "message_clients": "No hay Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.", - "register_hint": "Puedes registrarlos en", - "agents_link": "Agentes Aduanales", - "clients_link": "Clientes y Proveedores", - "and": "y", - "cancel": "Cancelar", - "accept": "Aceptar" - } - } -} \ No newline at end of file + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Hello, {name} from es!", + "sidebar": { + "dashboard": "Dashboard", + "reference_data": { + "title": "Catálogos Fijos", + "codes_pedimento_regimen": "Códigos de Pedimento y Régimen", + "containers": "Contenedores", + "countries": "Países", + "currency_types": "Tipos de moneda", + "customs_sections": "Secciones de aduanas", + "customs_warehouses": "Recintos", + "incoterms": "Incoterms", + "document_types_digitization": "Tipos de documento para digitalización", + "invoice_types": "Tipos de factura", + "material_types": "Tipos de material", + "payment_methods": "Métodos de pago", + "pedimento_codes": "Códigos de pedimento", + "pedimento_regimes": "Regímenes de pedimentos", + "sectors": "Sectores", + "states": "Estados", + "transportation_modes": "Métodos de transporte", + "transportation_types": "Tipos de transporte", + "valuation_methods": "Métodos de valoración", + "configuracion": "Configuración", + "general": "General", + "licencia": "Licencia", + "usuarios": "Usuarios", + "ayuda": "Ayuda" + }, + "general_catalogs": { + "title": "Catalogos Generales", + "company_information": "Información de la empresa", + "packages": "Bultos", + "concepts": "Conceptos", + "classification": "Clasificación", + "identifiers": "Identificadores", + "incoterms": "Incoterms", + "inpc": "I.N.P.C", + "fixed_legends": "Leyendas fijas", + "seals": "Precintos", + "valuation_methods": "Metódos de valoración", + "countries": "Países", + "ports": "Puertos", + "unit_measures": "Unidades de medida", + "um_customs_mex": "UM Aduanas MX", + "um_customs_ame": "UM Aduanas USA", + "um_ace": "UM ACE", + "um_oma": "UM OMA", + "conversions": "Conversiones", + "equivalences": "Equivalencias", + "exchange_rates": "Tipos de cambio", + "currency_types": "Tipos de moneda", + "multi_currency": "Multi Moneda", + "invoice_types": "Tipos de factura", + "electronic_signatures": "Firmas electrónicas", + "billing_errors": "Errores de facturación", + "customs_warehouses": "Recintos", + "locations": "Localizaciones", + "doda": "DODA", + "packing_list": "Packing List", + "prevalidators": "Prevalidadores", + "electronic_notices": "Avisos electrónicos", + "back_flush": "Back Flush", + "crossing_notice": "Aviso de cruce", + "customs_broker_concepts": "Conceptos de Agente Aduanal" + }, + "fractions": { + "title": "Fracciones", + "sitar": "Fracciones Sitar", + "sitar_seventh_amendment": "Fracciones Sitar - 7ma enmienda", + "sitar_us": "Fracciones Sitar US", + "american": "Fracciones US", + "canadian": "Fracciones Canadiense", + "historical": "Fracciones Historicas", + "sectors": "Sectores" + }, + "goods": { + "title": "Mercancías", + "classes": "Clases", + "parts": "Partes", + "fda_codes": "Códigos F.D.A." + }, + "pedimentos": { + "title": "Pedimentos", + "pedimento_management": "Gestión de Pedimentos", + "pedimento_codes": "Claves de Pedimento", + "customs_regimes": "Regímenes Aduaneros", + "payment_methods": "Formas de Pago", + "customs_sections": "Secciones Aduaneras", + "anexo_22_app_31": "Anexo 22 App 3" + }, + "import_invoices": { + "title": "Facturas de importación", + "temporary": "Temporal", + "definitive": "Definitiva", + "mexican_purchases": "Compras mexicanas", + "regime_change": "Cambio de régimen", + "repair": "Reparación" + }, + "export_invoices": { + "title": "Facturas de exportación", + "exportation": "Exportación", + "repair": "Reparación" + }, + "export": { + "title": "Exportación", + "catalog": "Catálogo de exportación", + "repair": "Reparación", + "manifest": "Manifiesto", + "proforma": "Proforma", + "reports": "Reportes", + "used_materials": "Módulo de materiales utilizados", + "destruction": "Destrucción", + "special_processes": "Procesos Especiales" + }, + "clients_and_providers": "Clientes y Proveedores", + "customs_brokers": "Agentes Aduanales", + "audit_logs": "Bitácora", + "audit_logs_title": "Bitácora de Movimientos", + "audit_logs_description": "Auditoría de operaciones y seguimiento de tareas en segundo plano (Celery).", + "audit_logs_tab_bitacora": "Bitácora", + "audit_logs_tab_tasks": "Tareas en segundo plano", + "audit_logs_tab_files": "Gestor de archivos", + "audit_logs_files_title": "Gestor de archivos", + "audit_logs_files_root": "Raíz de archivos", + "audit_logs_files_refresh": "Actualizar", + "audit_logs_files_list_title": "Contenido", + "audit_logs_files_error_prefix": "Error:", + "audit_logs_files_col_name": "Nombre", + "audit_logs_files_col_size": "Tamaño", + "audit_logs_files_col_modified": "Modificado", + "audit_logs_files_col_actions": "Acciones", + "audit_logs_files_loading": "Cargando archivos...", + "audit_logs_files_empty": "No hay archivos o carpetas en esta ubicación.", + "audit_logs_files_download": "Descargar", + "despacho": { + "title": "Despacho", + "digitalizacion": "Digitalización", + "doda": "DODA" + }, + "doda_alta": { + "title": "DODA", + "subtitle": "Declaración Operación Despacho Aduanero", + "new": "Nuevo", + "refresh": "Actualizar", + "table_title": "DODAs", + "col_integration_number": "No. Integración", + "col_patent": "Patente", + "col_status": "Estatus", + "col_dispatch_customs": "Aduana Despacho", + "col_operation_type": "Tipo Operación", + "col_actions": "Acciones", + "action_alta_doda": "Alta DODA", + "action_alta_pita": "Alta PITA", + "action_edit": "Editar", + "action_delete": "Borrar", + "action_new": "Nuevo DODA", + "progress_title": "Procesando alta DODA...", + "progress_success": "Alta DODA completada exitosamente.", + "progress_error": "Error en el alta DODA.", + "eligibility_error": "El DODA no cumple los requisitos para el alta.", + "eligibility_checking": "Verificando elegibilidad...", + "empty": "Sin DODAs", + "loading": "Cargando...", + "search_placeholder": "Buscar:", + "confirm_delete": "¿Está seguro de eliminar este DODA?", + "delete_success": "DODA eliminado correctamente", + "delete_error": "Error al eliminar DODA", + "delete_missing_company": "Selecciona una compañía", + "delete_select_one": "Selecciona un solo DODA en el listado", + "delete_not_found": "No se pudo localizar el DODA. Pulsa otra fila e inténtalo de nuevo", + "filter_integration_number": "No. Integración", + "filter_patent": "Patente", + "filter_status": "Estatus", + "filter_operation_type": "Tipo Operación", + "action_generar": "Generar", + "action_export_excel": "Reporte por fechas", + "action_export_pedimentos": "Reporte DODA", + "export_pedimentos_success": "Reporte DODA generado.", + "export_pedimentos_error": "No se pudo generar el reporte DODA.", + "export_excel_title": "Exportar listado DODA", + "export_excel_subtitle": "Filtra por Fecha DODA (en base de datos como AAAAMMDD).", + "export_excel_badge": "CATÁLOGO DODA", + "export_report_heading": "Reporte general por rango de fechas", + "export_fecha_inicio": "Fecha inicio", + "export_fecha_final": "Fecha final", + "export_julian_label": "Imprimir Fecha Juliana en archivo Excel.", + "export_report_generar": "Generar", + "export_date_from": "Desde", + "export_date_to": "Hasta", + "export_format": "Formato de archivo", + "export_date_mode": "Fechas y hora en el archivo", + "export_date_mode_formatted": "Formateado (DD/MM/YYYY y hora)", + "export_date_mode_raw": "Numérico (YYYYMMDD / crudo)", + "export_download": "Descargar", + "export_cancel": "Cerrar", + "export_excel_success": "Archivo generado.", + "export_excel_error": "No se pudo generar el archivo.", + "export_no_data": "No hay DODA en el rango de fechas elegido. Amplía el rango o prueba otras fechas.", + "export_excel_invalid_dates": "Indique fecha desde y hasta." + }, + "digitalizacion": { + "title": "Digitalización", + "subtitle": "Catálogo de Documentos Digitalizados", + "new": "Nuevo", + "refresh": "Actualizar", + "table_title": "Documentos digitalizados", + "col_consecutivo": "Consecutivo", + "col_tipo_documento": "Tipo Documento", + "col_e_document": "E-Document", + "col_fecha": "Fecha", + "col_num_operacion_vu": "Núm. Operación VU", + "col_actions": "Acciones", + "form_e_document": "E-Document", + "form_num_operacion": "Núm. Operación", + "form_tipo_documento": "Tipo Documento", + "form_archivo_digitalizado_en": "Archivo Digitalizado en", + "form_fecha": "Fecha", + "form_agente_aduanal": "Agente Aduanal", + "form_pedimento": "Pedimento", + "form_nombre_archivo": "Nombre del archivo", + "digitalizar_title": "Digitalizar Documento", + "digitalizar_subtitle": "Enviar documento a Ventanilla Única", + "digitalizar_file_label": "Archivo", + "digitalizar_rfc_consulta": "RFC Consulta", + "digitalizar_clave_documento": "Clave Documento", + "progress_title": "Digitalizando documento...", + "progress_step": "Paso", + "progress_success": "Digitalización completada exitosamente.", + "progress_download_acuse": "Descargar Acuse", + "action_digitalizar": "Digitalizar", + "action_download_zip": "Descargar ZIP", + "action_acuse": "Acuse", + "action_envio_xml": "Envío XML", + "action_respuesta_xml": "Respuesta XML", + "action_consulta_envio_xml": "Consulta Envío XML", + "action_consulta_respuesta_xml": "Consulta Respuesta XML", + "action_edit": "Editar", + "action_delete": "Borrar", + "empty": "Sin documentos digitalizados", + "loading": "Cargando...", + "search_placeholder": "Buscando:", + "confirm_delete": "¿Está seguro de eliminar este documento?" + }, + "client_provider_type": { + "client_indicator": "C", + "provider_indicator": "P", + "both_indicator": "A" + }, + "nav_user": { + "profile": "Perfil", + "settings": "Configuración" + }, + "transports": { + "title": "Transportes", + "transporters": "Transportistas", + "drivers": "Conductores", + "trailers": "Trailers", + "vehicles": "Vehículos" + }, + "reports": { + "title": "Reportes", + "invoices": "Facturas Impo/Expo", + "downloaded_parts": "Partes descargadas", + "expiration": "Reporte de Vencimiento" + }, + "settings": { + "general": "General" + }, + "doda_form": { + "shortcuts_scope": "Formulario DODA", + "title_new": "Nuevo DODA", + "title_edit": "Editar DODA", + "description_catalog": "Catálogos · DODA", + "tab_general": "General", + "tab_seals_sat": "Sellos y SAT", + "shortcuts_hint": "Alt+1/2 · Ctrl+S guardar · Esc cancelar", + "btn_cancel": "Cancelar", + "btn_save": "Guardar", + "btn_saving": "Guardando...", + "btn_save_changes": "Guardar cambios", + "btn_create_doda": "Crear DODA", + "btn_accept": "Aceptar", + "card_broker_customs": "Agente aduanal y aduana", + "card_transport": "Transporte", + "card_control": "Control y despacho", + "card_sat_chain": "Cadena original y firmas (SAT)", + "label_responsible": "Responsable", + "label_patent": "Patente", + "label_dispatch": "Aduana despacho", + "label_section_es": "Aduana sección E/S", + "label_operation_type": "Tipo operación", + "label_transporter": "Transportista", + "label_transport_id": "ID transporte", + "label_caat": "CAAT", + "label_doda_date": "Fecha DODA", + "label_status": "Estatus", + "label_dispatch_type": "Tipo despacho", + "label_unique_badge": "Gafete único", + "label_integration_num": "Núm. integración", + "label_transaction_num": "Núm. transacción", + "label_fast_id": "Fast ID", + "label_last_user": "Último usuario", + "label_original_chain": "Cadena original", + "label_serial_cert": "Núm. serie (certificado)", + "label_uuid_cp": "UUID carta porte", + "label_electronic_sig": "Firma electrónica", + "label_sat_cert": "Certificado SAT", + "label_sat_chain": "Cadena original SAT", + "ph_aga": "Clave AGA", + "ph_0000": "0000", + "ph_000": "000", + "ph_select": "Seleccionar", + "ph_plate": "Placa / ID vehículo", + "ph_dash": "—", + "ph_yyyymmdd": "AAAAMMDD", + "ph_badge_pita": "N/A — PITA", + "ph_badge_num": "Núm. gafete", + "ph_example_container": "Ej. 53056", + "op_import": "I — Importación", + "op_export": "E — Exportación", + "type_pita": "PITA", + "type_doda": "DODA", + "vu_checking": "Verificando VU DODA del agente…", + "vu_incomplete": "VU DODA incompleta: se requiere .cer, .key y clave FIEL DODA del agente.", + "vu_complete": "VU DODA completa para envío a API.", + "badge_required_hint": "Requerido para alta DODA en API.", + "pedimentos": "Pedimentos", + "lines": "líneas", + "containers": "Contenedores", + "american_pedimentos": "Pedimentos americanos", + "seals_block_title": "Precintos (candados) — total en el DODA: {n} / 8", + "seals_help": "Selecciona un contenedor en la tabla. Máximo 8 precintos en todo el DODA (regla SCAII).", + "seals_select_container": "Selecciona un contenedor en la tabla de contenedores para ver o editar sus precintos.", + "container_no_id_warning": "Contenedor sin id en el servidor. Completa el valor, pulsa Guardar (arriba); al guardar se envían contenedores nuevos y se recargan con id para precintos.", + "container_line_info": "Contenedor:", + "seal_on_line": "precinto(s) en esta línea", + "line_word": "Línea", + "btn_add_seal": "Agregar precinto", + "btn_seal_delete": "Eliminar", + "seals_empty_line": "Sin precintos en este contenedor.", + "col_line": "Línea", + "col_auth_patent": "Patente auth.", + "col_document": "Documento", + "col_remesa": "Remesa", + "col_cove": "COVE", + "col_umc": "UMC", + "col_cash_usd": "Efectivo USD", + "col_diff_usd": "Diferencia USD", + "col_dta_niu": "DTA NIU", + "col_art7": "Art. 7", + "col_container": "Contenedor", + "col_seals": "Precintos", + "col_seal_value": "Precinto", + "col_american_type": "Tipo", + "col_american_ped": "Pedimento americano", + "col_pedimento_only": "Pedimento americano", + "yes": "Sí", + "no": "No", + "child_empty": "Sin filas. «Nuevo» para añadir.", + "child_new": "Nuevo", + "child_edit": "Editar", + "child_delete": "Borrar", + "modal_container_new": "Nuevo contenedor", + "modal_container_edit": "Editar contenedor", + "modal_container_desc": "Captura el valor del contenedor para la declaración DODA.", + "label_container_value": "Valor contenedor", + "modal_seals_in_container": "Precintos del contenedor", + "seal_modal_title": "Contenedores > Precinto", + "seal_modal_desc": "Captura el valor del precinto para el contenedor seleccionado.", + "label_seal": "Precinto", + "ph_seal": "Valor del precinto", + "american_modal_title": "Pedimento Americano", + "american_modal_desc": "Captura el tipo y valor del pedimento americano.", + "label_american_type_short": "Tipo Ped. Americano", + "label_american_value": "Pedimento Americano", + "ph_american_value": "Valor pedimento americano", + "line_label": "Línea:", + "select_type": "Selecciona tipo", + "american_cat_6": "PEDIMENTO AMERICANO", + "american_cat_7": "AUTODECLARACION", + "american_cat_8": "NO PRESENTA", + "err_american_tipo_required": "El tipo de pedimento americano es obligatorio.", + "err_american_tipo_import": "El tipo de pedimento americano no es correcto para importación (debe ser 1, 2, 3, 4 o 5).", + "err_american_tipo_export": "El tipo de pedimento americano no es correcto para exportación (debe ser 6, 7 u 8).", + "err_american_op_undefined": "Define el tipo de operación (I/E) antes de validar el pedimento americano.", + "err_company": "Selecciona una compañía", + "err_responsible": "El Responsable es requerido", + "err_patent": "El Agente Aduanal (Patente) es requerido", + "err_transport": "La Identificación de Transporte es requerida. Selecciona un vehículo.", + "err_badge": "El Número de Gafete Único es requerido para Alta DODA.", + "err_vu_wait": "Espera a que termine la verificación VU DODA del agente e intenta de nuevo.", + "err_vu_config": "El agente aduanal no tiene configuración VU DODA completa (.cer, .key y clave FIEL DODA).", + "err_min_containers": "Agrega al menos un contenedor con valor para el envío a API.", + "err_american_new_lines": "Indique el valor del pedimento americano en cada línea nueva.", + "err_save": "Error al guardar", + "toast_saved": "Cambios guardados correctamente.", + "toast_created": "DODA creado correctamente.", + "load_error": "No se pudo cargar la información del DODA", + "warn_vu_incomplete": "El agente aduanal de este DODA no tiene VU DODA completa (.cer, .key y clave FIEL DODA).", + "warn_vu_fetch": "No se pudo validar la configuración VU del agente aduanal.", + "warn_broker_select": "El agente seleccionado no tiene VU DODA completa (.cer, .key y clave FIEL DODA). Configúralo en Agentes Aduanales antes de generar.", + "seal_save_first": "Guarda el DODA antes de gestionar precintos.", + "seal_pick_container": "Selecciona un contenedor en la tabla.", + "seal_not_persisted": "Este contenedor aún no está guardado en el servidor. Guarda el DODA (Guardar) y vuelve a abrir o recarga.", + "seal_empty": "El precinto no puede estar vacío.", + "seal_max": "El DODA ya tiene el máximo de 8 precintos.", + "seal_add_err": "Error al agregar el precinto", + "seal_delete_err": "Error al eliminar el precinto", + "pedimento_remove_blocked": "Los pedimentos guardados en servidor no se pueden quitar aquí.", + "container_delete_err": "Error al eliminar el contenedor", + "american_delete_err": "Error al eliminar el pedimento americano", + "container_update_err": "Error al actualizar el contenedor", + "american_cannot_edit_persisted": "Para editar pedimentos americanos guardados, elimínalo y créalo nuevamente.", + "err_american_value": "Indique el valor del pedimento americano.", + "err_american_type_or_value": "Capture tipo o valor del pedimento americano.", + "err_containers_max": "El DODA solo puede tener máximo 4 contenedores.", + "err_container_empty": "El valor del contenedor no puede estar vacío.", + "err_container_not_found": "No se encontró el contenedor a editar.", + "pedimento_selector_title": "Contenedores > Precinto", + "list_page_subtitle": "Gestiona tus Documentos de Operación Aduanera (DODA)", + "list_btn_new": "Nuevo DODA", + "list_card_title": "Listado de DODA", + "list_ph_folio": "Folio", + "list_ph_patent": "Patente", + "list_filter_status_ph": "Estatus", + "list_filter_status_all": "Todos", + "list_filter_op_import": "Importación", + "list_filter_op_export": "Exportación", + "list_filter_op": "Operación", + "list_filter_op_all": "Todas", + "list_btn_clear": "Limpiar", + "list_showing": "Mostrando {a} de {b} registros", + "list_active_filters": "Filtros activos: {n}", + "list_btn_edit": "Editar", + "list_btn_print": "Imprimir", + "list_toast_reload_error": "Error al recargar datos", + "list_elig_error_prefix": "Error al verificar elegibilidad: ", + "list_elig_not_meet": "El DODA no cumple con los requisitos de alta.", + "list_alta_error_prefix": "Error al enviar alta DODA: ", + "list_print_error": "Error al generar el PDF del DODA", + "list_alta_complete": "Alta DODA completada correctamente", + "list_shortcuts_scope": "Lista DODA", + "list_col_folio": "Folio", + "list_col_doda_date": "Fecha DODA", + "list_col_desp": "Desp.", + "list_col_patent": "Patente", + "list_col_pedimentos": "Pedimento(s)", + "list_col_remesas": "Remesa(s)", + "list_col_integracion": "Integración", + "list_col_trans": "Núm. Transacción", + "list_col_id_transport": "Id. Transporte", + "list_col_caat": "CAAT", + "list_col_user": "Usuario", + "list_col_status": "Estatus", + "list_loading_more": "Cargando más...", + "list_scroll_for_more": "Desplázate para cargar más", + "list_confirm_delete": "¿Está seguro de eliminar este registro DODA?", + "list_toast_delete_ok": "DODA eliminado correctamente", + "list_toast_delete_err": "Error al eliminar DODA", + "list_filter_i": "I - Importación", + "list_filter_e": "E - Exportación", + "list_no_results": "No hay resultados." + } + }, + "invoice_list": { + "skip_to_actions": "Ir a acciones de factura", + "header": { + "title": "Facturas", + "description": "Gestiona las facturas del sistema" + }, + "titles": { + "base": "CATALOGO DE FACTURAS", + "import": "DE IMPORTACION", + "export": "DE EXPORTACION", + "import_temporal": "DE IMPORTACION TEMPORAL", + "import_definitive": "DE IMPORTACION DEFINITIVA", + "import_mexican": "DE COMPRAS MEXICANAS", + "import_regime_change": "DE CAMBIO DE REGIMEN Y REGULARIZACION", + "import_repair": "DE IMPO. DE REPARACION", + "export_definitive": "DE SALIDA DEFINITIVA", + "export_repair": "DE REPARACION" + }, + "filters": { + "operation_label": "Tipo de Operacion", + "operation_all_option": "Operacion: Todas", + "invoice_type_label": "Tipo de Factura", + "invoice_type_all_option": "Factura: Todas", + "invoice_number_placeholder": "No. Factura", + "year_start_placeholder": "Ano inicio", + "year_end_placeholder": "Ano fin", + "active_filters": "Filtros activos" + }, + "actions": { + "parameters": "Parametros", + "new_invoice": "Nueva Factura", + "refresh": "Actualizar", + "reports": "Reportes", + "more_actions": "Mas Acciones", + "downloads": "Descargas", + "other_actions": "Otras Acciones", + "cancel": "Cancelar", + "continue": "Continuar", + "generate_cove": "Generar COVE", + "close": "Cerrar" + }, + "card": { + "invoice_list_title": "Listado de Facturas" + }, + "summary": { + "showing": "Mostrando", + "of": "de", + "records": "registros" + }, + "operation_types": { + "all": "Todas", + "import": "Importacion", + "export": "Exportacion" + }, + "cove_dialog": { + "title": "Generar COVE", + "description_prefix": "Selecciona el correo destinatario para la factura", + "recipient_label": "Correo destinatario", + "destination": "Destino COVE", + "select_email": "Selecciona un correo", + "fallback_email": "Se enviara al correo del usuario que genero la factura", + "search_email": "Buscar correo...", + "loading_emails": "Cargando correos disponibles...", + "no_emails": "No hay correos disponibles para COVE.", + "selected_badge": "Seleccionado" + }, + "progress": { + "title_pdf": "Generando PDF de Factura", + "title_consolidated": "Generando Consolidado", + "title_descargo": "Generando Reporte PEPS", + "title_packing_list": "Generando Packing List", + "title_winsaai": "Generando Reporte WINSAAI", + "title_process_invoice": "Procesando factura", + "title_revert_invoice": "Des-actualizando factura", + "title_validate_cove": "Validando datos para COVE", + "complete_processed": "Factura procesada correctamente", + "complete_reverted": "Factura des-actualizada correctamente", + "complete_cove_validation": "Validacion de COVE completada", + "complete_default": "Proceso completado" + }, + "steps": { + "load_invoice": "Cargando factura", + "validate_invoice_data": "Validando datos de la factura", + "review_classes_exchange_rate": "Revisando clases y tipo de cambio", + "calculate_item_values": "Calculando valores por partida", + "validate_items": "Validando partidas", + "validate_rule8_quotas": "Validando cupos de Regla Octava", + "update_totals": "Actualizando totales", + "validate_invoice_status": "Validando estatus de la factura", + "verify_item_balances": "Verificando saldos de partidas", + "confirm_changes": "Confirmando cambios" + }, + "dialogs": { + "revert_title_export": "Des-actualizar Factura de Exportacion", + "revert_title_import": "Des-actualizar Factura de Importacion", + "revert_description_intro": "Se va a des-actualizar la factura", + "revert_description_warning": "Esta operacion revertira los registros de saldos/descargos generados al procesar la factura.", + "revert_description_question": "Desea continuar?", + "winsaai_title": "Sistema de Control de Aduanas e Inventarios", + "winsaai_description_intro": "A la Factura", + "winsaai_of_type": "de tipo", + "winsaai_description_process": "se le ha asignado el proceso Generacion del Archivo WINSAAI.", + "winsaai_description_question": "Desea Continuar o Cancelar?" + }, + "footer": { + "toolbar_aria": "Acciones de factura", + "invoice_pdf": "Factura PDF", + "invoice_csv": "Factura CSV", + "consolidated": "Consolidado", + "consolidated_notice": "Aviso Consolidado", + "packing_list": "Packing List", + "four_copies_rem": "4 Copias Rem", + "descargo_peps": "Descargo PEPS", + "transferencia_electronica": "Transferencia Electronica", + "interface_vu": "Interface VU", + "vu_options_keyboard": "Opciones VU (teclado)", + "vu_consult": "Consulta", + "vu_addenda": "Adenda", + "vu_cove_receipt": "Acuse de COVE", + "vu_massive_cove": "COVE Masivos", + "cons_sed": "Cons SED", + "encomienda": "Encomienda", + "fact_mex_cons": "Fact Mex Cons", + "fact_mex_ord_cat": "Fact Mex Ord Cat", + "export_sia": "Export SIA", + "interface": "Interface", + "process_update": "Actualizar", + "unprocess": "Desactualizar", + "view_details": "Ver Detalles", + "customs_broker_interface": "Interface Agente Aduanal", + "edit": "Editar", + "delete": "Eliminar" + }, + "submenu": { + "consult_soon": "Consulta VU - Proximamente", + "addenda_soon": "Adenda VU - Proximamente", + "massive_cove_soon": "COVE Masivos - Proximamente", + "generate_invoice_csv_soon": "Generar Factura CSV - Proximamente", + "four_copies_soon": "4 Copias Rem - Proximamente", + "cons_sed_soon": "Cons SED - Proximamente", + "encomienda_soon": "Encomienda - Proximamente", + "fact_mex_cons_soon": "Factura Mex Consolidada - Proximamente", + "fact_mex_ord_cat_soon": "Factura Mex Orden Captura - Proximamente", + "export_sia_soon": "Export SIA - Proximamente", + "interface_soon": "Interface - Proximamente" + }, + "recipients": { + "company_vu_email": "Correo VU de la empresa", + "company_main_email": "Correo principal de la empresa", + "company_industrial_1": "Correo industrial 1", + "company_industrial_2": "Correo industrial 2", + "company_description": "Empresa {name}", + "single_window_email": "Correo de ventanilla unica", + "main_email": "Correo principal", + "company_user_email": "Usuario de la empresa - {email}", + "my_email": "Mi correo", + "authenticated_user": "Usuario autenticado - {email}", + "load_error": "No se pudieron cargar los correos disponibles para COVE", + "no_configured": "No hay correos configurados para COVE" + }, + "toasts": { + "select_invoice_for_cove": "Selecciona una factura para generar COVE", + "no_company_selected": "No hay empresa seleccionada", + "session_expired_reloading": "Sesión expirada. Recargando página...", + "load_more_error": "Error cargando más datos", + "apply_filters_error": "Error aplicando filtros", + "reload_data_error": "Error recargando datos", + "download_start_error": "No se pudo iniciar la descarga", + "consolidated_download_start_error": "No se pudo iniciar la descarga del consolidado", + "calculating_peps": "Calculando asignacion PEPS...", + "peps_calculation_error_prefix": "Error al calcular PEPS: {error}", + "peps_calculation_completed": "Calculo PEPS completado", + "peps_report_start_error": "No se pudo iniciar la descarga del reporte PEPS", + "aviso_consolidado_start_error": "No se pudo iniciar la descarga del Aviso Consolidado", + "packing_list_start_error": "No se pudo iniciar la descarga del Packing List", + "fast_interface_import_only": "La interfaz rapida solo esta disponible para facturas de Importacion", + "customs_broker_interface_start_error": "No se pudo iniciar la generacion de Interface Agente Aduanal", + "pdf_download_success": "PDF descargado exitosamente", + "invoice_processed_success": "Factura procesada correctamente", + "worker_error_prefix": "El worker reporto un error: {error}", + "task_result_process_error": "Error al procesar el resultado de la tarea", + "select_invoice_to_edit": "Seleccione una factura para editar", + "no_table_rows": "No hay filas en la tabla", + "select_invoice_for_reports": "Seleccione una factura para reportes", + "select_invoice_for_more_actions": "Seleccione una factura para mas acciones", + "select_invoice_to_revert": "Seleccione una factura para desactualizar", + "select_invoice_for_details": "Seleccione una factura para ver detalles", + "select_invoice": "Seleccione una factura", + "select_at_least_one_invoice_to_delete": "Seleccione al menos una factura para eliminar", + "select_invoice_for_pdf": "Seleccione una factura para descargar PDF", + "select_invoice_for_consolidated": "Seleccione una factura para descargar consolidado", + "select_invoice_to_change_status": "Seleccione una factura para cambiar su estatus", + "update_status_error_prefix": "Error al {action} factura: {error}", + "status_action_update": "actualizar", + "status_action_revert": "desactualizar", + "status_updated_success": "Factura actualizada correctamente", + "status_reverted_success": "Factura desactualizada correctamente", + "update_status_unexpected_error": "Error inesperado al cambiar el estatus", + "select_invoice_to_process": "Selecciona una factura para procesar", + "process_start_error_prefix": "Error al iniciar el proceso: {error}", + "process_start_error": "No se pudo iniciar el proceso", + "revert_start_error_prefix": "Error al iniciar la des-actualizacion: {error}", + "revert_start_error": "No se pudo iniciar la des-actualizacion", + "select_recipient_email_for_cove": "Selecciona un correo para enviar el COVE", + "cove_eligibility_error_prefix": "No se pudo validar elegibilidad COVE: {error}", + "cove_requirements_not_met": "La factura no cumple los requisitos para generar COVE", + "cove_verification_error": "No se pudo verificar si la factura puede generar COVE", + "cove_start_error_prefix": "Error al iniciar generacion de COVE: {error}", + "cove_start_error": "No se pudo iniciar la generacion de COVE", + "validation_extra_more": "\n...y {count} mas", + "validation_error_count": "{count} error(es) de validacion:\n{preview}{extra}", + "cove_external_queued_default": "Factura COVE iniciada en Ventanilla Unica. Use el task_id para consultar el estado." + } + }, + "invoice_table": { + "no_results": "No hay resultados.", + "loading_more": "Cargando mas...", + "scroll_to_load_more": "Desplazate para cargar mas", + "processed": "Procesada", + "pending": "Pendiente", + "operation": "Operacion", + "operation_import": "Importacion", + "operation_export": "Exportacion", + "invoice_type": "Tipo Factura", + "invoice_number": "Num. Factura", + "pedimento_18": "Pedimento 18", + "remesa": "Remesa", + "invoice_date": "Fecha Factura", + "pedimento_code": "Clave Ped.", + "document_type": "Tipo Doc.", + "total_items": "Total Partidas", + "currency": "Moneda", + "currency_type": "Tipo Moneda", + "weight_type": "Tipo Peso", + "mixed": "Mixto", + "related_doc": "Doc. Relacionado", + "yes": "Si", + "no": "No", + "not_available_short": "N/D" + }, + "invoice_selectors": { + "identifier_catalog": { + "title": "Seleccionar Identificador", + "description": "Busca y selecciona un identificador del catalogo (Apendice 8).", + "search_placeholder": "Buscar por clave o descripcion...", + "column_code": "Clave", + "column_description": "Descripcion", + "column_level": "Nivel", + "empty": "No se encontraron identificadores." + }, + "valuation_method": { + "title": "Seleccionar Metodo de Valoracion", + "description": "Busca y selecciona un metodo de valoracion de la lista.", + "search_placeholder": "Buscar por clave o descripcion...", + "column_code": "Clave", + "column_description": "Descripcion", + "empty": "No se encontraron metodos de valoracion." + }, + "location": { + "title": "Catalogo de ubicaciones (maquinaria y equipo)", + "no_company_selected": "No hay compania seleccionada", + "load_error": "Error al cargar ubicaciones", + "required_key": "La clave es requerida", + "save_error": "Error al guardar", + "key_label": "Clave *", + "key_placeholder": "Clave de localizacion", + "location_label": "Localizacion", + "location_placeholder": "Nombre o descripcion", + "department_label": "Departamento", + "responsible_label": "Responsable", + "observations_label": "Observaciones", + "optional_placeholder": "Opcional", + "back_to_list": "Volver al listado", + "save": "Guardar", + "search_placeholder": "Buscar por clave o localizacion...", + "register_new": "Registrar nueva ubicacion", + "column_key": "Clave", + "column_location": "Localizacion", + "no_results": "No se encontraron resultados", + "cancel": "Cancelar" + }, + "tariff_fraction": { + "title": "CATALOGO DE FRACCIONES SITAR - SCAII", + "search_label": "Buscando:", + "search_placeholder": "Buscar por fraccion, descripcion, NICO...", + "column_key": "Clave", + "column_fraction": "Fraccion", + "column_nico": "NICO", + "column_description": "Descripcion", + "column_umt": "U.M.T", + "column_adv_impo": "Adv. Impo", + "column_adv_expo": "Adv. Expo", + "column_dof": "DOF", + "column_aplica_ieps": "Aplica IEPS", + "loading": "Cargando fracciones...", + "empty": "No hay fracciones disponibles", + "cancel": "Cancelar" + }, + "us_tariff_fraction": { + "no_company_selected": "No hay empresa seleccionada", + "load_error_prefix": "Error: {error}", + "no_records_info": "No se encontraron fracciones US registradas", + "connection_error_prefix": "Error de conexion: {error}", + "title": "Seleccionar Fracción US", + "description": "Seleccione la fraccion arancelaria (HTS) del catalogo.", + "search_placeholder": "Buscar por codigo o descripcion...", + "loading_catalog": "Cargando catalogo...", + "no_results": "No se encontraron fracciones.", + "column_code": "Codigo (HTS)", + "column_description": "Descripcion", + "records_found": "{count} registros encontrados", + "cancel": "Cancelar" + }, + "invoice_selector_modal": { + "no_active_company": "No se ha seleccionado una empresa activa", + "search_error": "Error al buscar facturas", + "title_export": "Facturas de Exportacion", + "title_import": "Facturas de Importacion ({regimen})", + "description_export": "Selecciona una factura del catalogo para vincularla a la partida.", + "description_import": "Selecciona una factura de importacion procesada para el regimen {regimen}.", + "search_placeholder": "Buscar por numero de factura...", + "searching_button": "Buscando...", + "search_button": "Buscar", + "searching_available": "Buscando facturas disponibles...", + "no_invoices": "No se encontraron facturas", + "try_other_filter": "Intenta con otro numero de factura o filtro", + "processed_badge": "Procesada", + "pedimento_label": "Pedimento", + "no_date": "Sin fecha", + "not_available_short": "N/D", + "select": "Seleccionar", + "total_found": "Total: {count} facturas encontradas", + "close": "Cerrar" + }, + "port_selector": { + "title": "Seleccionar Puerto (Aduana/Sección)", + "description": "Busca y selecciona una sección aduanera de la lista.", + "search_placeholder": "Buscar por código o nombre...", + "column_code": "Código", + "column_name": "Nombre / Sección", + "loading": "Cargando secciones aduaneras...", + "empty": "No se encontraron resultados", + "cancel": "Cancelar" + }, + "manifest_selector": { + "title": "Seleccionar Manifiesto", + "description": "Busca y selecciona un manifiesto del catálogo de exportación para vincular a esta factura.", + "search_placeholder": "Buscar por número...", + "search_button": "Buscar", + "searching": "Buscando manifiestos...", + "column_number": "Número de Manifiesto", + "column_description": "Descripción", + "empty": "No se encontraron resultados" + } + }, + "invoice_edit": { + "new_title": "Nueva Factura", + "edit_title": "Editar Factura", + "new_description": "Ingresa los datos de la nueva factura", + "edit_description": "Modifica los datos de la factura", + "draft_badge": "Borrador", + "saved_success": "Todos los cambios se guardaron correctamente", + "invoice_number_prefix": "Número:", + "edit_details": "Edita los detalles de la factura", + "page_invoice_prefix": "Factura #", + "page_default_values_loaded_prefix": "Valores predeterminados cargados para {invoiceType}", + "page_save_error_prefix": "Error al guardar la factura", + "page_save_changes_error": "Error al guardar los cambios", + "page_console_hint": "Revisa la consola para más detalles", + "page_session_expired": "Sesión expirada. Recargando página...", + "tabs": { + "general": "General", + "compliance": "Cumplimiento", + "financials": "Financieros", + "observations": "Observaciones", + "items": "Partidas", + "others": "Otros", + "continuation": "Cont." + }, + "form": { + "operation_type_label": "Tipo de Operación *", + "operation_type_placeholder": "Seleccionar tipo", + "operation_type_import": "Importación", + "operation_type_export": "Exportación", + "invoice_number_label": "Número de Factura", + "invoice_number_placeholder": "Número de factura", + "invoice_type_label": "Tipo de Factura", + "invoice_type_placeholder": "Tipo de factura", + "no_company_selected": "No hay compañía seleccionada", + "exchange_rate_required": "El tipo de cambio es requerido (pestaña Financieros)", + "exchange_rate_positive": "El tipo de cambio debe ser mayor a 0 (pestaña Financieros)", + "save_error": "Error al guardar", + "loading_defaults_prefix": "Valores predeterminados cargados para", + "pedimento_pending": "¿Pedimento pendiente?", + "pedimento_label": "Pedimento", + "pedimento_placeholder": "Selecciona pedimento...", + "remesa_label": "Remesa", + "invoice_number_label_short": "Núm. Factura", + "invoice_date_label_exp": "Fecha", + "invoice_date_label_mex": "Fecha de Entrada", + "invoice_date_label_default": "Fecha Factura", + "emission_date_label": "Fecha Emisión", + "iva_factor_label": "Factor IVA", + "alternate_invoice_label": "Factura Alterna", + "project_number_label": "Número de Proyecto", + "project_number_placeholder": "Número de proyecto", + "purchase_order_label": "Orden de Compra", + "purchase_order_placeholder": "Orden de compra", + "invoice_date_label": "Fecha de Factura", + "validation": { + "trailer_required": "El Remolque es obligatorio cuando el Tipo de Transporte es distinto de Ninguno.", + "missing_fields": "Los siguientes campos son obligatorios:", + "check_transport_data": "Revisa los datos de transporte y logística", + "save_error": "Error al guardar los cambios" + }, + "traffic_light_status_label": "Semáforo", + "traffic_light_status_placeholder": "Estado del semáforo", + "observation_es_label": "Observaciones (Español)", + "observation_es_placeholder": "Observaciones en español", + "observation_en_label": "Observaciones (Inglés)", + "observation_en_placeholder": "Observaciones en inglés", + "remesa_placeholder": "Número de remesa", + "aduana_label": "Aduana", + "aduana_placeholder": "Código de aduana", + "customs_broker_label": "Agente Aduanal", + "customs_broker_placeholder": "ID del agente aduanal", + "provider_label": "Proveedor", + "provider_placeholder": "ID del proveedor", + "edocument_label": "E-Document", + "edocument_placeholder": "Número de e-document", + "is_mixed_label": "Operación Mixta", + "currency_placeholder": "MXN, USD, etc.", + "exchange_rate_placeholder": "Tipo de cambio", + "value_mn_label": "Valor MN", + "value_mn_placeholder": "Valor en moneda nacional", + "value_me_label": "Valor ME", + "value_me_placeholder": "Valor en moneda extranjera", + "customs_value_mn_label": "Valor Aduana MN", + "customs_value_mn_placeholder": "Valor de aduana en MN", + "freight_label": "Flete", + "freight_placeholder": "Costo de flete", + "insurance_label": "Seguro", + "insurance_placeholder": "Costo de seguro", + "iva_mn_label": "IVA MN", + "iva_mn_placeholder": "IVA en MN", + "total_quantity_label": "Cantidad Total", + "total_quantity_placeholder": "Cantidad total", + "gross_weight_label": "Peso Bruto", + "gross_weight_placeholder": "Peso bruto", + "net_weight_label": "Peso Neto", + "net_weight_placeholder": "Peso neto", + "bundle_count_label": "Número de Bultos", + "bundle_count_placeholder": "Número de bultos", + "update_button": "Actualizar", + "create_button": "Crear" + }, + "general": { + "pedimento_section": "Datos del pedimento", + "pedimento_date_from": "Fecha del:", + "pedimento_date_to": "Fecha al:", + "pedimento_code": "Clave:", + "pedimento_regimen": "Régimen:", + "clients_suppliers_broker": "Clientes - Proveedores - Agente Aduanal", + "provider_header_supplier": "Proveedor", + "provider_header_exporter": "Exportador", + "sold_to_header_consignado": "Consignado a", + "sold_to_header_vendido": "Vendido a", + "sold_to_header_exportado": "Exportado a", + "sold_to_header_importador": "Importador", + "shipped_to_header_enviado": "Enviado a", + "shipped_to_header_transferido": "Transferido a", + "shipped_to_header_donado": "Donado a", + "shipped_to_header_importador": "Importador", + "shipped_by_header_enviado_por": "Enviado Por", + "shipped_by_header_destinatario": "Destinatario", + "shipped_by_header_vendido_por": "Vendido Por", + "shipped_by_header_notificar": "Notificar a", + "select_header_placeholder": "Selecciona encabezado...", + "select_placeholder": "Selecciona...", + "select_broker_placeholder": "Selecciona...", + "broker_mex_label": "Agente Aduanal Mex:", + "broker_usa_label": "Agente Aduanal US:", + "currency_weight_section": "Tipo de Moneda - Pesos Netos y Brutos", + "exchange_rate": "Tipo de cambio:", + "currency_foreign": "Extranjera (Dlls)", + "currency_local": "Nacional (Pesos)", + "currency_manual": "De Captura", + "currency_label": "Moneda:", + "weight_type_label": "Tipo Peso:", + "weight_type_kgs": "Kilogramos (kg)", + "weight_type_lbs": "Libras (lb)", + "manifest_number_label": "Num. de Manifiesto:", + "manifest_placeholder": "Manifiesto...", + "transport_section": "Transportista", + "transport_label": "Transportista:", + "transport_key_label": "Clave Transporte:", + "transport_type_label": "Tipo Transporte:", + "trailer_label": "Remolque:", + "driver_label": "Conductor:", + "iva_label": "IVA:", + "customs_label": "Aduana y Sección de Despacho:", + "document_type_label": "Clave de Régimen Aduanero:", + "select_transporter_placeholder": "Selecciona transportista...", + "select_vehicle_placeholder": "Selecciona vehículo...", + "select_driver_placeholder": "Selecciona conductor...", + "select_trailer_placeholder": "Selecciona remolque...", + "select_customs_placeholder": "Selecciona aduana...", + "select_regimen_placeholder": "Selecciona régimen...", + "choose_transporter_first": "Primero elige transportista...", + "no_data": "Sin datos", + "no_drivers_for_transporter": "Sin conductores para este transportista", + "no_regimens_for_operation": "Sin regímenes para tipo", + "choose_operation_first": "Selecciona tipo de operación primero", + "transport_none": "Ninguno", + "transport_type_transport": "Transporte", + "transport_type_box": "Caja", + "transport_type_licence_plates": "Placas", + "transport_type_truck": "Camión", + "transport_type_vessel": "Buque", + "transport_type_rail_barge": "Ferrobarcaza", + "transport_type_container": "Contenedor", + "transport_type_airplane": "Avión", + "transport_type_gondola": "Góndola", + "transport_type_flatbed": "Plataforma", + "signature_label": "Firma Electrónica:", + "general_info": "Información General" + }, + "page": { + "saving_all_changes": "Guardando todos los cambios...", + "save_all_changes": "Guardar Todos los Cambios", + "cancel": "Cancelar" + }, + "observations": { + "mexican_observation": "Observaciones de la factura mexicana:", + "bilingual_observation": "Observación de la factura mexicana y bilingüe:", + "textarea_placeholder": "Escribe tus observaciones aquí.", + "fixed_legend": "Leyenda fija:", + "selected_legend_prefix": "Clave", + "select_legend_placeholder": "Selecciona leyenda...", + "add_to_observations": "Agregar a observaciones", + "american_observation": "Observaciones de la factura US:", + "identifiers_title": "Identificadores", + "first_label": "Primero:", + "second_label": "Segundo:", + "key_placeholder": "Clave...", + "complements_title": "Complementos", + "one_label": "1:", + "two_label": "2:", + "office_label": "Oficio:", + "incrementables_title": "Incrementables:", + "freight_label": "Flete:", + "insurance_label": "Seguros:", + "packaging_label": "Embalajes:", + "other_increments_label": "Otros increm.:", + "other_deductibles_label": "Otros deduc.:", + "seal_number_label": "Número de Precinto:", + "movement_type_label": "Tipo Movimiento:", + "alternate_invoice_label": "Factura Alterna:", + "proforma_number_label": "Número de Proforma:", + "subdivision_label": "Sub División:", + "yes": "Sí", + "no": "No", + "acts_as_cd_label": "Funge como CD:", + "incoterm_label": "Incoterm:", + "select_placeholder": "Selecciona...", + "valuation_method_label": "Método de Valoración:", + "mixed_label": "¿Es mixto?", + "seal_count_label": "Num Precintos:", + "delivery_title": "Datos Entrega", + "delivered_label": "Entregado", + "received_by_label": "Recibido por:", + "delivery_date_label": "Fecha Entrega:", + "rule_parties_label": "Regla 3.1.21 Partes II", + "status_comment_label": "Comentario Estatus:", + "status_comment_placeholder": "Comentario estatus", + "related_docs_label": "ID Relación Docs:", + "electronic_signature_label": "Firma Electrónica:", + "authorized_person_label": "Mandatario/Persona Autorizada:", + "contingency_mode_label": "Modo Contingencia", + "cove_label": "COVE:", + "operation_number_label": "Núm Operación:", + "adendas_label": "Adenda(s):", + "vu_observations_label": "Observaciones VU:", + "load_info": "Cargar Info.", + "entry_exit_date_label": "Fecha Entrada/Salida:", + "payment_date_label": "Fecha Pago:", + "certificate_number_label": "Número Certificado:", + "enclosure_label": "Recinto:", + "alternate_flags_title": "Factura Alterna & Flags", + "valuation_method_placeholder": "Selecciona...", + "mixed_label_short": "Es mixto?", + "errors_title": "Errores de Facturación", + "line": "Línea", + "key": "Clave", + "description": "Descripción", + "no_errors": "Sin errores registrados", + "insert": "Insertar", + "edit": "Editar", + "delete": "Borrar" + }, + "others": { + "transport_mode_label": "Modo de Transporte:", + "select_mode_placeholder": "Seleccionar modo", + "print_stamp_label": "Imprimir el Sello por Valor menor a 2500 dlls", + "mixed_label": "Es Mixto?", + "yes": "Sí", + "no": "No", + "master_bol_label": "Número Master BOL:", + "guide_number_label": "Número Guía:", + "shipment_number_label": "Número Embarque:", + "option_iv18_label": "Opción IV 18:", + "select_option_placeholder": "Seleccionar opción", + "delivery_title": "Datos Entrega", + "delivered_label": "Entregado", + "received_by_label": "Recibido por:", + "delivery_date_label": "Fecha Entrega:", + "rule_3121_label": "Regla 3.1.21 Partes II", + "status_comment_label": "Comentario Estatus:", + "status_comment_placeholder": "Comentario estatus", + "related_docs_label": "ID Relación Docs:", + "electronic_signature_label": "Firma Electrónica:", + "authorized_person_label": "Mandatario/Persona Autorizada:", + "contingency_mode_label": "Modo Contingencia", + "cove_label": "COVE:", + "operation_number_label": "Núm Operación:", + "adendas_label": "Adenda(s):", + "vu_observations_label": "Observaciones VU:", + "load_info": "Cargar Info.", + "entry_exit_date_label": "Fecha Entrada/Salida:", + "payment_date_label": "Fecha Pago:", + "certificate_number_label": "Número Certificado:", + "electronic_signature_2_label": "Firma Electrónica:", + "errors_title": "Errores de Facturación", + "line": "Línea", + "key": "Clave", + "description": "Descripción", + "no_errors": "Sin errores registrados", + "insert": "Insertar", + "edit": "Editar", + "delete": "Borrar" + }, + "items": { + "unsaved_invoice_title": "Factura no guardada", + "unsaved_invoice_description": "Debes guardar la factura primero antes de agregar partidas.", + "loaded_more_items": "Cargando más items...", + "deleted": "Partida eliminada", + "delete_failed": "No se pudo eliminar la partida", + "no_data_to_save": "No hay datos para guardar", + "required_fields": "Completa los campos necesarios (Clase o Descripción)", + "no_active_company": "No hay ID de empresa activo. Asegúrate de tener una empresa seleccionada.", + "no_invoice_id": "No hay ID de factura. La factura debe ser guardada antes de agregar partidas.", + "update_failed": "No se pudo actualizar la partida", + "updated": "Partida actualizada", + "create_failed": "No se pudo crear la partida", + "created": "Partida creada", + "save_error": "Error al guardar", + "saved_to_template": "Partida guardada en plantilla", + "save_invoice_first": "Primero guarda la factura para usar plantillas.", + "use_template_description": "Selecciona una plantilla predefinida para cargar sus partidas.", + "refresh": "Actualizar", + "search_templates_placeholder": "Buscar plantillas...", + "loading": "Cargando...", + "template_applied": "Plantilla aplicada", + "apply_template_error": "Error al aplicar plantilla", + "template_saved": "Plantilla guardada", + "save_template_error": "Error al guardar plantilla", + "title": "Items de la Factura", + "subtitle": "Carga partidas, crea o aplica plantillas sin salir de esta vista.", + "use_template": "Usar plantilla", + "create_template": "Crear plantilla", + "add_items": "Agregar Partidas", + "cancel": "Cancelar", + "applying": "Aplicando...", + "apply_template": "Aplicar Plantilla", + "create_template_dialog_title": "Crear plantilla", + "create_template_dialog_description": "Guarda los elementos actuales como una plantilla reutilizable para inyectar en otras partidas.", + "template_name_label": "Nombre de la Plantilla", + "template_name_placeholder": "Ej. Paquete estándar de refacciones", + "template_description_label": "Descripción", + "template_description_placeholder": "Indica para qué sirve esta plantilla...", + "template_items_count": "items/líneas", + "template_items_title": "Items de la plantilla", + "add_item_line": "Agregar Item/Línea", + "template_table_hash": "#", + "template_table_description": "Descripción", + "template_table_quantity": "Cant.", + "template_table_actions": "Acciones", + "template_empty": "Usa el botón \"Agregar Item/Línea\" para definir el contenido de la plantilla.", + "no_description": "Sin descripción", + "no_description_short": "Sin descripción disponible.", + "no_description_available": "Sin descripción disponible.", + "no_templates_found": "No se encontraron plantillas", + "select_template_to_view": "Selecciona una plantilla para ver sus detalles", + "created_label": "Creada", + "item_description": "Descripción del Item", + "quantity_short": "Cant.", + "quantities": "Cantidades:", + "template_empty_items": "Esta plantilla no contiene items.", + "imported_quantity": "Cant. Importada", + "reference": "Ref:", + "saving": "Guardando...", + "save_template": "Guardar plantilla", + "column_line": "Línea", + "column_impo_invoice": "Factura Impo", + "column_ps": "P/S", + "column_class": "Clase", + "column_part_number": "Número Parte", + "column_description": "Descripción", + "column_has_subitem": "Contiene Subpartida", + "column_main_item": "Partida Principal", + "column_class_description": "Descripción Clase", + "column_um": "U.M.", + "column_preference": "Preferencia", + "column_quantity": "Cantidad", + "column_actions": "Acciones", + "no_items_available": "No hay items disponibles", + "showing_lines": "Mostrando {displayed} de {total} líneas", + "spanish_description_label": "Descripción en español:", + "select_row_to_view_description": "Selecciona una fila para ver la descripción.", + "bultos": "Bultos:", + "imported": "Importada:", + "net_weight": "Peso neto:", + "gross_weight": "Peso bruto:", + "import_values_title": "Valores de importación:", + "dollars": "Dólares:", + "pesos": "Pesos:", + "capture_value": "De Captura:", + "customs_value_short": "Aduana:" + } + }, + "invoice_item_fa": { + "item_sheet": { + "tab_general": "Generales", + "tab_identifiers": "Identificadores", + "not_available_short": "N/D" + }, + "repair": { + "generate_discharge": "Genera Descarga?", + "export_invoice_label": "Factura de Expo", + "export_line_label": "Línea de Expo", + "type_search_label": "Tipo Búsqueda", + "import_type_label": "Tipo Importación:", + "import_invoice_label": "Factura Impo", + "line_label": "Línea", + "loading_line": "Cargando...", + "search_placeholder": "Seleccionar...", + "temporal": "TEM (Temporal)", + "definitive": "DEF (Definitiva)", + "loading_item_data": "Cargando datos de la partida...", + "close": "Cerrar", + "cancel": "Cancelar", + "save": "Guardar", + "select_line_title": "Seleccionar línea", + "import_title": "Partidas de Importación", + "import_description": "Selecciona una línea con saldo disponible para realizar la descarga.", + "loading_invoice_items": "Cargando partidas de la factura...", + "no_balance": "Sin saldo disponible", + "no_balance_description": "No hay líneas con saldo en esta factura para descargar.", + "no_description": "Sin descripción" + }, + "main_data": { + "legend": "Datos principales", + "quantity": "Cantidad", + "unit_cost": "Costo unitario", + "total_value": "Valor total", + "tariff_type": "Tipo arancelario" + }, + "packages": { + "legend": "Bultos", + "quantity": "Cantidad", + "package_code": "Clave bulto", + "weight": "Peso", + "description": "Descripcion", + "weights": "Pesos", + "net": "Neto", + "gross": "Bruto", + "space": "Espacio", + "permit_number": "Num. permiso", + "page_region": "Pag/Region", + "american_fraction": "Fracción US", + "brand": "Marca", + "model": "Modelo", + "purchase_order": "Orden de compra" + }, + "summary": { + "general_data": "DATOS GENERALES", + "return_quantity_subitems": "CANTIDAD DE RETORNO SUBPARTIDAS", + "temporary": "Temporal", + "replacement_or_change": "Reemplazo o cambio", + "definitive": "Definitiva", + "returned_values": "Valores retornados", + "weights_kilos": "PESOS (KILOS)", + "weights_pounds": "PESOS (LIBRAS)", + "net": "Neto", + "gross": "Bruto", + "costs_values": "COSTOS Y VALORES", + "dollars": "(Dolares)", + "pesos": "(Pesos)", + "cost": "Costo", + "value": "Valor", + "customs_value": "Valor aduana", + "capture_cost": "Costo captura", + "capture_value": "Valor captura" + }, + "continuation": { + "tax_paid": "IMPUESTO PAGADO", + "yes": "Si", + "no": "No", + "general_info": "Información General", + "transport_number_type": "Número/Tipo de Transporte:", + "vehicle_data": "Datos Vehículo:", + "is_rail": "Es Ferrocarril?", + "bill_number": "Número BL:", + "guide_count": "Cantidad de Guías de Embarque (BL):", + "destination_origin": "Destino/Origen:", + "destination_origin_placeholder": "FRANJA FRONT.", + "is_mixed": "Es Mixto?", + "entry_port": "Puerto Entrada:", + "export_reason": "Razón de exportación:", + "reason_sold": "Vendido", + "reason_not_sold": "No Vendido", + "reason_other": "Otro", + "payment_terms": "Términos de Pago:", + "handling_fees": "Maniobras (Handlings):", + "reviewed_equipment": "Fue Revisado el Equipo", + "subdivision": "Sub División", + "acts_as_cd": "Funge Como CD", + "pedimento_arrived": "Llegó el Pedimento", + "billing_errors": "Errores de Facturación", + "error_line": "Línea", + "error_key": "Clave", + "error_description": "Descripción", + "no_errors": "Sin errores registrados", + "insert": "Insertar", + "edit": "Editar", + "delete": "Borrar", + "traffic_light": "Semáforo", + "green_mx": "Verde MX", + "green_usa": "Verde USA", + "red_mx": "Rojo MX", + "red_usa": "Rojo USA", + "cfdi_data_title": "DATOS CFDI", + "cfdi_uuid_label": "CFDI UUId:", + "cfdi_pdf_label": "CFDI Path PDF:", + "cfdi_xml_label": "CFDI Path XML:", + "payment_method": "Forma de pago", + "igi_amount": "Monto IGI", + "dollars": "DOLARES", + "igi_payment_method": "Forma de pago IGI", + "has_fda_code": "Tiene clave FDA", + "has_certificate_of_origin": "Tiene certificado de origen?", + "certificate_number": "Num. certificado de origen", + "end_date": "Fecha fin", + "machinery_equipment_location": "Ubicacion de maquinaria y equipo", + "location_variable": "Variable de ubicacion", + "military_equipment_enable": "Habilitar si la partida contiene equipo militar", + "own_equipment": "Equipo propio", + "omit_annex31": "Omitir anexo 31", + "lot": "Lote", + "entry_number": "Num. entrada", + "eighth_rule_permit": "Permiso regla octava", + "eighth_rule_fraction": "Fraccion regla octava", + "line": "Linea", + "consider_a31": "Considerar en A31", + "extra_description_spanish": "Descripcion adicional en espanol" + }, + "configuration": { + "is": "Es", + "item": "Partida", + "subitem": "Subpartida", + "contains_subitems": "Contiene subpartidas", + "yes": "Si", + "main_item_number": "Numero de partida principal", + "main_item_number_placeholder": "Captura numero de partida principal", + "description_spanish": "Descripcion en espanol", + "description_english": "Descripcion en ingles" + }, + "labeling": { + "legend": "Etiquetado y Valoracion", + "label_number": "Numero de etiqueta", + "label_type": "Tipo de etiqueta", + "observations": "Observaciones", + "observations_placeholder": "Observaciones de etiquetado...", + "assets_series": "Activos / Series", + "asset_number_short": "Num. activo", + "actions_short": "Acc.", + "asset_number": "Numero de activo", + "cancel": "Cancelar", + "save": "Guardar" + }, + "identifiers": { + "asset_number": "Numero de activo", + "asset_tag_title": "Etiqueta de activo" + }, + "dialogs": { + "countries_load_error": "Error al cargar paises", + "states_load_error": "Error al cargar estados", + "packages_load_error": "Error al cargar bultos", + "units_load_error": "Error al cargar unidades de medida", + "payment_methods_load_error": "Error al cargar formas de pago" + }, + "invoice_item_inv": { + "edit_title": "Editar Item", + "add_title": "Agregar Nuevo Item", + "edit_description": "Modifica los campos del inventario y guarda los cambios.", + "add_description": "Completa la información del nuevo item de inventario.", + "line_prefix": "Línea", + "required_fields_hint": "Los campos marcados con * son obligatorios.", + "tab_general": "General", + "tab_classification": "Clasificación", + "tab_quantities": "Cantidades", + "tab_other": "Otros", + "invoice_info_title": "Información de la Factura", + "invoice_unsaved_warning": "Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la factura.", + "invoice_id": "ID Factura:", + "operation_type": "Tipo Operación:", + "invoice_number": "Número de Factura:", + "system": "Sistema:", + "class_label": "Clase", + "select_class_placeholder": "Selecciona una clase", + "quantity_label": "Cantidad", + "unit_label": "U.M.", + "select_unit_placeholder": "Selecciona U.M.", + "unit_cost_label": "Costo Unitario", + "country_label": "País de Origen", + "select_country_placeholder": "Selecciona país", + "fraction_label": "Fracción", + "select_fraction_placeholder": "Selecciona fracción", + "tariff_type_label": "Tipo de Tarifa", + "reference_number_label": "Número de Referencia", + "purchase_order_label": "Orden de Compra/Venta", + "warehouse_label": "Almacén", + "location_label": "Ubicación", + "description_es_label": "Descripción (Español)", + "description_es_placeholder": "Descripción en español", + "description_en_label": "Descripción (Inglés)", + "description_en_placeholder": "Description in English", + "sku_label": "SKU", + "sku_placeholder": "Código SKU del producto", + "batch_label": "Lote", + "batch_placeholder": "Número de lote", + "classification_fraction_label": "Fracción Arancelaria", + "fraction_digits_placeholder": "8 dígitos", + "product_type_label": "Tipo de Producto", + "product_type_placeholder": "Materia prima, producto terminado, etc.", + "material_type_label": "Tipo de Material", + "material_type_placeholder": "Metal, plástico, etc.", + "product_code_label": "Código de Producto", + "product_code_placeholder": "Código interno", + "country_origin_label": "País de Origen", + "country_code_placeholder": "Código del país", + "merchandise_category_label": "Categoría de Mercancía", + "merchandise_category_placeholder": "Categoría", + "quantity_tab_label": "Cantidad", + "unit_of_measure_label": "Unidad de Medida", + "unit_of_measure_placeholder": "PZA, KG, M, etc.", + "zero_placeholder": "0", + "decimal_placeholder": "0.00", + "net_weight_label": "Peso Neto (KG)", + "gross_weight_label": "Peso Bruto (KG)", + "unit_cost_usd_label": "Costo Unitario (USD)", + "total_value_label": "Valor Total (USD)", + "packages_label": "Número de Bultos", + "package_type_label": "Tipo de Empaque", + "package_type_placeholder": "Caja, pallet, etc.", + "imported_quantity_label": "Cantidad Importada", + "remaining_quantity_label": "Cantidad Remanente", + "brand_label": "Marca", + "brand_placeholder": "Marca del producto", + "expiration_date_label": "Fecha de Caducidad", + "production_date_label": "Fecha de Producción", + "min_stock_label": "Stock Mínimo", + "max_stock_label": "Stock Máximo", + "observations_label": "Observaciones", + "observations_placeholder": "Notas adicionales sobre el inventario...", + "loading_item_data": "Cargando datos de la partida...", + "loading_more_items": "Cargando más items...", + "invoice_line_info": "Información de la factura ({systemLabel})", + "select_line": "Seleccionar línea", + "import_title": "Partidas de Importación", + "import_description": "Selecciona una línea con saldo disponible para realizar la descarga.", + "loading_invoice_items": "Cargando partidas de la factura...", + "no_balance": "Sin saldo disponible", + "no_balance_description": "No hay líneas con saldo en esta factura para descargar.", + "balance_required": "Línea con saldo disponible", + "cancel": "Cancelar", + "close": "Cerrar", + "saving": "Guardando...", + "update": "Guardar", + "create": "Guardar" + }, + "prerequisites": { + "title": "Aviso", + "message_both": "No hay Agentes aduanales ni Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.", + "message_agents": "No hay Agentes aduanales registrados. Debes darlos de alta para poder trabajar en este módulo.", + "message_clients": "No hay Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.", + "register_hint": "Puedes registrarlos en", + "agents_link": "Agentes Aduanales", + "clients_link": "Clientes y Proveedores", + "and": "y", + "cancel": "Cancelar", + "accept": "Aceptar" + } + } +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 57197776..156ae2e6 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -552,6 +552,43 @@ async function fetchApiFormDataPostNo. Integración
+{item.integration_number || '-'}
+Responsable
+{item.responsible || '-'}
+Patente
+{item.patent || '-'}
+Aduana Despacho
+{item.dispatch_customs || '-'}
+Tipo Operación
+{item.operation_type || '-'}
+Task ID
+{item.task_id || '-'}
+Fecha Alta
++ {item.created_at + ? new Date(item.created_at).toLocaleString('es-MX') + : '-'} +
+{(() => {
+ try { return JSON.stringify(JSON.parse(item.result_json || '{}'), null, 2); }
+ catch { return item.result_json || ''; }
+ })()}
+ {m['sidebar.doda_alta.progress_success']()}
+{m['sidebar.doda_alta.progress_error']()}
+ {#if errorMsg} +{errorMsg}
+ {/if} +{currentStep}
+{progress}%
+ {/if} +Task ID:
+{taskId}
+${number}`
- };
- });
- return renderSnippet(numberSnippet, { number: row.original.id });
+ const n = row.original.id;
+ const s = createRawSnippet(() => ({
+ render: () =>
+ `${n}`
+ }));
+ return renderSnippet(s, {});
}
},
{
- accessorKey: 'created_at',
- header: 'Fecha doda',
+ accessorKey: 'doda_date',
+ header: t('list_col_doda_date'),
+ size: 100,
cell: ({ row }) => {
- const dateSnippet = createRawSnippet<[{ date: string }]>((getProps) => {
- const { date } = getProps();
- return {
- render: () =>
- `${number || 'N/A'}`
- };
- });
- return renderSnippet(numberSnippet, { number: row.original.integration_number });
+ const v = row.original.integration_number;
+ const s = createRawSnippet(() => ({
+ render: () =>
+ v
+ ? `${v}`
+ : `-`
+ }));
+ return renderSnippet(s, {});
}
},
{
accessorKey: 'transaction_number',
- header: 'No transaccion',
- cell: ({ row }) => row.original.transaction_number || 'N/A'
+ header: t('list_col_trans'),
+ cell: ({ row }) => {
+ const v = row.original.transaction_number || '-';
+ const s = createRawSnippet(() => ({
+ render: () =>
+ `${v}`
+ }));
+ return renderSnippet(s, {});
+ }
},
{
accessorKey: 'transport_identification',
- header: 'Id transporte',
- cell: ({ row }) => row.original.transport_identification || 'N/A'
+ header: t('list_col_id_transport'),
+ size: 120,
+ cell: ({ row }) => row.original.transport_identification || '-'
},
{
accessorKey: 'caat',
- header: 'CAAT',
- cell: ({ row }) => row.original.caat || 'N/A'
+ header: t('list_col_caat'),
+ size: 70,
+ cell: ({ row }) => row.original.caat || '-'
},
{
accessorKey: 'last_user',
- header: 'Usuario',
- cell: ({ row }) => row.original.last_user || 'N/A'
+ header: t('list_col_user'),
+ size: 90,
+ cell: ({ row }) => row.original.last_user || '-'
},
{
accessorKey: 'status',
- header: 'Estatus',
+ header: t('list_col_status'),
+ size: 110,
cell: ({ row }) => {
- const status = row.original.status;
- const statusSnippet = createRawSnippet<[{ status?: string | null }]>((getProps) => {
- const { status } = getProps();
- const colorClass = status === 'VALIDADO' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800';
- return {
- render: () =>
- `
- ${status || '-'}
- `
- };
- });
- return renderSnippet(statusSnippet, { status });
+ const status = (row.original.status || '').toUpperCase();
+ const cls = STATUS_CLASSES[status] ?? 'bg-gray-100 text-gray-700 dark:bg-gray-800/50 dark:text-gray-300';
+ const s = createRawSnippet(() => ({
+ render: () =>
+ `${status || '-'}`
+ }));
+ return renderSnippet(s, {});
}
},
{
diff --git a/frontend/src/lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte
deleted file mode 100644
index 5e4a54f1..00000000
--- a/frontend/src/lib/components/dashboard/general_catalogs/doda/create-edit-dialog.svelte
+++ /dev/null
@@ -1,392 +0,0 @@
-
-
-+ {dodaFormT(dodaLoc, 'shortcuts_hint')} +
+{error}
+{warning}
++ {dodaFormT(dodaLoc, 'vu_checking')} +
+ {:else} ++ {#if missingBrokerVu} + {dodaFormT(dodaLoc, 'vu_incomplete')} + {:else} + {dodaFormT(dodaLoc, 'vu_complete')} + {/if} +
+ {/if} + {/if} ++ {dodaFormT(dodaLoc, 'badge_required_hint')} +
+ {/if} ++ {dodaFormT(dodaLoc, 'seals_help')} +
+ {#if selectedContainerIndex == null} ++ {dodaFormT(dodaLoc, 'seals_select_container')} +
+ {:else} + {@const sel = formData.containers?.[selectedContainerIndex]} + {#if !sel?.id} ++ {dodaFormT(dodaLoc, 'container_no_id_warning')} +
+ {:else} ++ {dodaFormT(dodaLoc, 'container_line_info')} + {sel.container_value || dodaFormT(dodaLoc, 'ph_dash')} + + ({dodaFormT(dodaLoc, 'line_word').toLowerCase()} {sel.container_line}) + — {sel.seals_detail?.length ?? 0} + {dodaFormT(dodaLoc, 'seal_on_line')} +
+Catálogos Generales / Doda
-- Validación electrónica ante el SAT -
-