"""Bounded quote matching and Decimal arithmetic, independent of Flask/models.""" from decimal import Decimal, ROUND_HALF_UP import re from .pricing import dims_to_cm from .quote_intent import US, QUOTE_WORDS, parse_intent, is_followup def broad_zone(postal): if not postal: return None if postal.startswith(('8','9')): return '美西' if postal.startswith('46') or postal[0] in '0123': return '美东' return '美中' def variants(intent, book): if intent.get('allocations'): return [{**destination,'allocation':part} for part in intent['allocations'] for destination in variants({**intent,'allocations':None,'warehouse':part['warehouse'], 'warehouses':None,'postal':None},book)] if len(intent.get('warehouses') or [])>1: return [variant for warehouse in intent['warehouses'] for variant in variants({**intent,'warehouse':warehouse,'warehouses':None,'postal':None},book)] if not intent['warehouse'] and len(intent.get('zones') or [])>1: return [variant for zone in intent['zones'] for variant in variants({**intent,'zone':zone,'zones':None},book)] warehouse = intent['warehouse'] found = [r for r in book['warehouses'] if r['code'] == warehouse] if warehouse else [] zoned = [r for r in found if r['zone']] selected_destination=intent.get('plan_destination') if selected_destination and selected_destination['warehouse']==warehouse: zoned=[row for row in zoned if row['zone']==selected_destination['zone']] if zoned: unique = {} for row in zoned: postal = intent['postal'] or row['postal'][:5] key = (row['zone'], postal) unique.setdefault(key, {'label':f"{warehouse} · {row['zone']}", 'postal':postal, 'zone':broad_zone(postal), 'warehouse':warehouse}) return list(unique.values()) postal = intent['postal'] if not postal and found: match = re.search(r'(?0 else None volume_weight = ((volume or Decimal(0))*1000000/6000 if rate['kind']=='parcel' else (volume or Decimal(0))*167) charge = max(real, volume_weight,package_charge) if rate['kind'] == 'parcel': charge = max(charge, Decimal(12)*(intent['boxes'] or 1)) return charge if real or volume else None def match_rates(intent, book, variant): selected = [] kinds = intent['kinds'] or (['truck','parcel'] if variant['warehouse'] else ['parcel']) for rate in book['rates']: if intent.get('plan_unit') and rate['unit']!=intent['plan_unit']:continue if rate['kind'] not in kinds or (intent['origin'] and intent['origin'] != rate['origin']): continue if intent['services'] and not any(name in rate['service'] for name in intent['services']): continue if any(name in rate['service'] for name in intent.get('excluded_services') or []): continue if intent['tax'] and rate['tax'] != intent['tax']: continue if rate['kind'] == 'truck': if not variant['warehouse'] or variant['warehouse'] not in [value.upper() for value in rate['destinations']]: continue elif variant['zone'] and rate['zone'] != variant['zone']: continue selected.append(rate) groups = {} for rate in selected: key = (rate['service'],rate['origin'],rate['zone'],rate['unit'],rate['tax']) charge = quantity(intent,rate) if rate['kind'] == 'parcel' and Decimal(rate['min_weight']) > (charge or Decimal(12)): continue if key not in groups or Decimal(rate['min_weight']) > Decimal(groups[key]['min_weight']): groups[key] = rate return list(groups.values()) def number(value, places=2): return format(Decimal(value).quantize(Decimal(10)**-places, rounding=ROUND_HALF_UP), 'f').rstrip('0').rstrip('.') if places else str(value) def exact_number(value): text=format(Decimal(value),'f') return text.rstrip('0').rstrip('.') if '.' in text else text def quantity_number(value,intent): if Decimal(value)==Decimal(intent.get('weight') or '0') or Decimal(value).as_tuple().exponent>=-6: return exact_number(value) return '约'+number(value,6) def reference_days(rate): match=re.search(r'(?0 and high>=low else None def select_timed_rates(rates,slowest=False,by_origin=False): """Compare numeric day ranges within one region, service kind and billing basis.""" ranked=[] best={} for rate in rates: days=reference_days(rate) if not days:continue low,high=days key=(rate['zone'],rate['kind'],rate['unit'],rate['tax']) if by_origin:key+=(rate['origin'],) timing=(high,low) if slowest else (low,high) ranked.append((rate,key,timing)) choose=max if slowest else min best[key]=choose(best.get(key,timing),timing) return [rate for rate,key,timing in ranked if timing==best[key]] def select_cheapest_rates(intent,book,rates,fees,zone=None): from .quote_fees import charge_details ranked=[] best={} for rate in rates: amount=quantity(intent,rate) if amount is None:continue key=(zone or rate['zone'],rate['unit'],rate['tax']) value=amount*Decimal(rate['price'])+charge_details(intent,rate,fees,book)['cny'] ranked.append((rate,key,value)) best[key]=min(best.get(key,value),value) return [rate for rate,key,value in ranked if value==best[key]] def render_rates(intent, book, destinations, remote_fees): from .quote_fees import charge_details from .quote_language import translate_quote lines = [] plan_number=0 rate_selection=intent.get('selection') or intent.get('mode') if intent.get('mode')=='time': lines.append('美国线路参考时效:') elif intent['weight']: lines.append(f"美国 {exact_number(intent['weight'])}KG 基础运费:") else: lines.append('美国线路报价:') if intent.get('quantity_note'):lines.append(intent['quantity_note']) if intent.get('selected_plan_number'):lines.append(f'当前按方案{intent["selected_plan_number"]}继续核算。') if intent.get('mode')=='recommend': lines.append({'fastest':'按您优先时效的要求,可考虑下面的较快方案。', 'cheapest':'按您优先运费的要求,可考虑下面的较低价方案。'}.get(rate_selection, '更看重时效可选下面的较快方案,更看重运费可选较低价方案。')) if intent.get('allocations'): lines.append('按各目的仓分票计算,箱规需逐票核对:') if intent.get('metal_chargeable_weight') is not None: lines.append('请分别提供各目的仓的铁铝货物计费重,整票铁铝基数不能重复套用到每个仓。') elif len(intent.get('warehouses') or [])>1: lines.append('以下按各目的仓分别比较;实际分票请提供每个仓的重量。') if intent.get('air_requested') and not intent.get('air_only'): lines.append('当前资料没有美国空运费率,下面列出海运方案。') if intent.get('mode')=='difference': from .quote_comparison import render_difference return render_difference(intent,book,destinations,remote_fees) for destination, fees in zip(destinations, remote_fees): part=destination.get('allocation') billing=({**intent,'weight':part['weight'],'packages':part.get('packages'),'dims':None,'cbm':None,'boxes':part.get('boxes')} if part else intent) if part and intent.get('metal_chargeable_weight') is not None: billing.update(metal_chargeable_weight=None,metal_scope='partial') rates = match_rates(billing, book, destination) if rate_selection=='cheapest' and rates: rates=select_cheapest_rates(billing,book,rates,fees,zone=destination['zone']) or rates lines.append('按相同区域、计费单位及税费方式比较的较低价方案:') if not rates: lines.append(f"{destination['label']}:当前资料没有匹配的线路价格,提供目的仓码或邮编可继续查询。") continue if destination['label'] != '美国': lines.append(destination['label']+':') selections=[('',rates)] if rate_selection in ('fastest','slowest','speed_compare','fastest_cheapest'): selections=[] if rate_selection in ('fastest','speed_compare','fastest_cheapest'): selections.append(('最快参考方案',select_timed_rates(rates,by_origin=intent.get('mode')=='quote_detail'))) if rate_selection in ('slowest','speed_compare'): selections.append(('最慢参考方案',select_timed_rates(rates,slowest=True,by_origin=intent.get('mode')=='quote_detail'))) if rate_selection=='fastest_cheapest': selections.append(('最便宜参考方案',select_cheapest_rates(billing,book,rates,fees,zone=destination['zone']))) lines.append('按相同区域、运输方式、计费单位及税费方式比较。' if rate_selection=='fastest_cheapest' else '按相同区域、运输方式、计费单位及税费方式比较参考时效。最快按天数下限、最慢按天数上限选择。') grouped = {} for selection,selected_rates in selections: if selection and not selected_rates: plan_number+=1 lines.append(f'方案{plan_number} · '+selection+('暂时无法确定,请提供货物重量和箱规。' if selection=='最便宜参考方案' else '暂时无法确定,匹配线路未列明可比较的天数。')) for rate in selected_rates: key = (selection, rate['service'], rate['origin'], rate['unit'], rate['tax'], rate['eta']) grouped.setdefault(key, []).append(rate) last_selection=None for (selection,service, origin, unit, tax,eta_source), rows in grouped.items(): if selection and selection!=last_selection: plan_number+=1 lines.append(f'方案{plan_number} · '+selection) last_selection=selection eta = re.sub(r'\s*无时效赔偿', '', eta_source).strip() if rows[0]['kind']=='parcel' and eta and '提取' not in eta:eta+='提取' elif rows[0]['kind']=='truck' and eta and '船开' not in eta:eta='开船后参考'+eta if intent.get('mode')=='time': lines.append(f'- {service} · {origin},{eta or "当前资料未列明时效"}。') continue parts = [] for rate in rows: amount = quantity(billing,rate) price = Decimal(rate['price']) rate_text = f"{rate['zone']} " if rate['zone'] and not destination['zone'] else '' rate_text += f"{number(price)}元/{'KG' if unit == 'kg' else 'CBM'}" if amount is not None: total = amount*price details=charge_details(billing,rate,fees,book) total+=details['cny'] rate_text += f",{quantity_number(amount,billing)}{'KG' if unit == 'kg' else 'CBM'} = {number(total)}元" if details['items']:rate_text+='('+ ';'.join(details['items'])+')' for note in details['notes']: if note not in lines:lines.append(note) elif intent.get('cbm') and rate['unit']=='cbm': lines.append('计费体积四舍五入后不足0.01CBM,最低计费体积需人工确认,不能按零费用出价。') parts.append(rate_text) prefix='' if not selection and not intent.get('selected_plan_number'): plan_number+=1 prefix=f'方案{plan_number} · ' lines.append(f"- {prefix}{service} · {origin} · {'包税' if tax=='included' else '不包税'}:"+';'.join(parts) + (f'({eta})' if eta else '')) if fees: if fees['fee'] is None: lines.append('该邮编属于遥远地址,派送附加费需单独确认。') else: lines.append(f"海派已计入偏远附加费 {number(fees['fee'])}元/KG。") if intent.get('mode')=='time' and not any(intent.get(key) for key in ('warehouse','postal')): lines.append('发我目的仓码或邮编,可继续匹配到仓线路。') if intent.get('mode')=='time': lines.append('以上时效按开船后到提取计算,送达时间需结合末端派送进度核对。') if re.search(r'哪天|几号|什么时候|何时|收到',intent['text']): lines.append('请补充预计开船日期和目的仓码或邮编,再核对具体到货日期。') elif not (intent.get('mode')=='recommend' and rate_selection=='fastest_cheapest'): from .quote_followup_fields import missing_fields question=missing_fields(intent) if question:lines.append(question) if intent.get('mode')=='calculation': lines.append('计算过程:各箱实重与体积重取大;海派每箱不足12KG按12KG计费,再按整票计费重选择重量档位,乘对应单价,并单列已知附加费。') if intent.get('mode')=='recommend' and rate_selection=='fastest_cheapest': lines.append('您更看重预算还是时效?') text='\n'.join(lines) return translate_quote(text) if intent.get('english') else text