baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
"""批次 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