1
0
forked from erp-dev/erp
Files
erpnew/printing/management/commands/generate_fake_shipment_data.py

469 lines
20 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.
import random
from decimal import Decimal
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from basic_info import models as basic_models
from printing import models as printing_models
from shipment import models as shipment_models
class Command(BaseCommand):
help = (
"为开发环境生成一批假的 customer / printing_order / printing_job / sales_item 数据,"
"用于前端联调 shipment 与 printing 相关页面。"
)
def add_arguments(self, parser):
parser.add_argument("--merchant-id", type=int, default=1, help="目标商户 ID默认 1")
parser.add_argument("--user-id", type=int, default=None, help="创建人用户 ID可选")
parser.add_argument("--customers", type=int, default=10, help="客户数量,默认 10")
parser.add_argument("--orders", type=int, default=50, help="订单数量,默认 50")
parser.add_argument(
"--jobs-min", type=int, default=3, help="每个订单最少生成多少个 PrintingJob默认 3"
)
parser.add_argument(
"--jobs-max", type=int, default=30, help="每个订单最多生成多少个 PrintingJob默认 30"
)
parser.add_argument(
"--items-min", type=int, default=2, help="每个 job 最少生成多少个 SalesItem默认 2"
)
parser.add_argument(
"--items-max", type=int, default=20, help="每个 job 最多生成多少个 SalesItem默认 20"
)
parser.add_argument(
"--products", type=int, default=24, help="测试产品数量,默认 24"
)
parser.add_argument(
"--tag",
type=str,
default="DEVSHIP",
help="生成数据的标签前缀,默认 DEVSHIP",
)
parser.add_argument(
"--seed",
type=int,
default=20260331,
help="随机种子,默认 20260331",
)
parser.add_argument(
"--yes",
action="store_true",
help="跳过交互确认,直接执行(谨慎使用)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="仅输出执行前检查与预计写入量,不实际写入数据",
)
def handle(self, *args, **options):
random.seed(options["seed"])
self._validate_options(options)
merchant = basic_models.Merchant.objects.filter(id=options["merchant_id"]).first()
if merchant is None:
raise CommandError(f"merchant_id={options['merchant_id']} 不存在")
created_by = self._resolve_user(merchant=merchant, user_id=options["user_id"])
preview = self._collect_preview(merchant=merchant, created_by=created_by, options=options)
self._print_preview(preview)
if options["dry_run"]:
self.stdout.write(self.style.WARNING("dry-run 模式:未写入任何数据"))
return
if not options["yes"]:
self._confirm_or_abort()
with transaction.atomic():
counts = self._generate_data(
merchant=merchant,
created_by=created_by,
options=options,
)
self._print_summary(
merchant=merchant,
created_by=created_by,
tag=options["tag"],
counts=counts,
)
def _generate_data(self, *, merchant, created_by, options):
category = self._ensure_category(merchant=merchant, tag=options["tag"])
products = self._ensure_products(
merchant=merchant,
category=category,
count=options["products"],
tag=options["tag"],
)
customers = self._create_customers(
merchant=merchant,
count=options["customers"],
tag=options["tag"],
)
order_count = 0
job_count = 0
sales_item_count = 0
self.stdout.write(self.style.NOTICE("开始生成测试数据..."))
self.stdout.write("步骤 1/3: 准备客户与产品")
self.stdout.write(f" 客户数: {len(customers)}")
self.stdout.write(f" 产品数: {len(products)}")
self.stdout.write("步骤 2/3: 创建 PrintingOrder / PrintingJob / SalesItem")
for index in range(1, options["orders"] + 1):
customer = customers[(index - 1) % len(customers)]
order = printing_models.PrintingOrder.objects.create(
merchant=merchant,
customer=customer,
fabric=random.choice(["全棉", "涤纶", "尼龙", "人棉", "TC"]),
width=random.choice(["150cm", "160cm", "170cm", "180cm"]),
is_urgent=random.choice([False, False, False, True]),
area=customer.area or random.choice(["广州", "佛山", "绍兴", "杭州"]),
address=f"{customer.area or '广州'}-{index}号测试地址",
fabric_source=random.choice(["自带布", "仓库", "客户送布"]),
is_fabric_received=random.choice([True, False]),
craft=random.choice(["活性印花", "数码印花", "涂料印花"]),
description=f"{options['tag']} 测试印染订单 #{index}",
position=random.choice(["A区", "B区", "C区", "待排产"]),
printing_warn=random.choice(["", "注意色差", "先确认花型"]),
rolling_warn=random.choice(["", "注意卷边", "慢速滚筒"]),
production_warn=random.choice(["", "先打样", "优先安排"]),
created_by=created_by,
external_order_id=self._build_external_order_id(index=index),
external_customer_id=f"CUST-{customer.id}",
external_customer_name=customer.name,
external_employee_name=self._display_user(created_by),
)
order_count += 1
jobs_for_order = random.randint(options["jobs_min"], options["jobs_max"])
for job_index in range(1, jobs_for_order + 1):
product = random.choice(products)
job = printing_models.PrintingJob.objects.create(
merchant=merchant,
printing_order=order,
product=product,
work_state=random.choice(
[
printing_models.PrintingJobWorkStateEnum.PRODUCING,
printing_models.PrintingJobWorkStateEnum.WAITING_FOR_DELIVERY,
printing_models.PrintingJobWorkStateEnum.WAITING_FOR_INVOICE,
]
),
quantity=random.randint(80, 3000),
unit=random.choice(["", "", ""]),
size=random.choice(["", "120x120", "150x150", "200x200"]) or None,
pieces=random.choice([None, 1, 2, 4, 6, 8]),
description=f"{options['tag']} Job {index}-{job_index}",
created_by=created_by,
external_product_name=product.name,
)
job_count += 1
sales_items = []
items_for_job = random.randint(options["items_min"], options["items_max"])
for item_index in range(1, items_for_job + 1):
sales_items.append(
shipment_models.SalesItem(
shipment=None,
merchant=merchant,
name=f"{product.name}-销售品-{index}-{job_index}-{item_index}",
quantity=Decimal(
f"{random.randint(20, 3000)}.{random.randint(0, 99):02d}"
),
unit=random.choice(
[
shipment_models.UnitChoices.METER,
shipment_models.UnitChoices.PIECE,
shipment_models.UnitChoices.YARD,
shipment_models.UnitChoices.UNIT,
]
),
created_by=created_by,
printing_job_id=job.id,
customer_id=customer.id,
position=random.choice(
[
"A1-01",
"A1-02",
"B2-03",
"C3-05",
"待分区",
"待发货区",
]
),
remark=random.choice(["", "测试数据", "扫码联调用", "优先出货"]),
)
)
shipment_models.SalesItem.objects.bulk_create(sales_items, batch_size=500)
sales_item_count += len(sales_items)
if index % 10 == 0 or index == options["orders"]:
self.stdout.write(
f" 已完成订单 {index}/{options['orders']}"
f"累计 jobs={job_count}sales_items={sales_item_count}"
)
self.stdout.write("步骤 3/3: 数据写入完成")
return {
"customers_created": len(customers),
"printing_orders_created": order_count,
"printing_jobs_created": job_count,
"sales_items_created": sales_item_count,
}
def _resolve_user(self, *, merchant, user_id):
User = get_user_model()
if user_id is not None:
user = User.objects.filter(id=user_id).first()
if user is None:
raise CommandError(f"user_id={user_id} 不存在")
employee = getattr(user, "employee", None)
if employee is None or employee.merchant_id != merchant.id:
raise CommandError("指定 user 未绑定到目标商户的 employee")
return user
employee = (
basic_models.Employee.objects.select_related("sys_user")
.filter(merchant=merchant, sys_user__isnull=False)
.order_by("id")
.first()
)
if employee and employee.sys_user:
return employee.sys_user
username = f"seed_{merchant.id}_user"
user = User.objects.create_user(username=username, password="devpass123")
basic_models.Employee.objects.create(
merchant=merchant,
sys_user=user,
name=f"Seed User {merchant.id}",
mobile=f"1390000{merchant.id:04d}",
status=basic_models.EmployeeStatusEnum.ACTIVE,
)
return user
def _ensure_category(self, *, merchant, tag):
category, _ = basic_models.ProductCategory.objects.get_or_create(
merchant=merchant,
name=f"{tag}-测试分类",
defaults={"product_prefix": "TS"},
)
return category
def _ensure_products(self, *, merchant, category, count, tag):
products = list(
basic_models.Product.objects.filter(
merchant=merchant,
category=category,
name__startswith=f"{tag}-测试产品-",
).order_by("id")
)
missing = count - len(products)
for index in range(1, missing + 1):
number = len(products) + index
products.append(
basic_models.Product.objects.create(
merchant=merchant,
category=category,
name=f"{tag}-测试产品-{number:03d}",
human_id=f"{tag[:4]}P{number:03d}",
unit=random.choice(
[
basic_models.ProductUnitEnum.METER,
basic_models.ProductUnitEnum.YARD,
basic_models.ProductUnitEnum.SEGMENT,
]
),
color=random.choice(["", "", "", "", ""]),
width_size=Decimal(random.choice(["150.00", "160.00", "170.00"])),
description="开发环境自动生成的测试产品",
from_mdy=False,
)
)
return list(
basic_models.Product.objects.filter(
merchant=merchant,
category=category,
name__startswith=f"{tag}-测试产品-",
).order_by("id")[:count]
)
def _create_customers(self, *, merchant, count, tag):
customers = []
employee = (
basic_models.Employee.objects.filter(merchant=merchant).order_by("id").first()
)
if employee is None:
raise CommandError("目标商户下没有 employee无法创建 customer")
for index in range(1, count + 1):
name = f"{tag}-测试客户-{index:02d}"
customer, _ = basic_models.Customer.objects.get_or_create(
merchant=merchant,
name=name,
defaults={
"created_by": employee,
"mobile": f"138{merchant.id:02d}{index:06d}"[:11],
"area": random.choice(["广州", "佛山", "绍兴", "杭州", "苏州"]),
"contact": f"联系人{index:02d}",
"description": "开发环境自动生成的测试客户",
},
)
customers.append(customer)
return customers
def _build_external_order_id(self, *, index):
return f"KD{20135142 + index:08d}"
def _validate_options(self, options):
if options["customers"] <= 0:
raise CommandError("--customers 必须大于 0")
if options["orders"] <= 0:
raise CommandError("--orders 必须大于 0")
if options["products"] <= 0:
raise CommandError("--products 必须大于 0")
if options["jobs_min"] <= 0 or options["jobs_max"] <= 0:
raise CommandError("--jobs-min / --jobs-max 必须大于 0")
if options["items_min"] <= 0 or options["items_max"] <= 0:
raise CommandError("--items-min / --items-max 必须大于 0")
if options["jobs_min"] > options["jobs_max"]:
raise CommandError("--jobs-min 不能大于 --jobs-max")
if options["items_min"] > options["items_max"]:
raise CommandError("--items-min 不能大于 --items-max")
def _collect_preview(self, *, merchant, created_by, options):
db = settings.DATABASES["default"]
tag = options["tag"]
existing_customer_count = basic_models.Customer.objects.filter(
merchant=merchant,
name__startswith=f"{tag}-测试客户-",
).count()
existing_order_count = printing_models.PrintingOrder.objects.filter(
merchant=merchant,
description__startswith=f"{tag} 测试印染订单 #",
).count()
existing_job_count = printing_models.PrintingJob.objects.filter(
merchant=merchant,
description__startswith=f"{tag} Job ",
).count()
existing_sales_item_count = shipment_models.SalesItem.objects.filter(
merchant=merchant,
name__startswith=f"{tag}-测试产品-",
).count()
jobs_min_total = options["orders"] * options["jobs_min"]
jobs_max_total = options["orders"] * options["jobs_max"]
sales_items_min_total = jobs_min_total * options["items_min"]
sales_items_max_total = jobs_max_total * options["items_max"]
return {
"debug": getattr(settings, "DEBUG", None),
"db_host": db.get("HOST"),
"db_port": db.get("PORT"),
"db_name": db.get("NAME"),
"merchant_id": merchant.id,
"merchant_name": merchant.name,
"created_by_id": created_by.id,
"created_by_username": created_by.username,
"tag": tag,
"seed": options["seed"],
"customers": options["customers"],
"orders": options["orders"],
"jobs_min": options["jobs_min"],
"jobs_max": options["jobs_max"],
"items_min": options["items_min"],
"items_max": options["items_max"],
"products": options["products"],
"existing_customer_count": existing_customer_count,
"existing_order_count": existing_order_count,
"existing_job_count": existing_job_count,
"existing_sales_item_count": existing_sales_item_count,
"jobs_min_total": jobs_min_total,
"jobs_max_total": jobs_max_total,
"sales_items_min_total": sales_items_min_total,
"sales_items_max_total": sales_items_max_total,
}
def _print_preview(self, preview):
self.stdout.write(self.style.WARNING("即将生成开发环境测试数据,请先确认以下状态"))
self.stdout.write("=" * 72)
self.stdout.write(f"DEBUG : {preview['debug']}")
self.stdout.write(
f"数据库 : {preview['db_name']} @ {preview['db_host']}:{preview['db_port']}"
)
self.stdout.write(
f"目标商户 : {preview['merchant_id']} / {preview['merchant_name']}"
)
self.stdout.write(
f"创建人用户 : {preview['created_by_id']} / {preview['created_by_username']}"
)
self.stdout.write(f"数据标签 : {preview['tag']}")
self.stdout.write(f"随机种子 : {preview['seed']}")
self.stdout.write("-" * 72)
self.stdout.write(f"客户数 : {preview['customers']}")
self.stdout.write(f"订单数 : {preview['orders']}")
self.stdout.write(
f"每单 job 数 : {preview['jobs_min']} ~ {preview['jobs_max']}"
)
self.stdout.write(
f"每 job sales item 数: {preview['items_min']} ~ {preview['items_max']}"
)
self.stdout.write(f"测试产品数 : {preview['products']}")
self.stdout.write("-" * 72)
self.stdout.write(
f"预计新增 PrintingJob: {preview['jobs_min_total']} ~ {preview['jobs_max_total']}"
)
self.stdout.write(
"预计新增 SalesItem : "
f"{preview['sales_items_min_total']} ~ {preview['sales_items_max_total']}"
)
self.stdout.write("-" * 72)
self.stdout.write(
f"当前同 tag 客户数 : {preview['existing_customer_count']}"
)
self.stdout.write(
f"当前同 tag 订单数 : {preview['existing_order_count']}"
)
self.stdout.write(
f"当前同 tag job 数 : {preview['existing_job_count']}"
)
self.stdout.write(
f"当前同 tag sales item: {preview['existing_sales_item_count']}"
)
self.stdout.write("=" * 72)
def _confirm_or_abort(self):
answer = input("确认要继续写入这批测试数据吗?请输入 YES 继续: ").strip()
if answer != "YES":
raise CommandError("已取消执行,未写入任何数据")
def _print_summary(self, *, merchant, created_by, tag, counts):
self.stdout.write(self.style.SUCCESS("本次执行摘要"))
self.stdout.write("=" * 72)
self.stdout.write(f"merchant_id : {merchant.id}")
self.stdout.write(f"merchant_name : {merchant.name}")
self.stdout.write(f"created_by : {created_by.id}:{created_by.username}")
self.stdout.write(f"tag : {tag}")
self.stdout.write(f"customers_created : {counts['customers_created']}")
self.stdout.write(f"printing_orders : {counts['printing_orders_created']}")
self.stdout.write(f"printing_jobs : {counts['printing_jobs_created']}")
self.stdout.write(f"sales_items : {counts['sales_items_created']}")
self.stdout.write("=" * 72)
self.stdout.write(self.style.SUCCESS("假数据生成完成"))
def _display_user(self, user):
employee = getattr(user, "employee", None)
if employee:
return employee.name
return user.username