调价后的新价格写入报价单;多段段前的货物带到各段,协同群不再把体积里的星号吃掉。海运字段齐了先核对再查价。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -659,6 +659,246 @@ def _mark_sales_adjust(quote: dict[str, Any]) -> None:
|
||||
quote["source_label"] = "销售调价"
|
||||
|
||||
|
||||
def _sheet_follows_adjusted_fees(quote: dict[str, Any]) -> bool:
|
||||
"""
|
||||
产品、航线、销售改过的价,报价单必须跟着这份走。
|
||||
|
||||
航线只是确认沿用 TMS 时不改写舱位明细,避免把附加费之和当成总价。
|
||||
"""
|
||||
label = str(quote.get("source_label") or quote.get("sourceLabel") or "").replace(" ", "")
|
||||
if "确认TMS" in label:
|
||||
return False
|
||||
code = str(quote.get("source") or "").strip().upper()
|
||||
if code == "SALES_ADJUST" or "销售调价" in label:
|
||||
return True
|
||||
if code == "PRODUCT":
|
||||
return True
|
||||
return any(word in label for word in ("产品", "航线", "手工"))
|
||||
|
||||
|
||||
def _json_struct(value: Any) -> tuple[Any, bool]:
|
||||
"""字符串 JSON 先解开。返回 (对象, 原来是不是字符串)。"""
|
||||
if isinstance(value, str) and value.strip()[:1] in {"{", "["}:
|
||||
try:
|
||||
return json.loads(value), True
|
||||
except json.JSONDecodeError:
|
||||
return value, False
|
||||
return value, False
|
||||
|
||||
|
||||
def _put_struct(quote: dict[str, Any], key: str, value: Any, was_str: bool) -> None:
|
||||
quote[key] = json.dumps(value, ensure_ascii=False) if was_str else value
|
||||
|
||||
|
||||
def _fee_title(item: dict[str, Any]) -> str:
|
||||
return str(
|
||||
item.get("name") or item.get("feeName") or item.get("chargeName") or item.get("label") or ""
|
||||
).strip()
|
||||
|
||||
|
||||
def _write_price(item: dict[str, Any], plain: str) -> None:
|
||||
"""同一笔钱写进报价单会读的金额字段。单价有值也一起改,否则模板仍按旧单价重算。"""
|
||||
if not plain:
|
||||
return
|
||||
for key in (
|
||||
"amount",
|
||||
"costPrice",
|
||||
"cost_price",
|
||||
"salePrice",
|
||||
"sale_price",
|
||||
"unitPrice",
|
||||
"unit_price",
|
||||
"value",
|
||||
"price",
|
||||
):
|
||||
if item.get(key) not in (None, ""):
|
||||
item[key] = plain
|
||||
item["amount"] = plain
|
||||
if item.get("costPrice") not in (None, "") or item.get("cost_price") not in (None, "") or "costPrice" in item:
|
||||
item["costPrice"] = plain
|
||||
|
||||
|
||||
def _match_fee_index(name: str, fees: list[dict[str, Any]], used: set[int]) -> int:
|
||||
return next(
|
||||
(
|
||||
i
|
||||
for i, fee in enumerate(fees)
|
||||
if i not in used and fee_name_same(name, _fee_title(fee))
|
||||
),
|
||||
-1,
|
||||
)
|
||||
|
||||
|
||||
def _stamp_fee_items(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
|
||||
raw, was_str = _json_struct(quote.get("feeItems"))
|
||||
items = [dict(x) for x in raw if isinstance(x, dict)] if isinstance(raw, list) else []
|
||||
if not items:
|
||||
items = [
|
||||
{
|
||||
"name": _fee_title(fee),
|
||||
"feeName": _fee_title(fee),
|
||||
"amount": _plain_amount(str(fee.get("amount") or "")),
|
||||
"currency": fee.get("currency") or quote.get("currency") or "",
|
||||
}
|
||||
for fee in fees
|
||||
if _fee_title(fee)
|
||||
]
|
||||
else:
|
||||
used: set[int] = set()
|
||||
for item in items:
|
||||
idx = _match_fee_index(_fee_title(item), fees, used)
|
||||
if idx < 0:
|
||||
continue
|
||||
used.add(idx)
|
||||
_write_price(item, _plain_amount(str(fees[idx].get("amount") or "")))
|
||||
if not item.get("name"):
|
||||
item["name"] = _fee_title(fees[idx])
|
||||
if not item.get("feeName"):
|
||||
item["feeName"] = _fee_title(fees[idx])
|
||||
if items:
|
||||
_put_struct(quote, "feeItems", items, was_str)
|
||||
|
||||
|
||||
def _stamp_one_route(row: dict[str, Any], fees: list[dict[str, Any]]) -> None:
|
||||
"""一段线路:运输费写金额,提货/送货写到对应列。"""
|
||||
if len(fees) == 1:
|
||||
_write_price(row, _plain_amount(str(fees[0].get("amount") or "")))
|
||||
return
|
||||
transport = [fee for fee in fees if any(word in _fee_title(fee) for word in ("运输", "运费", "陆运"))]
|
||||
if len(transport) == 1:
|
||||
_write_price(row, _plain_amount(str(transport[0].get("amount") or "")))
|
||||
for fee in fees:
|
||||
title = _fee_title(fee)
|
||||
plain = _plain_amount(str(fee.get("amount") or ""))
|
||||
if not plain:
|
||||
continue
|
||||
if "提货" in title:
|
||||
row["pickup_fee"] = plain
|
||||
row["pickupFee"] = plain
|
||||
elif "送货" in title or "派送" in title:
|
||||
row["delivery_fee"] = plain
|
||||
row["deliveryFee"] = plain
|
||||
|
||||
|
||||
def _stamp_route_list(rows: list[Any], fees: list[dict[str, Any]]) -> None:
|
||||
legs = [row for row in rows if isinstance(row, dict)]
|
||||
if not legs or not fees:
|
||||
return
|
||||
if len(legs) == len(fees):
|
||||
for leg, fee in zip(legs, fees):
|
||||
_write_price(leg, _plain_amount(str(fee.get("amount") or "")))
|
||||
return
|
||||
if len(legs) == 1:
|
||||
_stamp_one_route(legs[0], fees)
|
||||
|
||||
|
||||
def _stamp_route_copies(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
|
||||
"""陆运报价单优先读 route_rows,不读已经改过的 fee_lines。"""
|
||||
for key in ("route_rows", "landSegments", "land_segments"):
|
||||
raw, was_str = _json_struct(quote.get(key))
|
||||
if not isinstance(raw, list) or not raw:
|
||||
continue
|
||||
copied = [dict(x) if isinstance(x, dict) else x for x in raw]
|
||||
_stamp_route_list(copied, fees)
|
||||
_put_struct(quote, key, copied, was_str)
|
||||
for key in ("selected_land_route", "selectedLandRoute"):
|
||||
raw, was_str = _json_struct(quote.get(key))
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
selected = dict(raw)
|
||||
legs = [dict(x) if isinstance(x, dict) else x for x in list(selected.get("legs") or [])]
|
||||
if not legs:
|
||||
continue
|
||||
_stamp_route_list(legs, fees)
|
||||
selected["legs"] = legs
|
||||
_put_struct(quote, key, selected, was_str)
|
||||
|
||||
|
||||
def _stamp_buckets(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
|
||||
"""海运报价单按始发港/海运/目的港三段读,不能只改扁平费用行。"""
|
||||
for key, _label in _SEA_BUCKETS:
|
||||
items = _bucket_items(quote, key)
|
||||
if not items:
|
||||
continue
|
||||
used: set[int] = set()
|
||||
changed = False
|
||||
for item in items:
|
||||
idx = _match_fee_index(_item_fee_name(item), fees, used)
|
||||
if idx < 0:
|
||||
continue
|
||||
used.add(idx)
|
||||
plain = _plain_amount(str(fees[idx].get("amount") or ""))
|
||||
if not plain:
|
||||
continue
|
||||
_write_price(item, plain)
|
||||
changed = True
|
||||
if changed:
|
||||
_store_bucket(quote, key, items)
|
||||
|
||||
|
||||
def _stamp_air_copies(quote: dict[str, Any], fees: list[dict[str, Any]]) -> None:
|
||||
"""空运报价单读舱位 departureCharges 和 salePrice,不读改过的 fee_lines。"""
|
||||
total_plain = _plain_amount(str(quote.get("total") or ""))
|
||||
for key in ("airOptions", "air_options", "selected_air_option", "selectedAirOption"):
|
||||
raw, was_str = _json_struct(quote.get(key))
|
||||
single = isinstance(raw, dict)
|
||||
options = [dict(raw)] if single else [dict(x) if isinstance(x, dict) else x for x in raw] if isinstance(raw, list) else []
|
||||
if not options:
|
||||
continue
|
||||
for opt in options:
|
||||
if not isinstance(opt, dict):
|
||||
continue
|
||||
charge_key = "departureCharges" if "departureCharges" in opt or "departure_charges" not in opt else "departure_charges"
|
||||
if charge_key not in opt and "departureCharges" not in opt and "departure_charges" not in opt:
|
||||
charge_key = ""
|
||||
if charge_key:
|
||||
charges, charges_str = _json_struct(opt.get(charge_key))
|
||||
if isinstance(charges, list) and charges:
|
||||
used: set[int] = set()
|
||||
copied = [dict(x) if isinstance(x, dict) else x for x in charges]
|
||||
for charge in copied:
|
||||
if not isinstance(charge, dict):
|
||||
continue
|
||||
idx = _match_fee_index(_fee_title(charge), fees, used)
|
||||
if idx < 0:
|
||||
continue
|
||||
used.add(idx)
|
||||
_write_price(charge, _plain_amount(str(fees[idx].get("amount") or "")))
|
||||
opt[charge_key] = json.dumps(copied, ensure_ascii=False) if charges_str else copied
|
||||
if total_plain:
|
||||
if opt.get("salePrice") not in (None, ""):
|
||||
opt["salePrice"] = total_plain
|
||||
if opt.get("sale_price") not in (None, ""):
|
||||
opt["sale_price"] = total_plain
|
||||
if single:
|
||||
_put_struct(quote, key, options[0], was_str)
|
||||
else:
|
||||
_put_struct(quote, key, options, was_str)
|
||||
|
||||
|
||||
def stamp_prices_for_sheet(quote: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""
|
||||
把当前生效费用写进报价单实际读取的每一份副本。
|
||||
|
||||
销售调价、产品报价、航线报价都会改 fee_lines,但陆运单读 route_rows,
|
||||
海运单读三段费用,空运单读舱位 departureCharges。这些副本不跟着改,
|
||||
报价单上就还是 TMS 原价。TMS 原价和「确认沿用 TMS」不改写这些副本。
|
||||
"""
|
||||
src = dict(quote or {})
|
||||
if not _sheet_follows_adjusted_fees(src):
|
||||
return src
|
||||
fees = [dict(row) for row in quote_fee_rows(src)]
|
||||
if not fees:
|
||||
return src
|
||||
src["fee_lines"] = fees
|
||||
src["fee_rows"] = fees
|
||||
_stamp_fee_items(src, fees)
|
||||
_stamp_route_copies(src, fees)
|
||||
_stamp_buckets(src, fees)
|
||||
_stamp_air_copies(src, fees)
|
||||
return src
|
||||
|
||||
|
||||
def compute_profit(*, x: Optional[float], baseline: str) -> Optional[float]:
|
||||
"""
|
||||
只在销售调价后算一个利润点。
|
||||
@@ -975,6 +1215,7 @@ def apply_adjust(
|
||||
if tms_sale and "tms_baseline_sale" not in out:
|
||||
out["tms_baseline_sale"] = tms_sale
|
||||
_mark_sales_adjust(out)
|
||||
out = stamp_prices_for_sheet(out)
|
||||
logger.info(
|
||||
"quote_adjust applied=%s unmatched=%s hint=%s profit=%s",
|
||||
[x.get("name") for x in applied],
|
||||
|
||||
Reference in New Issue
Block a user