feature/separacion-de-tax-id-de-rfc

This commit is contained in:
2026-06-02 09:05:29 -06:00
parent 08f99f102e
commit 619ef8d7d8
32 changed files with 358 additions and 163 deletions

View File

@@ -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

View File

@@ -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):
"""

View File

@@ -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)

View File

@@ -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),
)
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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"]),

View File

@@ -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

View File

@@ -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, FAG.
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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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}"

View File

@@ -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=(

View File

@@ -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 "",

View File

@@ -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=(

View File

@@ -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 "",

View File

@@ -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=(

View File

@@ -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 "",

View File

@@ -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

View File

@@ -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"

View File

@@ -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}")