Files
django-vue3-admin/backend/plugins/test/test_sync_misc_material.py
T

795 lines
32 KiB
Python
Raw 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.
"""
杂采料号、核价审核结果 EIP 同步:适配器、SyncManager、HTTP 接口。
数据库:项目以 **SQL Server** 为主;单元测试使用 ``application.test_settings``(复用业务库或
``PIS_TEST_USE_SQLITE=1`` 走 SQLite,见该模块说明)。
使用 pytest + django_db,客户端与 fixtures 与项目 conftest 一致(api_client / authenticate)。
查看 **HTTP 入参 / 出参**(接收请求与返回响应)::
$env:PIS_TEST_USE_SQLITE="1"; pytest plugins/test/test_sync_misc_material.py -s -q
(``-s`` 关闭输出捕获;否则仅在失败时可能看到部分输出。)
"""
import json
import sys
import uuid
import pytest
from rest_framework import status
from apps.pisadmin.basicinfo.models import SupplierUser
from apps.pisadmin.miscprocurement.models import Inquiry, MiscProcMaterial
from apps.pisadmin.procurement.models import ProcMaterial
from sync.adapters.misc_material import MiscMaterialSyncAdapter
from sync.adapters.pricing_result import PricingResultSyncAdapter
from sync.adapters.vnd_quote_perms import VendorQuotePermissionSyncAdapter
from sync.adapters.raw_material import RawMaterialSyncAdapter
from sync.base import SyncStatus
from sync.factory import SyncFactory
from sync.manager import SyncManager
from sync.models import SyncRecord
def _unique_inquiry_no() -> str:
return f"U{uuid.uuid4().hex[:16].upper()}"
def _inquiry_price_audit_pending(
*,
inquiry_no: str,
company_code: str = "VC01",
purchase_type: int = 2,
approval_number: str = "EIP-AUDIT-01",
status: int = 7,
) -> Inquiry:
return Inquiry.objects.create(
inquiry_no=inquiry_no,
title="单元测",
purchase_type=purchase_type,
template="TPL",
buyer="u1",
payment_method=1,
status=status,
company_code=company_code,
approval_number=approval_number,
)
def _pricing_audit_payload(inquiry_no: str, audit_status: str = "APPROVED") -> dict:
return {
"Company": "VC01",
"eipAuditNo": "EIP-AUDIT-01",
"purchase_category": "2",
"inquiryNo": inquiry_no,
"auditStatus": audit_status,
"auditUser": "aud1",
"auditTimestamp": "2026-04-08T10:00:00",
}
def _quote_permission_payload(
supplier_id: str = "SUP001",
supplier_name: str = "测试供应商",
quote_permission: int = 1,
email: str = "test@vendor.com",
name: str = "张三",
companycode: str = "VC01",
) -> dict:
return {
"applyNo": f"APP-{uuid.uuid4().hex[:8].upper()}",
"supplier_id": supplier_id,
"supplier_name": supplier_name,
"quote_permission": quote_permission,
"email": email,
"name": name,
"phone": "13800138000",
"status": "1",
"companycode": companycode,
}
def _raw_material_payload(
company_code: str = "VC01",
material_code: str = "RM-001",
material_name_zh: str = "原料A",
specification: str = "SPEC-001",
unit: str = "KG",
**overrides,
) -> dict:
data = {
"companyCode": company_code,
"materialCode": material_code,
"materialNameZh": material_name_zh,
"materialNameEn": "Raw Material A",
"materialNameVi": "Nguyen Lieu A",
"specification": specification,
"unit": unit,
"sourceCode": "SRC-001",
"modelType": "MODEL-A",
"productCategory": "CATEGORY-A",
"groupCode": "GRP-001",
"materialGroup": "MG-001",
"marketClass": "MC-001",
"approvalStatus": "APPROVED",
"createtime": "2026-04-01T09:00:00",
"updatetime": "2026-04-08T10:00:00",
}
data.update(overrides)
return data
def _safe_print(text: str) -> None:
try:
print(text, flush=True)
except (UnicodeEncodeError, OSError):
pass
def _log_http_roundtrip(title: str, url: str, payload, response) -> None:
"""在终端打印一次「接收到的请求」与「返回的响应」(需 ``pytest -s``)。"""
try:
try:
body_in = json.dumps(payload, ensure_ascii=False, indent=2)
except (TypeError, ValueError):
body_in = repr(payload)
data = getattr(response, "data", None)
if data is not None:
try:
body_out = json.dumps(data, ensure_ascii=False, indent=2, default=str)
except (TypeError, ValueError):
body_out = repr(data)
else:
raw = getattr(response, "content", b"") or b""
body_out = raw.decode("utf-8", errors="replace")[:4000]
sep = "=" * 56
_safe_print(
f"\n{sep}\n"
f" {title}\n"
f" -- 请求 (EIP -> PIS) ------------------------\n"
f" POST {url}\n"
f" Content-Type: application/json\n"
f" body:\n{body_in}\n"
f" -- 响应 (PIS -> EIP) ------------------------\n"
f" status: {response.status_code}\n"
f" body:\n{body_out}\n"
f"{sep}\n"
)
except Exception:
pass
@pytest.mark.django_db
class TestMiscMaterialSyncAdapter:
"""MiscMaterialSyncAdapter:校验、转换、落库。"""
def test_validate_requires_company_and_material(self):
adapter = MiscMaterialSyncAdapter()
assert adapter.validate({}) is False
assert adapter.validate({"companyCode": "A01"}) is False
assert adapter.validate({"materialCode": "P1"}) is False
assert adapter.validate({"companyCode": "A01", "materialCode": "P1"}) is True
assert adapter.validate({"company_code": "A01", "material_code": "P1"}) is True
def test_transform_maps_eip_fields(self):
adapter = MiscMaterialSyncAdapter()
data = {
"companyCode": "VC01",
"materialCode": "M-001",
"materialName": "说明A",
"specification": "SPEC-1",
"unit": "PCS",
"Category": "2",
"accountCode": "6001",
}
t = adapter.transform_to_local(data)
assert t["company_code"] == "VC01"
assert t["partid"] == "M-001"
assert t["partid_name"] == "说明A"
assert t["specification"] == "SPEC-1"
assert t["unit"] == "PCS"
assert t["partid_category_id"] == 2
assert t["external_id"] == "VC01_M-001"
def test_transform_non_numeric_category_goes_to_description(self):
adapter = MiscMaterialSyncAdapter()
data = {
"companyCode": "VC01",
"materialCode": "M-002",
"Category": "模治具",
"accountCode": "ACC1",
}
t = adapter.transform_to_local(data)
assert t["partid_category_id"] is None
assert "模治具" in (t.get("description") or "")
assert "ACC1" in (t.get("description") or "")
def test_push_to_local_creates_misc_proc_material(self):
adapter = MiscMaterialSyncAdapter()
payload = {
"companyCode": "VC01",
"materialCode": "NEW-PART",
"materialNameZh": "新料号",
"specification": "S",
"unit": "EA",
}
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = MiscProcMaterial.objects.get(company_code="VC01", partid="NEW-PART")
assert obj.partid_name == "新料号"
assert obj.specification == "S"
assert obj.unit == "EA"
def test_push_to_local_update_existing(self):
adapter = MiscMaterialSyncAdapter()
MiscProcMaterial.objects.create(
company_code="VC01",
partid="UP-1",
partid_name="旧名",
specification="old",
unit="U",
)
op = adapter.push_to_local(
{
"companyCode": "VC01",
"materialCode": "UP-1",
"materialName": "新名",
"specification": "new",
"unit": "U",
}
)
assert op.status == SyncStatus.SUCCESS
obj = MiscProcMaterial.objects.get(company_code="VC01", partid="UP-1")
assert obj.partid_name == "新名"
assert obj.specification == "new"
@pytest.mark.django_db
class TestSyncManager:
"""SyncManager.process_eip_webhook"""
def test_unknown_adapter(self):
mgr = SyncManager()
out = mgr.process_eip_webhook("no_such_adapter", {})
assert out["Status"] == "fail"
assert "未知" in out["Message"]
def test_success_writes_sync_record(self):
mgr = SyncManager()
payload = {
"companyCode": "VC01",
"materialCode": "SR-1",
"materialName": "记录测试",
"specification": "",
"unit": "",
}
out = mgr.process_eip_webhook("misc_material", payload)
assert out["Status"] == "success"
assert out["Message"] == mgr.SUCCESS_MESSAGE_MISC
assert SyncRecord.objects.filter(adapter_name="misc_material", status="success").exists()
def test_validation_failure_writes_failed_record(self):
mgr = SyncManager()
out = mgr.process_eip_webhook("misc_material", {"companyCode": "X"})
assert out["Status"] == "fail"
assert SyncRecord.objects.filter(adapter_name="misc_material", status="failed").exists()
def test_pricing_audit_success_writes_sync_record(self):
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no)
mgr = SyncManager()
out = mgr.process_pricing_audit_webhook(_pricing_audit_payload(inq_no))
assert out["Status"] is True
assert out["Message"] == "审核结果已接收并更新本地单据状态"
assert SyncRecord.objects.filter(adapter_name="pricing_result", status="success").exists()
assert Inquiry.objects.get(inquiry_no=inq_no).status == 8
def test_pricing_audit_validation_failure_writes_failed_record(self):
mgr = SyncManager()
out = mgr.process_pricing_audit_webhook({"Company": "VC01"})
assert out["Status"] is False
assert SyncRecord.objects.filter(adapter_name="pricing_result", status="failed").exists()
def test_vendor_quote_permission_success_writes_sync_record(self):
mgr = SyncManager()
payload = _quote_permission_payload(supplier_id="VQP-SUP", email="vqp@test.com")
out = mgr.process_eip_webhook("vendor_quote_permission", payload)
assert out["Status"] == "success"
assert SyncRecord.objects.filter(adapter_name="vendor_quote_permission", status="success").exists()
assert SupplierUser.objects.filter(supplier_id="VQP-SUP", user_email="vqp@test.com").exists()
def test_vendor_quote_permission_validation_failure_writes_failed_record(self):
mgr = SyncManager()
out = mgr.process_eip_webhook("vendor_quote_permission", {"applyNo": "X"})
assert out["Status"] == "fail"
assert SyncRecord.objects.filter(adapter_name="vendor_quote_permission", status="failed").exists()
@pytest.mark.django_db
class TestPricingResultSyncAdapter:
"""PricingResultSyncAdapter:校验、转换、落库。"""
def test_validate_requires_all_fields(self):
adapter = PricingResultSyncAdapter()
assert adapter.validate({}) is False
base = _pricing_audit_payload("X1")
assert adapter.validate(base) is True
assert adapter.validate({**base, "Company": ""}) is False
def test_transform_maps_eip_fields(self):
adapter = PricingResultSyncAdapter()
data = {
"Company": "VC01",
"eipAuditNo": "EIP-1",
"purchase_category": "2",
"inquiryNo": "RFQ99",
"auditStatus": "APPROVED",
"auditUser": "u1",
"auditTimestamp": "2026-01-02T08:30:00",
}
t = adapter.transform_to_local(data)
assert t["company"] == "VC01"
assert t["eip_audit_no"] == "EIP-1"
assert t["purchase_category"] == 2
assert t["inquiry_no"] == "RFQ99"
assert t["audit_status"] == "APPROVED"
assert t["external_id"] == "RFQ99_EIP-1"
def test_push_to_local_approved_updates_inquiry(self):
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no)
adapter = PricingResultSyncAdapter()
op = adapter.push_to_local(_pricing_audit_payload(inq_no, "APPROVED"))
assert op.status == SyncStatus.SUCCESS
obj = Inquiry.objects.get(inquiry_no=inq_no)
assert obj.status == 8
assert obj.approval_status == 1
def test_push_to_local_rejected_sets_lost(self):
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no)
adapter = PricingResultSyncAdapter()
op = adapter.push_to_local(_pricing_audit_payload(inq_no, "REJECTED"))
assert op.status == SyncStatus.SUCCESS
obj = Inquiry.objects.get(inquiry_no=inq_no)
assert obj.status == 9
assert obj.approval_status == 2
def test_push_to_local_fails_when_not_price_audit_status(self):
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no, status=5)
adapter = PricingResultSyncAdapter()
op = adapter.push_to_local(_pricing_audit_payload(inq_no))
assert op.status == SyncStatus.FAILED
@pytest.mark.django_db
class TestMiscMaterialSyncAPI:
"""POST /api/sync/materials/misc(DRF force_authenticate 绕过 AK/SK 验签,仅测业务链)。"""
URL = "/api/sync/materials/misc"
def test_post_success(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
payload = {
"companyCode": "VC01",
"materialCode": "API-001",
"materialName": "接口测",
"specification": "SP",
"unit": "KG",
}
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"杂采料号抛转 · 成功", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert "物料信息已成功抛转至PIS" in response.data["Message"]
assert MiscProcMaterial.objects.filter(partid="API-001").exists()
def test_post_validation_error(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
bad = {"companyCode": ""}
response = api_client.post(self.URL, data=bad, format="json")
_log_http_roundtrip("杂采料号抛转 · 校验失败", self.URL, bad, response)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data["Status"] == "fail"
def test_post_allow_any_without_login(self, api_client):
"""EIP 抛转接口当前为 AllowAny:未携带 JWT 也应能调通(与生产验签策略无关时)。"""
payload = {
"companyCode": "VC01",
"materialCode": "NOAUTH-001",
"materialName": "n",
"specification": "",
"unit": "",
}
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"杂采料号抛转 · 未登录可访问 (AllowAny)", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert MiscProcMaterial.objects.filter(partid="NOAUTH-001").exists()
@pytest.mark.django_db
class TestPricingAuditResultSyncAPI:
"""POST /api/pricing/applications/result"""
URL = "/api/pricing/applications/result"
def test_post_success_approved(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no)
payload = _pricing_audit_payload(inq_no)
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"核价审核结果抛转 · 审核通过 APPROVED", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] is True
assert "审核结果已接收" in response.data["Message"]
assert Inquiry.objects.get(inquiry_no=inq_no).status == 8
def test_post_validation_error(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
bad = {"Company": ""}
response = api_client.post(self.URL, data=bad, format="json")
_log_http_roundtrip("核价审核结果抛转 · 校验失败", self.URL, bad, response)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data["Status"] is False
def test_post_business_error_wrong_status(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
inq_no = _unique_inquiry_no()
_inquiry_price_audit_pending(inquiry_no=inq_no, status=5)
payload = _pricing_audit_payload(inq_no)
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"核价审核结果抛转 · 业务失败(询价单非价格审核)", self.URL, payload, response
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data["Status"] is False
@pytest.mark.django_db
def test_sync_factory_registers_eip_adapters():
names = SyncFactory.list_adapters()
assert "misc_material" in names
assert "pricing_result" in names
assert "vendor_quote_permission" in names
assert "raw_material" in names
assert isinstance(SyncFactory.create("misc_material"), MiscMaterialSyncAdapter)
assert isinstance(SyncFactory.create("pricing_result"), PricingResultSyncAdapter)
assert isinstance(SyncFactory.create("vendor_quote_permission"), VendorQuotePermissionSyncAdapter)
assert isinstance(SyncFactory.create("raw_material"), RawMaterialSyncAdapter)
@pytest.mark.django_db
class TestVendorQuotePermissionSyncAdapter:
"""VendorQuotePermissionSyncAdapter:校验、转换、落库。"""
def test_validate_requires_all_required_fields(self):
adapter = VendorQuotePermissionSyncAdapter()
assert adapter.validate({}) is False
assert adapter.validate({"applyNo": "A1"}) is False
assert adapter.validate({"supplier_id": "S1"}) is False
base = _quote_permission_payload()
assert adapter.validate(base) is True
assert adapter.validate({**base, "supplier_id": ""}) is False
assert adapter.validate({**base, "companycode": ""}) is False
def test_validate_accepts_camel_case_fields(self):
adapter = VendorQuotePermissionSyncAdapter()
data = {
"applyNo": "APP-001",
"supplier_id": "SUP001",
"supplier_name": "供应商A",
"quotePermission": 2,
"email": "a@b.com",
"name": "李四",
"companyCode": "VC02",
}
assert adapter.validate(data) is True
def test_transform_maps_eip_fields(self):
adapter = VendorQuotePermissionSyncAdapter()
data = {
"applyNo": "APP-001",
"supplier_id": "SUP001",
"supplier_name": "测试供应商",
"quote_permission": 2,
"email": "test@vendor.com",
"name": "张三",
"phone": "13800138000",
"status": "1",
"companycode": "VC01,VC02",
"createtime": "2026-04-01T09:00:00",
"updatetime": "2026-04-08T10:00:00",
}
t = adapter.transform_to_local(data)
assert t["supplier_id"] == "SUP001"
assert t["supplier_name"] == "测试供应商"
assert t["quote_permission"] == 2
assert t["user_email"] == "test@vendor.com"
assert t["user_name"] == "张三"
assert t["user_phone"] == "13800138000"
assert t["status"] == 1
assert t["company_list"] == ["VC01", "VC02"]
def test_transform_quote_permission_3_maps_correctly(self):
adapter = VendorQuotePermissionSyncAdapter()
data = _quote_permission_payload(quote_permission=3)
t = adapter.transform_to_local(data)
assert t["quote_permission"] == 3
assert t["user_email"] == "test@vendor.com"
def test_transform_quote_permission_role_mapping(self):
adapter = VendorQuotePermissionSyncAdapter()
data1 = _quote_permission_payload(quote_permission=1)
assert adapter.transform_to_local(data1)["quote_permission"] == 1
data2 = _quote_permission_payload(quote_permission=2)
assert adapter.transform_to_local(data2)["quote_permission"] == 2
def test_push_to_local_creates_supplier_user(self):
adapter = VendorQuotePermissionSyncAdapter()
payload = _quote_permission_payload(supplier_id="NEW-SUP", email="new@vendor.com")
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = SupplierUser.objects.get(supplier_id="NEW-SUP", user_email="new@vendor.com")
assert obj.supplier_name == "测试供应商"
assert obj.user_name == "张三"
assert obj.status == 1
assert obj.supplier_role == 3
def test_push_to_local_role_mapping_misc_quote(self):
adapter = VendorQuotePermissionSyncAdapter()
payload = _quote_permission_payload(
supplier_id="MISC-SUP",
email="misc@vendor.com",
quote_permission=2,
)
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = SupplierUser.objects.get(supplier_id="MISC-SUP", user_email="misc@vendor.com")
assert obj.supplier_role == 2
def test_push_to_local_role_mapping_raw_quote(self):
adapter = VendorQuotePermissionSyncAdapter()
payload = _quote_permission_payload(
supplier_id="RAW-SUP",
email="raw@vendor.com",
quote_permission=3,
)
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = SupplierUser.objects.get(supplier_id="RAW-SUP", user_email="raw@vendor.com")
assert obj.supplier_role == 1
def test_push_to_local_update_existing(self):
SupplierUser.objects.create(
supplier_id="EXIST-SUP",
supplier_name="旧名称",
supplier_role=1,
user_email="existing@vendor.com",
user_name="旧用户",
user_phone="111",
status=0,
)
adapter = VendorQuotePermissionSyncAdapter()
op = adapter.push_to_local(
_quote_permission_payload(
supplier_id="EXIST-SUP",
supplier_name="新名称",
email="existing@vendor.com",
name="新用户",
)
)
assert op.status == SyncStatus.SUCCESS
obj = SupplierUser.objects.get(supplier_id="EXIST-SUP", user_email="existing@vendor.com")
assert obj.supplier_name == "新名称"
assert obj.user_name == "新用户"
assert obj.status == 1
def test_push_to_local_multi_company_validation(self):
"""多厂区逗号分隔时会逐个创建,但 SupplierUser.supplier_id + user_email 为唯一键,
故同一 supplier_id 不同 email 会创建多条记录。"""
adapter = VendorQuotePermissionSyncAdapter()
op1 = adapter.push_to_local(
_quote_permission_payload(
supplier_id="MULTI-SUP",
email="user1@vendor.com",
companycode="VC01,VC02,VC03",
)
)
assert op1.status == SyncStatus.SUCCESS
op2 = adapter.push_to_local(
_quote_permission_payload(
supplier_id="MULTI-SUP",
email="user2@vendor.com",
companycode="VC01,VC02",
)
)
assert op2.status == SyncStatus.SUCCESS
assert SupplierUser.objects.filter(supplier_id="MULTI-SUP").count() == 2
@pytest.mark.django_db
class TestVendorQuotePermissionSyncAPI:
"""POST /api/sync/vendors/quote-permissions"""
URL = "/api/sync/vendors/quote-permissions"
def test_post_success(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
payload = _quote_permission_payload(supplier_id="API-SUP", email="api@vendor.com")
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"供应商报价权限抛转 · 成功", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert "权限信息已成功跳转至PIS" in response.data["Message"]
assert SupplierUser.objects.filter(supplier_id="API-SUP", user_email="api@vendor.com").exists()
def test_post_validation_error(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
bad = {"applyNo": "X"}
response = api_client.post(self.URL, data=bad, format="json")
_log_http_roundtrip("供应商报价权限抛转 · 校验失败", self.URL, bad, response)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data["Status"] == "fail"
def test_post_allow_any_without_login(self, api_client):
"""EIP 抛转接口当前为 AllowAny:未携带 JWT 也应能调通。"""
payload = _quote_permission_payload(supplier_id="NOAUTH-SUP", email="noauth@vendor.com")
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"供应商报价权限抛转 · 未登录可访问 (AllowAny)", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert SupplierUser.objects.filter(supplier_id="NOAUTH-SUP", user_email="noauth@vendor.com").exists()
@pytest.mark.django_db
class TestRawMaterialSyncAdapter:
"""RawMaterialSyncAdapter:校验、转换、落库(占位实现)。"""
def test_validate_requires_company_and_material(self):
adapter = RawMaterialSyncAdapter()
assert adapter.validate({}) is False
assert adapter.validate({"companyCode": "A01"}) is False
assert adapter.validate({"materialCode": "P1"}) is False
assert adapter.validate({"companyCode": "A01", "materialCode": "P1"}) is True
assert adapter.validate({"company_code": "A01", "material_code": "P1"}) is True
def test_validate_accepts_snake_case_fields(self):
adapter = RawMaterialSyncAdapter()
data = {
"company_code": "VC01",
"material_code": "RM-001",
}
assert adapter.validate(data) is True
def test_transform_maps_all_eip_fields(self):
adapter = RawMaterialSyncAdapter()
data = _raw_material_payload()
t = adapter.transform_to_local(data)
assert t["company_code"] == "VC01"
assert t["material_code"] == "RM-001"
assert t["material_name_zh"] == "原料A"
assert t["material_name_en"] == "Raw Material A"
assert t["material_name_vi"] == "Nguyen Lieu A"
assert t["specification"] == "SPEC-001"
assert t["unit"] == "KG"
assert t["source_code"] == "SRC-001"
assert t["model_type"] == "MODEL-A"
assert t["product_category"] == "CATEGORY-A"
assert t["group_code"] == "GRP-001"
assert t["material_group"] == "MG-001"
assert t["market_class"] == "MC-001"
assert t["approval_status"] == "APPROVED"
assert t["external_id"] == "VC01_RM-001"
def test_push_to_local_creates_proc_material(self):
adapter = RawMaterialSyncAdapter()
payload = _raw_material_payload(material_code="NEW-PROC-001")
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = ProcMaterial.objects.get(company_code="VC01", material_code="NEW-PROC-001")
assert obj.material_name_zh == "原料A"
assert obj.material_name_en == "Raw Material A"
assert obj.specification == "SPEC-001"
assert obj.unit == "KG"
def test_push_to_local_updates_existing_proc_material(self):
ProcMaterial.objects.create(
company_code="VC01",
material_code="UPDATE-PROC-001",
material_name_zh="旧名称",
specification="old",
unit="U",
)
adapter = RawMaterialSyncAdapter()
op = adapter.push_to_local(
_raw_material_payload(
material_code="UPDATE-PROC-001",
material_name_zh="新名称",
specification="new",
)
)
assert op.status == SyncStatus.SUCCESS
obj = ProcMaterial.objects.get(company_code="VC01", material_code="UPDATE-PROC-001")
assert obj.material_name_zh == "新名称"
assert obj.specification == "new"
def test_push_to_local_maps_all_extended_fields(self):
adapter = RawMaterialSyncAdapter()
payload = _raw_material_payload(
material_code="EXT-FIELD-001",
material_name_zh="扩展字段测试",
)
payload["sourceCode"] = "SRC-TEST"
payload["modelType"] = "MODEL-TEST"
payload["productCategory"] = "CAT-TEST"
payload["groupCode"] = "GRP-TEST"
payload["materialGroup"] = "MG-TEST"
payload["marketClass"] = "MC-TEST"
payload["approvalStatus"] = "PENDING"
op = adapter.push_to_local(payload)
assert op.status == SyncStatus.SUCCESS
obj = ProcMaterial.objects.get(company_code="VC01", material_code="EXT-FIELD-001")
assert obj.source_code == "SRC-TEST"
assert obj.model_type == "MODEL-TEST"
assert obj.product_category == "CAT-TEST"
assert obj.group_code == "GRP-TEST"
assert obj.material_group == "MG-TEST"
assert obj.market_class == "MC-TEST"
assert obj.approval_status == "PENDING"
@pytest.mark.django_db
class TestRawMaterialSyncAPI:
"""POST /api/sync/materials/raw"""
URL = "/api/sync/materials/raw"
def test_post_success(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
payload = _raw_material_payload(material_code="API-RM-001")
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"原料料号抛转 · 成功", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert "物料信息已成功推送至PIS" in response.data["Message"]
assert ProcMaterial.objects.filter(material_code="API-RM-001").exists()
obj = ProcMaterial.objects.get(material_code="API-RM-001")
assert obj.material_name_zh == "原料A"
assert obj.company_code == "VC01"
def test_post_validation_error(self, api_client, admin_user):
api_client.force_authenticate(user=admin_user)
bad = {"companyCode": ""}
response = api_client.post(self.URL, data=bad, format="json")
_log_http_roundtrip("原料料号抛转 · 校验失败", self.URL, bad, response)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.data["Status"] == "fail"
def test_post_allow_any_without_login(self, api_client):
"""EIP 抛转接口当前为 AllowAny:未携带 JWT 也应能调通。"""
payload = _raw_material_payload(material_code="NOAUTH-RM-001")
response = api_client.post(self.URL, data=payload, format="json")
_log_http_roundtrip(
"原料料号抛转 · 未登录可访问 (AllowAny)", self.URL, payload, response
)
assert response.status_code == status.HTTP_200_OK
assert response.data["Status"] == "success"
assert "物料信息已成功推送至PIS" in response.data["Message"]
assert ProcMaterial.objects.filter(material_code="NOAUTH-RM-001").exists()