dingdanquanliucheng/backend/app/services/order_service.py
2026-05-28 22:59:04 +08:00

1207 lines
57 KiB
Python

from datetime import datetime
import json
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from backend.app.core.error_codes import ErrorCode
from backend.app.core.exceptions import AppException
from backend.app.repositories.customer_repository import CustomerRepository
from backend.app.repositories.config_repository import ConfigRepository
from backend.app.repositories.file_repository import FileRepository
from backend.app.repositories.logistics_repository import LogisticsRepository
from backend.app.repositories.order_approve_log_repository import OrderApproveLogRepository
from backend.app.repositories.order_repository import OrderRepository
from backend.app.repositories.reminder_repository import ReminderRepository
from backend.app.services.arrears_service import arrears_service
from backend.app.services.audit_service import audit_service
from backend.app.services.demo_store import demo_store
class OrderService:
def __init__(self) -> None:
self.order_repository = OrderRepository()
self.customer_repository = CustomerRepository()
self.config_repository = ConfigRepository()
self.order_approve_log_repository = OrderApproveLogRepository()
self.logistics_repository = LogisticsRepository()
self.file_repository = FileRepository()
self.reminder_repository = ReminderRepository()
def list_orders(self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None) -> list[dict]:
if session is not None:
try:
result = self.order_repository.list_orders_by_filters(session, filters or {})
orders = result["list"]
role_code = (current_user or {}).get("role_code")
return [
self._filter_order_row(
{
"order_id": item.id,
"order_no": item.order_no,
"customer_name": item.customer_name,
"customer_mobile": item.customer_mobile,
"salesman_name": "",
"order_status": item.order_status,
"order_source": item.order_source,
"sale_price_total": float(item.sale_price_total or 0),
"profit_total": float(item.profit_total or 0),
"commission_amount": float(item.commission_amount or 0),
"created_at": item.created_at.strftime("%Y-%m-%d %H:%M:%S") if item.created_at else "",
},
role_code,
)
for item in orders
]
except SQLAlchemyError:
pass
return demo_store.list_orders()
def get_order(
self,
order_id: int,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
supplier = self.order_repository.get_supplier(session, order.factory_id)
items = self.order_repository.list_order_items(session, order.id)
approve_logs = self.order_approve_log_repository.list_by_order_id(session, order.id)
task = self.logistics_repository.get_task_by_order_id(session, order.id)
traces = self.logistics_repository.list_traces_by_order_id(session, order.id)
order_attachments = self.file_repository.list_attachments(session, "sales_order", order.id)
task_attachments = self.file_repository.list_attachments(session, "logistics_task", task.id) if task else []
result = {
"order_id": order.id,
"order_no": order.order_no,
"customer_id": order.customer_id,
"customer_name": order.customer_name,
"customer_mobile": order.customer_mobile,
"customer_address": order.customer_address,
"salesman_id": order.salesman_id,
"salesman_name": "",
"order_source": order.order_source,
"order_status": order.order_status,
"delivery_type": order.delivery_type,
"factory_id": order.factory_id,
"factory_name": supplier.supplier_name if supplier else "",
"sale_price_total": float(order.sale_price_total or 0),
"cost_price_total": float(order.cost_price_total or 0),
"rebate_total": float(order.rebate_total or 0),
"freight_total": float(order.freight_total or 0),
"tax_total": float(order.tax_total or 0),
"other_fee_total": float(order.other_fee_total or 0),
"profit_total": float(order.profit_total or 0),
"profit_rate": float(order.profit_rate or 0),
"commission_amount": float(order.commission_amount or 0),
"items": [
{
"item_id": item.id,
"product_id": item.product_id,
"product_name": item.product_name,
"specification": item.specification,
"unit": item.unit,
"quantity": float(item.quantity or 0),
"sale_price": float(item.sale_price or 0),
"cost_price": float(item.cost_price or 0),
"rebate_amount": float(item.rebate_amount or 0),
"freight_amount": float(item.freight_amount or 0),
"tax_amount": float(item.tax_amount or 0),
"other_fee_amount": float(item.other_fee_amount or 0),
"remark": item.remark,
"pricing_type": getattr(item, "pricing_type", None),
"length_m": float(item.length_m) if getattr(item, "length_m", None) else None,
"width_m": float(item.width_m) if getattr(item, "width_m", None) else None,
"area_sqm": float(item.area_sqm) if getattr(item, "area_sqm", None) else None,
"surcharge_detail": getattr(item, "surcharge_detail", None),
"processing_detail": getattr(item, "processing_detail", None),
"supplier_id": getattr(item, "supplier_id", None),
"supplier_model": getattr(item, "supplier_model", None),
"price_tier": getattr(item, "price_tier", None),
}
for item in items
],
"approve_logs": [
{
"log_id": log.id,
"approve_type": log.approve_type,
"approve_result": log.approve_result,
"before_status": log.before_status,
"after_status": log.after_status,
"approve_opinion": log.approve_opinion,
"operator_id": log.operator_id,
"approve_time": log.created_at.strftime("%Y-%m-%d %H:%M:%S") if log.created_at else "",
}
for log in approve_logs
],
"logistics_info": {
"task": self._build_task_summary(task),
"trace_list": [
{
"task_id": item.task_id,
"node_time": item.node_time.strftime("%Y-%m-%d %H:%M:%S") if item.node_time else "",
"node_desc": item.node_desc,
"node_type": item.node_type,
"source_platform": item.source_platform,
"remark": item.remark,
}
for item in traces
],
},
"attachments": [
*[self._build_attachment_row(item) for item in order_attachments],
*[self._build_attachment_row(item) for item in task_attachments],
],
"calculation_detail": self._build_calculation_detail(order, items),
"profit_alert_threshold": float(self._get_config_value(session, "profit_alert_threshold", "0")),
"remark": order.remark,
}
return self._filter_order_detail_by_role(result, current_user)
except SQLAlchemyError:
pass
return demo_store.get_order(order_id)
def update_order(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user, owner_only=True)
self._ensure_status(order.order_status, {"draft", "rejected"}, "当前状态不允许编辑订单")
if not payload["customer_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
if not payload["customer_mobile"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
customer = self.customer_repository.find_by_name_and_mobile(
session,
payload["customer_name"],
payload["customer_mobile"],
)
if customer is None:
customer = self.customer_repository.create_customer(
session,
{
"customer_name": payload["customer_name"],
"mobile": payload["customer_mobile"],
"address": payload.get("customer_address"),
"settlement_type": None,
"settlement_days": 0,
"customer_type": None,
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
"credit_limit": 0,
"remark": "订单编辑时自动创建",
},
)
items = payload["items"]
if not items:
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
sale_total = sum(item["quantity"] * item["sale_price"] for item in items)
cost_total = sum(item["quantity"] * item["cost_price"] for item in items)
profit_total = sale_total - cost_total - payload["rebate_total"] - payload["freight_total"] - payload["tax_total"] - payload["other_fee_total"]
profit_rate = round((profit_total / sale_total) * 100, 2) if sale_total else 0
before_status = order.order_status
self.order_repository.update_order_with_items(
session,
order,
{
"customer_id": customer.id,
"customer_name": payload["customer_name"],
"customer_mobile": payload["customer_mobile"],
"customer_address": payload.get("customer_address"),
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
"order_source": payload.get("order_source"),
"delivery_type": payload.get("delivery_type"),
"factory_id": payload.get("factory_id"),
"sale_price_total": sale_total,
"cost_price_total": cost_total,
"rebate_total": payload["rebate_total"],
"freight_total": payload["freight_total"],
"tax_total": payload["tax_total"],
"other_fee_total": payload["other_fee_total"],
"profit_total": profit_total,
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
"remark": payload.get("remark"),
},
items,
)
audit_service.write_log(
session,
{
"operate_type": "order_update",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": before_status},
"after_value": {
"order_no": order.order_no,
"order_status": order.order_status,
"sale_price_total": round(sale_total, 2),
"cost_price_total": round(cost_total, 2),
"profit_total": round(profit_total, 2),
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
},
"remark": f"更新订单 {order.order_no}",
},
)
session.commit()
return {
"order_id": order.id,
"order_no": order.order_no,
"order_status": order.order_status,
"sale_price_total": round(sale_total, 2),
"cost_price_total": round(cost_total, 2),
"profit_total": round(profit_total, 2),
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return None
def submit_order(
self,
order_id: int,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user, owner_only=True)
self._ensure_status(order.order_status, {"draft", "rejected"}, "当前状态不允许提交审核")
before_status = order.order_status
self.order_repository.update_order_status(session, order, "pending_approve")
audit_service.write_log(
session,
{
"operate_type": "order_submit",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": before_status},
"after_value": {"order_status": order.order_status},
"remark": f"提交订单审核 {order.order_no}",
},
)
self._notify_status_change(session, order, before_status, order.order_status, current_user.get("user_id") if current_user else None)
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
"submitted_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
self._ensure_status(order["order_status"], {"draft", "rejected"}, "当前状态不允许提交审核")
order["order_status"] = "pending_approve"
order["submitted_at"] = "2026-05-14 12:30:00"
return order
def cancel_order(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
self._validate_cancel_payload(payload)
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user, owner_only=True)
if order.order_status in {"canceled", "settled"}:
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前状态不允许取消订单", status_code=400)
allow_cancel_config = self._get_config_value(session, "allow_cancel_statuses", "draft,pending_approve,approved,pending_factory")
allow_cancel_statuses = {s.strip() for s in allow_cancel_config.split(",") if s.strip()}
if order.order_status not in allow_cancel_statuses:
raise AppException(code=ErrorCode.INVALID_STATUS, message=f"当前状态「{order.order_status}」不允许取消", status_code=400)
_DIRECT_CANCEL_STATUSES = {"draft", "pending_approve"}
_APPROVED_CANCEL_STATUSES = {"approved"}
_FULFILLMENT_CANCEL_STATUSES = {"pending_factory", "pending_driver", "accepted", "picked_up", "delivered", "production", "shipped"}
if order.order_status in _DIRECT_CANCEL_STATUSES:
next_status = "canceled"
elif order.order_status in _APPROVED_CANCEL_STATUSES:
next_status = "cancel_pending"
elif order.order_status in _FULFILLMENT_CANCEL_STATUSES:
next_status = "cancel_fulfillment_pending"
else:
next_status = "cancel_pending"
previous_status = order.order_status
self.order_repository.update_cancel_fields(
session,
order,
{
"order_status": next_status,
"cancel_requested_by": current_user.get("user_id") if current_user else None,
"cancel_requested_at": datetime.now(),
"cancel_reason": payload["cancel_reason"].strip(),
"cancel_opinion": payload.get("cancel_opinion"),
"cancel_previous_status": None if next_status == "canceled" else previous_status,
},
)
if next_status == "canceled":
self.order_repository.clear_cancel_request(session, order)
self.order_repository.update_order_status(session, order, "canceled")
audit_service.write_log(
session,
{
"operate_type": "order_cancel",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": previous_status},
"after_value": {
"order_status": order.order_status,
"cancel_reason": order.cancel_reason,
"cancel_previous_status": order.cancel_previous_status,
"cancel_requested_by": order.cancel_requested_by,
},
"remark": f"发起订单取消 {order.order_no}",
},
)
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None)
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
"previous_status": previous_status,
"canceled_at": order.cancel_requested_at.strftime("%Y-%m-%d %H:%M:%S") if order.cancel_requested_at else None,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
if order["order_status"] in {"canceled", "settled"}:
raise AppException(code=ErrorCode.INVALID_STATUS, message="当前状态不允许取消订单", status_code=400)
return {
"order_id": order_id,
"order_status": "canceled" if order["order_status"] in {"draft", "pending_approve"} else "cancel_pending",
"previous_status": order["order_status"],
"canceled_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def approve_order(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
self._validate_approve_payload(payload)
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
self._ensure_status(order.order_status, {"pending_approve"}, "当前状态不允许审批")
previous_status = order.order_status
target_status = "approved" if payload["approve_result"] == "pass" else "rejected"
self.order_repository.update_order_status(session, order, target_status)
self.order_approve_log_repository.create_log(
session,
{
"order_id": order.id,
"approve_type": "order",
"approve_result": payload["approve_result"],
"before_status": previous_status,
"after_status": target_status,
"approve_opinion": payload.get("approve_opinion"),
"operator_id": current_user.get("user_id") if current_user else None,
},
)
audit_service.write_log(
session,
{
"operate_type": "order_approve",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": previous_status},
"after_value": {
"order_status": order.order_status,
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
},
"remark": f"订单审批 {order.order_no}",
},
)
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None, {"approve_opinion": payload.get("approve_opinion")})
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
"profit_total": float(order.profit_total or 0),
"profit_rate": float(order.profit_rate or 0),
"approve_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
self._ensure_status(order["order_status"], {"pending_approve"}, "当前状态不允许审批")
return {
"order_id": order_id,
"order_status": "approved" if payload["approve_result"] == "pass" else "rejected",
"profit_total": order.get("profit_total", 0),
"profit_rate": order.get("profit_rate", 0),
"approve_time": "2026-05-14 10:30:00",
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
}
def cancel_approve_order(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
self._validate_cancel_approve_payload(payload)
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
self._ensure_status(order.order_status, {"cancel_pending", "cancel_fulfillment_pending"}, "当前状态不允许取消审批")
previous_status = order.order_status
target_status = "canceled" if payload["approve_result"] == "pass" else (order.cancel_previous_status or "approved")
self.order_repository.update_order_status(session, order, target_status)
if target_status != "canceled":
self.order_repository.clear_cancel_request(session, order)
self.order_approve_log_repository.create_log(
session,
{
"order_id": order.id,
"approve_type": "cancel_order",
"approve_result": payload["approve_result"],
"before_status": previous_status,
"after_status": target_status,
"approve_opinion": payload.get("approve_opinion"),
"operator_id": current_user.get("user_id") if current_user else None,
},
)
audit_service.write_log(
session,
{
"operate_type": "order_cancel_approve",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {
"order_status": previous_status,
"cancel_previous_status": order.cancel_previous_status,
},
"after_value": {
"order_status": order.order_status,
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
},
"remark": f"订单取消审批 {order.order_no}",
},
)
self._notify_status_change(session, order, previous_status, order.order_status, current_user.get("user_id") if current_user else None)
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
self._ensure_status(order["order_status"], {"cancel_pending", "cancel_fulfillment_pending"}, "当前状态不允许取消审批")
return {
"order_id": order_id,
"order_status": "canceled" if payload["approve_result"] == "pass" else "approved",
"approve_result": payload["approve_result"],
"approve_opinion": payload.get("approve_opinion"),
}
def get_supplier_text(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许生成发厂文案")
supplier_id = payload.get("supplier_id") or order.factory_id
if not supplier_id:
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
supplier = self.order_repository.get_supplier(session, supplier_id)
if supplier is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
items = self.order_repository.list_order_items(session, order.id)
template_type = supplier.template_type or "default"
text_content = self._build_supplier_text(order, items, template_type)
return {
"order_id": order.id,
"supplier_id": supplier.id,
"template_type": template_type,
"text_content": text_content,
}
except SQLAlchemyError:
pass
order = demo_store.get_order(order_id)
if order is None:
return None
self._ensure_status(order["order_status"], {"approved"}, "当前状态不允许生成发厂文案")
supplier_id = payload.get("supplier_id") or order.get("factory_id")
if not supplier_id:
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
return {
"order_id": order_id,
"supplier_id": supplier_id,
"template_type": "default",
"text_content": f"订单 {order['order_no']} 请安排生产与发货",
}
def confirm_supplier_text(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if not (payload.get("text_content") or "").strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="发厂文案不能为空", status_code=400)
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
self._ensure_status(order.order_status, {"approved"}, "当前状态不允许确认发厂")
supplier_id = payload.get("supplier_id") or order.factory_id
if not supplier_id:
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
supplier = self.order_repository.get_supplier(session, supplier_id)
if supplier is None:
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
before_status = order.order_status
self.order_repository.update_supplier_text_confirm(session, order, current_user.get("user_id") if current_user else None)
self._record_supplier_text_log(session, order, {**payload, "supplier_id": supplier_id}, current_user)
audit_service.write_log(
session,
{
"operate_type": "order_supplier_text_confirm",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": before_status},
"after_value": {
"order_status": order.order_status,
"supplier_id": supplier_id,
"text_content": payload["text_content"],
"remark": payload.get("remark"),
},
"remark": f"确认发厂文案 {order.order_no}",
},
)
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
"confirmed_at": order.supplier_text_confirmed_at.strftime("%Y-%m-%d %H:%M:%S")
if order.supplier_text_confirmed_at
else datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
self._ensure_status(order["order_status"], {"approved"}, "当前状态不允许确认发厂")
supplier_id = payload.get("supplier_id") or order.get("factory_id")
if not supplier_id:
raise AppException(code=ErrorCode.NOT_FOUND, message="订单未绑定工厂", status_code=404)
return {
"order_id": order_id,
"order_status": "pending_factory",
"confirmed_at": "2026-05-14 13:00:00",
}
def change_order_status(
self,
order_id: int,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict | None:
if session is not None:
try:
order = self.order_repository.get_order(session, order_id)
if order is None:
return None
self._ensure_order_access(order, current_user)
target_status = (payload.get("target_status") or "").strip()
if not target_status:
raise AppException(code=ErrorCode.PARAM_ERROR, message="目标状态不能为空", status_code=400)
allowed_statuses = {
"approved",
"pending_factory",
"pending_driver",
"accepted",
"picked_up",
"delivered",
"production",
"shipped",
"completed",
"settled",
"canceled",
}
if target_status not in allowed_statuses:
raise AppException(code=ErrorCode.PARAM_ERROR, message="目标状态不支持", status_code=400)
self._validate_status_transition(order.order_status, target_status)
before_status = order.order_status
self.order_repository.update_order_status(session, order, target_status)
arrears_service.sync_order_arrears(session, order)
audit_service.write_log(
session,
{
"operate_type": "order_status_change",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": {"order_status": before_status},
"after_value": {"order_status": order.order_status},
"remark": payload.get("remark") or f"订单状态变更为 {target_status}",
},
)
self._notify_status_change(session, order, before_status, order.order_status, current_user.get("user_id") if current_user else None)
session.commit()
return {
"order_id": order.id,
"order_status": order.order_status,
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
order = demo_store.get_order(order_id)
if order is None:
return None
return {
"order_id": order_id,
"order_status": payload.get("target_status", order.get("order_status")),
}
def create_order(
self,
payload: dict,
session: Session | None = None,
current_user: dict | None = None,
) -> dict:
if session is not None:
try:
if not payload["customer_name"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户姓名不能为空", status_code=400)
if not payload["customer_mobile"].strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="客户手机号不能为空", status_code=400)
customer = self.customer_repository.find_by_name_and_mobile(
session,
payload["customer_name"],
payload["customer_mobile"],
)
if customer is None:
customer = self.customer_repository.create_customer(
session,
{
"customer_name": payload["customer_name"],
"mobile": payload["customer_mobile"],
"address": payload.get("customer_address"),
"settlement_type": None,
"settlement_days": 0,
"customer_type": None,
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
"credit_limit": 0,
"remark": "订单录入时自动创建",
},
)
items = payload["items"]
if not items:
raise AppException(code=ErrorCode.PARAM_ERROR, message="订单明细不能为空", status_code=400)
sale_total = sum(item["quantity"] * item["sale_price"] for item in items)
cost_total = sum(item["quantity"] * item["cost_price"] for item in items)
profit_total = (
sale_total
- cost_total
- payload["rebate_total"]
- payload["freight_total"]
- payload["tax_total"]
- payload["other_fee_total"]
)
profit_rate = round((profit_total / sale_total) * 100, 2) if sale_total else 0
order = self.order_repository.create_order(
session,
{
"order_no": f"SO{datetime.now().strftime('%Y%m%d%H%M%S')}",
"customer_id": customer.id,
"customer_name": payload["customer_name"],
"customer_mobile": payload["customer_mobile"],
"customer_address": payload.get("customer_address"),
"salesman_id": current_user.get("user_id") if current_user else payload.get("salesman_id"),
"order_status": "draft",
"order_source": payload.get("order_source"),
"delivery_type": payload.get("delivery_type"),
"factory_id": payload.get("factory_id"),
"sale_price_total": sale_total,
"cost_price_total": cost_total,
"rebate_total": payload["rebate_total"],
"freight_total": payload["freight_total"],
"tax_total": payload["tax_total"],
"other_fee_total": payload["other_fee_total"],
"profit_total": profit_total,
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
"remark": payload.get("remark"),
},
items,
)
audit_service.write_log(
session,
{
"operate_type": "order_create",
"biz_type": "sales_order",
"biz_id": order.id,
"before_value": None,
"after_value": {
"order_no": order.order_no,
"order_status": order.order_status,
"sale_price_total": round(sale_total, 2),
"cost_price_total": round(cost_total, 2),
"profit_total": round(profit_total, 2),
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
},
"remark": f"创建订单 {order.order_no}",
},
)
session.commit()
return {
"order_id": order.id,
"order_no": order.order_no,
"order_status": order.order_status,
"sale_price_total": round(sale_total, 2),
"cost_price_total": round(cost_total, 2),
"profit_total": round(profit_total, 2),
"profit_rate": profit_rate,
"commission_amount": payload["commission_amount"],
}
except AppException:
session.rollback()
raise
except SQLAlchemyError:
session.rollback()
return demo_store.create_order(payload)
def normalize_list_filters(self, filters: dict | None, current_user: dict | None = None) -> dict:
normalized = dict(filters or {})
if not current_user:
return normalized
if current_user.get("role_code") == "salesman":
normalized["salesman_id"] = current_user.get("user_id")
return normalized
_STATUS_LABELS = {
"draft": "草稿",
"pending_approve": "待审核",
"approved": "已通过",
"rejected": "已退回",
"pending_factory": "待下发工厂",
"pending_driver": "待司机接单",
"accepted": "已接单",
"picked_up": "已揽货",
"delivered": "已送达",
"canceled": "已取消",
"cancel_pending": "取消审批中",
"cancel_fulfillment_pending": "履约取消审批中",
"production": "生产中",
"shipped": "已发货",
"completed": "已完成",
"settled": "已结算",
}
def _notify_status_change(self, session: Session, order, before_status: str, after_status: str, operator_id: int | None, extra_info: dict | None = None) -> None:
before_label = self._STATUS_LABELS.get(before_status, before_status)
after_label = self._STATUS_LABELS.get(after_status, after_status)
_NOTIFY_MANAGER_STATUSES = {"pending_approve", "cancel_pending", "cancel_fulfillment_pending"}
_NOTIFY_SALESMAN_STATUSES = {"approved", "rejected", "canceled", "delivered", "completed", "settled"}
receiver = order.salesman_id or 0
if receiver > 0 and after_status in _NOTIFY_SALESMAN_STATUSES | _NOTIFY_MANAGER_STATUSES:
reminder_content = f"订单 {order.order_no} 状态由「{before_label}」变更为「{after_label}」。"
if after_status == "rejected" and extra_info and extra_info.get("approve_opinion"):
reminder_content += f" 退回原因:{extra_info['approve_opinion']}"
self.reminder_repository.create_reminder(
session,
{
"reminder_type": "order_status_change",
"biz_type": "sales_order",
"biz_id": order.id,
"receiver_user_id": receiver,
"reminder_title": f"订单状态变更 - {order.order_no}",
"reminder_content": reminder_content,
"status": "pending",
"sent_at": datetime.now(),
},
)
if after_status in _NOTIFY_MANAGER_STATUSES:
managers = self._find_manager_user_ids(session)
for manager_id in managers:
self.reminder_repository.create_reminder(
session,
{
"reminder_type": "order_approval_needed",
"biz_type": "sales_order",
"biz_id": order.id,
"receiver_user_id": manager_id,
"reminder_title": f"订单待审批 - {order.order_no}",
"reminder_content": f"订单 {order.order_no}(客户:{order.customer_name})需要审批处理。",
"status": "pending",
"sent_at": datetime.now(),
},
)
def _find_manager_user_ids(self, session: Session) -> list[int]:
try:
from backend.app.models.system import SysUser, SysRole
from sqlalchemy import select
stmt = (
select(SysUser.id)
.join(SysRole, SysRole.id == SysUser.role_id)
.where(SysRole.role_code == "manager", SysUser.status == 1, SysUser.deleted == 0)
)
return list(session.execute(stmt).scalars())
except Exception:
return []
def _ensure_order_access(self, order: object, current_user: dict | None, owner_only: bool = False) -> None:
if not current_user:
return
role_code = current_user.get("role_code")
if role_code in {"admin", "manager"}:
return
if role_code == "salesman":
if order.salesman_id != current_user.get("user_id"):
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问他人订单", status_code=403)
return
raise AppException(code=ErrorCode.FORBIDDEN, message="无权限访问", status_code=403)
def _filter_order_detail_by_role(self, order: dict, current_user: dict | None) -> dict:
if not current_user:
return order
role_code = current_user.get("role_code")
if role_code == "driver":
filtered = dict(order)
filtered["customer_name"] = None
filtered["customer_mobile"] = None
filtered["customer_address"] = None
filtered["salesman_id"] = None
filtered["salesman_name"] = ""
filtered["sale_price_total"] = None
filtered["cost_price_total"] = None
filtered["rebate_total"] = None
filtered["freight_total"] = None
filtered["tax_total"] = None
filtered["other_fee_total"] = None
filtered["profit_total"] = None
filtered["profit_rate"] = None
filtered["commission_amount"] = None
filtered["order_source"] = None
filtered["items"] = []
filtered["approve_logs"] = []
filtered["remark"] = None
return filtered
if role_code == "salesman":
filtered = dict(order)
filtered["cost_price_total"] = None
filtered["rebate_total"] = None
filtered["profit_total"] = None
filtered["profit_rate"] = None
filtered["calculation_detail"] = None
filtered["items"] = [
{
**item,
"cost_price": None,
"rebate_amount": None,
}
for item in order.get("items", [])
]
return filtered
return order
def _filter_order_row(self, row: dict, role_code: str | None) -> dict:
if role_code == "salesman":
filtered = dict(row)
filtered["profit_total"] = None
filtered["commission_amount"] = None
return filtered
return row
# 需求 6.1 定义的合法状态流转路径
_VALID_STATUS_TRANSITIONS = {
"draft": {"pending_approve", "canceled"},
"pending_approve": {"approved", "rejected", "canceled"},
"rejected": {"draft", "pending_approve"},
"approved": {"pending_factory", "cancel_pending", "canceled"},
"pending_factory": {"pending_driver", "cancel_fulfillment_pending", "cancel_pending"},
"pending_driver": {"accepted", "cancel_fulfillment_pending"},
"accepted": {"picked_up", "cancel_fulfillment_pending"},
"picked_up": {"delivered", "cancel_fulfillment_pending"},
"delivered": {"completed", "cancel_fulfillment_pending"},
"production": {"shipped", "cancel_fulfillment_pending"},
"shipped": {"delivered", "completed", "cancel_fulfillment_pending"},
"completed": {"settled"},
"settled": set(),
"canceled": set(),
"cancel_pending": {"canceled"},
"cancel_fulfillment_pending": {"canceled"},
}
def _validate_status_transition(self, from_status: str, to_status: str) -> None:
allowed = self._VALID_STATUS_TRANSITIONS.get(from_status, set())
if to_status not in allowed:
raise AppException(
code=ErrorCode.INVALID_STATUS,
message=f"不允许从「{from_status}」变更为「{to_status}",
status_code=400,
)
def _ensure_status(self, current_status: str, allowed_statuses: set[str], message: str) -> None:
if current_status not in allowed_statuses:
raise AppException(code=ErrorCode.INVALID_STATUS, message=message, status_code=400)
def _get_config_value(self, session: Session, config_key: str, default: str) -> str:
config = self.config_repository.get_by_key(session, config_key)
return config.config_value if config is not None and config.config_value else default
def _validate_cancel_payload(self, payload: dict) -> None:
if not (payload.get("cancel_reason") or "").strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消原因不能为空", status_code=400)
def _validate_approve_payload(self, payload: dict) -> None:
approve_result = (payload.get("approve_result") or "").strip()
if approve_result not in {"pass", "reject"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="审批结果仅支持 pass 或 reject", status_code=400)
if approve_result == "reject" and not (payload.get("approve_opinion") or "").strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="审批驳回时必须填写意见", status_code=400)
def _validate_cancel_approve_payload(self, payload: dict) -> None:
approve_result = (payload.get("approve_result") or "").strip()
if approve_result not in {"pass", "refuse"}:
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消审批结果仅支持 pass 或 refuse", status_code=400)
if approve_result == "refuse" and not (payload.get("approve_opinion") or "").strip():
raise AppException(code=ErrorCode.PARAM_ERROR, message="取消审批驳回时必须填写意见", status_code=400)
def _build_calculation_detail(self, order, items) -> dict:
sale_total = float(order.sale_price_total or 0)
cost_total = float(order.cost_price_total or 0)
rebate_total = float(order.rebate_total or 0)
freight_total = float(order.freight_total or 0)
tax_total = float(order.tax_total or 0)
other_fee_total = float(order.other_fee_total or 0)
profit_total = float(order.profit_total or 0)
profit_rate = float(order.profit_rate or 0)
item_details = []
for item in items:
qty = float(item.quantity or 0)
sale_price = float(item.sale_price or 0)
cost_price = float(item.cost_price or 0)
item_sale_amount = round(qty * sale_price, 2)
item_cost_amount = round(qty * cost_price, 2)
detail_row = {
"product_name": item.product_name,
"quantity": qty,
"sale_price": sale_price,
"cost_price": cost_price,
"sale_amount": item_sale_amount,
"cost_amount": item_cost_amount,
}
# 报价引擎快照
pricing_type = getattr(item, "pricing_type", None)
if pricing_type:
detail_row["pricing_type"] = pricing_type
detail_row["length_m"] = float(item.length_m) if getattr(item, "length_m", None) else None
detail_row["width_m"] = float(item.width_m) if getattr(item, "width_m", None) else None
detail_row["area_sqm"] = float(item.area_sqm) if getattr(item, "area_sqm", None) else None
surcharge_detail = getattr(item, "surcharge_detail", None)
if surcharge_detail:
try:
detail_row["surcharge_items"] = json.loads(surcharge_detail)
except (json.JSONDecodeError, TypeError):
detail_row["surcharge_items"] = []
detail_row["supplier_model"] = getattr(item, "supplier_model", None)
detail_row["price_tier"] = getattr(item, "price_tier", None)
item_details.append(detail_row)
return {
"formula": "利润 = 销售价 - 成本价 - 回扣 - 运费 - 税费 - 其他费用",
"rate_formula": "利润率 = 利润 / 销售价 x 100%",
"sale_total": sale_total,
"cost_total": cost_total,
"rebate_total": rebate_total,
"freight_total": freight_total,
"tax_total": tax_total,
"other_fee_total": other_fee_total,
"profit_total": profit_total,
"profit_rate": profit_rate,
"item_details": item_details,
}
def _build_task_summary(self, task) -> dict:
if task is None:
return {}
return {
"task_id": task.id,
"task_no": task.task_no,
"driver_id": task.driver_id,
"factory_id": task.factory_id,
"pickup_address": task.pickup_address,
"delivery_address": task.delivery_address,
"pickup_content": task.pickup_content,
"quantity": float(task.quantity or 0),
"status": task.status,
"remark": task.remark,
"created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") if task.created_at else "",
}
def _build_attachment_row(self, attachment) -> dict:
return {
"attachment_id": attachment.id,
"biz_type": attachment.biz_type,
"biz_id": attachment.biz_id,
"file_name": attachment.file_name,
"file_url": attachment.file_url,
"file_type": attachment.file_type,
"file_size": attachment.file_size,
"file_category": attachment.file_category,
"object_key": attachment.object_key,
"created_at": attachment.created_at.strftime("%Y-%m-%d %H:%M:%S") if attachment.created_at else "",
}
def _record_supplier_text_log(self, session: Session, order, payload: dict, current_user: dict | None) -> None:
self.order_repository.create_supplier_text_log(
session,
{
"order_id": order.id,
"supplier_id": payload["supplier_id"],
"template_type": payload.get("template_type") or "default",
"text_content": payload["text_content"],
"confirmed": 1,
"confirmed_by": current_user.get("user_id") if current_user else None,
"confirmed_at": datetime.now(),
"created_by": current_user.get("user_id") if current_user else None,
},
)
def _build_supplier_text(self, order, items, template_type: str) -> str:
item_lines = []
for item in items:
qty = float(item.quantity or 0)
spec = item.specification or ""
unit = item.unit or ""
item_lines.append(f" {item.product_name} {spec} {qty}{unit}")
items_text = "\n".join(item_lines) if item_lines else " (无明细)"
if template_type == "brief":
total_qty = sum(float(item.quantity or 0) for item in items)
return f"订单 {order.order_no}\n客户:{order.customer_name}\n总数量:{total_qty}\n请安排发货。"
return (
f"订单编号:{order.order_no}\n"
f"客户:{order.customer_name}\n"
f"发货方式:{order.delivery_type or '待定'}\n"
f"产品明细:\n{items_text}\n"
f"备注:{order.remark or ''}\n"
f"请安排生产与发货。"
)
order_service = OrderService()