feat(ops,fin,crm): reglas de negocio del PDF (decisiones, cierre, facturación, continuidad, RBAC)

Cierra los huecos de la auditoría contra "SOFTWARE PARA AGENTES DE CARGA":

- ops (Diag. 2/3): bitácora con puntos de decisión (kind=decision) y ciclo de
  corrección (parent_event_id/attempt) para ¿Cut Off? y ¿despacho autorizado?
  (R-E-05/13, R-I-06). Reprogramación de salida (previous_etd, R-E-06). Hitos
  operativos completos export/import. Cierre operativo con costos finales
  (close_shipment, R-E-22).
- fin (Diag. 4): facturación con gate por cierre operativo y sin duplicar
  (R-F-01), costos de operación arrastrados (ops_cost_total, R-F-02), envío con
  PDF generado y guardado en MinIO (send_invoice + pdf.py sin dependencias,
  R-F-05) y revisión del cliente (en_revision_cliente + aprobación, R-F-06).
- crm (Diag. 1): opportunity_id enlaza embudo→RFQ (R-C-02), contacto como etapa
  (first_contact_at, R-C-04), re-cotización (clone_quote + reopen, R-C-12).
- transversal: catálogo de Incoterms y participantes/actores incl. autoridad
  aduanera (R-T-01/10), enforcement de permisos por carril (RBAC) con roles
  sembrados y dependencias dev-safe (R-T-07).
- Migración d5e6f7a8b9c0 con downgrade. Seed extendido. 70 tests (12 nuevos).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aduanasoft
2026-07-15 08:46:53 -06:00
parent 116d2e7f5a
commit e79705e6e3
26 changed files with 1407 additions and 68 deletions

View File

@@ -7,7 +7,7 @@ from api.v1.modules.crm.quotes.dto import QuoteCreate, QuoteItemCreate
from api.v1.modules.fin.invoices import service
from api.v1.modules.fin.invoices.dto import InvoiceCreate, InvoiceItemCreate, PaymentCreate
from api.v1.modules.ops.shipments import service as shipments_service
from api.v1.modules.ops.shipments.dto import ShipmentCreate
from api.v1.modules.ops.shipments.dto import ShipmentCloseInput, ShipmentCreate
T, C = 1, 1
@@ -42,11 +42,16 @@ def test_generate_from_shipment_copies_quote_items(db):
quotes_service.create_quote_item(db, QuoteItemCreate(quote_id=quote.id, concept="flete_internacional", quantity=1, unit_cost=1000, unit_sale=1500), T, C)
quotes_service.accept_quote(db, quote.id, T, C)
shipment = shipments_service.create_shipment_from_quote(db, quote.id, T, C)
# El embarque debe cerrarse operativamente antes de facturar (R-F-01)
shipments_service.close_shipment(
db, shipment.id, ShipmentCloseInput(actual_cost_total=Decimal("1000"), cost_currency="USD"), T, C
)
inv = service.generate_from_shipment(db, shipment.id, T, C)
assert inv.shipment_id == shipment.id
assert inv.account_id == acc.id
assert inv.currency == "USD"
assert float(inv.ops_cost_total) == 1000.0 # costos de operación arrastrados (R-F-02)
items = service.get_items(db, inv.id, T, C)
assert len(items) == 1 and float(items[0].unit_amount) == 1500.0
assert float(inv.subtotal) == 1500.0