Files
plantillas-proyectos/backend/tests/fixtures/builders.py

458 lines
13 KiB
Python

from __future__ import annotations
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import NamedTuple
from sqlalchemy.orm import Session
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from api.v1.modules.a76.clients_and_providers.models import (
ClientOrProviderEnum,
ClientProvider,
ClientProviderAddress,
)
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
from api.v1.modules.a76.invoices.models import (
Currency,
InvoiceComplianceMx,
InvoiceFinancials,
InvoiceHeader,
InvoiceLogistics,
InvoiceStatus,
OperationType,
)
from api.v1.modules.a76.items.line_customs.models import LineCustom
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
from api.v1.modules.a76.items.line_financials.models import LineFinancial
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.core.tenants.models import Tenant, TenantType
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
from api.v1.modules.public.reference_data.pedimento_regimens.models import RegimenPedimento
class EphemeralOrgIds(NamedTuple):
"""Tenant + company creados por test (IDs altos, baja colisión con datos reales / CI)."""
tenant_id: int
company_id: int
def allocate_ephemeral_tenant_company_ids(db: Session) -> tuple[int, int]:
"""
Genera par (tenant_id, company_id) no usados en esta sesión.
Mismo entero para ambas PKs es válido (tablas distintas).
"""
for _ in range(64):
n = uuid.uuid4().int
tid = 1_500_000_000 + (n % 99_000_000)
if db.get(Tenant, tid) is not None:
continue
if db.get(Company, tid) is not None:
continue
return tid, tid
raise RuntimeError(
"No se pudo asignar tenant/company efímero para tests (reintentar o revisar datos en BD)."
)
def ensure_reference_data(db: Session) -> None:
for key, desc in [
("TEM", "IMPORTACION TEMPORAL"),
("DEF", "IMPORTACION DEFINITIVA"),
("MEX", "COMPRAS MEXICANAS"),
("DONAC", "DONACION"),
]:
if db.get(InvoiceType, key) is None:
operation = "exp" if key == "DONAC" else "imp"
db.add(
InvoiceType(
key=key,
description=desc,
note="seed for tests",
type="both",
operation=operation,
)
)
if db.get(RegimenPedimento, "A1") is None:
db.add(
RegimenPedimento(
code="A1",
description="Regimen de prueba",
)
)
db.flush()
def ensure_tenant_company(db: Session, tenant_id: int, company_id: int) -> Company:
tenant = db.get(Tenant, tenant_id)
if tenant is None:
tenant = Tenant(
id=tenant_id,
name=f"Tenant {tenant_id}",
slug=f"pytest-{tenant_id}-{uuid.uuid4().hex[:10]}",
type=TenantType.SHARED,
keycloak_realm="test",
is_active=True,
)
db.add(tenant)
# El FK de a76.company → core.tenants exige que el tenant exista en esta transacción
# antes del INSERT de company; un solo flush al final puede ordenar mal (Tenant híbrido Column+Mapped).
db.flush()
company = db.get(Company, company_id)
if company is None:
company = Company(
id=company_id,
tenant_id=tenant_id,
name=f"Company {company_id}",
rfc="TST010101AAA",
prosec=False,
)
db.add(company)
db.flush()
return company
def _ensure_client_provider_address(
db: Session,
client: ClientProvider,
*,
tenant_id: int,
company_id: int,
country: str,
) -> None:
"""
Garantiza que el cliente tenga fila de dirección. Si el ORM no tiene `address`
cargada pero en BD ya existe (datos previos en CI / DB compartida), reutiliza
esa fila en lugar de INSERT duplicado (pkey id = client_id).
"""
if client.address is not None:
return
existing = (
db.query(ClientProviderAddress)
.filter(ClientProviderAddress.client_id == client.id)
.first()
)
if existing is not None:
client.address = existing
return
client.address = ClientProviderAddress(
id=client.id,
client_id=client.id,
tenant_id=tenant_id,
company_id=company_id,
country=country,
)
def create_business_catalogs(db: Session, tenant_id: int, company_id: int) -> dict:
provider = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.name == "Proveedor Test",
)
.first()
)
if provider is None:
provider = ClientProvider(
tenant_id=tenant_id,
company_id=company_id,
name="Proveedor Test",
client_or_provider=ClientOrProviderEnum.BOTH,
is_active=True,
)
db.add(provider)
db.flush()
_ensure_client_provider_address(
db,
provider,
tenant_id=tenant_id,
company_id=company_id,
country="MEX",
)
sold_to = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.name == "Cliente Test",
)
.first()
)
if sold_to is None:
sold_to = ClientProvider(
tenant_id=tenant_id,
company_id=company_id,
name="Cliente Test",
client_or_provider=ClientOrProviderEnum.BOTH,
is_active=True,
)
db.add(sold_to)
db.flush()
_ensure_client_provider_address(
db,
sold_to,
tenant_id=tenant_id,
company_id=company_id,
country="USA",
)
shipped_to = (
db.query(ClientProvider)
.filter(
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
ClientProvider.name == "Destinatario Test",
)
.first()
)
if shipped_to is None:
shipped_to = ClientProvider(
tenant_id=tenant_id,
company_id=company_id,
name="Destinatario Test",
client_or_provider=ClientOrProviderEnum.BOTH,
is_active=True,
)
db.add(shipped_to)
db.flush()
_ensure_client_provider_address(
db,
shipped_to,
tenant_id=tenant_id,
company_id=company_id,
country="USA",
)
broker = (
db.query(CustomsBroker)
.filter(
CustomsBroker.tenant_id == tenant_id,
CustomsBroker.company_id == company_id,
CustomsBroker.broker_key == "A1234",
)
.first()
)
if broker is None:
broker = CustomsBroker(
tenant_id=tenant_id,
company_id=company_id,
broker_key="A1234",
name="Agente Test",
)
db.add(broker)
db.flush()
uom = (
db.query(UnitOfMeasure)
.filter(
UnitOfMeasure.tenant_id == tenant_id,
UnitOfMeasure.company_id == company_id,
UnitOfMeasure.code == "PZA",
)
.first()
)
if uom is None:
uom = UnitOfMeasure(
tenant_id=tenant_id,
company_id=company_id,
code="PZA",
description="Pieza",
)
db.add(uom)
db.flush()
exr = (
db.query(ExchangeRate)
.filter(
ExchangeRate.tenant_id == tenant_id,
ExchangeRate.company_id == company_id,
ExchangeRate.date == datetime(2026, 3, 17, 0, 0, 0),
)
.first()
)
if exr is None:
exr = ExchangeRate(
tenant_id=tenant_id,
company_id=company_id,
date=datetime(2026, 3, 17, 0, 0, 0),
value=Decimal("17.250000"),
local_currency="MN",
foreign_currency="ME",
)
db.add(exr)
db.flush()
return {
"provider_id": provider.id,
"sold_to_id": sold_to.id,
"shipped_to_id": shipped_to.id,
"broker_id": broker.id,
"uom_id": uom.id,
}
def create_import_invoice_with_line(
db: Session,
tenant_id: int,
company_id: int,
catalogs: dict,
invoice_type: str = "TEM",
invoice_number: str = "IMP-0001",
qty: Decimal = Decimal("10"),
) -> tuple[InvoiceHeader, LineItem]:
invoice = InvoiceHeader(
tenant_id=tenant_id,
company_id=company_id,
system="fixed_asset",
operation_type=OperationType.IMP,
invoice_type=invoice_type,
document_type="A1",
invoice_number=invoice_number,
invoice_date=date(2026, 3, 17),
status=InvoiceStatus.PENDING,
)
invoice.compliance_mx = InvoiceComplianceMx(
tenant_id=tenant_id,
company_id=company_id,
provider_id=catalogs["provider_id"],
sold_to_id=catalogs["sold_to_id"],
shipped_to_id=catalogs["shipped_to_id"],
customs_broker_id=catalogs["broker_id"],
is_pedimento_pending=True,
was_reviewed_by_company=True,
)
invoice.financials = InvoiceFinancials(
tenant_id=tenant_id,
company_id=company_id,
currency=Currency.FOREIGN,
exchange_rate=Decimal("17.250000"),
iva_factor="16",
)
invoice.logistics = InvoiceLogistics(
tenant_id=tenant_id,
company_id=company_id,
weight_type="kgs",
equipment_reviewed=True,
)
db.add(invoice)
db.flush()
line = LineItem(
tenant_id=tenant_id,
company_id=company_id,
invoice_id=invoice.id,
line_number=1,
unit_of_measure=catalogs["uom_id"],
)
line.quantity = LineQuantity(
quantity=qty,
net_weight=Decimal("50"),
gross_weight=Decimal("55"),
package_quantity=1,
)
line.financial = LineFinancial(
unit_cost_capture=Decimal("10"),
value_usd=Decimal("100"),
value_mxn=Decimal("1725"),
)
line.customs = LineCustom(origin_country="MEX", fraction_type="GENERAL")
line.description = LineDescription(has_serial=False)
db.add(line)
db.flush()
return invoice, line
def create_export_invoice_with_line(
db: Session,
tenant_id: int,
company_id: int,
catalogs: dict,
source_import_invoice: InvoiceHeader,
source_import_line: LineItem,
qty: Decimal = Decimal("4"),
) -> tuple[InvoiceHeader, LineItem]:
invoice = InvoiceHeader(
tenant_id=tenant_id,
company_id=company_id,
system="fixed_asset",
operation_type=OperationType.EXP,
invoice_type="DONAC",
document_type="A1",
invoice_number="EXP-0001",
invoice_date=date(2026, 3, 18),
status=InvoiceStatus.PENDING,
)
invoice.compliance_mx = InvoiceComplianceMx(
tenant_id=tenant_id,
company_id=company_id,
provider_id=catalogs["provider_id"],
sold_to_id=catalogs["sold_to_id"],
shipped_to_id=catalogs["shipped_to_id"],
customs_broker_id=catalogs["broker_id"],
is_pedimento_pending=True,
was_reviewed_by_company=True,
)
invoice.financials = InvoiceFinancials(
tenant_id=tenant_id,
company_id=company_id,
currency=Currency.FOREIGN,
exchange_rate=Decimal("17.250000"),
iva_factor="16",
)
invoice.logistics = InvoiceLogistics(
tenant_id=tenant_id,
company_id=company_id,
weight_type="kgs",
equipment_reviewed=True,
)
db.add(invoice)
db.flush()
line = LineItem(
tenant_id=tenant_id,
company_id=company_id,
invoice_id=invoice.id,
line_number=1,
unit_of_measure=catalogs["uom_id"],
)
line.quantity = LineQuantity(
quantity=qty,
net_weight=Decimal("20"),
gross_weight=Decimal("22"),
package_quantity=1,
)
line.financial = LineFinancial(
unit_cost_capture=Decimal("12"),
value_usd=Decimal("48"),
value_mxn=Decimal("828"),
)
line.customs = LineCustom(origin_country="MEX", origin_procedure="TEM", fraction_type="GENERAL")
line.description = LineDescription(has_serial=False)
db.add(line)
db.flush()
db.add(
FaLineItem(
id=line.id,
tenant_id=tenant_id,
company_id=company_id,
discharge=True,
search_invoice=source_import_invoice.invoice_number,
search_line=source_import_line.line_number,
is_subitem=False,
)
)
db.flush()
return invoice, line