feature/separacion-de-tax-id-de-rfc
This commit is contained in:
@@ -10,6 +10,34 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from .validators import is_valid_rfc, is_valid_tax_id
|
||||
|
||||
# Mensajes de error reutilizados por las validaciones de identificador fiscal
|
||||
_RFC_FORMAT_ERROR = "El RFC no tiene el formato correcto. Ejemplo: XAXX010101000."
|
||||
_TAX_ID_FORMAT_ERROR = (
|
||||
"El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres."
|
||||
)
|
||||
|
||||
|
||||
def _validate_fiscal_format(rfc: Optional[str], tax_id: Optional[str]) -> None:
|
||||
"""
|
||||
Valida el formato de cada identificador fiscal de forma independiente (no excluyente):
|
||||
un registro puede traer RFC y TAX-ID a la vez; cada uno valida solo si tiene valor.
|
||||
"""
|
||||
if (rfc or "").strip() and not is_valid_rfc(rfc):
|
||||
raise ValueError(_RFC_FORMAT_ERROR)
|
||||
if (tax_id or "").strip() and not is_valid_tax_id(tax_id):
|
||||
raise ValueError(_TAX_ID_FORMAT_ERROR)
|
||||
|
||||
|
||||
def _require_fiscal_id_by_procedencia(
|
||||
rfc: Optional[str], tax_id: Optional[str], type_nat_foreign: Optional[str]
|
||||
) -> None:
|
||||
"""Exige el identificador que corresponde a la procedencia: E ⇒ TAX-ID, otro ⇒ RFC."""
|
||||
is_foreign = (type_nat_foreign or "").strip().upper().startswith("E")
|
||||
if is_foreign and not (tax_id or "").strip():
|
||||
raise ValueError("El TAX-ID es obligatorio para registros extranjeros.")
|
||||
if not is_foreign and not (rfc or "").strip():
|
||||
raise ValueError("El RFC es obligatorio para registros nacionales.")
|
||||
|
||||
|
||||
# DTOs para dirección
|
||||
class ClientProviderAddressDTO(BaseModel):
|
||||
@@ -97,7 +125,10 @@ class ClientProviderCreateDTO(BaseModel):
|
||||
)
|
||||
name: Optional[str] = Field(None, max_length=256, description="Name")
|
||||
short_name: Optional[str] = Field(None, max_length=10, description="Short name")
|
||||
rfc: Optional[str] = Field(None, max_length=30, description="RFC")
|
||||
rfc: Optional[str] = Field(None, max_length=30, description="RFC (nacional)")
|
||||
tax_id: Optional[str] = Field(
|
||||
None, max_length=30, description="TAX-ID (identificador fiscal extranjero)"
|
||||
)
|
||||
curp: Optional[str] = Field(None, max_length=19, description="CURP")
|
||||
client_or_provider: Optional[Literal["client", "provider", "both"]] = Field(
|
||||
None, description="Client or provider"
|
||||
@@ -127,20 +158,9 @@ class ClientProviderCreateDTO(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_rfc_or_tax_id_format(self):
|
||||
rfc = self.rfc
|
||||
if not rfc or not (rfc := (rfc or "").strip()):
|
||||
return self
|
||||
proc = (self.type_nat_foreign or "N").strip().upper()[:1]
|
||||
if proc == "E":
|
||||
if not is_valid_tax_id(rfc):
|
||||
raise ValueError(
|
||||
"El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres."
|
||||
)
|
||||
else:
|
||||
if not is_valid_rfc(rfc):
|
||||
raise ValueError(
|
||||
"El RFC no tiene el formato correcto. Ejemplo: XAXX010101000."
|
||||
)
|
||||
# Valida formato de ambos campos y exige el que corresponda a la procedencia
|
||||
_validate_fiscal_format(self.rfc, self.tax_id)
|
||||
_require_fiscal_id_by_procedencia(self.rfc, self.tax_id, self.type_nat_foreign)
|
||||
return self
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -154,7 +174,10 @@ class ClientProviderUpdateDTO(BaseModel):
|
||||
)
|
||||
name: Optional[str] = Field(None, max_length=256, description="Name")
|
||||
short_name: Optional[str] = Field(None, max_length=10, description="Short name")
|
||||
rfc: Optional[str] = Field(None, max_length=30, description="RFC")
|
||||
rfc: Optional[str] = Field(None, max_length=30, description="RFC (nacional)")
|
||||
tax_id: Optional[str] = Field(
|
||||
None, max_length=30, description="TAX-ID (identificador fiscal extranjero)"
|
||||
)
|
||||
curp: Optional[str] = Field(None, max_length=19, description="CURP")
|
||||
client_or_provider: Optional[Literal["client", "provider", "both"]] = Field(
|
||||
None, description="Client or provider"
|
||||
@@ -184,20 +207,8 @@ class ClientProviderUpdateDTO(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_rfc_or_tax_id_format(self):
|
||||
rfc = self.rfc
|
||||
if not rfc or not (rfc := (rfc or "").strip()):
|
||||
return self
|
||||
proc = (self.type_nat_foreign or "N").strip().upper()[:1]
|
||||
if proc == "E":
|
||||
if not is_valid_tax_id(rfc):
|
||||
raise ValueError(
|
||||
"El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres."
|
||||
)
|
||||
else:
|
||||
if not is_valid_rfc(rfc):
|
||||
raise ValueError(
|
||||
"El RFC no tiene el formato correcto. Ejemplo: XAXX010101000."
|
||||
)
|
||||
# Update es parcial (PATCH): solo se valida formato de lo que venga, sin exigir requerido
|
||||
_validate_fiscal_format(self.rfc, self.tax_id)
|
||||
return self
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -211,6 +222,7 @@ class ClientProviderResponseDTO(BaseModel):
|
||||
name: Optional[str] = None
|
||||
short_name: Optional[str] = None
|
||||
rfc: Optional[str] = None
|
||||
tax_id: Optional[str] = None
|
||||
curp: Optional[str] = None
|
||||
client_or_provider: Optional[str] = None
|
||||
linking: Optional[str] = None
|
||||
@@ -239,6 +251,7 @@ class ClientProviderBasicDTO(BaseModel):
|
||||
name: Optional[str] = None
|
||||
short_name: Optional[str] = None
|
||||
rfc: Optional[str] = None
|
||||
tax_id: Optional[str] = None
|
||||
client_or_provider: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
@@ -45,8 +45,10 @@ class ClientProvider(Base, TenantScopedMixin, TimestampMixin):
|
||||
type_nat_foreign: Mapped[Optional[str]] = mapped_column(String(1)) # TIPO NACIONAL/EXTRANJERO
|
||||
name: Mapped[Optional[str]] = mapped_column(String(256))
|
||||
short_name: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
# Identificador fiscal único: RFC (nacional) o TAX-ID (extranjero); no usar programs.tax_id para lo mismo
|
||||
# Identificadores fiscales separados: rfc = RFC mexicano (nacional); tax_id = identificador fiscal extranjero.
|
||||
# La procedencia (type_nat_foreign) rige cuál es obligatorio, pero un registro puede traer ambos.
|
||||
rfc: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
tax_id: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
curp: Mapped[Optional[str]] = mapped_column(String(19))
|
||||
client_or_provider: Mapped[ClientOrProviderEnum] = mapped_column(PgEnum(ClientOrProviderEnum, name="entity_client_or_provider", create_type=True, native_enum=True),nullable=False)
|
||||
linking: Mapped[Optional[str]] = mapped_column(String(1))
|
||||
@@ -66,6 +68,18 @@ class ClientProvider(Base, TenantScopedMixin, TimestampMixin):
|
||||
back_populates="clients_and_providers", uselist=False, cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@property
|
||||
def fiscal_id(self) -> Optional[str]:
|
||||
"""
|
||||
Identificador fiscal efectivo según procedencia: RFC para nacionales,
|
||||
TAX-ID para extranjeros. Punto único de verdad para consumidores downstream
|
||||
(facturas, reportes, transmisiones) que antes leían solo `rfc`.
|
||||
Extranjero ⟺ type_nat_foreign == 'E' (misma convención que DTOs/frontend);
|
||||
cualquier otro valor (N, NULL, vacío) se trata como nacional.
|
||||
"""
|
||||
is_foreign = (self.type_nat_foreign or "").strip().upper().startswith("E")
|
||||
return self.tax_id if is_foreign else self.rfc
|
||||
|
||||
|
||||
class ClientProviderAddress(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
|
||||
@@ -34,7 +34,8 @@ router.include_router(imports_router, prefix="/imports", tags=["clients_and_prov
|
||||
async def get_clients_and_providers(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
name: Optional[str] = Query(None, description="Filter by name (contains)"),
|
||||
rfc: Optional[str] = Query(None, description="Filter by RFC/TAX-ID (contains)"),
|
||||
rfc: Optional[str] = Query(None, description="Filter by RFC (contains)"),
|
||||
tax_id: Optional[str] = Query(None, description="Filter by TAX-ID (contains)"),
|
||||
short_name: Optional[str] = Query(
|
||||
None, description="Filter by short name / clave (exact match, case-insensitive)"
|
||||
),
|
||||
@@ -71,6 +72,9 @@ async def get_clients_and_providers(
|
||||
if rfc:
|
||||
query = query.filter(ClientProvider.rfc.ilike(f"%{rfc.strip()}%"))
|
||||
|
||||
if tax_id:
|
||||
query = query.filter(ClientProvider.tax_id.ilike(f"%{tax_id.strip()}%"))
|
||||
|
||||
if short_name:
|
||||
sn = short_name.strip().upper()
|
||||
query = query.filter(func.upper(ClientProvider.short_name) == sn)
|
||||
|
||||
@@ -56,6 +56,7 @@ class ClientProviderService:
|
||||
ClientProvider.name.ilike(search_pattern),
|
||||
ClientProvider.short_name.ilike(search_pattern),
|
||||
ClientProvider.rfc.ilike(search_pattern),
|
||||
ClientProvider.tax_id.ilike(search_pattern),
|
||||
)
|
||||
)
|
||||
if filters.get("client_or_provider"):
|
||||
@@ -360,6 +361,7 @@ class ClientProviderService:
|
||||
ClientProvider.name.ilike(search_pattern),
|
||||
ClientProvider.short_name.ilike(search_pattern),
|
||||
ClientProvider.rfc.ilike(search_pattern),
|
||||
ClientProvider.tax_id.ilike(search_pattern),
|
||||
ClientProvider.client_id.ilike(search_pattern),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -318,7 +318,8 @@ class FacturaCoveDomainService:
|
||||
|
||||
tipo_nat = (cp.type_nat_foreign or "").strip().upper()
|
||||
tipo_identificador = "0" if tipo_nat == "E" else "1"
|
||||
identificacion = (cp.rfc or "").strip().upper()
|
||||
# Identificador fiscal efectivo: RFC (nacional) o TAX-ID (extranjero)
|
||||
identificacion = (cp.fiscal_id or "").strip().upper()
|
||||
|
||||
raw_country = (addr.country or "") if addr and getattr(addr, "country", None) else ""
|
||||
country_code = self._normalize_country_to_m3(raw_country)
|
||||
|
||||
@@ -1452,8 +1452,9 @@ class InvoiceService:
|
||||
def _get_rfc(client_id):
|
||||
if not client_id:
|
||||
return ''
|
||||
row = db.query(ClientProvider.rfc).filter(ClientProvider.id == client_id).first()
|
||||
return row.rfc if row else ''
|
||||
# Identificador fiscal efectivo: RFC (nacional) o TAX-ID (extranjero)
|
||||
cp = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
|
||||
return (cp.fiscal_id or '') if cp else ''
|
||||
|
||||
provider_rfc = _get_rfc(compliance.provider_id)
|
||||
shipped_to_rfc = _get_rfc(compliance.shipped_to_id)
|
||||
@@ -1738,8 +1739,9 @@ class InvoiceService:
|
||||
def _get_rfc(client_id):
|
||||
if not client_id:
|
||||
return ''
|
||||
row = db.query(ClientProvider.rfc).filter(ClientProvider.id == client_id).first()
|
||||
return row.rfc if row else ''
|
||||
# Identificador fiscal efectivo: RFC (nacional) o TAX-ID (extranjero)
|
||||
cp = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
|
||||
return (cp.fiscal_id or '') if cp else ''
|
||||
|
||||
provider_rfc = _get_rfc(compliance.provider_id)
|
||||
shipped_to_rfc = _get_rfc(compliance.shipped_to_id)
|
||||
@@ -2376,9 +2378,10 @@ class InvoiceService:
|
||||
# Clarion: TIPOEXTNAC N→RFC, E→TAXID truncado a 9 chars
|
||||
if not cp:
|
||||
return ''
|
||||
val = cp.fiscal_id or ''
|
||||
if cp.type_nat_foreign == 'E':
|
||||
return (cp.rfc or '')[:9]
|
||||
return cp.rfc or ''
|
||||
return val[:9]
|
||||
return val
|
||||
|
||||
def _proceso(prov):
|
||||
# Dirección de tráfico según ciudad del proveedor
|
||||
|
||||
@@ -40,32 +40,32 @@ def check_required_max(row: Dict[str, Any], col: str, max_len: int, line_num: in
|
||||
return None
|
||||
|
||||
|
||||
def check_rfc_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
|
||||
"""Col E (RFC): si tiene valor, debe cumplir formato RFC México."""
|
||||
val = (row.get("RFC") or "").strip()
|
||||
def check_rfc_format(row: Dict[str, Any], line_num: int, col: str = "RFC") -> Optional[Dict[str, Any]]:
|
||||
"""Columna RFC: si tiene valor, debe cumplir formato RFC México."""
|
||||
val = (row.get(col) or "").strip()
|
||||
if not val:
|
||||
return None
|
||||
if not RFC_PATTERN.match(val.upper()):
|
||||
return {
|
||||
"line": line_num,
|
||||
"col": "RFC",
|
||||
"col": col,
|
||||
"msg": "El formato del RFC es inválido.",
|
||||
"solution": "Capturar en la columna E un RFC con formato válido (ej. XAXX010101000).",
|
||||
"solution": "Capturar un RFC con formato válido (ej. XAXX010101000).",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def check_tax_id_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
|
||||
"""Col E (TAX-ID): 2 dígitos, guión y resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres."""
|
||||
val = (row.get("RFC") or "").strip()
|
||||
def check_tax_id_format(row: Dict[str, Any], line_num: int, col: str = "TAX_ID") -> Optional[Dict[str, Any]]:
|
||||
"""Columna TAX-ID: 2 dígitos, guión y resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres."""
|
||||
val = (row.get(col) or "").strip()
|
||||
if not val:
|
||||
return None
|
||||
if len(val) > 30 or not TAX_ID_PATTERN.match(val):
|
||||
return {
|
||||
"line": line_num,
|
||||
"col": "RFC",
|
||||
"col": col,
|
||||
"msg": "El formato del TAX-ID es inválido.",
|
||||
"solution": "Capturar en la columna E un TAX-ID con formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.",
|
||||
"solution": "Capturar un TAX-ID con formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ CLIENT_PROVIDER_CSV_COLUMN_MAX_CHARS: Dict[str, int] = {
|
||||
"SHORT_NAME": 10,
|
||||
"NOMBRE": 256,
|
||||
"RFC": 30,
|
||||
"TAX_ID": 30,
|
||||
"DIRECCION": 100,
|
||||
"NUM_EXT": 20,
|
||||
"CODIGO POSTAL": 15,
|
||||
|
||||
@@ -9,6 +9,7 @@ El CSV se alimenta en base a las tablas/modelos:
|
||||
|
||||
Las validaciones (validators) aplican reglas de negocio Clarion y respetan longitudes máximas de los modelos.
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
@@ -20,8 +21,11 @@ from .common_validators import (
|
||||
parse_active,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_LEN = {
|
||||
"rfc": 30,
|
||||
"tax_id": 30,
|
||||
"name": 256,
|
||||
"short_name": 10,
|
||||
"curp": 19,
|
||||
@@ -84,21 +88,37 @@ def row_to_client_provider_data(
|
||||
"""
|
||||
Mapea fila normalizada a datos para ClientProvider, ClientProviderAddress y ClientProviderPrograms.
|
||||
Devuelve (cp_data, address_data_or_none, programs_data_or_none).
|
||||
Identificador fiscal unificado: solo se guarda en ClientProvider.rfc (RFC o TAX-ID según procedencia).
|
||||
Se toma de columna RFC (E); si viene vacía y hay TAX_ID_PROGRAMS (AB), se usa esa para la misma columna rfc.
|
||||
Identificadores fiscales separados: RFC (nacional) → cp.rfc; TAX-ID (extranjero) → cp.tax_id.
|
||||
Compatibilidad: archivos legacy de columna única traían el TAX-ID en la columna RFC; si el registro es
|
||||
extranjero y no hay columna TAX_ID dedicada, ese valor se rutea a tax_id (salvaguarda no silenciosa).
|
||||
"""
|
||||
rfc_col = _str_or_none(row_norm.get("RFC"), MAX_LEN["rfc"])
|
||||
tax_id_programs_col = _str_or_none(row_norm.get("TAX_ID_PROGRAMS"), MAX_LEN["rfc"])
|
||||
# Una sola columna: identificador fiscal en cp.rfc (nacional=RFC, extranjero=TAX-ID)
|
||||
rfc_unified = rfc_col or tax_id_programs_col
|
||||
short_name = _str_or_none(row_norm.get("SHORT_NAME"), MAX_LEN["short_name"])
|
||||
if not rfc_unified and not short_name:
|
||||
return ({}, None, None)
|
||||
|
||||
client_or_provider = parse_client_or_provider(row_norm.get("TIPO")) or ClientOrProviderEnum.BOTH
|
||||
procedencia = _str_or_none(row_norm.get("PROCEDENCIA"), 1)
|
||||
if procedencia:
|
||||
procedencia = procedencia.upper()[:1]
|
||||
is_foreign = procedencia == "E"
|
||||
|
||||
rfc_col = _str_or_none(row_norm.get("RFC"), MAX_LEN["rfc"])
|
||||
tax_id_col = _str_or_none(row_norm.get("TAX_ID"), MAX_LEN["tax_id"])
|
||||
tax_id_programs_col = _str_or_none(row_norm.get("TAX_ID_PROGRAMS"), MAX_LEN["tax_id"])
|
||||
|
||||
rfc_final = rfc_col
|
||||
tax_id_final = tax_id_col
|
||||
if is_foreign and not tax_id_final:
|
||||
# Salvaguarda legacy: extranjero sin columna TAX_ID dedicada → el TAX-ID estaba en RFC (o AB).
|
||||
if rfc_col:
|
||||
tax_id_final = rfc_col
|
||||
rfc_final = None
|
||||
logger.info(
|
||||
"CSV clients_providers: TAX-ID tomado de columna RFC (archivo legacy de columna única)"
|
||||
)
|
||||
elif tax_id_programs_col:
|
||||
tax_id_final = tax_id_programs_col
|
||||
|
||||
short_name = _str_or_none(row_norm.get("SHORT_NAME"), MAX_LEN["short_name"])
|
||||
if not rfc_final and not tax_id_final and not short_name:
|
||||
return ({}, None, None)
|
||||
|
||||
client_or_provider = parse_client_or_provider(row_norm.get("TIPO")) or ClientOrProviderEnum.BOTH
|
||||
|
||||
# Vinculación 0/1/2 → string
|
||||
vinc = (row_norm.get("VINCULACION") or "").strip()
|
||||
@@ -113,7 +133,8 @@ def row_to_client_provider_data(
|
||||
cp_data = {
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"rfc": rfc_unified or None,
|
||||
"rfc": rfc_final or None,
|
||||
"tax_id": tax_id_final or None,
|
||||
"name": _str_or_none(row_norm.get("NOMBRE"), MAX_LEN["name"]),
|
||||
"short_name": short_name,
|
||||
"curp": _str_or_none(row_norm.get("CURP"), MAX_LEN["curp"]),
|
||||
|
||||
@@ -221,8 +221,8 @@ def _do_commit(job_id: str) -> Dict[str, Any]:
|
||||
skipped_details.append(
|
||||
{
|
||||
"line": i,
|
||||
"reason": "Fila sin RFC ni Clave",
|
||||
"solution": "Capturar en el CSV el RFC (columna RFC) o la clave del cliente/proveedor (columna SHORT_NAME).",
|
||||
"reason": "Fila sin RFC/TAX-ID ni Clave",
|
||||
"solution": "Capturar en el CSV el RFC (columna RFC), el TAX-ID (columna TAX_ID) o la clave del cliente/proveedor (columna SHORT_NAME).",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Configuración de plantilla CSV para Clientes y Proveedores (EstructuraCatClienteProv.xls).
|
||||
Layout Clarion: Col A = PROCEDENCIA (E/N), B = TIPO (C/P/A), C = CLAVE (máx 8), D = NOMBRE, E = RFC, F–AG.
|
||||
Solo se leen columnas definidas aquí; el resto se ignora.
|
||||
Solo se leen columnas definidas aquí; el resto se ignora. El parseo es por nombre de cabecera (no posicional).
|
||||
RFC y TAX-ID viven en columnas separadas: "RFC" (nacional) y "TAX_ID" (identificador fiscal extranjero).
|
||||
Compatibilidad: archivos legacy de columna única (TAX-ID en "RFC") se rutean a tax_id según procedencia (ver mappers).
|
||||
Correspondencia Clarion → canonical: A=PROCEDENCIA, B=TIPO, C=SHORT_NAME, D=NOMBRE, E=RFC, F=CALLES(DIRECCION),
|
||||
G=NUM_EXT, H=CODIGO POSTAL, I=COLONIA, J=CIUDAD, K=ESTADO, L=PAIS, M=TELEFONO, N=FAX, O=EMAIL, P=CURP,
|
||||
Q=TIPO_PROGRAMA_SECON, R=NUM_PROGRAMA_SECON, S=FECHA_AUT_SECON, T=ES_PROSEC, U=NUM_AUT_PROSEC, V=VINCULACION,
|
||||
@@ -38,8 +40,10 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
{"canonical": "SHORT_NAME", "aliases": ["CLAVE", "CLAVE CORTA", "NOMBRE CORTO", "SIGLAS"]},
|
||||
# Col D - Nombre
|
||||
{"canonical": "NOMBRE", "aliases": ["RAZON SOCIAL", "NAME", "RAZON SOCIAL O NOMBRE"]},
|
||||
# Col E - RFC
|
||||
{"canonical": "RFC", "aliases": ["TAX_ID", "TAXID", "IDENTIFICADOR FISCAL", "IDENTIFICACION FISCAL"]},
|
||||
# Col E - RFC (solo RFC mexicano / nacional)
|
||||
{"canonical": "RFC", "aliases": ["RFC NACIONAL"]},
|
||||
# Col E2 - TAX-ID dedicado (identificador fiscal extranjero), separado del RFC
|
||||
{"canonical": "TAX_ID", "aliases": ["TAXID", "TAX-ID", "IDENTIFICADOR FISCAL", "IDENTIFICACION FISCAL"]},
|
||||
# Col F - Calles
|
||||
{"canonical": "DIRECCION", "aliases": ["CALLES", "DOMICILIO", "DIRECCION FISCAL", "CALLE"]},
|
||||
# Col G - Número exterior
|
||||
@@ -84,8 +88,8 @@ TEMPLATE_COLUMNS: Dict[str, List[Dict[str, Any]]] = {
|
||||
{"canonical": "CONTACTO", "aliases": ["CONTACT", "PERSONA CONTACTO"]},
|
||||
# Col AA - Manufacturer ID
|
||||
{"canonical": "MANUFACTURER_ID", "aliases": ["MANUFACTURERID", "MANUFACTURER ID"]},
|
||||
# Col AB - Tax ID (programas)
|
||||
{"canonical": "TAX_ID_PROGRAMS", "aliases": ["TAX ID", "TAXID PROGRAMS"]},
|
||||
# Col AB - Tax ID (programas). Alias "TAX ID" se quitó para no colisionar con la columna TAX_ID dedicada.
|
||||
{"canonical": "TAX_ID_PROGRAMS", "aliases": ["TAXID PROGRAMS"]},
|
||||
# Col AC - Broker exportación
|
||||
{"canonical": "BROKER_EXPO", "aliases": ["BROKER EXPO", "BROKER EXPORTACION"]},
|
||||
# Col AD - Broker importación
|
||||
|
||||
@@ -149,32 +149,40 @@ def validate_row_client_provider(
|
||||
else:
|
||||
errors.extend(valida_toda_cliente_o_prov(row, line_num, actualizar=actualizar))
|
||||
|
||||
# Identificador fiscal requerido: Nacional = RFC, Extranjero = TAX-ID (mismo campo "RFC" en layout).
|
||||
# Identificador fiscal requerido y validado por procedencia:
|
||||
# Nacional (N) -> RFC en columna RFC
|
||||
# Extranjero (E) -> TAX-ID en columna TAX_ID (compat: archivos legacy lo traían en columna RFC)
|
||||
procedencia = (row.get("PROCEDENCIA") or "").strip().upper()[:1]
|
||||
|
||||
err = check_required_max(row, "RFC", RFC_MAX, line_num)
|
||||
if err:
|
||||
if procedencia == "E":
|
||||
err = {
|
||||
if procedencia == "E":
|
||||
# Columna efectiva: TAX_ID dedicada o, en su defecto, RFC (archivo legacy de columna única).
|
||||
effective_col = "TAX_ID" if (row.get("TAX_ID") or "").strip() else "RFC"
|
||||
if not (row.get(effective_col) or "").strip():
|
||||
errors.append({
|
||||
"line": line_num,
|
||||
"col": "RFC",
|
||||
"col": "TAX_ID",
|
||||
"msg": "Requerido (TAX-ID)",
|
||||
"solution": "Capturar el TAX-ID del cliente/proveedor extranjero en la columna E (RFC/TAX-ID).",
|
||||
}
|
||||
errors.append(err)
|
||||
"solution": "Capturar el TAX-ID del cliente/proveedor extranjero en la columna TAX_ID.",
|
||||
})
|
||||
else:
|
||||
err = check_tax_id_format(row, line_num, col=effective_col)
|
||||
if err:
|
||||
errors.append(err)
|
||||
else:
|
||||
# Validar formato según procedencia: RFC (N) o TAX-ID (E).
|
||||
if procedencia == "N":
|
||||
err = check_rfc_format(row, line_num)
|
||||
elif procedencia == "E":
|
||||
err = check_tax_id_format(row, line_num)
|
||||
# Nacional: RFC requerido + formato
|
||||
err = check_required_max(row, "RFC", RFC_MAX, line_num)
|
||||
if err:
|
||||
errors.append(err)
|
||||
else:
|
||||
err = check_rfc_format(row, line_num)
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
for err in [
|
||||
check_max_length(row, "NOMBRE", NAME_MAX, line_num),
|
||||
check_max_length(row, "SHORT_NAME", 10, line_num), # modelo permite 10
|
||||
check_max_length(row, "CURP", CURP_MAX, line_num),
|
||||
check_max_length(row, "TAX_ID", RFC_MAX, line_num),
|
||||
]:
|
||||
if err:
|
||||
errors.append(err)
|
||||
|
||||
@@ -136,8 +136,8 @@ class AvisoConsolidadoExportacionService:
|
||||
if client_obj:
|
||||
# Fetch Address
|
||||
c_addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == target_client_id).first()
|
||||
# Identificador fiscal único en ClientProvider.rfc (RFC o TAX-ID)
|
||||
c_rfc = getattr(client_obj, "rfc", "") or ""
|
||||
# Identificador fiscal efectivo: RFC (nacional) o TAX-ID (extranjero)
|
||||
c_rfc = getattr(client_obj, "fiscal_id", "") or ""
|
||||
|
||||
c_dir_str = "DOMICILIO NO REGISTRADO"
|
||||
if c_addr:
|
||||
|
||||
@@ -31,17 +31,8 @@ class ScaiiProcessor:
|
||||
else:
|
||||
pais = pais_raw[:2]
|
||||
|
||||
tax_id = ""
|
||||
if pais == "MX":
|
||||
tax_id = cliente.rfc or ""
|
||||
else:
|
||||
# Try generic tax_id field if exists, else generic field or RFC as fallback
|
||||
# Providing a fallback to extra_information or web_key if needed, but per model inspection:
|
||||
# We don't see a specific 'tax_id' field in ClientProvider model snippet.
|
||||
# We see 'rfc'. Let's use RFC as generic holder or look for 'tax_id' if I missed it.
|
||||
# Re-reading model: rfc is the only obvious one.
|
||||
# Let's use RFC field for foreign tax id too unless instructed otherwise.
|
||||
tax_id = cliente.rfc or ""
|
||||
# Identificador fiscal del cliente: RFC (nacional) o TAX-ID (extranjero)
|
||||
tax_id = cliente.fiscal_id or ""
|
||||
|
||||
data = {
|
||||
"nombre": (cliente.name or "")[:39],
|
||||
@@ -123,10 +114,10 @@ class ScaiiProcessor:
|
||||
if factura.compliance_mx:
|
||||
if factura.compliance_mx.provider_id:
|
||||
s_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first()
|
||||
if s_obj: s_rfc = s_obj.rfc or ""
|
||||
if s_obj: s_rfc = s_obj.fiscal_id or ""
|
||||
if factura.compliance_mx.sold_to_id:
|
||||
c_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first()
|
||||
if c_obj: c_rfc = c_obj.rfc or ""
|
||||
if c_obj: c_rfc = c_obj.fiscal_id or ""
|
||||
|
||||
lineas.append(
|
||||
f"IV01{factura.invoice_number[:15]:<15}"
|
||||
|
||||
@@ -128,7 +128,7 @@ class ConsolidadoImportacionMexService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "MEX") if addr else "MEX",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=(
|
||||
|
||||
@@ -82,7 +82,7 @@ class ConsolidadoImportacionMexService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "MEX") if addr else "MEX",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "",
|
||||
|
||||
@@ -162,7 +162,7 @@ class FacturaImportacionMexService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "MEX") if addr else "MEX",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=(
|
||||
|
||||
@@ -82,7 +82,7 @@ class FacturaImportacionMexService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "MEX") if addr else "MEX",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "",
|
||||
|
||||
@@ -156,7 +156,7 @@ class FacturaImportacionUsaService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "USA") if addr else "USA",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=(
|
||||
|
||||
@@ -103,7 +103,7 @@ class PackingListService:
|
||||
ciudad=(addr.city or "") if addr else "",
|
||||
estado=(addr.state or "") if addr else "",
|
||||
pais=(addr.country or "MEX") if addr else "MEX",
|
||||
tax_id=getattr(main, "rfc", "") or "",
|
||||
tax_id=getattr(main, "fiscal_id", "") or "",
|
||||
programa="IMMEX" if (prog and prog.program) else "",
|
||||
autorizacion=prog.program_number if prog else "",
|
||||
prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "",
|
||||
|
||||
@@ -31,8 +31,9 @@ class ScaiiProcessor:
|
||||
pais = "US"
|
||||
else:
|
||||
pais = pais_raw[:2]
|
||||
|
||||
tax_id = cliente.rfc or ""
|
||||
|
||||
# Identificador fiscal del cliente: RFC (nacional) o TAX-ID (extranjero)
|
||||
tax_id = cliente.fiscal_id or ""
|
||||
|
||||
data = {
|
||||
"nombre": (cliente.name or "")[:39],
|
||||
@@ -161,10 +162,10 @@ class ScafDefProcessor(ScaiiProcessor):
|
||||
if factura.compliance_mx:
|
||||
if factura.compliance_mx.sold_to_id:
|
||||
c_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first()
|
||||
if c_obj: c_tax = (c_obj.rfc or "")[:12]
|
||||
if c_obj: c_tax = (c_obj.fiscal_id or "")[:12]
|
||||
if factura.compliance_mx.provider_id:
|
||||
s_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first()
|
||||
if s_obj: s_tax = (s_obj.rfc or "")[:12]
|
||||
if s_obj: s_tax = (s_obj.fiscal_id or "")[:12]
|
||||
|
||||
# IV01 uses 11 spaces then 'C' per Clarion logic
|
||||
# Adjusted validation for RFCs to avoid crashes or None
|
||||
|
||||
@@ -31,8 +31,9 @@ class ScaiiProcessor:
|
||||
pais = "US"
|
||||
else:
|
||||
pais = pais_raw[:2]
|
||||
|
||||
tax_id = cliente.rfc or ""
|
||||
|
||||
# Identificador fiscal del cliente: RFC (nacional) o TAX-ID (extranjero)
|
||||
tax_id = cliente.fiscal_id or ""
|
||||
|
||||
data = {
|
||||
"nombre": (cliente.name or "")[:39],
|
||||
@@ -178,10 +179,10 @@ class ScafTempProcessor(ScaiiProcessor):
|
||||
if factura.compliance_mx:
|
||||
if factura.compliance_mx.sold_to_id:
|
||||
c_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first()
|
||||
if c_obj: c_tax = c_obj.rfc[:12] if c_obj.rfc else ""
|
||||
if c_obj: c_tax = c_obj.fiscal_id[:12] if c_obj.fiscal_id else ""
|
||||
if factura.compliance_mx.provider_id:
|
||||
s_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first()
|
||||
if s_obj: s_tax = s_obj.rfc[:12] if s_obj.rfc else ""
|
||||
if s_obj: s_tax = s_obj.fiscal_id[:12] if s_obj.fiscal_id else ""
|
||||
|
||||
lineas.append(
|
||||
f"IV01{factura.invoice_number[:15]:<15}{f_fecha}{entry_port:<5}{' ':<11}C"
|
||||
|
||||
@@ -113,19 +113,19 @@ class DatabaseHelper:
|
||||
|
||||
try:
|
||||
sql = text("""
|
||||
SELECT cp.name, cp.rfc
|
||||
SELECT cp.name, cp.rfc, cp.tax_id
|
||||
FROM a76.clients_and_providers cp
|
||||
WHERE cp.id = :client_code
|
||||
AND (cp.client_or_provider = :client_type OR cp.client_or_provider = 'both')
|
||||
""")
|
||||
result = db.execute(sql, {"client_code": client_code, "client_type": client_type}).fetchone()
|
||||
|
||||
|
||||
if result:
|
||||
# Unified identifier: cp.rfc contains either RFC (national) or TAX-ID (foreign)
|
||||
# Identificadores separados: rfc (nacional) y tax_id (extranjero), cada uno en su columna
|
||||
return {
|
||||
"name": result[0],
|
||||
"rfc": result[1],
|
||||
"tax_id": result[1],
|
||||
"tax_id": result[2],
|
||||
}
|
||||
else:
|
||||
logger.debug(f"Client {client_code} not found as {client_type}")
|
||||
|
||||
Reference in New Issue
Block a user