Compare commits

...

2 Commits

Author SHA1 Message Date
fa17eb2089 7-3 修复旧订单成本为0及业务员需求规格缺失
后端 get_order 新增 _enrich_items_from_db,对 cost_price=0 的
订单项用定价引擎重算并回写数据库,解决旧订单成本始终显示0的问题。
业务员订单明细表增加需求规格列,移除角色过滤后无意义的成本单价列。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 17:23:25 +08:00
a77f67b456 7-3 修复定价引擎成本为0:前端传递需求规格尺寸给后端
calcDemandResult 解析需求规格时同步写入 length_m/width_m,
提交和编辑保存时一并传递,修复定价引擎因缺少尺寸输入而计算出0的问题。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 17:12:13 +08:00
4 changed files with 138 additions and 30 deletions

View File

@ -149,6 +149,124 @@ class OrderService:
return enriched return enriched
def _enrich_items_from_db(self, session: Session, items: list) -> list[dict]:
"""读取数据库订单项,对 cost_price=0 的项用定价引擎重算。
解决旧订单在定价引擎集成前创建cost_price 存为 0 的问题
仅在 cost_price 0 且存在对应定价规则时触发计算
Args:
session: 数据库会话
items: SalesOrderItem ORM 对象列表
Returns:
序列化后的订单项字典列表
"""
from backend.app.models.business import ProductPricingRule, Product
# 一次性加载所有有定价规则的产品属性和规则
product_ids = [item.product_id for item in items if item.product_id]
rules_map = {}
products_map = {}
if product_ids:
for rule in session.query(ProductPricingRule).filter(
ProductPricingRule.product_id.in_(product_ids),
ProductPricingRule.deleted == 0,
ProductPricingRule.status == 1,
).all():
rules_map[rule.product_id] = rule
for product in session.query(Product).filter(Product.id.in_(product_ids)).all():
products_map[product.id] = product
result = []
for item in items:
item_dict = {
"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,
"demand_specification": getattr(item, "demand_specification", None),
"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),
}
# cost_price 为 0 且有定价规则时,尝试用引擎重算
if item_dict["cost_price"] == 0 and item.product_id and item.product_id in rules_map:
rule = rules_map[item.product_id]
product = products_map.get(item.product_id)
product_attrs = {}
if product:
if product.thickness:
product_attrs["thickness"] = product.thickness
if product.weight_gsm:
product_attrs["weight_gsm"] = product.weight_gsm
if product.default_width_m:
product_attrs["default_width_m"] = float(product.default_width_m)
user_inputs = {}
if getattr(item, "length_m", None):
user_inputs["length"] = float(item.length_m)
user_inputs["length_unit"] = "m"
if getattr(item, "width_m", None):
user_inputs["width"] = float(item.width_m)
user_inputs["width_unit"] = "m"
if item.quantity:
user_inputs["quantity"] = float(item.quantity)
if getattr(item, "price_tier", None):
user_inputs["price_tier"] = item.price_tier
surcharge_detail = getattr(item, "surcharge_detail", None)
if surcharge_detail:
try:
surcharges = json.loads(surcharge_detail) if isinstance(surcharge_detail, str) else surcharge_detail
if isinstance(surcharges, list):
for s in surcharges:
key = s.get("key")
if key:
user_inputs[f"surcharge_{key}"] = True
except (json.JSONDecodeError, TypeError):
pass
try:
calc_result = pricing_engine.calculate_for_order_item(rule, user_inputs, product_attrs)
snapshot = calc_result.get("item_snapshot", {})
if snapshot.get("cost_price") is not None and snapshot["cost_price"] > 0:
item_dict["cost_price"] = snapshot["cost_price"]
item_dict["pricing_type"] = snapshot.get("pricing_type") or item_dict["pricing_type"]
if calc_result.get("area_sqm"):
item_dict["area_sqm"] = calc_result["area_sqm"]
# 回写数据库,避免下次重复计算
try:
item.cost_price = snapshot["cost_price"]
if snapshot.get("pricing_type"):
item.pricing_type = snapshot["pricing_type"]
if calc_result.get("area_sqm"):
item.area_sqm = calc_result["area_sqm"]
session.flush()
except Exception:
pass
except Exception:
pass
result.append(item_dict)
return result
def list_orders(self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None) -> dict: def list_orders(self, session: Session | None = None, filters: dict | None = None, current_user: dict | None = None) -> dict:
"""查询订单列表。 """查询订单列表。
@ -318,34 +436,7 @@ class OrderService:
"order_type": order.order_type, "order_type": order.order_type,
"self_delivery": order.self_delivery or 0, "self_delivery": order.self_delivery or 0,
"tracking_number": order.tracking_number, "tracking_number": order.tracking_number,
"items": [ "items": self._enrich_items_from_db(session, 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,
"demand_specification": getattr(item, "demand_specification", None),
"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": [ "approve_logs": [
{ {
"log_id": log.id, "log_id": log.id,

View File

@ -396,6 +396,8 @@ function buildDefaultItem() {
product_name: "", product_name: "",
specification: "", specification: "",
demand_specification: "", demand_specification: "",
length_m: null,
width_m: null,
_demandResult: "", _demandResult: "",
_demandCalcHint: "", _demandCalcHint: "",
unit: "", unit: "",
@ -793,6 +795,8 @@ function calcDemandResult(row) {
if (!spec) { if (!spec) {
row._demandResult = ""; row._demandResult = "";
row._demandCalcHint = ""; row._demandCalcHint = "";
row.length_m = null;
row.width_m = null;
return; return;
} }
@ -819,6 +823,9 @@ function calcDemandResult(row) {
const area = m1 * m2; const area = m1 * m2;
row._demandResult = `${area.toFixed(4)}`; row._demandResult = `${area.toFixed(4)}`;
row._demandCalcHint = `${v1}${u1} × ${v2}${u2} = ${m1}m × ${m2}m = ${area.toFixed(4)}`; row._demandCalcHint = `${v1}${u1} × ${v2}${u2} = ${m1}m × ${m2}m = ${area.toFixed(4)}`;
// 使
row.length_m = m2;
row.width_m = m1;
return; return;
} }
@ -829,12 +836,16 @@ function calcDemandResult(row) {
const unit = weightMatch[2]; const unit = weightMatch[2];
row._demandResult = `${value}${unit}`; row._demandResult = `${value}${unit}`;
row._demandCalcHint = `重量: ${value}${unit}`; row._demandCalcHint = `重量: ${value}${unit}`;
row.length_m = null;
row.width_m = null;
return; return;
} }
// //
row._demandResult = ""; row._demandResult = "";
row._demandCalcHint = ""; row._demandCalcHint = "";
row.length_m = null;
row.width_m = null;
} }
const paymentChannels = ref([]); const paymentChannels = ref([]);

View File

@ -1148,10 +1148,13 @@ function addEditItem() {
editItems.value.push({ editItems.value.push({
product_name: '', product_name: '',
specification: '', specification: '',
demand_specification: '',
unit: '', unit: '',
quantity: 1, quantity: 1,
sale_price: 0, sale_price: 0,
cost_price: 0, cost_price: 0,
length_m: null,
width_m: null,
}); });
} }
@ -1191,10 +1194,13 @@ async function saveEdit() {
product_id: item.product_id || null, product_id: item.product_id || null,
product_name: item.product_name.trim(), product_name: item.product_name.trim(),
specification: item.specification?.trim() || '', specification: item.specification?.trim() || '',
demand_specification: item.demand_specification?.trim() || '',
unit: item.unit?.trim() || '', unit: item.unit?.trim() || '',
quantity: Number(item.quantity) || 1, quantity: Number(item.quantity) || 1,
sale_price: Number(item.sale_price) || 0, sale_price: Number(item.sale_price) || 0,
cost_price: Number(item.cost_price) || 0, cost_price: Number(item.cost_price) || 0,
length_m: item.length_m != null ? Number(item.length_m) : null,
width_m: item.width_m != null ? Number(item.width_m) : null,
})); }));
const rawStatus = detail.order.rawStatus; const rawStatus = detail.order.rawStatus;

View File

@ -139,18 +139,18 @@
<tr> <tr>
<th>产品名称</th> <th>产品名称</th>
<th>规格</th> <th>规格</th>
<th>需求规格</th>
<th>数量</th> <th>数量</th>
<th>计价方式</th> <th>计价方式</th>
<th>成本单价</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="item in orderItems" :key="item.id"> <tr v-for="item in orderItems" :key="item.id">
<td>{{ item.productName }}</td> <td>{{ item.productName }}</td>
<td>{{ item.specification }}</td> <td>{{ item.specification }}</td>
<td>{{ item.demandSpecification || '-' }}</td>
<td>{{ item.quantity }}{{ item.unit }}</td> <td>{{ item.quantity }}{{ item.unit }}</td>
<td>{{ getPricingTypeLabel(item.pricingType) }}</td> <td>{{ getPricingTypeLabel(item.pricingType) }}</td>
<td>¥{{ item.costPrice }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>