feat(fin,ops): Facturación y Cobranza (Diag. 4), bitácora de embarque (Diag. 3) y subida a MinIO

- schema fin: fin.invoices + fin.invoice_items + fin.payments; totales con IVA,
  estados borrador→emitida→enviada→pagada, cobranza (pagos) y saldo automático
- generar-factura-desde-embarque (toma conceptos de venta de la cotización)
- ops.shipment_events: bitácora/hitos del embarque con secuencia por defecto
  según operación (importación/exportación) — cubre Diagrama 3
- subida de documentos a MinIO: POST /crm/uploads (multipart) + URL firmada
- migración c4d5e6f7a8b9, routers/permisos (fin), seed del flujo hasta factura
- 58 tests pytest en verde

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aduanasoft
2026-07-15 07:18:11 -06:00
parent 6f200b4505
commit 0b12ad5354
20 changed files with 1167 additions and 3 deletions

View File

@@ -26,6 +26,10 @@ from api.v1.modules.crm.quotes.models import Quote, QuoteItem
from api.v1.modules.crm.service_requests.models import ServiceRequest
from api.v1.modules.crm.suppliers.models import Supplier
from api.v1.modules.ops.shipments.models import Shipment, ShipmentDocument
from api.v1.modules.ops.shipments import service as shipments_service
from api.v1.modules.fin.invoices.models import Invoice
from api.v1.modules.fin.invoices import service as invoices_service
from api.v1.modules.fin.invoices.dto import PaymentCreate
from core.database import CoreSessionLocal
# Deben coincidir con DEV_LOCAL_AUTH_TENANT_ID / DEV_LOCAL_AUTH_COMPANY_ID
@@ -323,6 +327,39 @@ def seed_commercial_and_ops(db) -> None:
print("✓ Flujo comercial: 1 solicitud, 1 cotización aceptada (3 conceptos), 1 embarque + documento MBL")
def seed_invoicing_and_events(db) -> None:
"""Factura desde el embarque (con pago parcial) + hitos de la bitácora."""
from decimal import Decimal
if db.query(Invoice).filter(
Invoice.tenant_id == TENANT_ID, Invoice.company_id == COMPANY_ID, Invoice.deleted_at.is_(None)
).first():
print("• Ya existe facturación; se omite")
return
shipment = (
db.query(Shipment)
.filter(Shipment.tenant_id == TENANT_ID, Shipment.company_id == COMPANY_ID, Shipment.deleted_at.is_(None))
.order_by(Shipment.id.asc())
.first()
)
if not shipment:
print("• Sin embarque; se omite facturación")
return
# Bitácora de hitos (según tipo de operación)
shipments_service.seed_default_milestones(db, shipment.id, TENANT_ID, COMPANY_ID)
# Factura generada desde el embarque (toma conceptos de la cotización)
invoice = invoices_service.generate_from_shipment(db, shipment.id, TENANT_ID, COMPANY_ID)
invoices_service.emit_invoice(db, invoice.id, TENANT_ID, COMPANY_ID)
invoices_service.create_payment(
db, PaymentCreate(invoice_id=invoice.id, amount=Decimal("1000"), method="transferencia", reference="SPEI-001"),
TENANT_ID, COMPANY_ID,
)
print("✓ Factura emitida desde el embarque con pago parcial + hitos de la bitácora")
def main() -> None:
db = CoreSessionLocal()
try:
@@ -331,6 +368,7 @@ def main() -> None:
seed_sample_data(db, pipeline, stages)
seed_suppliers_and_related(db)
seed_commercial_and_ops(db)
seed_invoicing_and_events(db)
print("\nSeed CRM completado.")
finally:
db.close()