Merge pull request 'feature/client-provider-taxid-rfc-fix' (#218) from feature/client-provider-taxid-rfc-fix into development

Reviewed-on: ADUANASOFT/anexo76#218
This commit is contained in:
2026-03-18 03:51:43 +00:00
24 changed files with 519 additions and 233 deletions

View File

@@ -6,7 +6,9 @@ Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
from decimal import Decimal
from typing import List, Literal, Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, model_validator
from .validators import is_valid_rfc, is_valid_tax_id
# DTOs para dirección
@@ -54,7 +56,6 @@ class ClientProviderProgramsDTO(BaseModel):
manufacturer_id: Optional[str] = Field(
None, max_length=25, description="Manufacturer ID"
)
tax_id: Optional[str] = Field(None, max_length=30, description="Tax ID")
broker: Optional[str] = Field(None, max_length=6, description="Broker")
import_broker: Optional[str] = Field(
None, max_length=6, description="Import broker"
@@ -116,7 +117,6 @@ class ClientProviderCreateDTO(BaseModel):
)
position: Optional[str] = Field(None, max_length=30, description="Position")
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
is_national_provider: Optional[bool] = None
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
@@ -127,6 +127,24 @@ class ClientProviderCreateDTO(BaseModel):
None, description="Programs information"
)
@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."
)
return self
class Config:
from_attributes = True
@@ -157,7 +175,6 @@ class ClientProviderUpdateDTO(BaseModel):
)
position: Optional[str] = Field(None, max_length=30, description="Position")
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
is_national_provider: Optional[bool] = None
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
# Nested DTOs
@@ -168,6 +185,24 @@ class ClientProviderUpdateDTO(BaseModel):
None, description="Programs information"
)
@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."
)
return self
class Config:
from_attributes = True
@@ -189,7 +224,6 @@ class ClientProviderResponseDTO(BaseModel):
responsible: Optional[str] = None
position: Optional[str] = None
incoterm: Optional[str] = None
is_national_provider: Optional[bool] = None
is_active: Optional[bool] = None
tenant_id: int
company_id: int

View File

@@ -45,6 +45,7 @@ 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
rfc: 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)
@@ -55,7 +56,6 @@ class ClientProvider(Base, TenantScopedMixin, TimestampMixin):
responsible: Mapped[Optional[str]] = mapped_column(String(80))
position: Mapped[Optional[str]] = mapped_column(String(30))
incoterm: Mapped[Optional[str]] = mapped_column(String(19))
is_national_provider: Mapped[Optional[bool]] = mapped_column(Boolean)
is_active: Mapped[Optional[bool]] = mapped_column(Boolean)
# Relationships
@@ -140,7 +140,6 @@ class ClientProviderPrograms(Base, TenantScopedMixin, TimestampMixin):
prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20))
secon_auth_date: Mapped[Optional[int]] = mapped_column(Integer)
manufacturer_id: Mapped[Optional[str]] = mapped_column(String(25))
tax_id: Mapped[Optional[str]] = mapped_column(String(30))
broker: Mapped[Optional[str]] = mapped_column(String(6))
import_broker: Mapped[Optional[str]] = mapped_column(String(6))
transfer_key: Mapped[Optional[str]] = mapped_column(String(8))

View File

@@ -0,0 +1,27 @@
"""
Validadores de formato para RFC (Nacional) y TAX-ID (Extranjero) en clientes y proveedores.
"""
import re
# Formato RFC México: 3-4 letras (A-Z, &, Ñ), 6 dígitos (fecha), 3 caracteres homoclave. Ej: XAXX010101000
RFC_PATTERN = re.compile(r"^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$", re.IGNORECASE)
# TAX-ID extranjero: 2 dígitos, guión, resto alfanumérico. Ej: 12-3456789 (EIN US). Total máx 30.
TAX_ID_PATTERN = re.compile(r"^\d{2}-[A-Z0-9]{1,27}$", re.IGNORECASE)
def is_valid_rfc(value: str) -> bool:
"""Valida formato RFC mexicano. Acepta cadena vacía/None como inválida (no opcional aquí)."""
if not value or not isinstance(value, str):
return False
normalized = value.strip().upper()
return bool(normalized and RFC_PATTERN.match(normalized))
def is_valid_tax_id(value: str) -> bool:
"""Valida formato TAX-ID (extranjero): 2 dígitos, guión y resto alfanumérico. Ej: 12-3456789."""
if not value or not isinstance(value, str):
return False
normalized = value.strip()
return bool(normalized and len(normalized) <= 30 and TAX_ID_PATTERN.match(normalized))

View File

@@ -12,6 +12,22 @@ from ..common.cell_value import cell_to_str
# Valores que indican que la primera fila es cabecera (primera columna normalizada)
FIRST_COLUMN_HEADER_VALUES = ("CLAVE CLASE", "CLASE")
_ENCODING_FALLBACKS: Tuple[str, ...] = ("utf-8-sig", "utf-8", "cp1252", "latin-1")
def _read_text_sample(file_path: str, sample_bytes: int = 2048) -> str:
with open(file_path, "rb") as f:
raw = f.read(sample_bytes)
last_err: Optional[Exception] = None
for enc in _ENCODING_FALLBACKS:
try:
return raw.decode(enc)
except Exception as e:
last_err = e
if last_err:
raise last_err
return ""
def detect_headers_or_data(
file_path: str,
@@ -26,8 +42,15 @@ def detect_headers_or_data(
- Si no -> has_header=False, fieldnames=TEMPLATE_DOWNLOAD_HEADERS (la primera fila es dato).
"""
try:
with open(file_path, "r", encoding=encoding) as f:
sample = f.read(2048)
# `encoding` se mantiene por compatibilidad; si falla, hacemos fallback para CSVs tipo Excel (cp1252/latin-1).
if encoding and encoding.lower() not in ("auto", "detect"):
try:
with open(file_path, "r", encoding=encoding) as f:
sample = f.read(2048)
except Exception:
sample = _read_text_sample(file_path, sample_bytes=2048)
else:
sample = _read_text_sample(file_path, sample_bytes=2048)
except Exception:
return None, True
lines = sample.splitlines()

View File

@@ -3,11 +3,16 @@ Validadores reutilizables para import CSV de clientes y proveedores.
Paridad Clarion: procedencia E/N, tipo C/P/A, clave máx 8, SECON, Prosec, Vinculación,
Es Empresa Certificada, Transformador/SubMaq, desfase.
"""
import re
from typing import Dict, Any, Optional
from api.v1.modules.a76.clients_and_providers.models import ClientOrProviderEnum
RFC_MAX = 30
# Formato RFC México: 3-4 letras, 6 dígitos, 3 homoclave. TAX-ID: 2 dígitos, guión, resto. Ej: 12-3456789.
RFC_PATTERN = re.compile(r"^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$", re.IGNORECASE)
TAX_ID_PATTERN = re.compile(r"^\d{2}-[A-Z0-9]{1,27}$", re.IGNORECASE)
NAME_MAX = 256
SHORT_NAME_MAX = 10
# Clarion: Col C máx 8 caracteres
@@ -29,6 +34,36 @@ 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()
if not val:
return None
if not RFC_PATTERN.match(val.upper()):
return {
"line": line_num,
"col": "RFC",
"msg": "El formato del RFC es inválido.",
"solution": "Capturar en la columna E 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()
if not val:
return None
if len(val) > 30 or not TAX_ID_PATTERN.match(val):
return {
"line": line_num,
"col": "RFC",
"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.",
}
return None
def check_max_length(row: Dict[str, Any], col: str, max_len: int, line_num: int) -> Optional[Dict[str, Any]]:
val = (row.get(col) or "").strip()
if not val:

View File

@@ -43,7 +43,6 @@ MAX_LEN = {
"prosec_authorization": 20,
"secon_authorization": 20,
"manufacturer_id": 25,
"tax_id_programs": 30,
"broker": 6,
"import_broker": 6,
"transfer_key": 8,
@@ -85,11 +84,15 @@ 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).
Para compatibilidad con Clarion: se requiere RFC o SHORT_NAME para considerar la fila válida.
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.
"""
rfc = _str_or_none(row_norm.get("RFC"), MAX_LEN["rfc"])
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 and not 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
@@ -110,7 +113,7 @@ def row_to_client_provider_data(
cp_data = {
"tenant_id": tenant_id,
"company_id": company_id,
"rfc": rfc or None,
"rfc": rfc_unified 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"]),
@@ -124,7 +127,6 @@ def row_to_client_provider_data(
"position": _str_or_none(row_norm.get("POSICION"), MAX_LEN["position"]),
"incoterm": _str_or_none(row_norm.get("INCOTERM"), MAX_LEN["incoterm"]),
"is_active": parse_active(row_norm.get("ACTIVO")),
"is_national_provider": True if procedencia == "N" else (False if procedencia == "E" else None),
}
# Address
@@ -174,12 +176,12 @@ def row_to_client_provider_data(
tipo_prog or num_prog or fecha_secon is not None or prosec_val or num_aut_prosec
or is_certified or reg_cert
or _str_or_none(row_norm.get("MANUFACTURER_ID"))
or _str_or_none(row_norm.get("TAX_ID_PROGRAMS"))
or _str_or_none(row_norm.get("BROKER_EXPO"))
or _str_or_none(row_norm.get("BROKER_IMPO"))
or _str_or_none(row_norm.get("CLAVE_TRANSFER"))
or applied_proportion is not None
):
# Identificador fiscal solo en ClientProvider.rfc (columna única)
programs_data = {
"program": tipo_prog[:7] if tipo_prog else None,
"program_number": num_prog,
@@ -190,7 +192,6 @@ def row_to_client_provider_data(
"is_certified_company": is_certified,
"certified_company_registry": reg_cert,
"manufacturer_id": _str_or_none(row_norm.get("MANUFACTURER_ID"), MAX_LEN["manufacturer_id"]),
"tax_id": _str_or_none(row_norm.get("TAX_ID_PROGRAMS"), MAX_LEN["tax_id_programs"]),
"broker": _str_or_none(row_norm.get("BROKER_EXPO"), MAX_LEN["broker"]),
"import_broker": _str_or_none(row_norm.get("BROKER_IMPO"), MAX_LEN["import_broker"]),
"transfer_key": _str_or_none(row_norm.get("CLAVE_TRANSFER"), MAX_LEN["transfer_key"]),

View File

@@ -13,6 +13,8 @@ from ..common.common_validators import (
CURP_MAX,
check_required_max,
check_max_length,
check_rfc_format,
check_tax_id_format,
check_tipo_client_provider,
check_procedencia,
check_short_name_max_clarion,
@@ -161,8 +163,23 @@ def validate_row_client_provider(
if err:
return err
# Compatibilidad: RFC requerido y longitudes (como antes)
# Identificador fiscal requerido: Nacional = RFC, Extranjero = TAX-ID (mismo campo "RFC" en layout).
procedencia = (row.get("PROCEDENCIA") or "").strip().upper()[:1]
err = check_required_max(row, "RFC", RFC_MAX, line_num)
if err:
if procedencia == "E":
err = {
"line": line_num,
"col": "RFC",
"msg": "Requerido (TAX-ID)",
"solution": "Capturar el TAX-ID del cliente/proveedor extranjero en la columna E (RFC/TAX-ID).",
}
return err
# 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)
if err:
return err
err = check_max_length(row, "NOMBRE", NAME_MAX, line_num)

View File

@@ -5,7 +5,7 @@ Si se pasa headerless_first_cell_values, se detecta si la primera fila es cabece
"""
import csv
import io
from typing import Iterator, Tuple, Dict, Any, Optional, List, Set
from typing import Iterator, Tuple, Dict, Any, Optional, List, Set, Sequence
def _normalize_empty_headers(headers: List[str]) -> List[str]:
@@ -21,6 +21,32 @@ def _normalize_empty_headers(headers: List[str]) -> List[str]:
return result
_ENCODING_FALLBACKS: Sequence[str] = ("utf-8-sig", "utf-8", "cp1252", "latin-1")
def _detect_text_encoding(
file_path: str,
encodings: Sequence[str] = _ENCODING_FALLBACKS,
sample_bytes: int = 8192,
) -> str:
"""
Detecta encoding por prueba de decodificación en un sample en binario.
Nota: latin-1 decodifica cualquier byte; por eso debe ir al final.
"""
with open(file_path, "rb") as f:
raw = f.read(sample_bytes)
last_err: Optional[Exception] = None
for enc in encodings:
try:
raw.decode(enc)
return enc
except Exception as e:
last_err = e
if last_err:
raise last_err
return "utf-8-sig"
def iter_csv_rows(
file_path: str,
fieldnames: Optional[List[str]] = None,
@@ -34,7 +60,8 @@ def iter_csv_rows(
(quitando BOM, strip, upper) está en headerless_first_cell_values, se trata como dato y se usan fieldnames.
headerless_second_cell_key_pattern se ignora si no se usa (reservado para otros layouts).
"""
with open(file_path, "r", encoding="utf-8-sig") as f:
encoding = _detect_text_encoding(file_path)
with open(file_path, "r", encoding=encoding) as f:
sample = f.read(2048)
f.seek(0)
try:
@@ -91,6 +118,7 @@ def iter_csv_rows(
def count_csv_rows(file_path: str, has_header: bool = True) -> int:
"""Cuenta filas del CSV. Si has_header=True (por defecto), no cuenta la cabecera."""
with open(file_path, "r", encoding="utf-8-sig") as f:
encoding = _detect_text_encoding(file_path)
with open(file_path, "r", encoding=encoding) as f:
total_lines = sum(1 for _ in f)
return total_lines if not has_header else max(0, total_lines - 1)

View File

@@ -136,12 +136,8 @@ class AvisoConsolidadoExportacionService:
if client_obj:
# Fetch Address
c_addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == target_client_id).first()
# Fetch Fiscal Data (RFC)
c_prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == target_client_id).first()
c_rfc = ""
if c_prog and c_prog.tax_id: c_rfc = c_prog.tax_id
elif hasattr(client_obj, 'rfc'): c_rfc = client_obj.rfc
# Identificador fiscal único en ClientProvider.rfc (RFC o TAX-ID)
c_rfc = getattr(client_obj, "rfc", "") or ""
c_dir_str = "DOMICILIO NO REGISTRADO"
if c_addr:

View File

@@ -127,11 +127,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=(
prog.tax_id
if (prog and prog.tax_id)
else (getattr(main, "rfc", "") or "")
),
tax_id=getattr(main, "rfc", "") 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=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""),
tax_id=getattr(main, "rfc", "") 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

@@ -159,11 +159,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=(
prog.tax_id
if (prog and prog.tax_id)
else (getattr(main, "rfc", "") or "")
),
tax_id=getattr(main, "rfc", "") 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=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""),
tax_id=getattr(main, "rfc", "") 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

@@ -153,11 +153,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=(
prog.tax_id
if (prog and prog.tax_id)
else (getattr(main, "rfc", "") or "")
),
tax_id=getattr(main, "rfc", "") 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=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""),
tax_id=getattr(main, "rfc", "") 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

@@ -108,22 +108,24 @@ class DatabaseHelper:
if not client_code:
return {"name": None, "rfc": None, "tax_id": None}
client_type = 'PROVIDER' if is_supplier else 'CLIENT'
# In our schema, enum values are lowercase: 'client', 'provider', 'both'
client_type = 'provider' if is_supplier else 'client'
try:
sql = text("""
SELECT cp.name, cp.rfc, cpp.tax_id
SELECT cp.name, cp.rfc
FROM a76.clients_and_providers cp
LEFT JOIN a76.clients_and_providers_programs cpp ON cpp.client_id = cp.id
WHERE cp.id = :client_code AND cp.client_or_provider = :client_type
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)
return {
"name": result[0],
"rfc": result[1],
"tax_id": result[2]
"tax_id": result[1],
}
else:
logger.debug(f"Client {client_code} not found as {client_type}")

View File

@@ -25,7 +25,6 @@ export interface ClientProviderPrograms {
secon_auth_date?: number | null; // YYYYMMDD
prosec?: number | null;
manufacturer_id?: string | null;
tax_id?: string | null;
ctpat_svi?: string | null;
is_certified_company?: string | null;
}
@@ -45,8 +44,6 @@ export interface ClientProvider {
responsible?: string | null;
position?: string | null;
// Booleanos (Coincidiendo con la BD)
is_national_provider?: boolean | null;
is_active?: boolean;
// Relaciones Anidadas

View File

@@ -17,7 +17,7 @@ export function createColumns(onSuccess) {
},
{
accessorKey: "rfc",
header: "RFC",
header: "RFC / TAX-ID",
cell: ({ row }) => {
const snippet = createRawSnippet((getData) => {
const { val } = getData();

View File

@@ -21,21 +21,26 @@
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
// Address fields
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
// Programs fields
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
// Programs fields (subset)
program: "",
program_number: "",
manufacturer_id: ""
});
let loading = $state(false);
@@ -48,43 +53,59 @@
rfc: item.rfc,
name: item.name,
curp: item.curp || "",
residence_country: item.residence_country || "",
domicile_fiscal: item.domicile_fiscal || "",
foreign_tax_id: item.foreign_tax_id || "",
type_nat_foreign: item.type_nat_foreign || "N",
client_or_provider: item.client_or_provider || "client",
is_active: item.is_active ?? true,
street: item.address?.street || "",
streets: item.address?.streets || "",
exterior_number: item.address?.exterior_number || "",
interior_number: item.address?.interior_number || "",
neighborhood: item.address?.neighborhood || "",
municipality: item.address?.municipality || "",
city: item.address?.city || "",
state: item.address?.state || "",
country: item.address?.country || "",
zip_code: item.address?.zip_code || "",
program_code: item.programs?.program_code || "",
authorization_date: item.programs?.authorization_date || ""
postal_code: item.address?.postal_code || "",
email: item.address?.email || "",
phone: item.address?.phone || "",
contact: item.address?.contact || "",
program: item.programs?.program || "",
program_number: item.programs?.program_number || "",
manufacturer_id: item.programs?.manufacturer_id || ""
};
} else {
formData = {
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
program: "",
program_number: "",
manufacturer_id: ""
};
}
});
const isEditing = $derived(!!item);
const isForeign = $derived((formData.type_nat_foreign || "N").toUpperCase() === "E");
// Validación de formato: RFC (Nacional) o TAX-ID (Extranjero)
const RFC_REGEX = /^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i;
// TAX-ID: 2 dígitos, guión, resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres.
const TAX_ID_REGEX = /^\d{2}-[A-Z0-9]{1,27}$/i;
async function handleSubmit(e: Event) {
e.preventDefault();
@@ -94,6 +115,19 @@
return;
}
const rfcVal = (formData.rfc || "").trim();
if (isForeign) {
if (!TAX_ID_REGEX.test(rfcVal)) {
error = "El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.";
return;
}
} else {
if (!RFC_REGEX.test(rfcVal)) {
error = "El RFC no tiene el formato correcto. Ejemplo: XAXX010101000.";
return;
}
}
loading = true;
error = null;
@@ -104,23 +138,27 @@
rfc: formData.rfc,
name: formData.name,
curp: formData.curp || null,
residence_country: formData.residence_country || null,
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
type_nat_foreign: (formData.type_nat_foreign || null) as any,
is_active: formData.is_active,
address: {
street: formData.street || null,
streets: formData.streets || null,
exterior_number: formData.exterior_number || null,
interior_number: formData.interior_number || null,
neighborhood: formData.neighborhood || null,
municipality: formData.municipality || null,
city: formData.city || null,
state: formData.state || null,
country: formData.country || null,
zip_code: formData.zip_code || null,
postal_code: formData.postal_code || null,
email: formData.email || null,
phone: formData.phone || null,
contact: formData.contact || null,
},
programs: {
program_code: formData.program_code || null,
authorization_date: formData.authorization_date || null,
program: formData.program || null,
program_number: formData.program_number || null,
manufacturer_id: formData.manufacturer_id || null,
}
};
response = await clientsProvidersApi.update(item.id, companyStore.activeCompany.id, payload);
@@ -129,22 +167,27 @@
rfc: formData.rfc,
name: formData.name,
curp: formData.curp || null,
residence_country: formData.residence_country || null,
domicile_fiscal: formData.domicile_fiscal || null,
foreign_tax_id: formData.foreign_tax_id || null,
client_or_provider: formData.client_or_provider as "client" | "provider" | "both" | null,
type_nat_foreign: (formData.type_nat_foreign || null) as any,
is_active: formData.is_active,
address: {
street: formData.street || null,
streets: formData.streets || null,
exterior_number: formData.exterior_number || null,
interior_number: formData.interior_number || null,
neighborhood: formData.neighborhood || null,
municipality: formData.municipality || null,
city: formData.city || null,
state: formData.state || null,
country: formData.country || null,
zip_code: formData.zip_code || null,
postal_code: formData.postal_code || null,
email: formData.email || null,
phone: formData.phone || null,
contact: formData.contact || null,
},
programs: {
program_code: formData.program_code || null,
authorization_date: formData.authorization_date || null,
program: formData.program || null,
program_number: formData.program_number || null,
manufacturer_id: formData.manufacturer_id || null,
}
};
response = await clientsProvidersApi.create(companyStore.activeCompany.id, payload);
@@ -182,19 +225,24 @@
rfc: "",
name: "",
curp: "",
residence_country: "",
domicile_fiscal: "",
foreign_tax_id: "",
type_nat_foreign: "N",
client_or_provider: "client",
is_active: true,
street: "",
streets: "",
exterior_number: "",
interior_number: "",
neighborhood: "",
municipality: "",
city: "",
state: "",
country: "",
zip_code: "",
program_code: "",
authorization_date: ""
postal_code: "",
email: "",
phone: "",
contact: "",
program: "",
program_number: "",
manufacturer_id: ""
};
error = null;
}
@@ -226,14 +274,27 @@
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Básica</h3>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="rfc">RFC *</Label>
<Label for="type_nat_foreign">Procedencia *</Label>
<select
id="type_nat_foreign"
bind:value={formData.type_nat_foreign}
required
disabled={loading}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="N">Nacional</option>
<option value="E">Extranjero</option>
</select>
</div>
<div class="space-y-2">
<Label for="rfc">{isForeign ? "TAX-ID" : "RFC"} *</Label>
<Input
id="rfc"
bind:value={formData.rfc}
placeholder="Ej: XAXX010101000"
maxlength={13}
placeholder={isForeign ? "Ej: 12-3456789 (según país)" : "Ej: XAXX010101000"}
maxlength={30}
required
disabled={loading}
/>
@@ -292,48 +353,45 @@
</div>
</div>
<!-- Información fiscal -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
<div class="space-y-2">
<Label for="domicile_fiscal">Domicilio Fiscal</Label>
<Input
id="domicile_fiscal"
bind:value={formData.domicile_fiscal}
placeholder="Domicilio fiscal completo"
maxlength={256}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="foreign_tax_id">ID Fiscal Extranjero</Label>
<Input
id="foreign_tax_id"
bind:value={formData.foreign_tax_id}
placeholder="Para contribuyentes extranjeros"
maxlength={64}
disabled={loading}
/>
</div>
</div>
<!-- Dirección -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-2">
<Label for="street">Calle</Label>
<Label for="streets">Calles</Label>
<Input
id="street"
bind:value={formData.street}
placeholder="Calle y número"
id="streets"
bind:value={formData.streets}
placeholder="Calle y referencias"
maxlength={256}
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="exterior_number">Número exterior</Label>
<Input
id="exterior_number"
bind:value={formData.exterior_number}
placeholder="Ej: 123"
maxlength={20}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="interior_number">Número interior</Label>
<Input
id="interior_number"
bind:value={formData.interior_number}
placeholder="Ej: 4B"
maxlength={20}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="neighborhood">Colonia</Label>
@@ -347,10 +405,10 @@
</div>
<div class="space-y-2">
<Label for="zip_code">Código Postal</Label>
<Label for="postal_code">Código Postal</Label>
<Input
id="zip_code"
bind:value={formData.zip_code}
id="postal_code"
bind:value={formData.postal_code}
placeholder="Ej: 12345"
maxlength={10}
disabled={loading}
@@ -359,6 +417,17 @@
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="municipality">Municipio</Label>
<Input
id="municipality"
bind:value={formData.municipality}
placeholder="Municipio"
maxlength={150}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="city">Ciudad</Label>
<Input
@@ -392,30 +461,76 @@
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
bind:value={formData.email}
placeholder="correo@dominio.com"
maxlength={100}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="phone">Teléfono</Label>
<Input
id="phone"
bind:value={formData.phone}
placeholder="Teléfono"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="contact">Contacto</Label>
<Input
id="contact"
bind:value={formData.contact}
placeholder="Persona de contacto"
maxlength={50}
disabled={loading}
/>
</div>
</div>
<!-- Programas -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Programas</h3>
<div class="grid grid-cols-2 gap-4">
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="program_code">Código de Programa</Label>
<Label for="program">Programa</Label>
<Input
id="program_code"
bind:value={formData.program_code}
placeholder="Código del programa"
maxlength={32}
id="program"
bind:value={formData.program}
placeholder="Ej: IMMEX"
maxlength={7}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="authorization_date">Fecha de Autorización</Label>
<Label for="program_number">Número de Programa</Label>
<Input
id="authorization_date"
type="date"
bind:value={formData.authorization_date}
id="program_number"
bind:value={formData.program_number}
placeholder="Número"
maxlength={40}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="manufacturer_id">Manufacturer ID</Label>
<Input
id="manufacturer_id"
bind:value={formData.manufacturer_id}
placeholder="Manufacturer ID"
maxlength={25}
disabled={loading}
/>
</div>

View File

@@ -2,7 +2,7 @@
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { ClientProvider } from "./columns.js";
import type { ClientProvider } from "$lib/api/dashboard/a76/clients-providers";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
import { clientsProvidersApi } from "$lib/api/dashboard/a76/clients-providers";
@@ -29,6 +29,9 @@
navigator.clipboard.writeText(item.rfc);
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
function handleViewDetails() {
showDetailsDialog = true;
}
@@ -77,7 +80,7 @@
Copiar ID
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleCopyRfc}>
Copiar RFC
Copiar {isForeign(item) ? 'TAX-ID' : 'RFC'}
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />

View File

@@ -51,6 +51,9 @@
}
open = newOpen;
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
</script>
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
@@ -66,7 +69,7 @@
<span class="font-semibold">{item.id}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="font-medium">RFC:</span>
<span class="font-medium">{item ? (isForeign(item) ? 'TAX-ID:' : 'RFC:') : 'RFC:'}</span>
<code class="font-mono font-semibold">{item.rfc}</code>
</div>
<div class="flex flex-col gap-1 text-sm">

View File

@@ -15,6 +15,9 @@
function handleOpenChange(newOpen: boolean) {
open = newOpen;
}
const isForeign = (cp: ClientProvider) =>
(cp.type_nat_foreign || "N").toUpperCase() === "E";
</script>
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
@@ -39,7 +42,9 @@
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">RFC</span>
<span class="text-xs font-medium text-muted-foreground">
{isForeign(item) ? "TAX-ID" : "RFC"}
</span>
<code class="text-sm font-mono font-semibold">{item.rfc}</code>
</div>
</div>
@@ -63,7 +68,7 @@
<div class="space-y-2">
<h3 class="text-sm font-semibold">Clasificación</h3>
<div class="grid grid-cols-2 gap-2">
<div class="grid grid-cols-3 gap-2">
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Tipo</span>
{#if item.client_or_provider === 'client'}
@@ -82,6 +87,13 @@
<span class="text-sm text-muted-foreground">No especificado</span>
{/if}
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Procedencia</span>
<span class="text-sm font-medium">
{isForeign(item) ? "Extranjero" : "Nacional"}
</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Estado</span>
@@ -100,44 +112,16 @@
<Separator />
</div>
<!-- Información fiscal -->
<div class="space-y-2">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
{#if item.residence_country}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">País de Residencia</span>
<span class="text-sm">{item.residence_country}</span>
</div>
{/if}
{#if item.domicile_fiscal}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Domicilio Fiscal</span>
<span class="text-sm">{item.domicile_fiscal}</span>
</div>
{/if}
{#if item.foreign_tax_id}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">ID Fiscal Extranjero</span>
<code class="text-sm font-mono">{item.foreign_tax_id}</code>
</div>
{/if}
<Separator />
</div>
<!-- Dirección (si existe) -->
{#if item.address}
<div class="space-y-2">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-1">
{#if item.address.street}
{#if item.address.streets}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Calle</span>
<span class="text-sm">{item.address.street}</span>
<span class="text-xs font-medium text-muted-foreground">Calles</span>
<span class="text-sm">{item.address.streets}</span>
</div>
{/if}
@@ -149,15 +133,22 @@
</div>
{/if}
{#if item.address.zip_code}
{#if item.address.postal_code}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Código Postal</span>
<span class="text-sm">{item.address.zip_code}</span>
<span class="text-sm">{item.address.postal_code}</span>
</div>
{/if}
</div>
<div class="grid grid-cols-2 gap-2">
{#if item.address.municipality}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Municipio</span>
<span class="text-sm">{item.address.municipality}</span>
</div>
{/if}
{#if item.address.city}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Ciudad</span>
@@ -179,6 +170,29 @@
<span class="text-sm">{item.address.country}</span>
</div>
{/if}
{#if item.address.email || item.address.phone || item.address.contact}
<div class="grid grid-cols-2 gap-2 pt-2">
{#if item.address.email}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Email</span>
<span class="text-sm">{item.address.email}</span>
</div>
{/if}
{#if item.address.phone}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Teléfono</span>
<span class="text-sm">{item.address.phone}</span>
</div>
{/if}
{#if item.address.contact}
<div class="flex flex-col gap-1 col-span-2">
<span class="text-xs font-medium text-muted-foreground">Contacto</span>
<span class="text-sm">{item.address.contact}</span>
</div>
{/if}
</div>
{/if}
</div>
<Separator />
@@ -190,17 +204,27 @@
<div class="space-y-2">
<h3 class="text-sm font-semibold">Programas</h3>
{#if item.programs.program_code}
{#if item.programs.program}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Código de Programa</span>
<code class="text-sm font-mono">{item.programs.program_code}</code>
<span class="text-xs font-medium text-muted-foreground">Programa</span>
<code class="text-sm font-mono">{item.programs.program}</code>
</div>
{/if}
{#if item.programs.authorization_date}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Fecha de Autorización</span>
<span class="text-sm">{new Date(item.programs.authorization_date).toLocaleDateString()}</span>
{#if item.programs.program_number || item.programs.manufacturer_id}
<div class="grid grid-cols-2 gap-2">
{#if item.programs.program_number}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Número</span>
<span class="text-sm">{item.programs.program_number}</span>
</div>
{/if}
{#if item.programs.manufacturer_id}
<div class="flex flex-col gap-1">
<span class="text-xs font-medium text-muted-foreground">Manufacturer ID</span>
<span class="text-sm font-mono">{item.programs.manufacturer_id}</span>
</div>
{/if}
</div>
{/if}
</div>

View File

@@ -118,6 +118,12 @@
selectedItem = item;
}
function taxIdOrRfcLabel(cp: ClientProvider | null): string {
if (!cp) return 'RFC';
const proc = (cp.type_nat_foreign || 'N').toUpperCase();
return proc === 'E' ? 'TAX-ID' : 'RFC';
}
function handleEdit() {
if (selectedItem) goto(`/dashboard/clients_and_providers/edit/${selectedItem.id}`);
}
@@ -182,7 +188,7 @@
<div class="p-4 space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold">Filtros</h2>
<span class="text-xs text-muted-foreground">Busque por nombre, RFC o tipo</span>
<span class="text-xs text-muted-foreground">Busque por nombre, RFC/TAX-ID o tipo</span>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="space-y-2">
@@ -195,10 +201,10 @@
/>
</div>
<div class="space-y-2">
<Label class="text-xs">RFC</Label>
<Label class="text-xs">RFC / TAX-ID</Label>
<Input
bind:value={searchRfc}
placeholder="RFC..."
placeholder="RFC / TAX-ID..."
class="h-9"
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
/>
@@ -249,7 +255,7 @@
<thead class="bg-muted text-muted-foreground border-b">
<tr>
<th class="px-3 py-2 text-left w-8">#</th>
<th class="px-3 py-2 text-left">RFC</th>
<th class="px-3 py-2 text-left">RFC / TAX-ID</th>
<th class="px-3 py-2 text-left">Nombre</th>
<th class="px-3 py-2 text-left">Tipo</th>
<th class="px-3 py-2 text-left">Estatus</th>
@@ -352,6 +358,7 @@
{selectedItem?.name || '---'}
</h2>
<div class="flex items-center gap-2 mt-1">
<span class="text-xs font-medium text-muted-foreground">{taxIdOrRfcLabel(selectedItem)}:</span>
<span class="text-xs font-mono text-muted-foreground">{selectedItem?.rfc || ''}</span>
</div>
</div>
@@ -434,10 +441,6 @@
<span class="text-xs text-muted-foreground block">Número</span>
<span class="font-medium">{selectedItem.programs.program_number || '-'}</span>
</div>
<div>
<span class="text-xs text-muted-foreground block">TAX ID</span>
<span class="font-medium">{selectedItem.programs.tax_id || '-'}</span>
</div>
</div>
</div>
{/if}

View File

@@ -73,7 +73,6 @@
responsible: '',
position: '',
is_active: true,
is_national_provider: false,
streets: '',
exterior_number: '',
@@ -94,7 +93,6 @@
authorization_date_str: '', // String para input date
prosec: '',
manufacturer_id: '',
tax_id: '',
ctpat_svi: '',
is_certified_company: false
});
@@ -152,7 +150,6 @@
responsible: item.responsible || '',
position: item.position || '',
is_active: !!item.is_active,
is_national_provider: !!item.is_national_provider,
streets: addr.streets || '',
exterior_number: addr.exterior_number || '',
@@ -172,7 +169,6 @@
authorization_date_str: intDateToString(prog.secon_auth_date),
prosec: prog.prosec ? String(prog.prosec) : '',
manufacturer_id: prog.manufacturer_id || '',
tax_id: prog.tax_id || '',
ctpat_svi: prog.ctpat_svi || '',
is_certified_company: prog.is_certified_company === '1'
};
@@ -185,6 +181,11 @@
}
}
// Validación de formato: RFC (Nacional) o TAX-ID (Extranjero)
const RFC_REGEX = /^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$/i;
// TAX-ID: 2 dígitos, guión, resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres.
const TAX_ID_REGEX = /^\d{2}-[A-Z0-9]{1,27}$/i;
// --- ENVÍO DE DATOS ---
async function handleSubmit() {
if (!companyStore.activeCompany) {
@@ -193,11 +194,27 @@
return;
}
if (!formData.rfc.trim() || !formData.name.trim()) {
error = 'RFC y Nombre obligatorios';
error = 'Identificador fiscal (RFC/TAX-ID) y Nombre son obligatorios';
toast.error(error);
return;
}
const rfcVal = formData.rfc.trim();
const isForeign = (formData.type_nat_foreign || 'N').toUpperCase() === 'E';
if (isForeign) {
if (!TAX_ID_REGEX.test(rfcVal)) {
error = 'El TAX-ID debe tener formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.';
toast.error(error);
return;
}
} else {
if (!RFC_REGEX.test(rfcVal)) {
error = 'El RFC no tiene el formato correcto. Ejemplo: XAXX010101000.';
toast.error(error);
return;
}
}
loading = true;
error = null;
@@ -213,7 +230,6 @@
responsible: clean(formData.responsible),
position: clean(formData.position),
is_active: formData.is_active,
is_national_provider: formData.is_national_provider,
address: {
streets: clean(formData.streets),
@@ -236,7 +252,6 @@
secon_auth_date: stringDateToInt(formData.authorization_date_str),
prosec: clean(formData.prosec),
manufacturer_id: clean(formData.manufacturer_id),
tax_id: clean(formData.tax_id),
ctpat_svi: clean(formData.ctpat_svi),
is_certified_company: formData.is_certified_company ? '1' : '0'
}
@@ -333,12 +348,12 @@
<Card.Content class="space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="rfc">RFC <span class="text-destructive">*</span></Label>
<Label for="rfc">{formData.type_nat_foreign?.toUpperCase() === 'E' ? 'TAX-ID' : 'RFC'} <span class="text-destructive">*</span></Label>
<Input
id="rfc"
bind:value={formData.rfc}
placeholder="XAXX010101000"
maxlength={13}
placeholder={formData.type_nat_foreign?.toUpperCase() === 'E' ? 'Ej: 12-3456789 (según país)' : 'XAXX010101000'}
maxlength={30}
disabled={loading}
/>
</div>
@@ -659,19 +674,6 @@
</div>
<Separator />
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="grid gap-2">
<Label for="tax_id" class="flex items-center gap-2">
<Globe size={14} class="text-muted-foreground" />
Tax ID (Extranjero)
</Label>
<Input
id="tax_id"
bind:value={formData.tax_id}
maxlength={30}
disabled={loading}
placeholder="Identificador fiscal extranjero"
/>
</div>
<div class="grid gap-2">
<Label for="man_id" class="flex items-center gap-2">
<FileText size={14} class="text-muted-foreground" />
@@ -746,17 +748,6 @@
</p>
</div>
</div>
<div class="flex items-center gap-3 rounded-lg border bg-card p-4">
<Switch
id="is_national"
bind:checked={formData.is_national_provider}
disabled={loading}
/>
<div class="grid gap-0.5">
<Label for="is_national">Proveedor Nacional</Label>
<p class="text-xs text-muted-foreground">Marcar si es un proveedor nacional</p>
</div>
</div>
</div>
</Card.Content>
</Card.Root>