forked from erp-dev/erp
feat: shipment change && version modelize
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import json
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from api_v1.external_datetime import parse_external_china_datetime
|
||||
from printing.models import PrintingOrder
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '从 PrintingOrder.external_raw.first_record.KdRiQi 批量回填或修正 kd_riqi'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--dry-run', action='store_true', help='只统计不写入')
|
||||
parser.add_argument('--batch-size', type=int, default=1000, help='批量处理大小')
|
||||
parser.add_argument('--limit', type=int, default=None, help='最多处理多少条候选记录')
|
||||
parser.add_argument('--merchant-id', type=int, default=None, help='限定商户ID')
|
||||
parser.add_argument('--external-order-id', default=None, help='限定外部订单编号')
|
||||
parser.add_argument('--overwrite', action='store_true', help='覆盖已有 kd_riqi,用于修正历史错误值')
|
||||
parser.add_argument('--update-outgoing-date', action='store_true', help='同时用 KdRiQi 修正 outgoing_date')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = bool(options['dry_run'])
|
||||
batch_size = max(1, int(options['batch_size'] or 1000))
|
||||
limit = options.get('limit')
|
||||
overwrite = bool(options['overwrite'])
|
||||
update_outgoing_date = bool(options['update_outgoing_date'])
|
||||
|
||||
queryset = PrintingOrder.objects.exclude(external_raw={})
|
||||
if not overwrite:
|
||||
queryset = queryset.filter(kd_riqi__isnull=True)
|
||||
merchant_id = options.get('merchant_id')
|
||||
if merchant_id:
|
||||
queryset = queryset.filter(merchant_id=merchant_id)
|
||||
|
||||
external_order_id = (options.get('external_order_id') or '').strip()
|
||||
if external_order_id:
|
||||
queryset = queryset.filter(external_order_id=external_order_id)
|
||||
|
||||
queryset = queryset.order_by('id').only('id', 'external_raw', 'kd_riqi', 'outgoing_date')
|
||||
if limit is not None:
|
||||
queryset = queryset[: max(0, int(limit))]
|
||||
|
||||
matched = 0
|
||||
updated = 0
|
||||
skipped_missing = 0
|
||||
skipped_invalid = 0
|
||||
pending_updates = []
|
||||
update_fields = ['kd_riqi']
|
||||
if update_outgoing_date:
|
||||
update_fields.append('outgoing_date')
|
||||
|
||||
for order in queryset.iterator(chunk_size=batch_size):
|
||||
matched += 1
|
||||
try:
|
||||
first_record = (order.external_raw or {}).get('first_record') or {}
|
||||
raw_value = first_record.get('KdRiQi')
|
||||
except AttributeError:
|
||||
raw_value = None
|
||||
|
||||
if not raw_value:
|
||||
skipped_missing += 1
|
||||
continue
|
||||
|
||||
parsed = parse_external_china_datetime(raw_value)
|
||||
if parsed is None:
|
||||
skipped_invalid += 1
|
||||
continue
|
||||
|
||||
order.kd_riqi = parsed
|
||||
if update_outgoing_date:
|
||||
order.outgoing_date = parsed
|
||||
updated += 1
|
||||
if dry_run:
|
||||
continue
|
||||
|
||||
pending_updates.append(order)
|
||||
if len(pending_updates) >= batch_size:
|
||||
PrintingOrder.objects.bulk_update(pending_updates, update_fields)
|
||||
pending_updates = []
|
||||
|
||||
if pending_updates:
|
||||
PrintingOrder.objects.bulk_update(pending_updates, update_fields)
|
||||
|
||||
payload = {
|
||||
'dry_run': dry_run,
|
||||
'matched': matched,
|
||||
'updated': 0 if dry_run else updated,
|
||||
'would_update': updated if dry_run else 0,
|
||||
'skipped_missing': skipped_missing,
|
||||
'skipped_invalid': skipped_invalid,
|
||||
'overwrite': overwrite,
|
||||
'update_outgoing_date': update_outgoing_date,
|
||||
}
|
||||
self.stdout.write(self.style.SUCCESS(json.dumps(payload, ensure_ascii=False)))
|
||||
125
printing/test_backfill_printing_order_kd_riqi_command.py
Normal file
125
printing/test_backfill_printing_order_kd_riqi_command.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import json
|
||||
from datetime import datetime, timezone as dt_timezone
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
|
||||
|
||||
class BackfillPrintingOrderKdRiQiCommandTest(TestCase):
|
||||
def setUp(self):
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试印染厂',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
)
|
||||
|
||||
def _create_order(self, *, external_order_id, external_raw, kd_riqi=None, merchant=None):
|
||||
return printing_models.PrintingOrder.objects.create(
|
||||
merchant=merchant or self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试面料',
|
||||
width='160cm',
|
||||
external_order_id=external_order_id,
|
||||
external_raw=external_raw,
|
||||
kd_riqi=kd_riqi,
|
||||
)
|
||||
|
||||
def test_dry_run_reports_without_writing(self):
|
||||
order = self._create_order(
|
||||
external_order_id='KD001',
|
||||
external_raw={'first_record': {'KdRiQi': '2026-01-26T20:00:52Z'}},
|
||||
)
|
||||
output = StringIO()
|
||||
|
||||
call_command('backfill_printing_order_kd_riqi', '--dry-run', stdout=output)
|
||||
|
||||
order.refresh_from_db()
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertIsNone(order.kd_riqi)
|
||||
self.assertEqual(payload['would_update'], 1)
|
||||
self.assertEqual(payload['updated'], 0)
|
||||
|
||||
def test_backfills_missing_kd_riqi_from_external_raw(self):
|
||||
order = self._create_order(
|
||||
external_order_id='KD001',
|
||||
external_raw={'first_record': {'KdRiQi': '2026-01-26T20:00:52Z'}},
|
||||
)
|
||||
output = StringIO()
|
||||
|
||||
call_command('backfill_printing_order_kd_riqi', stdout=output)
|
||||
|
||||
order.refresh_from_db()
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertEqual(timezone.localtime(order.kd_riqi).isoformat(), '2026-01-26T20:00:52+08:00')
|
||||
self.assertEqual(payload['updated'], 1)
|
||||
|
||||
def test_skips_missing_and_invalid_values(self):
|
||||
missing = self._create_order(
|
||||
external_order_id='KD001',
|
||||
external_raw={'first_record': {}},
|
||||
)
|
||||
invalid = self._create_order(
|
||||
external_order_id='KD002',
|
||||
external_raw={'first_record': {'KdRiQi': 'invalid'}},
|
||||
)
|
||||
output = StringIO()
|
||||
|
||||
call_command('backfill_printing_order_kd_riqi', stdout=output)
|
||||
|
||||
missing.refresh_from_db()
|
||||
invalid.refresh_from_db()
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertIsNone(missing.kd_riqi)
|
||||
self.assertIsNone(invalid.kd_riqi)
|
||||
self.assertEqual(payload['skipped_missing'], 1)
|
||||
self.assertEqual(payload['skipped_invalid'], 1)
|
||||
|
||||
def test_filters_by_external_order_id(self):
|
||||
target = self._create_order(
|
||||
external_order_id='KD001',
|
||||
external_raw={'first_record': {'KdRiQi': '2026-01-26T20:00:52Z'}},
|
||||
)
|
||||
other = self._create_order(
|
||||
external_order_id='KD002',
|
||||
external_raw={'first_record': {'KdRiQi': '2026-01-27T20:00:52Z'}},
|
||||
)
|
||||
|
||||
call_command('backfill_printing_order_kd_riqi', '--external-order-id', 'KD001', stdout=StringIO())
|
||||
|
||||
target.refresh_from_db()
|
||||
other.refresh_from_db()
|
||||
self.assertIsNotNone(target.kd_riqi)
|
||||
self.assertIsNone(other.kd_riqi)
|
||||
|
||||
def test_overwrite_can_update_kd_riqi_and_outgoing_date(self):
|
||||
order = self._create_order(
|
||||
external_order_id='KD001',
|
||||
external_raw={'first_record': {'KdRiQi': '2026-01-26T20:00:52Z'}},
|
||||
kd_riqi=datetime(2026, 1, 26, 20, 0, 52, tzinfo=dt_timezone.utc),
|
||||
)
|
||||
order.outgoing_date = datetime(2026, 1, 26, 20, 0, 52, tzinfo=dt_timezone.utc)
|
||||
order.save(update_fields=['outgoing_date'])
|
||||
output = StringIO()
|
||||
|
||||
call_command(
|
||||
'backfill_printing_order_kd_riqi',
|
||||
'--overwrite',
|
||||
'--update-outgoing-date',
|
||||
stdout=output,
|
||||
)
|
||||
|
||||
order.refresh_from_db()
|
||||
payload = json.loads(output.getvalue())
|
||||
self.assertEqual(timezone.localtime(order.kd_riqi).isoformat(), '2026-01-26T20:00:52+08:00')
|
||||
self.assertEqual(timezone.localtime(order.outgoing_date).isoformat(), '2026-01-26T20:00:52+08:00')
|
||||
self.assertEqual(payload['updated'], 1)
|
||||
self.assertTrue(payload['overwrite'])
|
||||
self.assertTrue(payload['update_outgoing_date'])
|
||||
Reference in New Issue
Block a user