444 lines
21 KiB
Python
444 lines
21 KiB
Python
from datetime import datetime
|
|
|
|
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.order_approve_log_repository import OrderApproveLogRepository
|
|
from backend.app.repositories.order_repository import OrderRepository
|
|
from backend.app.services.demo_store import demo_store
|
|
|
|
|
|
class OrderService:
|
|
def __init__(self) -> None:
|
|
self.order_repository = OrderRepository()
|
|
self.customer_repository = CustomerRepository()
|
|
self.order_approve_log_repository = OrderApproveLogRepository()
|
|
|
|
def list_orders(self, session: Session | None = None, filters: dict | None = None) -> list[dict]:
|
|
if session is not None:
|
|
try:
|
|
orders = self.order_repository.list_orders_by_filters(session, filters or {})
|
|
return [
|
|
{
|
|
"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 "",
|
|
}
|
|
for item in orders
|
|
]
|
|
except SQLAlchemyError:
|
|
pass
|
|
return demo_store.list_orders()
|
|
|
|
def get_order(self, order_id: int, session: Session | 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
|
|
|
|
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)
|
|
return {
|
|
"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,
|
|
}
|
|
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": {}, "trace_list": []},
|
|
"attachments": [],
|
|
"remark": order.remark,
|
|
}
|
|
except SQLAlchemyError:
|
|
pass
|
|
return demo_store.get_order(order_id)
|
|
|
|
def submit_order(self, order_id: int, session: Session | 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_status(order.order_status, {"draft", "rejected"}, "当前状态不允许提交审核")
|
|
self.order_repository.update_order_status(session, order, "pending_approve")
|
|
session.commit()
|
|
return {
|
|
"order_id": order.id,
|
|
"order_status": order.order_status,
|
|
"submitted_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
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) -> dict | None:
|
|
if session is not None:
|
|
try:
|
|
order = self.order_repository.get_order(session, 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,
|
|
)
|
|
|
|
next_status = "canceled" if order.order_status in {"draft", "pending_approve"} else "cancel_pending"
|
|
previous_status = order.order_status
|
|
self.order_repository.update_cancel_fields(
|
|
session,
|
|
order,
|
|
{
|
|
"order_status": next_status,
|
|
"cancel_reason": payload["cancel_reason"],
|
|
"cancel_opinion": payload.get("cancel_opinion"),
|
|
"cancel_previous_status": None if next_status == "canceled" else previous_status,
|
|
},
|
|
)
|
|
session.commit()
|
|
return {
|
|
"order_id": order.id,
|
|
"order_status": order.order_status,
|
|
"previous_status": previous_status,
|
|
"canceled_at": None,
|
|
}
|
|
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": None,
|
|
}
|
|
|
|
def approve_order(self, order_id: int, payload: dict, session: Session | 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_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": None,
|
|
},
|
|
)
|
|
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 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) -> 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_status(order.order_status, {"cancel_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)
|
|
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": 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 SQLAlchemyError:
|
|
session.rollback()
|
|
|
|
order = demo_store.get_order(order_id)
|
|
if order is None:
|
|
return None
|
|
self._ensure_status(order["order_status"], {"cancel_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) -> 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_status(order.order_status, {"approved"}, "当前状态不允许生成发厂文案")
|
|
supplier = self.order_repository.get_supplier(session, payload["supplier_id"])
|
|
if supplier is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
|
|
return {
|
|
"order_id": order.id,
|
|
"supplier_id": supplier.id,
|
|
"template_type": supplier.template_type or "default",
|
|
"text_content": f"订单 {order.order_no} 请安排生产与发货,客户:{order.customer_name}",
|
|
}
|
|
except SQLAlchemyError:
|
|
pass
|
|
|
|
order = demo_store.get_order(order_id)
|
|
if order is None:
|
|
return None
|
|
self._ensure_status(order["order_status"], {"approved"}, "当前状态不允许生成发厂文案")
|
|
return {
|
|
"order_id": order_id,
|
|
"supplier_id": payload["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) -> 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_status(order.order_status, {"approved"}, "当前状态不允许确认发厂")
|
|
supplier = self.order_repository.get_supplier(session, payload["supplier_id"])
|
|
if supplier is None:
|
|
raise AppException(code=ErrorCode.NOT_FOUND, message="工厂不存在", status_code=404)
|
|
|
|
# 当前阶段先把确认发厂和状态流转打通,文案内容后续再落库存档。
|
|
self.order_repository.update_supplier_text_confirm(session, order)
|
|
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 SQLAlchemyError:
|
|
session.rollback()
|
|
|
|
order = demo_store.get_order(order_id)
|
|
if order is None:
|
|
return None
|
|
self._ensure_status(order["order_status"], {"approved"}, "当前状态不允许确认发厂")
|
|
return {
|
|
"order_id": order_id,
|
|
"order_status": "pending_factory",
|
|
"confirmed_at": "2026-05-14 13:00:00",
|
|
}
|
|
|
|
def create_order(self, payload: dict, session: Session | 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": None,
|
|
"credit_limit": 0,
|
|
"remark": None,
|
|
},
|
|
)
|
|
|
|
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": None,
|
|
"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,
|
|
)
|
|
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 SQLAlchemyError:
|
|
session.rollback()
|
|
|
|
return demo_store.create_order(payload)
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
order_service = OrderService()
|