Jair Cedillo dbda0b5755 feat(fin): la factura hereda los datos de facturación del cliente
Forma de pago, método de pago y moneda se capturaban a mano en cada factura aunque ya
vivieran en la ficha del cliente — y son justo las claves que detienen el timbrado en
validación si faltan. Ni el alta manual ni generate_from_shipment las prellenaban.

- _inherit_account_billing espeja _resolve_item_concept, el patrón de herencia que ya
  usa el módulo: lo explícito manda sobre la ficha, y se completa sin borrar. Si la
  ficha trae texto que no resuelve a una clave del SAT no se asigna nada, así que
  cambiar de cliente nunca vacía un dato ya capturado.
- find_by_code traduce el texto del Account a id de catálogo. Normaliza ('3' -> '03',
  'pue' -> 'PUE') y devuelve None sin lanzar: una ficha mal capturada no puede impedir
  facturar, el faltante lo reporta el timbrado junto al resto.
- Se invoca al crear, al cambiar de cliente (re-herencia) y en generate_from_shipment,
  donde la moneda del embarque gana sobre la de la ficha: es la que se coteó y operó.

Incluye el candado de inmutabilidad con timbre, que la herencia hacía necesario: había
un solo campo protegido (stamping_mode) y todo lo demás de una factura ya timbrada era
editable — cliente, folio, moneda, partidas e impuestos — con lo que la factura y su
CFDI podían contar cosas distintas. _reject_if_stamped generaliza esa guarda sobre una
lista cerrada de campos del comprobante, y sin lista en partidas e impuestos. Cobrar y
anotar siguen permitidos: no alteran el CFDI. send_invoice deja de regenerar el PDF de
una factura timbrada, que reescribía en MinIO el documento que el cliente ya recibió.

Dos cosas que la herencia obligaba a arreglar:

1. currency y tax_rate tenían default no nulo en el DTO y el frontend sembraba
   {currency:'MXN', tax_rate:16}, así que el backend nunca podía distinguir "no lo
   eligió" de "eligió eso" y la herencia habría sido código muerto. Ahora son
   opcionales; un None se retira del payload para que mande el default de la columna.
2. saveHeader mandaba el objeto completo, con lo que al cambiar de cliente el PATCH
   llevaba las claves del cliente anterior. Ahora manda solo el delta.

Se agrega fin.invoices.exchange_rate: heredar una moneda distinta de MXN producía
facturas no timbrables en silencio, porque _build_data pasaba exchange_rate=None
siempre y el validador lo exige. La validación sigue siendo del builder, que acumula
todos los faltantes juntos.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:15:20 -05:00

CRM — Aduanasoft

Sistema CRM construido sobre la plantilla Workspace de Aduanasoft (SvelteKit 5 + FastAPI + PostgreSQL, multi-tenant con Keycloak/Hub). Gestiona cuentas, contactos, prospectos, oportunidades (pipeline Kanban) y actividades comerciales.

Base: plantillas-proyectos. Este repo conserva el core de la plantilla (auth, tenants, permisos, licencias) y agrega el dominio CRM en backend y frontend.


Stack

Capa Tecnología
Frontend SvelteKit 5 (runes) + Tailwind + shadcn-svelte
Backend FastAPI + SQLAlchemy 2.0 + Pydantic v2
Auth Keycloak (OIDC) vía Workspace Hub
Base de datos PostgreSQL (schema crm)
Cache / Queue Valkey (Redis) + Celery
Storage MinIO (S3-compatible)
Contenedores Docker Compose

Módulo CRM

Esquema dedicado crm con 7 tablas multi-tenant (tenant_id + company_id, soft delete):

Entidad Tabla Descripción
Cuentas crm.accounts Empresas cliente/prospecto (importador, IMMEX, agencia aduanal, transportista). Incluye RFC y patente aduanal.
Contactos crm.contacts Personas asociadas a una cuenta.
Prospectos crm.leads Leads sin calificar; se convierten en cuenta + contacto + oportunidad.
Embudos crm.pipelines Embudos de venta por compañía.
Etapas crm.pipeline_stages Columnas del Kanban (con probabilidad y etapas terminales ganada/perdida).
Oportunidades crm.opportunities Negocios que avanzan por el embudo.
Actividades crm.activities Llamadas, reuniones, tareas, correos y notas.

Endpoints (prefijo /v1/crm)

Todos reciben company_id como query param y validan permisos vía Keycloak/PermissionService.

  • GET|POST /accounts, GET|PATCH|DELETE /accounts/{id}
  • GET|POST /contacts, GET|PATCH|DELETE /contacts/{id}
  • GET|POST /leads, GET|PATCH|DELETE /leads/{id}, POST /leads/{id}/convert
  • GET|POST /pipelines, PATCH|DELETE /pipelines/{id}
  • GET|POST /stages, PATCH|DELETE /stages/{id}
  • GET|POST /opportunities, GET|PATCH|DELETE /opportunities/{id}, PATCH /opportunities/{id}/move
  • GET|POST /activities, GET|PATCH|DELETE /activities/{id}, PATCH /activities/{id}/complete
  • GET /metrics — KPIs y embudo por etapa para el dashboard

Permisos

Se registran en el PermissionRegistry al arrancar (25 permisos: crm.access + crm.{account,contact,lead,opportunity,pipeline,activity}.{view,create,edit,delete}). Para persistirlos en BD: POST /v1/core/permissions/sync (o el CLI de sincronización).


Inicio rápido (dev)

cp .env.example .env          # ajustar CORE_DB_NAME, Keycloak, etc.
./scripts/auth-mode.sh local  # login local sin workspace
docker compose up -d

Abre http://localhost:5173CRM en el sidebar.

Migraciones

cd backend
alembic upgrade head          # crea el schema crm y sus tablas (revisión f1a2b3c4d5e6)
alembic downgrade -1          # revierte el CRM (down() probado)

Pruebas

Backend (servicios CRM):

cd backend
pytest tests/ -q              # 24 pruebas de servicios (SQLite en memoria)

En CI/PostgreSQL, define TEST_DATABASE_URL para ejercitar el esquema real y las políticas RLS (ver docs/ARCHITECTURE.md).


Estructura del CRM

backend/api/v1/modules/crm/
├── router.py            # agrega submódulos bajo /crm (y registra permisos)
├── permissions.py       # alta de permisos CRM en el registry
├── accounts/  contacts/  leads/  pipelines/  opportunities/  activities/  metrics/
│   └── models.py · dto.py · service.py · routes.py
backend/alembic/versions/f1a2b3c4d5e6_crm_schema.py

frontend/src/
├── lib/api/crm/         # clientes API tipados por entidad
├── lib/components/crm/  # helpers de formato/etiquetas
└── routes/dashboard/crm/
    ├── +page.svelte             # panel (KPIs + embudo)
    ├── cuentas/  contactos/  prospectos/  actividades/   # CRUD
    └── oportunidades/           # Kanban con drag & drop

Convenciones

  • Commits: Conventional Commits (feat:, fix:, refactor:, chore:)
  • Ramas: feature/AS-###-desc, fix/AS-###-desc
  • Código: nombres en inglés, comentarios de negocio en español
  • Backend: Pydantic v2, routers por dominio, filtros multi-tenant explícitos
  • Frontend: runes ($state, $derived, $effect, $props), Tailwind utility-first
Description
No description provided
Readme 2.5 MiB
Languages
Python 55.9%
Svelte 21.3%
TypeScript 12%
HTML 9.6%
XSLT 0.6%
Other 0.5%