Files

191 lines
7.3 KiB
Python

"""电商与渠道服务层:
1. sync_channel_orders: 从外部平台拉取订单并入库
2. convert_channel_order_to_sales_order: 自动转内部销售订单
3. handle_inbound_webhook: 接收外部 Webhook 并分发
"""
from decimal import Decimal
from datetime import date
from django.db import transaction
from apps.sales.models import SalesOrder, SalesOrderLine
from apps.catalog.models import Product
from apps.partner.models import Customer
from .models import ChannelAccount, ChannelOrder, WebhookEvent
from .adapters import get_adapter_for_account
@transaction.atomic
def convert_channel_order_to_sales_order(channel_order: ChannelOrder) -> SalesOrder:
"""将渠道拉取的订单转为内部标准的销售订单(SalesOrder)。"""
if channel_order.sync_status == "converted" and channel_order.sales_order:
return channel_order.sales_order
tenant = channel_order.tenant
account = channel_order.channel_account
# 客户解析:优先使用店铺配置的默认客户,不存在则动态查找或创建
customer = account.default_customer
if not customer:
customer, _ = Customer.objects.get_or_create(
tenant=tenant,
code=f"C_CH_{account.platform}_{account.shop_id}",
defaults={
"name": f"{account.shop_name}客户(挂账)",
"phone": channel_order.buyer_phone or "13800000000",
"address": channel_order.receiver_address,
},
)
# 仓库解析:优先店铺默认仓库,不存在则取租户首个可用仓库
from apps.inventory.models import Warehouse
warehouse = account.default_warehouse or Warehouse.objects.filter(tenant=tenant).first()
if not warehouse:
warehouse = Warehouse.objects.create(tenant=tenant, code="WH_DEFAULT", name="默认仓库")
items = channel_order.raw_payload.get("items", [])
if not items:
channel_order.sync_status = "failed"
channel_order.sync_error = "外部订单缺少明细商品 (items)"
channel_order.save(update_fields=["sync_status", "sync_error", "updated_at"])
raise ValueError("外部订单明细不能为空")
# 创建 SalesOrder
today_str = date.today().strftime("%Y%m%d")
from apps.core.services import next_bill_no
so_no = next_bill_no(tenant, "SO", SalesOrder)
sales_order = SalesOrder.objects.create(
tenant=tenant,
bill_no=so_no,
customer=customer,
warehouse=warehouse,
bill_date=date.today(),
total_amount=Decimal("0"),
state="confirmed", # 电商已付订单默认直接转为 confirmed 待开单发货
remark=f"来自电商渠道 {account.shop_name},外部单号: {channel_order.external_order_id},买家: {channel_order.buyer_name}",
source_channel=account.platform,
)
total_amount = Decimal("0")
for item in items:
p_code = item.get("product_code")
product = Product.objects.filter(tenant=tenant, code=p_code, status="active").first()
if not product:
product = Product.objects.filter(tenant=tenant, code=p_code).first()
if not product:
channel_order.sync_status = "failed"
channel_order.sync_error = f"商品编码 {p_code} 在系统商品中心不存在"
channel_order.save(update_fields=["sync_status", "sync_error", "updated_at"])
raise ValueError(f"商品编码 {p_code} 不存在")
qty = Decimal(str(item.get("quantity", 1)))
price = Decimal(str(item.get("unit_price", product.sale_price)))
amount = qty * price
SalesOrderLine.objects.create(
tenant=tenant,
order=sales_order,
product=product,
quantity=qty,
unit_price=price,
amount=amount,
)
total_amount += amount
sales_order.total_amount = total_amount
sales_order.save(update_fields=["total_amount", "updated_at"])
channel_order.sales_order = sales_order
channel_order.sync_status = "converted"
channel_order.sync_error = ""
channel_order.save(update_fields=["sales_order", "sync_status", "sync_error", "updated_at"])
return sales_order
@transaction.atomic
def sync_channel_orders(account: ChannelAccount, auto_convert: bool = True) -> dict:
"""从渠道拉取订单并记录,可自动转为内部销售单。"""
adapter = get_adapter_for_account(account)
raw_orders = adapter.fetch_orders(account)
pulled_count = 0
converted_count = 0
failed_count = 0
for ro in raw_orders:
ext_id = ro["external_order_id"]
corder, was_created = ChannelOrder.objects.get_or_create(
tenant=account.tenant,
channel_account=account,
external_order_id=ext_id,
defaults={
"order_status": ro.get("order_status", "PAID"),
"order_amount": Decimal(str(ro.get("order_amount", 0))),
"buyer_name": ro.get("buyer_name", ""),
"buyer_phone": ro.get("buyer_phone", ""),
"receiver_address": ro.get("receiver_address", ""),
"raw_payload": ro,
"sync_status": "pending",
},
)
if was_created:
pulled_count += 1
if auto_convert and corder.sync_status == "pending":
try:
convert_channel_order_to_sales_order(corder)
converted_count += 1
except Exception:
failed_count += 1
return {
"shop_id": account.shop_id,
"pulled_count": pulled_count,
"converted_count": converted_count,
"failed_count": failed_count,
}
def handle_inbound_webhook(tenant, platform: str, event_type: str, payload: dict) -> WebhookEvent:
"""处理外部 Webhook 回调。"""
event = WebhookEvent.objects.create(
tenant=tenant,
direction="inbound",
source=platform,
event_type=event_type,
payload=payload,
status="received",
)
try:
# 如果是订单支付事件,且包含外部单号与店铺ID,则自动建单
if event_type == "order.paid":
shop_id = payload.get("shop_id")
account = ChannelAccount.objects.filter(tenant=tenant, platform=platform, shop_id=shop_id).first()
if account and payload.get("external_order_id"):
corder, _ = ChannelOrder.objects.get_or_create(
tenant=tenant,
channel_account=account,
external_order_id=payload["external_order_id"],
defaults={
"order_status": "PAID",
"order_amount": Decimal(str(payload.get("order_amount", 0))),
"buyer_name": payload.get("buyer_name", ""),
"buyer_phone": payload.get("buyer_phone", ""),
"receiver_address": payload.get("receiver_address", ""),
"raw_payload": payload,
},
)
convert_channel_order_to_sales_order(corder)
event.status = "processed"
event.save(update_fields=["status", "updated_at"])
except Exception as e:
event.status = "failed"
event.error_message = str(e)
event.save(update_fields=["status", "error_message", "updated_at"])
return event