forked from erp-dev/erp
feat: backfill external order image upgrade to beat schedule by 1 hour
This commit is contained in:
136
api_v1/external_product_image_backfill.py
Normal file
136
api_v1/external_product_image_backfill.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from printing.models import PrintingJob
|
||||
|
||||
|
||||
def extract_request_name(job: PrintingJob) -> str:
|
||||
raw = job.external_raw or {}
|
||||
if isinstance(raw, dict):
|
||||
request_name = str(raw.get("YanSe") or "").strip()
|
||||
if request_name:
|
||||
return request_name
|
||||
return str(job.external_product_name or "").strip()
|
||||
|
||||
|
||||
def run_external_product_image_backfill(
|
||||
*,
|
||||
fetch_image: Callable[[str], dict],
|
||||
upload_image: Callable[[object, dict], None],
|
||||
merchant_id: int | None = None,
|
||||
product_id: int | None = None,
|
||||
limit: int | None = None,
|
||||
dry_run: bool = False,
|
||||
on_success: Callable[[str], None] | None = None,
|
||||
on_error: Callable[[str], None] | None = None,
|
||||
on_info: Callable[[str], None] | None = None,
|
||||
) -> dict:
|
||||
queryset = (
|
||||
PrintingJob.objects.filter(
|
||||
printing_order__external_order_id__isnull=False,
|
||||
)
|
||||
.exclude(printing_order__external_order_id="")
|
||||
.filter(Q(product__image__isnull=True) | Q(product__image=""))
|
||||
.select_related("product", "printing_order", "product__merchant")
|
||||
.order_by("-id")
|
||||
)
|
||||
|
||||
if merchant_id:
|
||||
queryset = queryset.filter(product__merchant_id=merchant_id)
|
||||
|
||||
if product_id:
|
||||
queryset = queryset.filter(product_id=product_id)
|
||||
|
||||
if limit is not None:
|
||||
limit = int(limit)
|
||||
if limit <= 0:
|
||||
raise ValueError("limit must be greater than 0")
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
skipped_with_image_count = 0
|
||||
skipped_without_request_name_count = 0
|
||||
skipped_duplicate_product_count = 0
|
||||
selected_count = 0
|
||||
scanned_count = 0
|
||||
seen_product_ids = set()
|
||||
|
||||
for job in queryset.iterator(chunk_size=1000):
|
||||
scanned_count += 1
|
||||
product = job.product
|
||||
if product.image:
|
||||
skipped_with_image_count += 1
|
||||
continue
|
||||
|
||||
if product.id in seen_product_ids:
|
||||
skipped_duplicate_product_count += 1
|
||||
continue
|
||||
|
||||
request_name = extract_request_name(job)
|
||||
if not request_name:
|
||||
skipped_without_request_name_count += 1
|
||||
continue
|
||||
|
||||
seen_product_ids.add(product.id)
|
||||
selected_count += 1
|
||||
|
||||
if dry_run:
|
||||
if on_info:
|
||||
on_info(
|
||||
"[DRY-RUN] "
|
||||
f"job_id={job.id} "
|
||||
f"product_id={product.id} "
|
||||
f"merchant_id={product.merchant_id} "
|
||||
f"product_name={product.name} "
|
||||
f"request_name={request_name}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
image_payload = fetch_image(request_name)
|
||||
upload_image(product, image_payload)
|
||||
except Exception as exc:
|
||||
failed_count += 1
|
||||
if on_error:
|
||||
on_error(
|
||||
"补图失败: "
|
||||
f"job_id={job.id} "
|
||||
f"product_id={product.id} "
|
||||
f"merchant_id={product.merchant_id} "
|
||||
f"product_name={product.name} "
|
||||
f"request_name={request_name} "
|
||||
f"error={exc}"
|
||||
)
|
||||
else:
|
||||
success_count += 1
|
||||
if on_success:
|
||||
on_success(
|
||||
"补图成功: "
|
||||
f"job_id={job.id} "
|
||||
f"product_id={product.id} "
|
||||
f"merchant_id={product.merchant_id} "
|
||||
f"product_name={product.name} "
|
||||
f"request_name={request_name}"
|
||||
)
|
||||
|
||||
if limit is not None and selected_count >= limit:
|
||||
break
|
||||
|
||||
payload = {
|
||||
"scanned": scanned_count,
|
||||
"selected": selected_count,
|
||||
"success": success_count,
|
||||
"failed": failed_count,
|
||||
"skipped_with_image": skipped_with_image_count,
|
||||
"skipped_without_request_name": skipped_without_request_name_count,
|
||||
"skipped_duplicate_product": skipped_duplicate_product_count,
|
||||
"merchant_id": merchant_id,
|
||||
"product_id": product_id,
|
||||
"limit": limit,
|
||||
"dry_run": dry_run,
|
||||
}
|
||||
|
||||
if dry_run and on_info:
|
||||
on_info("dry-run 完成")
|
||||
|
||||
return payload
|
||||
@@ -1,8 +1,7 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db.models import Q
|
||||
|
||||
from api_v1.external_product_image_backfill import run_external_product_image_backfill
|
||||
from api_v1.tasks import _fetch_external_product_image, _upload_product_image
|
||||
from printing.models import PrintingJob
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@@ -18,114 +17,25 @@ class Command(BaseCommand):
|
||||
help='仅输出将处理的产品,不实际请求外部 API 或写库',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_request_name(job: PrintingJob) -> str:
|
||||
raw = job.external_raw or {}
|
||||
if isinstance(raw, dict):
|
||||
request_name = str(raw.get('YanSe') or '').strip()
|
||||
if request_name:
|
||||
return request_name
|
||||
return str(job.external_product_name or '').strip()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
queryset = (
|
||||
PrintingJob.objects.filter(
|
||||
printing_order__external_order_id__isnull=False,
|
||||
)
|
||||
.exclude(printing_order__external_order_id='')
|
||||
.filter(Q(product__image__isnull=True) | Q(product__image=''))
|
||||
.select_related('product', 'printing_order', 'product__merchant')
|
||||
.order_by('-id')
|
||||
)
|
||||
|
||||
merchant_id = options.get('merchant_id')
|
||||
if merchant_id:
|
||||
queryset = queryset.filter(product__merchant_id=merchant_id)
|
||||
|
||||
product_id = options.get('product_id')
|
||||
if product_id:
|
||||
queryset = queryset.filter(product_id=product_id)
|
||||
|
||||
limit = options.get('limit')
|
||||
if limit is not None:
|
||||
if int(limit) <= 0:
|
||||
raise CommandError('--limit 必须大于 0')
|
||||
limit = int(limit)
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
skipped_with_image_count = 0
|
||||
skipped_without_request_name_count = 0
|
||||
skipped_duplicate_product_count = 0
|
||||
selected_count = 0
|
||||
scanned_count = 0
|
||||
dry_run = bool(options.get('dry_run'))
|
||||
seen_product_ids = set()
|
||||
|
||||
for job in queryset.iterator(chunk_size=1000):
|
||||
scanned_count += 1
|
||||
product = job.product
|
||||
if product.image:
|
||||
skipped_with_image_count += 1
|
||||
continue
|
||||
|
||||
if product.id in seen_product_ids:
|
||||
skipped_duplicate_product_count += 1
|
||||
continue
|
||||
|
||||
request_name = self._extract_request_name(job)
|
||||
if not request_name:
|
||||
skipped_without_request_name_count += 1
|
||||
continue
|
||||
|
||||
seen_product_ids.add(product.id)
|
||||
|
||||
selected_count += 1
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
'[DRY-RUN] '
|
||||
f'job_id={job.id} '
|
||||
f'product_id={product.id} '
|
||||
f'merchant_id={product.merchant_id} '
|
||||
f'product_name={product.name} '
|
||||
f'request_name={request_name}'
|
||||
)
|
||||
else:
|
||||
try:
|
||||
image_payload = _fetch_external_product_image(request_name)
|
||||
_upload_product_image(product, image_payload)
|
||||
except Exception as exc:
|
||||
failed_count += 1
|
||||
self.stderr.write(
|
||||
self.style.ERROR(
|
||||
f'补图失败: job_id={job.id} product_id={product.id} merchant_id={product.merchant_id} '
|
||||
f'product_name={product.name} request_name={request_name} error={exc}'
|
||||
)
|
||||
)
|
||||
else:
|
||||
success_count += 1
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'补图成功: job_id={job.id} product_id={product.id} merchant_id={product.merchant_id} '
|
||||
f'product_name={product.name} request_name={request_name}'
|
||||
)
|
||||
)
|
||||
|
||||
if limit is not None and selected_count >= limit:
|
||||
break
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.SUCCESS('dry-run 完成'))
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
'完成: '
|
||||
f'scanned={scanned_count} '
|
||||
f'selected={selected_count} '
|
||||
f'success={success_count} '
|
||||
f'failed={failed_count} '
|
||||
f'skipped_with_image={skipped_with_image_count} '
|
||||
f'skipped_without_request_name={skipped_without_request_name_count} '
|
||||
f'skipped_duplicate_product={skipped_duplicate_product_count}'
|
||||
)
|
||||
payload = run_external_product_image_backfill(
|
||||
fetch_image=_fetch_external_product_image,
|
||||
upload_image=_upload_product_image,
|
||||
merchant_id=merchant_id,
|
||||
product_id=product_id,
|
||||
limit=limit,
|
||||
dry_run=bool(options.get('dry_run')),
|
||||
on_success=lambda message: self.stdout.write(self.style.SUCCESS(message)),
|
||||
on_error=lambda message: self.stderr.write(self.style.ERROR(message)),
|
||||
on_info=lambda message: self.stdout.write(message),
|
||||
)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'完成: {payload}'))
|
||||
|
||||
@@ -31,6 +31,7 @@ from api_v1.mdy_plate_order_staging_tiia_upload import (
|
||||
build_default_tiia_rate_limiter,
|
||||
upload_mdy_plate_order_staging_plate_images_to_tencent_tiia,
|
||||
)
|
||||
from api_v1.external_product_image_backfill import run_external_product_image_backfill
|
||||
from printing import models as printing_models
|
||||
|
||||
|
||||
@@ -424,6 +425,37 @@ def _upload_product_image(product, image_payload: dict):
|
||||
product.save(update_fields=['image', 'updated_at'])
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def backfill_external_product_images(
|
||||
self,
|
||||
merchant_id: int | None = None,
|
||||
product_id: int | None = None,
|
||||
limit: int = 500,
|
||||
dry_run: bool = False,
|
||||
):
|
||||
"""
|
||||
为来自外部印染订单的产品补图。
|
||||
|
||||
仅处理:
|
||||
- printing_order.external_order_id 非空
|
||||
- product.image 为空
|
||||
"""
|
||||
payload = run_external_product_image_backfill(
|
||||
fetch_image=_fetch_external_product_image,
|
||||
upload_image=_upload_product_image,
|
||||
merchant_id=merchant_id,
|
||||
product_id=product_id,
|
||||
limit=max(1, int(limit or 500)),
|
||||
dry_run=bool(dry_run),
|
||||
on_success=lambda message: logger.info(message),
|
||||
on_error=lambda message: logger.error(message),
|
||||
on_info=lambda message: logger.info(message),
|
||||
)
|
||||
payload["task_id"] = self.request.id
|
||||
logger.info("外部印染订单产品补图完成: %s", payload)
|
||||
return payload
|
||||
|
||||
|
||||
def _record_printing_external_sync_failure(*, run_date, record: dict, error: str):
|
||||
external_record_id = _extract_positive_int(record.get('ID'), field_name='ID', allow_blank=False)
|
||||
obj, _created = api_models.PrintingExternalSyncFailure.objects.get_or_create(
|
||||
|
||||
@@ -33,6 +33,7 @@ from .views.parameters import StateParameterViewSet
|
||||
from .views.users import CreateUserWithProfileView
|
||||
from .views.mingdaoyun import MDYPlateOrderStagingViewSet
|
||||
from .views.shipment import (
|
||||
SalesItemDetailView,
|
||||
SalesItemByCustomerView,
|
||||
SalesItemByPrintingOrderView,
|
||||
SalesItemCreateView,
|
||||
@@ -322,6 +323,11 @@ urlpatterns = [
|
||||
SalesItemByPrintingOrderView.as_view(),
|
||||
name="sales_items_by_printing_order",
|
||||
),
|
||||
path(
|
||||
"shipment/sales-items/<int:pk>/",
|
||||
SalesItemDetailView.as_view(),
|
||||
name="sales_item_detail",
|
||||
),
|
||||
path(
|
||||
"shipment/sales-items/", SalesItemCreateView.as_view(), name="sales_item_create"
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ Shipment API 模块
|
||||
提供出货单和销售品相关的 API 接口
|
||||
"""
|
||||
from .views import (
|
||||
SalesItemDetailView,
|
||||
SalesItemByCustomerView,
|
||||
SalesItemByPrintingOrderView,
|
||||
SalesItemCreateView,
|
||||
@@ -14,6 +15,7 @@ from .views import (
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'SalesItemDetailView',
|
||||
'SalesItemByCustomerView',
|
||||
'SalesItemByPrintingOrderView',
|
||||
'SalesItemCreateView',
|
||||
|
||||
@@ -249,6 +249,7 @@ class SalesItemSerializer(serializers.Serializer):
|
||||
remark = serializers.CharField(read_only=True)
|
||||
printing_job_id = serializers.IntegerField(read_only=True)
|
||||
printing_order_id = serializers.SerializerMethodField()
|
||||
external_order_id = serializers.SerializerMethodField()
|
||||
customer_id = serializers.IntegerField(read_only=True)
|
||||
customer_name = serializers.SerializerMethodField()
|
||||
shipment_id = serializers.IntegerField(
|
||||
@@ -276,6 +277,16 @@ class SalesItemSerializer(serializers.Serializer):
|
||||
return printing_job.printing_order_id
|
||||
return None
|
||||
|
||||
def get_external_order_id(self, obj):
|
||||
external_order_id_map = self.context.get("external_order_id_map", {})
|
||||
if obj.printing_job_id in external_order_id_map:
|
||||
return external_order_id_map[obj.printing_job_id]
|
||||
|
||||
printing_job = obj.get_printing_job()
|
||||
if printing_job and getattr(printing_job.printing_order, "external_order_id", None):
|
||||
return printing_job.printing_order.external_order_id
|
||||
return None
|
||||
|
||||
def get_customer_name(self, obj):
|
||||
customer_name_map = self.context.get("customer_name_map", {})
|
||||
if obj.customer_id in customer_name_map:
|
||||
@@ -293,6 +304,36 @@ class SalesItemSerializer(serializers.Serializer):
|
||||
return None
|
||||
|
||||
|
||||
class SalesItemDetailSerializer(SalesItemSerializer):
|
||||
"""
|
||||
销售品详情序列化器。
|
||||
|
||||
在列表字段基础上补充关联印染任务的产品图片。
|
||||
"""
|
||||
|
||||
product_image_url = serializers.SerializerMethodField()
|
||||
|
||||
def get_product_image_url(self, obj):
|
||||
product_image_map = self.context.get("product_image_map", {})
|
||||
if obj.printing_job_id in product_image_map:
|
||||
return product_image_map[obj.printing_job_id]
|
||||
|
||||
printing_job = obj.get_printing_job()
|
||||
if not printing_job or not getattr(printing_job, "product", None):
|
||||
return None
|
||||
|
||||
primary_url = printing_job.product.get_primary_image_url()
|
||||
if primary_url:
|
||||
return primary_url
|
||||
|
||||
if printing_job.product.image:
|
||||
request = self.context.get("request")
|
||||
if request:
|
||||
return request.build_absolute_uri(printing_job.product.image.url)
|
||||
return printing_job.product.image.url
|
||||
return None
|
||||
|
||||
|
||||
class ShipmentSalesItemCustomerSerializer(serializers.Serializer):
|
||||
"""
|
||||
由未出货销售品反推的客户摘要序列化器。
|
||||
|
||||
@@ -81,6 +81,7 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
width="150cm",
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
external_order_id="EXT-PO-001",
|
||||
)
|
||||
|
||||
# 创建印染任务
|
||||
@@ -216,6 +217,8 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
self.assertEqual(item["position"], "A1-01")
|
||||
self.assertEqual(item["remark"], "备注信息")
|
||||
self.assertEqual(item["printing_job_id"], self.printing_job1.id)
|
||||
self.assertEqual(item["printing_order_id"], self.printing_order.id)
|
||||
self.assertEqual(item["external_order_id"], "EXT-PO-001")
|
||||
self.assertIsNone(item["shipment_id"])
|
||||
self.assertIsNone(item["shipment_date"])
|
||||
self.assertIsNotNone(item["created_at"])
|
||||
@@ -484,6 +487,7 @@ class SalesItemByCustomerAPITestCase(TestCase):
|
||||
width="150cm",
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
external_order_id="EXT-CUST-001",
|
||||
)
|
||||
self.printing_job = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
@@ -555,6 +559,7 @@ class SalesItemByCustomerAPITestCase(TestCase):
|
||||
self.assertEqual(item["customer_name"], self.customer.name)
|
||||
self.assertEqual(item["printing_job_id"], self.printing_job.id)
|
||||
self.assertEqual(item["printing_order_id"], self.printing_order.id)
|
||||
self.assertEqual(item["external_order_id"], "EXT-CUST-001")
|
||||
self.assertEqual(item["position"], "A1-01")
|
||||
|
||||
def test_get_sales_items_by_customer_include_shipped_and_paginate(self):
|
||||
@@ -571,6 +576,45 @@ class SalesItemByCustomerAPITestCase(TestCase):
|
||||
result_ids = [item["id"] for item in data["results"]]
|
||||
self.assertEqual(result_ids, [self.sales_item2.id, self.sales_item_shipped.id])
|
||||
|
||||
def test_get_sales_items_by_customer_supports_external_order_id_filter(self):
|
||||
other_printing_order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric="其它面料",
|
||||
width="160cm",
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
external_order_id="EXT-CUST-OTHER",
|
||||
)
|
||||
other_printing_job = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
printing_order=other_printing_order,
|
||||
product=self.product,
|
||||
quantity=50,
|
||||
unit="米",
|
||||
created_by=self.user,
|
||||
)
|
||||
other_sales_item = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="其它外部订单销售品",
|
||||
quantity=Decimal("66.00"),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=other_printing_job.id,
|
||||
customer_id=self.customer.id,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
resp = self.client.get(
|
||||
f"/api/v1/shipment/sales-items/by-customer/{self.customer.id}/"
|
||||
"?external_order_id=EXT-CUST-001"
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
data = resp.json()
|
||||
result_ids = [item["id"] for item in data["results"]]
|
||||
self.assertEqual(set(result_ids), {self.sales_item1.id, self.sales_item2.id})
|
||||
self.assertNotIn(other_sales_item.id, result_ids)
|
||||
|
||||
def test_get_sales_items_by_customer_not_found(self):
|
||||
resp = self.client.get("/api/v1/shipment/sales-items/by-customer/99999/")
|
||||
|
||||
@@ -584,6 +628,112 @@ class SalesItemByCustomerAPITestCase(TestCase):
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class SalesItemDetailAPITestCase(APITestCase):
|
||||
"""销售品详情 API 测试"""
|
||||
|
||||
def setUp(self):
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name="详情测试商户", type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
self.other_merchant = basic_models.Merchant.objects.create(
|
||||
name="其它详情测试商户", type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
|
||||
self.user = User.objects.create_user(
|
||||
username="sales_item_detail_user",
|
||||
password="testpass123",
|
||||
email="sales_item_detail@example.com",
|
||||
)
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name="详情测试员工",
|
||||
mobile="13800138031",
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
|
||||
self.other_user = User.objects.create_user(
|
||||
username="sales_item_detail_other_user",
|
||||
password="testpass123",
|
||||
email="sales_item_detail_other@example.com",
|
||||
)
|
||||
self.other_employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.other_user,
|
||||
merchant=self.other_merchant,
|
||||
name="其它详情测试员工",
|
||||
mobile="13800138032",
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="详情测试客户",
|
||||
mobile="13900139033",
|
||||
area="杭州",
|
||||
)
|
||||
self.category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="详情分类",
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name="详情产品",
|
||||
human_id="DETAIL001",
|
||||
mdy_image_url="https://example.com/product-image.jpg",
|
||||
)
|
||||
self.state = stateflow_models.State.objects.create(name="详情状态")
|
||||
self.process = stateflow_models.Process.objects.create(name="详情流程")
|
||||
self.process.replace_nodes([self.state])
|
||||
self.printing_order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric="详情面料",
|
||||
width="150cm",
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
external_order_id="EXT-DETAIL-001",
|
||||
)
|
||||
self.printing_job = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
printing_order=self.printing_order,
|
||||
product=self.product,
|
||||
quantity=20,
|
||||
unit="米",
|
||||
created_by=self.user,
|
||||
)
|
||||
self.sales_item = shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="详情销售品",
|
||||
quantity=Decimal("88.00"),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=self.printing_job.id,
|
||||
customer_id=self.customer.id,
|
||||
position="B2-03",
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_get_sales_item_detail_success(self):
|
||||
resp = self.client.get(f"/api/v1/shipment/sales-items/{self.sales_item.id}/")
|
||||
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["id"], self.sales_item.id)
|
||||
self.assertEqual(data["printing_order_id"], self.printing_order.id)
|
||||
self.assertEqual(data["external_order_id"], "EXT-DETAIL-001")
|
||||
self.assertEqual(data["customer_name"], self.customer.name)
|
||||
self.assertEqual(data["product_image_url"], "https://example.com/product-image.jpg")
|
||||
|
||||
def test_get_sales_item_detail_other_merchant_404(self):
|
||||
self.client.force_authenticate(user=self.other_user)
|
||||
|
||||
resp = self.client.get(f"/api/v1/shipment/sales-items/{self.sales_item.id}/")
|
||||
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
class ShipmentCreateAPITestCase(TestCase):
|
||||
"""测试创建出货单 API"""
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from flower.viewsets import LimitedLimitOffsetPagination
|
||||
from shipment.models import Shipment
|
||||
|
||||
from .serializers import (
|
||||
SalesItemDetailSerializer,
|
||||
SalesItemSerializer,
|
||||
ShipmentSerializer,
|
||||
ShipmentCreateNormalSerializer,
|
||||
@@ -35,18 +36,39 @@ def _build_sales_item_serializer_context(items):
|
||||
)
|
||||
|
||||
printing_order_map = {}
|
||||
external_order_id_map = {}
|
||||
product_image_map = {}
|
||||
if printing_job_ids:
|
||||
from printing.models import PrintingJob
|
||||
|
||||
printing_order_map = dict(
|
||||
PrintingJob.objects.filter(id__in=printing_job_ids).values_list(
|
||||
"id", "printing_order_id"
|
||||
)
|
||||
printing_jobs = list(
|
||||
PrintingJob.objects.filter(id__in=printing_job_ids)
|
||||
.select_related("printing_order", "product")
|
||||
)
|
||||
printing_order_map = {
|
||||
job.id: job.printing_order_id for job in printing_jobs
|
||||
}
|
||||
external_order_id_map = {
|
||||
job.id: getattr(job.printing_order, "external_order_id", None)
|
||||
for job in printing_jobs
|
||||
}
|
||||
for job in printing_jobs:
|
||||
if not getattr(job, "product", None):
|
||||
product_image_map[job.id] = None
|
||||
continue
|
||||
primary_url = job.product.get_primary_image_url()
|
||||
if primary_url:
|
||||
product_image_map[job.id] = primary_url
|
||||
elif job.product.image:
|
||||
product_image_map[job.id] = job.product.image.url
|
||||
else:
|
||||
product_image_map[job.id] = None
|
||||
|
||||
return {
|
||||
"customer_name_map": customer_name_map,
|
||||
"printing_order_map": printing_order_map,
|
||||
"external_order_id_map": external_order_id_map,
|
||||
"product_image_map": product_image_map,
|
||||
}
|
||||
|
||||
|
||||
@@ -444,6 +466,7 @@ class SalesItemByCustomerView(GenericAPIView):
|
||||
request.query_params.get("include_already_has_shipment", "false").lower()
|
||||
== "true"
|
||||
)
|
||||
external_order_id = (request.query_params.get("external_order_id") or "").strip()
|
||||
|
||||
from shipment.services import get_sales_items_by_customer
|
||||
|
||||
@@ -451,6 +474,7 @@ class SalesItemByCustomerView(GenericAPIView):
|
||||
merchant=customer.merchant,
|
||||
customer_id=customer.id,
|
||||
include_already_has_shipment=include_already_has_shipment,
|
||||
external_order_id=external_order_id or None,
|
||||
)
|
||||
page = self.paginate_queryset(queryset)
|
||||
items = list(page) if page is not None else list(queryset)
|
||||
@@ -529,5 +553,48 @@ class SalesItemCreateView(APIView):
|
||||
return Response({"detail": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 返回创建的销售品
|
||||
response_serializer = SalesItemSerializer(sales_item)
|
||||
response_serializer = SalesItemSerializer(
|
||||
sales_item,
|
||||
context=_build_sales_item_serializer_context([sales_item]),
|
||||
)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class SalesItemDetailView(GenericAPIView):
|
||||
"""
|
||||
销售品详情。
|
||||
|
||||
GET /api/v1/shipment/sales-items/<id>/
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = SalesItemDetailSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
from shipment.models import SalesItem
|
||||
|
||||
qs = SalesItem.objects.select_related("shipment", "created_by").order_by("id")
|
||||
|
||||
user = self.request.user
|
||||
if getattr(user, "is_superuser", False):
|
||||
return qs
|
||||
|
||||
emp = getattr(user, "employee", None)
|
||||
merchant = getattr(emp, "merchant", None) if emp else None
|
||||
if not merchant:
|
||||
return SalesItem.objects.none()
|
||||
return qs.filter(merchant=merchant)
|
||||
|
||||
def get(self, request, pk: int):
|
||||
sales_item = self.get_queryset().filter(id=pk).first()
|
||||
if sales_item is None:
|
||||
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
sales_item,
|
||||
context={
|
||||
**_build_sales_item_serializer_context([sales_item]),
|
||||
"request": request,
|
||||
},
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
Reference in New Issue
Block a user