1207 lines
57 KiB
Python
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()
|