"""Extracted pricing rules with unchanged business behavior.""" import math import re try: from . import rates_extra as RX except Exception: RX = None RATES = { "sea": { "name": "欧洲海运快线(双清/递延)", "tiers": [(2000, 10.0), (500, 10.2), (101, 10.3), (21, 12.3)], "divisor": 6000, "min_kg": 21, "time": "开船后 38-45 天提取", "min_pcs_kg": None, "remote": 7, "note": "德国 UPS/DHL/DPD/CWE 统配;欧洲递延 -1.7元/kg;樱花快线 +0.3元/kg;因查验/罢工/节假日顺延", }, "rail": { "name": "义乌-中欧铁路专线", "tiers": [(1000, 13.5), (100, 13.5), (21, 15.5)], "divisor": 6000, "min_kg": 21, "time": "发车后 36-40 天提取", "min_pcs_kg": 13, "light_fee": 5, "remote": 7, "note": "义乌装柜;VAT 递延 -1元/kg;单件计费重不足 13kg 按超轻费加收", }, "truck": { "name": "中欧卡航", "tiers": [(4000, 17.3), (2000, 17.3), (500, 17.4), (101, 17.4), (21, 19.4)], "divisor": 6000, "min_kg": 21, "time": "发车后欧盟 16-20 天提取", "min_pcs_kg": 13, "light_fee": 5, "remote": 7, "note": "递延 -1.5元/kg;纯纺织品 +5元/kg;普卡时效 18-22 天", }, "air": { "name": "欧洲空运专线(空派)", "tiers": [(101, 36), (71, 37), (45, 38), (21, 39)], "divisor": 6000, "min_kg": 21, "time": "10-13 天左右提取", "min_pcs_kg": 12, "light_fee": None, "remote": 7, "note": "一票多件单件不足 12kg 按 12kg 计费;快线 8-13 天;直飞快线 12-15 天", }, "sea_uk": { "name": "英国海运(双清)", "tiers": [(301, 10.9), (101, 10.9), (21, 12.8)], "divisor": 6000, "min_kg": 21, "time": "参考时效 40 天提取", "remote": None, "note": "VAT自税/递延 10.0/8.1 元/kg;按KG卡派 12.2/10.3;限时达 13.5/11.6;北极航线 25-28 天(义乌装)", }, "rail_uk": { "name": "英国铁路(双清)", "tiers": [(301, 16.2), (101, 16.2), (21, 18.0)], "divisor": 6000, "min_kg": 21, "time": "发车后 25-30 天提取", "remote": None, "note": "VAT自税/递延 15/13.2 元/kg;限时达 19.2/17.4(38天);按KG卡派 17.3/15.5", }, "truck_uk": { "name": "中英卡航(双清)", "tiers": [(301, 19.4), (101, 19.4), (21, 21.4)], "divisor": 6000, "min_kg": 21, "time": "发车后 20-22 天提取", "remote": None, "note": "VAT自税/递延 18.4/16.4 元/kg;卡派 20.9/18.9;单件不超30kg,尺寸≤98×68×59cm", }, "air_uk": { "name": "英国空运专线(双清)", "tiers": [(101, 42), (51, 44), (22, 48)], "divisor": 6000, "min_kg": 22, "time": "8-10 个自然日提取", "min_pcs_kg": 12, "light_fee": None, "remote": None, "note": "不包税 VAT 自税/递延 44/40/38;带电 47.5/42.5/41.5;拒收北爱尔兰/苏格兰高地偏远地址", }, } CBM_CHANNELS = ("卡航", "普卡", "铁路", "海运") def parse_dims(t): """解析长x宽x高,支持小数、空格及 x/×/* 分隔。 用"最长连续数字"匹配三组,要求每组前后都不与其它数字相连, 这样 "100x80x60 200kg" 里的 60 不会被后面的 200 吞成 6020。 """ match = re.search(r'(?= min_w: return price, min_w return tiers[-1][1], tiers[-1][0] def calc_warehouse_card(cbm, channel, warehouse_key=None, country=None): """海外仓卡派:按方(CBM)计费,1CBM+ 和 0.5CBM+ 两档 country 传入国家代码(DE/FR/PL/CZ/ES)时,仅选择包含该后缀的仓点组;无匹配不报价。 """ if not RX or not RX.WAREHOUSE_CARD: return None keys = list(RX.WAREHOUSE_CARD) key = warehouse_key if key and key not in RX.WAREHOUSE_CARD: return None if not key and country: for k in keys: if ("-" + country) in k.upper(): key = k break if not key: return None if not key: key = keys[0] row = RX.WAREHOUSE_CARD.get(key) or RX.WAREHOUSE_CARD[keys[0]] p = row.get(channel) or row["海运"] price = p["cbm1"] if cbm >= 1 else p["cbm05"] if price is None: return None tier = "1CBM+" if cbm >= 1 else "0.5CBM+" stores = [s for s in key.splitlines() if s.strip()] if country: stores = [s for s in stores if ("-" + country) in s.upper()] if not stores: return None else: stores = stores[:1] note = "报价表中的仓点分组:" + "、".join(stores) return { "warehouse": "\n".join(stores), "stores": stores, "channel": channel, "cbm": cbm, "note": note, "price_cbm": price, "tier": tier, "total": round(cbm * price, 2), "formula": f"{cbm:g} CBM × {price:,.0f} 元/CBM = {cbm * price:,.0f} 元", } def detect_surcharge(text): """识别产品类型,返回报价表中的附加费条目""" if not RX: return None best = None for product, fee in RX.PRODUCT_SURCHARGES: # 用产品名里的前几个字做匹配(如“手机”“电池”“玩具”) for seg in re.split(r"[,,、\s()()]", product): seg = seg.strip() if len(seg) >= 2 and seg in text: if best is None or len(seg) > len(best[2]): best = (product, fee, seg) if best: return best[0], best[1] return None def check_island(postcode): """按邮编判断是否属于 DPD 岛屿偏远地区(附加费 35 欧/件)""" if not RX: return None pc = re.sub(r"[^0-9]", "", str(postcode)) if not pc: return None for country, info in RX.DPD_ISLAND_POSTCODES.items(): seg = info["text"] # 直接匹配的 4~5 位邮编 for m in re.finditer(r"\d{4,5}", seg): if pc.startswith(m.group(0)): return country, info["code"], m.group(0) # 区间形式 07000–07999 for m in re.finditer(r"(\d{4,5})\s*[–\-]\s*(\d{4,5})", seg): lo, hi = int(m.group(1)), int(m.group(2)) if len(m.group(1)) == len(pc) and lo <= int(pc) <= hi: return country, info["code"], m.group(0) return None def surcharge_reply(product, fee, weight_kg=None): """附加费说明(能算就给出金额)""" txt = f"【产品附加费】{product}:{fee}" if weight_kg and "/KG" in fee.upper(): m = re.search(r"\+?(\d+(?:\.\d+)?)", fee) if m: extra = float(m.group(1)) * weight_kg txt += f"\n 按 {weight_kg:g} kg 计:约 {extra:,.0f} 元(以最终计费重为准)" return txt def calc_freight(weight_kg, channel, dims=None, unit="cm"): """按报价表规则计算运费(数字全部由代码算) 报价表规则: - 阶梯单价:按总重量落入的档位取价 - 材积:长×宽×高(cm) ÷ 6000,与实重取大值作为计费重量 - 单件最低计费重 / 超轻费:见 RATES 里的 min_pcs_kg、light_fee - 偏远地址:+7元/kg(不含油),最低 250 元/票 """ r = RATES[channel] cm = dims_to_cm(dims, unit) if dims else None divisor = r.get("divisor", 6000) vol = round(cm[0] * cm[1] * cm[2] / divisor, 2) if cm else None real = weight_kg or 0 charge = round(max(real, vol) if vol else real, 2) # 实重与体积重取大 min_kg = r.get("min_kg") or 0 charge = max(charge, min_kg) # 报价表起收档 price, tier = pick_rate(charge, r["tiers"]) total = round(charge * price, 2) unit_note = "" if dims and unit != "cm": unit_note = "(原尺寸 %g×%g×%g %s,已换算为 %g×%g×%g cm)" % (*dims, unit, *cm) return { "channel": channel, "name": r["name"], "real": real, "vol": vol, "charge": charge, "dims": dims, "dims_cm": cm, "unit": unit, "unit_note": unit_note, "dim_rule": ("长×宽×高÷%d = %g×%g×%g÷%d = %g kg" % (divisor, *cm, divisor, vol)) if cm else None, "price": price, "tier": tier, "formula": f"{charge:g} kg × {price} 元/kg = {total:,.0f} 元", "total": total, "time": r["time"], "min_pcs_kg": r.get("min_pcs_kg"), "light_fee": r.get("light_fee"), "remote": r.get("remote"), "note": r.get("note"), } def freight_reply(intent): """根据已知信息拼出可读的计算说明(给模型当参考,也直接展示)""" w = intent["weight"] or 0 dims = intent["dims"] unit = intent.get("dim_unit") or "cm" lines = [] default_channels = ['sea_uk', 'air_uk'] if intent.get('region') == 'uk' else ['sea', 'air'] channels = [intent["channel"]] if intent["channel"] else default_channels for ch in channels: c = calc_freight(w, ch, dims, unit) parts = [f"实重 {w:g} kg" if w else "实重:未提供"] if c["vol"]: parts.append(f"体积重 {c['vol']:g} kg") body = ( f"【{c['name']}】按时效 {c['time']},计费方式:阶梯单价 × 计费重量\n" + (f" 尺寸:{c['unit_note']}\n" if c["unit_note"] else "") + f" 计费重量 {c['charge']:g} kg(" + ",".join(parts) + ",取较大值)\n" + (f" 材积算法:{c['dim_rule']}\n" if c["dim_rule"] else "") + f" 适用档位:{c['tier']}KG+ 档,单价 {c['price']} 元/kg\n" + f" 运费:{c['formula']}\n" ) if c["min_pcs_kg"] and c["charge"] < c["min_pcs_kg"]: body += f" 注意:单件计费重不足 {c['min_pcs_kg']}kg,需按 {c['min_pcs_kg']}kg 起算或加收超轻费 {c['light_fee']} 元/kg\n" if c["remote"]: body += f" 偏远地址:额外 +{c['remote']} 元/kg,单票最低 250 元\n" if c["note"]: body += f" 其他:{c['note']}\n" lines.append(body.rstrip("\n")) return "\n\n".join(lines)