Files
CRM_AGENTES_CARGA/backend/api/v1/modules/crm/accounts/models.py
Jair Cedillo f4ef6a037d feat(fin,crm): catálogo c_UsoCFDI y claves fiscales del receptor
Cierra las decisiones pendientes 1 y 5. Agrega sat.cfdi_uses con su endpoint de
solo lectura y amarra la ficha del cliente a los catálogos del SAT con
crm.accounts.tax_regime_id y cfdi_use_id.

Las columnas de texto libre tax_regime y cfdi_use se conservan intactas: la
migración hace un backfill conservador que solo resuelve lo inequívoco (la clave
del catálogo, o la descripción exacta sin distinguir mayúsculas ni espacios) y
deja en NULL lo que no case, porque deducir el régimen de un receptor a partir
de texto libre provoca CFDI rechazados. La UI muestra el texto anterior junto al
selector para que el usuario elija la clave que corresponde.

El selector de régimen se acota al tipo de persona de la cuenta, y el service
valida ambas claves contra el catálogo.

sync_catalogs ahora omite los catálogos cuya tabla todavía no existe: al correr
el historial desde cero, la migración anterior la invoca antes de que se creen
los catálogos agregados después.

Las claves de c_UsoCFDI quedan pendientes de validación con el área Fiscal antes
de producción, igual que el subset de c_ClaveProdServ; no se cargaron las
banderas de persona física/moral ni la compatibilidad por régimen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:50:26 -05:00

83 lines
4.8 KiB
Python

from decimal import Decimal
from sqlalchemy import ForeignKey, Integer, Numeric, String, Text, text
from sqlalchemy.orm import Mapped, mapped_column
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from api.v1.modules.fin.catalogs.models import CfdiUse, TaxRegime # noqa: F401 (resuelve las FK)
from core.database import Base
class Account(Base, TenantScopedMixin, TimestampMixin):
"""Catálogo de Clientes / Prospectos.
Un mismo registro puede ser Cliente o Prospecto (``record_type``). Concentra
datos generales, comerciales y fiscales; las direcciones, contactos y
documentos viven en tablas relacionadas (crm.addresses, crm.contacts,
crm.documents).
"""
__tablename__ = "accounts"
__table_args__ = {"schema": "crm"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
# ----- Datos generales -----
name: Mapped[str] = mapped_column(String(255), nullable=False) # razón social
trade_name: Mapped[str | None] = mapped_column(String(255), nullable=True) # nombre comercial
rfc: Mapped[str | None] = mapped_column(String(13), nullable=True, index=True)
curp: Mapped[str | None] = mapped_column(String(18), nullable=True) # persona física
# Tipo de registro: cliente | prospecto
record_type: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'cliente'"), index=True)
# Tipo de persona: fisica | moral
person_type: Mapped[str | None] = mapped_column(String(10), nullable=True)
industry: Mapped[str | None] = mapped_column(String(120), nullable=True) # giro / industria
# Tipo operativo (immex | agencia_aduanal | importador | exportador | transportista | otro)
account_type: Mapped[str | None] = mapped_column(String(40), nullable=True)
# Estatus: active | inactive
status: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'active'"))
# ----- Información comercial -----
# Clasificación: importador | exportador | ambos
commercial_classification: Mapped[str | None] = mapped_column(String(20), nullable=True)
# Medio de contacto preferido: llamada | correo | videollamada | whatsapp | otro
preferred_contact_method: Mapped[str | None] = mapped_column(String(20), nullable=True)
language: Mapped[str | None] = mapped_column(String(40), nullable=True)
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
phone: Mapped[str | None] = mapped_column(String(40), nullable=True)
website: Mapped[str | None] = mapped_column(String(255), nullable=True)
# ----- Información fiscal -----
# Régimen fiscal y uso de CFDI en texto libre: se conservan como capturó el usuario
# para no perder lo ya registrado, pero lo que vale al timbrar son las FK de abajo.
tax_regime: Mapped[str | None] = mapped_column(String(120), nullable=True) # régimen fiscal
cfdi_use: Mapped[str | None] = mapped_column(String(60), nullable=True) # uso de CFDI
# Claves del receptor contra los catálogos del SAT (c_RegimenFiscal y c_UsoCFDI).
tax_regime_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("sat.tax_regimes.id"), nullable=True
)
cfdi_use_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("sat.cfdi_uses.id"), nullable=True
)
payment_method: Mapped[str | None] = mapped_column(String(60), nullable=True) # método de pago
payment_form: Mapped[str | None] = mapped_column(String(60), nullable=True) # forma de pago
currency: Mapped[str | None] = mapped_column(String(3), nullable=True) # moneda
credit_limit: Mapped[Decimal | None] = mapped_column(Numeric(14, 2), nullable=True)
credit_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
commercial_terms: Mapped[str | None] = mapped_column(Text, nullable=True) # condiciones comerciales
# ----- Datos aduaneros / ubicación rápida -----
patente_aduanal: Mapped[str | None] = mapped_column(String(20), nullable=True)
# Ubicación de referencia (el detalle vive en crm.addresses)
address: Mapped[str | None] = mapped_column(Text, nullable=True)
city: Mapped[str | None] = mapped_column(String(120), nullable=True)
state: Mapped[str | None] = mapped_column(String(120), nullable=True)
country: Mapped[str | None] = mapped_column(String(2), nullable=True, server_default=text("'MX'"))
# ----- Observaciones y auditoría -----
notes: Mapped[str | None] = mapped_column(Text, nullable=True) # comentarios generales
internal_notes: Mapped[str | None] = mapped_column(Text, nullable=True) # notas internas
owner_user_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) # vendedor
created_by: Mapped[str | None] = mapped_column(String(64), nullable=True)
updated_by: Mapped[str | None] = mapped_column(String(64), nullable=True)