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>
416 lines
18 KiB
Python
416 lines
18 KiB
Python
from decimal import Decimal
|
|
|
|
from api.v1.modules.crm.accounts import service as accounts_service
|
|
from api.v1.modules.crm.accounts.dto import AccountCreate
|
|
from api.v1.modules.crm.quotes import service as quotes_service
|
|
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,
|
|
InvoiceItemUpdate,
|
|
InvoiceUpdate,
|
|
PaymentCreate,
|
|
)
|
|
from api.v1.modules.ops.shipments import service as shipments_service
|
|
from api.v1.modules.ops.shipments.dto import ShipmentCloseInput, ShipmentCreate
|
|
|
|
T, C = 1, 1
|
|
|
|
|
|
def test_invoice_totals_with_tax(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-001", currency="MXN", tax_rate=Decimal("16")), T, C)
|
|
service.create_item(db, InvoiceItemCreate(invoice_id=inv.id, concept="flete_internacional", quantity=1, unit_amount=1000), T, C)
|
|
service.create_item(db, InvoiceItemCreate(invoice_id=inv.id, concept="despacho_aduanal", quantity=1, unit_amount=500), T, C)
|
|
inv = service.get_invoice(db, inv.id, T, C)
|
|
assert float(inv.subtotal) == 1500.0
|
|
assert float(inv.tax_amount) == 240.0 # 16% de 1500
|
|
assert float(inv.total) == 1740.0
|
|
assert float(inv.balance) == 1740.0
|
|
|
|
|
|
def test_payment_marks_paid(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-002", tax_rate=Decimal("0")), T, C)
|
|
service.create_item(db, InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000), T, C)
|
|
service.emit_invoice(db, inv.id, T, C)
|
|
service.create_payment(db, PaymentCreate(invoice_id=inv.id, amount=Decimal("400"), method="transferencia"), T, C)
|
|
inv = service.get_invoice(db, inv.id, T, C)
|
|
assert float(inv.paid_amount) == 400.0 and float(inv.balance) == 600.0
|
|
assert inv.status == "emitida"
|
|
service.create_payment(db, PaymentCreate(invoice_id=inv.id, amount=Decimal("600")), T, C)
|
|
inv = service.get_invoice(db, inv.id, T, C)
|
|
assert float(inv.balance) == 0.0 and inv.status == "pagada" and inv.paid_at is not None
|
|
|
|
|
|
def test_generate_from_shipment_copies_quote_items(db):
|
|
acc = accounts_service.create_account(db, AccountCreate(name="Cliente"), T, C)
|
|
quote = quotes_service.create_quote(db, QuoteCreate(reference="COT-9", account_id=acc.id, currency="USD"), T, C)
|
|
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
|
|
|
|
|
|
# ----- Impuestos derivados por partida (para el CFDI) -----
|
|
# El CFDI exige el desglose por partida, pero la factura captura un % global. El traslado de
|
|
# IVA se deriva de ese %; estas pruebas fijan que la derivación no invente ni borre nada.
|
|
|
|
def _iva_de(db, item_id):
|
|
from api.v1.modules.fin.invoices.models import InvoiceItemTax
|
|
return db.query(InvoiceItemTax).filter(
|
|
InvoiceItemTax.invoice_item_id == item_id, InvoiceItemTax.deleted_at.is_(None)
|
|
).all()
|
|
|
|
|
|
def _obj_imp(db, code):
|
|
from api.v1.modules.fin.catalogs.models import TaxObject
|
|
return db.query(TaxObject).filter(TaxObject.code == code).first().id
|
|
|
|
|
|
def test_iva_se_deriva_cuando_la_partida_es_objeto_de_impuesto(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-IVA", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="flete_internacional", quantity=1,
|
|
unit_amount=1000, tax_object_id=_obj_imp(db, "02")),
|
|
T, C,
|
|
)
|
|
taxes = _iva_de(db, item.id)
|
|
assert len(taxes) == 1
|
|
assert float(taxes[0].rate) == 0.16
|
|
assert float(taxes[0].amount) == 160.0
|
|
assert taxes[0].is_withholding is False
|
|
|
|
|
|
def test_sin_objeto_de_impuesto_no_se_deriva_nada(db):
|
|
"""ObjetoImp 01 con nodo de impuestos es motivo de rechazo del SAT."""
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-NOOBJ", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000,
|
|
tax_object_id=_obj_imp(db, "01")),
|
|
T, C,
|
|
)
|
|
assert _iva_de(db, item.id) == []
|
|
|
|
|
|
def test_cambiar_el_porcentaje_recalcula_las_partidas(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-REC", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000,
|
|
tax_object_id=_obj_imp(db, "02")),
|
|
T, C,
|
|
)
|
|
service.update_invoice(db, inv.id, InvoiceUpdate(tax_rate=Decimal("8")), T, C)
|
|
taxes = _iva_de(db, item.id)
|
|
assert float(taxes[0].rate) == 0.08
|
|
assert float(taxes[0].amount) == 80.0
|
|
|
|
|
|
def test_cambiar_el_importe_recalcula_el_iva(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-IMP", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000,
|
|
tax_object_id=_obj_imp(db, "02")),
|
|
T, C,
|
|
)
|
|
service.update_item(db, item.id, InvoiceItemUpdate(unit_amount=Decimal("2000")), T, C)
|
|
assert float(_iva_de(db, item.id)[0].amount) == 320.0
|
|
|
|
|
|
def test_quitar_el_objeto_de_impuesto_retira_el_traslado(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-QUITA", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000,
|
|
tax_object_id=_obj_imp(db, "02")),
|
|
T, C,
|
|
)
|
|
assert len(_iva_de(db, item.id)) == 1
|
|
service.update_item(db, item.id, InvoiceItemUpdate(tax_object_id=_obj_imp(db, "01")), T, C)
|
|
assert _iva_de(db, item.id) == []
|
|
|
|
|
|
def test_la_derivacion_no_pisa_una_retencion_capturada(db):
|
|
"""Ajustar los impuestos a mano desactiva el automatismo para esa partida.
|
|
|
|
Es la diferencia entre un valor por defecto útil y un automatismo que borra trabajo ajeno.
|
|
"""
|
|
from api.v1.modules.fin.catalogs.models import Tax
|
|
from api.v1.modules.fin.invoices import taxes_service
|
|
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-RET", tax_rate=Decimal("16")), T, C)
|
|
item = service.create_item(
|
|
db,
|
|
InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=1000,
|
|
tax_object_id=_obj_imp(db, "02")),
|
|
T, C,
|
|
)
|
|
isr = db.query(Tax).filter(Tax.code == "001").first()
|
|
taxes_service.set_item_tax(db, item.id, isr.id, Decimal("0.10"), True, T, C)
|
|
|
|
iva = db.query(Tax).filter(Tax.code == "002").first()
|
|
|
|
# Cambiar el % ya no debe tocar ESTA partida: ni la retención capturada ni el IVA, que se
|
|
# queda con la tasa que tenía cuando se intervino a mano.
|
|
service.update_invoice(db, inv.id, InvoiceUpdate(tax_rate=Decimal("8")), T, C)
|
|
taxes = {t.tax_id: t for t in _iva_de(db, item.id)}
|
|
assert len(taxes) == 2, "se perdió un impuesto capturado a mano"
|
|
assert float(taxes[isr.id].amount) == 100.0, "se pisó la retención"
|
|
# Éste es el assert que distingue: sin el guard, el IVA habría bajado a 0.08 / 80.0.
|
|
assert float(taxes[iva.id].rate) == 0.16, "el automatismo recalculó una partida intervenida"
|
|
assert float(taxes[iva.id].amount) == 160.0
|
|
|
|
|
|
# ── Herencia de los datos de facturación del cliente ─────────────────────────
|
|
#
|
|
# Forma y método de pago 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.
|
|
|
|
def _cliente_con_datos_fiscales(db, **overrides):
|
|
datos = {
|
|
"name": "Importadora Delta",
|
|
"payment_form": "03", # Transferencia electrónica de fondos
|
|
"payment_method": "PPD", # Pago en parcialidades o diferido
|
|
"currency": "MXN",
|
|
}
|
|
datos.update(overrides)
|
|
return accounts_service.create_account(db, AccountCreate(**datos), T, C)
|
|
|
|
|
|
def _clave(db, model, code: str) -> int:
|
|
from api.v1.modules.fin.catalogs import service as catalogs_service
|
|
|
|
fila = catalogs_service.find_by_code(db, model, code)
|
|
assert fila is not None, f"el catálogo de pruebas no tiene la clave {code!r}"
|
|
return fila.id
|
|
|
|
|
|
def test_la_factura_hereda_forma_metodo_y_moneda_del_cliente(db):
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm, PaymentMethod
|
|
|
|
acc = _cliente_con_datos_fiscales(db, currency="USD")
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H1", account_id=acc.id), T, C)
|
|
|
|
assert inv.payment_form_id == _clave(db, PaymentForm, "03")
|
|
assert inv.payment_method_id == _clave(db, PaymentMethod, "PPD")
|
|
assert inv.currency == "USD"
|
|
|
|
|
|
def test_lo_explicito_manda_sobre_la_ficha_del_cliente(db):
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm
|
|
|
|
acc = _cliente_con_datos_fiscales(db)
|
|
efectivo = _clave(db, PaymentForm, "01")
|
|
inv = service.create_invoice(
|
|
db,
|
|
InvoiceCreate(reference="F-H2", account_id=acc.id, payment_form_id=efectivo, currency="EUR"),
|
|
T, C,
|
|
)
|
|
|
|
assert inv.payment_form_id == efectivo, "la ficha del cliente pisó un dato capturado"
|
|
assert inv.currency == "EUR"
|
|
|
|
|
|
def test_una_forma_de_pago_no_resoluble_no_impide_facturar(db):
|
|
"""``crm.accounts.payment_form`` es texto libre sin FK: puede traer basura histórica.
|
|
|
|
Una ficha que diga "Transferencia" en vez de "03" no puede impedir crear una factura. El
|
|
faltante lo reporta después la validación del timbrado, junto al resto de los pendientes.
|
|
"""
|
|
acc = _cliente_con_datos_fiscales(db, payment_form="Transferencia", payment_method="Contado")
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H3", account_id=acc.id), T, C)
|
|
|
|
assert inv.payment_form_id is None
|
|
assert inv.payment_method_id is None
|
|
|
|
|
|
def test_clave_de_forma_de_pago_de_un_digito_se_normaliza(db):
|
|
"""c_FormaPago son dos dígitos: '3' y '03' son la misma forma de pago."""
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm
|
|
|
|
acc = _cliente_con_datos_fiscales(db, payment_form="3", payment_method="pue")
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H4", account_id=acc.id), T, C)
|
|
|
|
assert inv.payment_form_id == _clave(db, PaymentForm, "03")
|
|
assert inv.payment_method_id is not None, "el método de pago en minúsculas debió resolverse"
|
|
|
|
|
|
def test_sin_cliente_no_se_hereda_nada(db):
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H5"), T, C)
|
|
|
|
assert inv.payment_form_id is None
|
|
assert inv.payment_method_id is None
|
|
assert inv.currency == "MXN", "sin ficha ni captura debe mandar el default de la columna"
|
|
|
|
|
|
def test_cambiar_de_cliente_rehereda_los_datos_de_facturacion(db):
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm
|
|
|
|
uno = _cliente_con_datos_fiscales(db, name="Cliente Uno", payment_form="03")
|
|
otro = _cliente_con_datos_fiscales(db, name="Cliente Dos", payment_form="01", currency="USD")
|
|
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H6", account_id=uno.id), T, C)
|
|
assert inv.payment_form_id == _clave(db, PaymentForm, "03")
|
|
|
|
inv = service.update_invoice(db, inv.id, InvoiceUpdate(account_id=otro.id), T, C)
|
|
assert inv.payment_form_id == _clave(db, PaymentForm, "01"), "no se re-heredó al cambiar cliente"
|
|
assert inv.currency == "USD"
|
|
|
|
|
|
def test_al_cambiar_de_cliente_lo_explicito_del_patch_gana(db):
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm
|
|
|
|
uno = _cliente_con_datos_fiscales(db, name="Cliente Uno", payment_form="03")
|
|
otro = _cliente_con_datos_fiscales(db, name="Cliente Dos", payment_form="01")
|
|
cheque = _clave(db, PaymentForm, "02")
|
|
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H7", account_id=uno.id), T, C)
|
|
inv = service.update_invoice(
|
|
db, inv.id, InvoiceUpdate(account_id=otro.id, payment_form_id=cheque), T, C
|
|
)
|
|
|
|
assert inv.payment_form_id == cheque
|
|
|
|
|
|
def test_cambiar_a_un_cliente_sin_datos_no_vacia_lo_capturado(db):
|
|
"""Completar, nunca borrar: una ficha vacía no puede tirar un dato que ya se capturó."""
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm
|
|
|
|
uno = _cliente_con_datos_fiscales(db, name="Cliente Uno", payment_form="03")
|
|
pelon = accounts_service.create_account(db, AccountCreate(name="Cliente Sin Datos"), T, C)
|
|
|
|
inv = service.create_invoice(db, InvoiceCreate(reference="F-H8", account_id=uno.id), T, C)
|
|
inv = service.update_invoice(db, inv.id, InvoiceUpdate(account_id=pelon.id), T, C)
|
|
|
|
assert inv.payment_form_id == _clave(db, PaymentForm, "03")
|
|
assert inv.currency == "MXN"
|
|
|
|
|
|
def test_generate_from_shipment_hereda_datos_de_facturacion(db):
|
|
"""La moneda del EMBARQUE gana sobre la de la ficha: es la que se coteó y se operó."""
|
|
from api.v1.modules.fin.catalogs.models import PaymentForm, PaymentMethod
|
|
|
|
acc = _cliente_con_datos_fiscales(db, currency="EUR")
|
|
quote = quotes_service.create_quote(
|
|
db, QuoteCreate(reference="COT-H", account_id=acc.id, currency="USD"), T, C
|
|
)
|
|
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)
|
|
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.payment_form_id == _clave(db, PaymentForm, "03")
|
|
assert inv.payment_method_id == _clave(db, PaymentMethod, "PPD")
|
|
assert inv.currency == "USD", "la moneda del embarque debe ganar sobre la de la ficha"
|
|
|
|
|
|
# ── Una factura timbrada no admite cambios ───────────────────────────────────
|
|
#
|
|
# El CFDI ya existe ante el SAT: editar la factura después haría que ella y su comprobante
|
|
# contaran cosas distintas. Se corrige cancelando y refacturando.
|
|
|
|
def _timbra(db, invoice) -> None:
|
|
from api.v1.modules.fin.stamping.models import STATUS_STAMPED, InvoiceStamp
|
|
|
|
db.add(
|
|
InvoiceStamp(
|
|
invoice_id=invoice.id, mode="pruebas", status=STATUS_STAMPED,
|
|
uuid="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
|
tenant_id=T, company_id=C,
|
|
)
|
|
)
|
|
db.commit()
|
|
|
|
|
|
def _factura_timbrada(db, reference="F-T1"):
|
|
acc = _cliente_con_datos_fiscales(db)
|
|
inv = service.create_invoice(db, InvoiceCreate(reference=reference, account_id=acc.id), T, C)
|
|
item = service.create_item(
|
|
db, InvoiceItemCreate(invoice_id=inv.id, concept="flete_internacional", quantity=1, unit_amount=1000), T, C
|
|
)
|
|
_timbra(db, inv)
|
|
return inv, item
|
|
|
|
|
|
def test_no_se_puede_cambiar_el_cliente_de_una_factura_timbrada(db):
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
inv, _ = _factura_timbrada(db)
|
|
otro = _cliente_con_datos_fiscales(db, name="Otro")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
service.update_invoice(db, inv.id, InvoiceUpdate(account_id=otro.id), T, C)
|
|
assert exc.value.status_code == 409
|
|
|
|
|
|
def test_una_factura_timbrada_si_admite_notas_y_pagos(db):
|
|
"""El candado es una lista cerrada: cobrar y anotar no alteran el comprobante."""
|
|
inv, _ = _factura_timbrada(db, reference="F-T2")
|
|
|
|
inv = service.update_invoice(db, inv.id, InvoiceUpdate(notes="Pagada por transferencia"), T, C)
|
|
assert inv.notes == "Pagada por transferencia"
|
|
|
|
service.create_payment(db, PaymentCreate(invoice_id=inv.id, amount=Decimal("100")), T, C)
|
|
assert float(service.get_invoice(db, inv.id, T, C).paid_amount) == 100.0
|
|
|
|
|
|
def test_no_se_pueden_tocar_las_partidas_de_una_factura_timbrada(db):
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
inv, item = _factura_timbrada(db, reference="F-T3")
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
service.create_item(
|
|
db, InvoiceItemCreate(invoice_id=inv.id, concept="otros", quantity=1, unit_amount=50), T, C
|
|
)
|
|
assert exc.value.status_code == 409
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
service.update_item(db, item.id, InvoiceItemUpdate(unit_amount=Decimal("2000")), T, C)
|
|
assert exc.value.status_code == 409
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
service.delete_item(db, item.id, T, C)
|
|
assert exc.value.status_code == 409
|
|
|
|
|
|
def test_no_se_puede_editar_el_desglose_de_impuestos_de_una_factura_timbrada(db):
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from api.v1.modules.fin.invoices import taxes_service
|
|
|
|
from api.v1.modules.fin.catalogs.models import Tax
|
|
|
|
inv, item = _factura_timbrada(db, reference="F-T4")
|
|
iva = db.query(Tax).filter(Tax.code == "002").first()
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
taxes_service.set_item_tax(db, item.id, iva.id, Decimal("0.16"), False, T, C)
|
|
assert exc.value.status_code == 409
|