1
0
forked from erp-dev/erp

feat: hpname and external_order_id + product_name unique together

This commit is contained in:
2026-03-23 19:06:11 +08:00
parent b58667ca52
commit eb5fae1ce7
6 changed files with 251 additions and 10 deletions

View File

@@ -0,0 +1,90 @@
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from api_v1.tasks import _fetch_external_product_image, _upload_product_image
from basic_info.models import Product
class Command(BaseCommand):
help = '为没有图片的 Product 通过外部 image API 回补图片(使用 name_b64=base64url(product.name)'
def add_arguments(self, parser):
parser.add_argument('--merchant-id', type=int, help='仅处理指定商户的产品')
parser.add_argument('--product-id', type=int, help='仅处理指定产品')
parser.add_argument('--limit', type=int, help='最多处理多少条产品')
parser.add_argument(
'--dry-run',
action='store_true',
help='仅输出将处理的产品,不实际请求外部 API 或写库',
)
def handle(self, *args, **options):
queryset = Product.objects.filter(
Q(image__isnull=True) | Q(image=''),
).exclude(
name__isnull=True,
).exclude(
name='',
).select_related('merchant')
merchant_id = options.get('merchant_id')
if merchant_id:
queryset = queryset.filter(merchant_id=merchant_id)
product_id = options.get('product_id')
if product_id:
queryset = queryset.filter(id=product_id)
queryset = queryset.order_by('id')
limit = options.get('limit')
if limit is not None:
if int(limit) <= 0:
raise CommandError('--limit 必须大于 0')
queryset = queryset[: int(limit)]
products = list(queryset)
self.stdout.write(f'待处理产品数: {len(products)}')
if options.get('dry_run'):
for product in products:
self.stdout.write(
f'[DRY-RUN] product_id={product.id} merchant_id={product.merchant_id} name={product.name}'
)
self.stdout.write(self.style.SUCCESS('dry-run 完成'))
return
success_count = 0
failed_count = 0
skipped_count = 0
for product in products:
if product.image:
skipped_count += 1
continue
try:
image_payload = _fetch_external_product_image(product.name)
_upload_product_image(product, image_payload)
except Exception as exc:
failed_count += 1
self.stderr.write(
self.style.ERROR(
f'补图失败: product_id={product.id} merchant_id={product.merchant_id} '
f'name={product.name} error={exc}'
)
)
continue
success_count += 1
self.stdout.write(
self.style.SUCCESS(
f'补图成功: product_id={product.id} merchant_id={product.merchant_id} name={product.name}'
)
)
self.stdout.write(
self.style.SUCCESS(
f'完成: total={len(products)} success={success_count} failed={failed_count} skipped={skipped_count}'
)
)

View File

@@ -500,12 +500,13 @@ def _build_external_order_data(
return {
'merchant': merchant,
'customer': customer,
'fabric': '',
'fabric': str(first.get('HpName') or '').strip(),
'width': str(first.get('SeHao') or '').strip(),
'fabric_source': fabric_source or None,
'craft': craft or None,
'rolling_warn': str(first.get('BeiZhu') or '').strip() or None,
'curve': str(first.get('MeoA') or '').strip() or None,
'position': str(first.get('FidJ') or '').strip() or None,
'outgoing_date': _parse_external_datetime(first.get('KdRiQi')),
'created_by': created_by_user,
'external_order_id': external_order_id,
@@ -547,6 +548,7 @@ def _upsert_external_printing_order(*, merchant, external_order_id: str, group_r
def _upsert_external_printing_job(*, merchant, printing_order, record: dict, sync_user, category):
record_id = _extract_positive_int(record.get('ID'), field_name='ID', allow_blank=False)
external_product_name = str(record.get('YanSe') or '').strip()
created_by_user, _external_employee_name = _resolve_external_employee_user(
merchant=merchant,
external_employee_name=str(record.get('CaoZY') or '').strip(),
@@ -554,7 +556,7 @@ def _upsert_external_printing_job(*, merchant, printing_order, record: dict, syn
)
product, _ = _ensure_external_product(
merchant=merchant,
external_product_name=str(record.get('YanSe') or '').strip(),
external_product_name=external_product_name,
category=category,
)
job_data = {
@@ -574,7 +576,7 @@ def _upsert_external_printing_job(*, merchant, printing_order, record: dict, syn
existing_job = (
printing_models.PrintingJob.objects.filter(
printing_order=printing_order,
original_id=record_id,
product=product,
)
.order_by('id')
.first()

View File

@@ -71,6 +71,7 @@ class ExternalPrintingRecordsSyncTaskTest(TestCase):
'BianHaoKD': '1.27',
'CaoZY': '钰涵',
'FidJ': r'\\fw\\2026年-LWQ15\\2026\\H鸿烨\\Tj1712#',
'HpName': '120克本白四面弹单定',
'ID': record_id,
'JiJiaDW': '/段',
'KdRiQi': '2026-01-26T20:00:52Z',
@@ -153,6 +154,36 @@ class ExternalPrintingRecordsSyncTaskTest(TestCase):
job = printing_models.PrintingJob.objects.get(original_id=1000001)
self.assertEqual(job.product, product)
self.assertEqual(job.printing_order.position, r'\\fw\\2026年-LWQ15\\2026\\H鸿烨\\Tj1712#')
self.assertEqual(job.printing_order.fabric, '120克本白四面弹单定')
@patch('api_v1.tasks._advance_external_printing_cursor')
@patch('api_v1.tasks._fetch_external_product_image')
@patch('api_v1.tasks._fetch_external_printing_records')
def test_order_position_uses_first_record_fidj(
self,
mock_fetch_records,
mock_fetch_image,
mock_advance_cursor,
):
first_record = self._build_record(record_id=1000000, product_name=self.existing_product.name)
second_record = self._build_record(record_id=1000001, product_name=self.existing_product.name)
first_record['FidJ'] = r'\\fw\\path\\first'
second_record['FidJ'] = r'\\fw\\path\\second'
mock_fetch_records.return_value = self._build_payload([first_record, second_record])
mock_advance_cursor.return_value = {'updated': True}
result = sync_external_printing_records.run(limit=100)
self.assertEqual(result['orders_created'], 1)
self.assertEqual(result['jobs_created'], 1)
self.assertEqual(result['jobs_updated'], 1)
self.assertEqual(mock_fetch_image.call_count, 0)
order = printing_models.PrintingOrder.objects.get(external_order_id='KD20432358')
self.assertEqual(order.position, r'\\fw\\path\\first')
self.assertEqual(order.fabric, '120克本白四面弹单定')
self.assertEqual(printing_models.PrintingJob.objects.count(), 1)
@patch('api_v1.tasks._advance_external_printing_cursor')
@patch('api_v1.tasks._fetch_external_product_image')
@@ -192,3 +223,40 @@ class ExternalPrintingRecordsSyncTaskTest(TestCase):
)
self.assertEqual(failure.external_order_id, 'KD20432358')
self.assertIn('外部图片不存在', failure.error)
@patch('api_v1.tasks._advance_external_printing_cursor')
@patch('api_v1.tasks._fetch_external_product_image')
@patch('api_v1.tasks._fetch_external_printing_records')
def test_same_order_and_product_overwrites_existing_job_instead_of_creating_new_one(
self,
mock_fetch_records,
mock_fetch_image,
mock_advance_cursor,
):
initial_record = self._build_record(record_id=1000000, product_name=self.existing_product.name)
updated_record = self._build_record(record_id=1000001, product_name=self.existing_product.name)
updated_record['ShuLiang'] = '20.00'
updated_record['ShuLiangZ'] = '3.15'
updated_record['BeiZhuC'] = '20件'
mock_fetch_image.return_value = {'bytes': b'', 'content_type': 'image/jpeg', 'name': 'unused.jpg'}
mock_advance_cursor.return_value = {'updated': True}
mock_fetch_records.return_value = self._build_payload([initial_record])
first_result = sync_external_printing_records.run(limit=100)
mock_fetch_records.return_value = self._build_payload([updated_record])
second_result = sync_external_printing_records.run(limit=100)
self.assertEqual(first_result['jobs_created'], 1)
self.assertEqual(second_result['jobs_created'], 0)
self.assertEqual(second_result['jobs_updated'], 1)
self.assertEqual(mock_fetch_image.call_count, 0)
jobs = printing_models.PrintingJob.objects.all()
self.assertEqual(jobs.count(), 1)
job = jobs.get()
self.assertEqual(job.original_id, 1000001)
self.assertEqual(job.quantity, 20)
self.assertEqual(job.size, '3.15')
self.assertEqual(job.pieces, 20)
self.assertEqual(job.printing_order.fabric, '120克本白四面弹单定')