Files
dealerhub/backend/tests/test_printing_extended.py

243 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""批次 D5 · 打印扩展测试:58mm 小票 / 三联送货单 / 对账单二维码。
重点:二维码必须**可解码回原文**(往返验证),避免交付"看着像二维码但扫不出来"。
"""
import base64
import re
import pytest
from decimal import Decimal
from model_bakery import baker
from rest_framework.test import APIClient
from rest_framework_simplejwt.tokens import RefreshToken
from apps.catalog.models import Product
from apps.inventory.models import Warehouse
from apps.partner.models import Customer
from apps.printing import services as printing_services
from apps.printing.models import PrintTemplate
from apps.printing.qr import qr_svg_data_uri, statement_qr_context, _pick_version
from apps.printing.qr_selftest import decode as qr_decode
from apps.sales import services as sales_services
@pytest.fixture
def auth_client(db, user, tenant):
c = APIClient()
refresh = RefreshToken.for_user(user)
c.credentials(
HTTP_AUTHORIZATION=f"Bearer {refresh.access_token}",
HTTP_X_TENANT_ID=tenant.code,
)
return c
@pytest.fixture
def anon_client(db):
return APIClient()
@pytest.fixture
def warehouse(db, tenant):
return baker.make(Warehouse, tenant=tenant, code="WH01", name="主仓")
@pytest.fixture
def customer(db, tenant):
return baker.make(Customer, tenant=tenant, code="C001", name="张三便利店",
phone="13800000000", credit_limit=Decimal("99999"))
@pytest.fixture
def product(db, tenant):
return baker.make(Product, tenant=tenant, code="P001", name="可乐",
sale_price=Decimal("5"))
def _bill(tenant, warehouse, customer, product, *, round_to=None):
from apps.inventory import services as inv
inv.inbound(tenant=tenant, warehouse=warehouse, product=product,
quantity=Decimal("100"), unit_cost=Decimal("3"))
bill = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 3, "unit_price": 5}],
round_to=round_to,
)
sales_services.confirm_sales_bill(bill)
return bill
@pytest.mark.parametrize("text", [
"/api/v1/open/statements/9eb70e10-82e1-47d1-bc5b-fb3e0716ba19/",
"https://dealerhub.example.com/api/v1/open/statements/c71ac955-27d6-4942-968c-720a6afe5394/",
"HELLO-DEALERHUB-2026",
"1",
"x" * 60,
"对账单-中文测试",
])
def test_qr_roundtrip(text):
"""生成 → 解码 必须完全一致(等价于"能不能真扫出来")。"""
uri = qr_svg_data_uri(text, scale=4)
svg = base64.b64decode(uri.split(",", 1)[1]).decode("utf-8")
assert qr_decode(svg) == text
def test_qr_is_svg_data_uri():
uri = qr_svg_data_uri("test")
assert uri.startswith("data:image/svg+xml;base64,")
svg = base64.b64decode(uri.split(",", 1)[1]).decode()
assert svg.startswith("<svg")
assert "<rect" in svg
def test_qr_version_scales_with_length():
assert _pick_version(10) == 1
assert _pick_version(60) >= 2
assert _pick_version(90) >= 3
def test_qr_rejects_too_long():
with pytest.raises(ValueError):
_pick_version(10000)
def test_statement_qr_context_creates_share(db, tenant, customer):
ctx = statement_qr_context(tenant, customer)
assert ctx is not None
assert ctx["url"].startswith("/api/v1/open/statements/")
assert ctx["data_uri"].startswith("data:image/svg+xml")
svg = base64.b64decode(ctx["data_uri"].split(",", 1)[1]).decode()
assert qr_decode(svg) == ctx["url"]
from apps.finance.models import StatementShare
assert StatementShare.objects.filter(
tenant=tenant, customer=customer, token=ctx["token"]
).exists()
def test_statement_qr_reuses_existing_share(db, tenant, customer):
a = statement_qr_context(tenant, customer)
b = statement_qr_context(tenant, customer)
assert a["token"] == b["token"]
def test_statement_qr_none_without_customer(db, tenant):
assert statement_qr_context(tenant, None) is None
def test_receipt_template_registered(db, tenant):
printing_services.ensure_default_templates(tenant)
codes = set(PrintTemplate.objects.filter(tenant=tenant).values_list("code", flat=True))
assert {"xs-default", "xs-receipt-58", "xs-triplicate"} <= codes
def test_receipt_render_uses_58mm_shell(db, auth_client, tenant, warehouse,
customer, product):
bill = _bill(tenant, warehouse, customer, product)
resp = auth_client.get(
f"/api/v1/printing/render/sales_bill/{bill.id}/?template=xs-receipt-58&autoprint=0"
)
assert resp.status_code == 200, resp.content
html = resp.content.decode("utf-8")
assert "size: 58mm auto" in html
assert "销 售 单" in html
assert "应收合计" in html
assert bill.bill_no in html
def test_receipt_shows_round_off(db, auth_client, tenant, warehouse, customer, product):
"""有小数零头时才显示抹零(3 × 5.17 = 15.51 → 抹到 15,抹零 0.51)。"""
from apps.inventory import services as inv
bill = _bill(tenant, warehouse, customer, product, round_to="1")
# 换一张有零头的单(直接在既有单上加行不便于控制,这里新建)
bill2 = sales_services.create_sales_bill(
tenant=tenant, customer=customer, warehouse=warehouse,
lines=[{"product": product, "quantity": 3, "unit_price": "5.17"}],
round_to="1",
)
assert bill2.round_off == Decimal("0.5100")
resp = auth_client.get(
f"/api/v1/printing/render/sales_bill/{bill2.id}/?template=xs-receipt-58&autoprint=0"
)
html = resp.content.decode("utf-8")
assert "抹零" in html
assert "0.5100" in html or "0.51" in html
def test_triplicate_renders_three_slips(db, auth_client, tenant, warehouse,
customer, product):
bill = _bill(tenant, warehouse, customer, product)
resp = auth_client.get(
f"/api/v1/printing/render/sales_bill/{bill.id}/?template=xs-triplicate&autoprint=0"
)
assert resp.status_code == 200, resp.content
html = resp.content.decode("utf-8")
assert "A4 landscape" in html
for tag in ("存根联", "客户联", "财务联"):
assert tag in html, tag
assert html.count('class="tpl-slip"') == 3
# 单号:标题 1 次 + 三份联次各 1 次
assert html.count(bill.bill_no) == 4
assert html.count("张三便利店") == 3
def test_triplicate_has_line_items(db, auth_client, tenant, warehouse, customer, product):
bill = _bill(tenant, warehouse, customer, product)
resp = auth_client.get(
f"/api/v1/printing/render/sales_bill/{bill.id}/?template=xs-triplicate&autoprint=0"
)
html = resp.content.decode("utf-8")
assert html.count("可乐") == 3
def test_sales_bill_context_has_qr(db, tenant, warehouse, customer, product):
bill = _bill(tenant, warehouse, customer, product)
ctx = printing_services.sales_bill_context(bill)
assert ctx["statement_qr"] is not None
assert ctx["statement_qr"]["data_uri"].startswith("data:image/svg+xml")
def test_sales_bill_render_includes_qr(db, auth_client, tenant, warehouse,
customer, product):
bill = _bill(tenant, warehouse, customer, product)
resp = auth_client.get(f"/api/v1/printing/render/sales_bill/{bill.id}/?autoprint=0")
html = resp.content.decode("utf-8")
assert "qr-block" in html
assert "扫码查看对账单" in html
assert "data:image/svg+xml;base64," in html
def test_qr_code_encodes_working_public_url(db, auth_client, anon_client,
tenant, warehouse, customer, product):
"""端到端:单据二维码 → 解码 URL → 匿名访问该 URL 得 200。"""
bill = _bill(tenant, warehouse, customer, product)
resp = auth_client.get(f"/api/v1/printing/render/sales_bill/{bill.id}/?autoprint=0")
html = resp.content.decode("utf-8")
m = re.search(r'src="data:image/svg\+xml;base64,([^"]+)"', html)
assert m, "未找到二维码"
svg = base64.b64decode(m.group(1)).decode("utf-8")
url = qr_decode(svg)
assert url and url.startswith("/api/v1/open/statements/")
resp2 = anon_client.get(url + "?view=json")
assert resp2.status_code == 200, resp2.content
assert resp2.json()["customer"]["code"] == "C001"
def test_purchase_bill_has_no_qr(db, tenant, warehouse, product):
from apps.partner.models import Supplier
from apps.purchase import services as purchase_services
supplier = baker.make(Supplier, tenant=tenant, code="S001", name="上游厂")
bill = purchase_services.create_purchase_bill(
tenant=tenant, supplier=supplier, warehouse=warehouse,
lines=[{"product": product, "quantity": 5, "unit_price": 3}],
)
ctx = printing_services.purchase_bill_context(bill)
assert "statement_qr" not in ctx