Files
baiyue-logistics-chat/logistics_chat/quote_fees.py
T

123 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Amounts come from the active book; explicit customer-approved fee policies live here."""
from decimal import Decimal
import re
from .pricing import dims_to_cm
def document(book,required,service=None):
rows=[d for d in book['documents'] if (not service or d['sheet'] in (service,'海派'))
and all(term in d['content'] for term in required)]
return rows[0]['content'] if rows else ''
def amount(text,pattern):
match=re.search(pattern,text,re.I)
return Decimal(match.group(1)) if match else None
def material_fee(book):
text=document(book,('铁铝','附加费'))
return amount(text,r'铁铝制品[^\n]*?[¥¥]\s*(\d+(?:\.\d+)?)\s*/KG')
def material_charge(intent,charged,unit,book):
result={'cny':Decimal(0),'items':[],'notes':[]}
material=intent.get('material')
if material not in ('metal','unknown_metal'):return result
per_kg=material_fee(book)
if per_kg is None:
result['notes'].append('铁铝附加费费率尚未明确,当前金额未包含该项,请联系人工确认。')
return result
if material=='unknown_metal':
result['notes'].append('请先确认金属是否属于铁铝制品;铁铝附加费只按铁铝货物的计费重收取,当前金额未包含待核实的材质费用。')
return result
basis=intent.get('metal_chargeable_weight')
if basis is None and intent.get('metal_scope')=='partial':
result['notes'].append(f'铁铝附加费{per_kg:g}元/KG只按铁铝货物的计费重收取,不能按整票重量加费;请提供铁铝部分的计费重,当前金额未包含该项。')
return result
if basis is None and unit!='kg':
result['notes'].append(f'铁铝附加费为{per_kg:g}元/KG,当前按方运费未包含该项;需确认铁铝货物计费重量后单列,不能把CBM直接乘公斤附加费。')
return result
basis=Decimal(basis) if basis is not None else charged
if basis is None:return result
if unit=='kg' and charged is not None and basis>charged:
result['notes'].append('铁铝货物计费重高于当前整票可计算的计费重,请补充各箱实重和尺寸核对;当前金额未包含该项。')
return result
fee=basis*per_kg
result['cny']=fee
result['items'].append(f'铁铝附加费{per_kg:g}元/KG×{basis:g}KG={fee:g}元')
return result
def specimens(intent):
if intent.get('packages'):
return [{**p,'dims':p.get('dims') or intent.get('dims'),
'dim_unit':p.get('dim_unit') if p.get('dims') else intent.get('dim_unit','cm')} for p in intent['packages']]
if intent.get('boxes')==1 and intent.get('weight'):
return [{'count':1,'weight':intent['weight'],'dims':intent.get('dims'),'dim_unit':intent.get('dim_unit','cm')}]
return []
def limit_fees(intent,book):
"""Maximum per-piece limit fee, not the sum of overlapping weight/size charges.
The malformed 0<length<274 rule does not create a new threshold. Only two
independently explicit large-piece conditions can establish that tier.
"""
normal=document(book,('正常货物',))
cleaned=re.sub(r'\s+','',normal).replace('<','<').upper()
limits=[amount(cleaned,p) for p in (r'0KG<单件重量<(\d+)KG',r'0CM<最长单边长度<(\d+)CM',
r'0CM<第二边<(\d+)CM',r'最长边<(\d+)CM')]
normal_fee=amount(document(book,('超重收',)),r'超重收\s*[++]?\s*(\d+(?:\.\d+)?)')
size_text=document(book,('普通超尺寸',))
size_fee=amount(size_text,r'加收\s*(\d+(?:\.\d+)?)\s*RMB/件')
large_text=document(book,('大尺寸货物','任意2个'))
large_fee=amount(large_text,r'加收\s*(\d+(?:\.\d+)?)\s*RMB/件')
huge=document(book,('超大件货物',))
huge_fee=amount(huge,r'加收\s*USD\s*(\d+(?:\.\d+)?)')
result={'cny':Decimal(0),'items':[],'notes':[]}
if not all(limits) or normal_fee is None:return result
max_weight,max_length,max_second,max_girth=limits
for piece in specimens(intent):
weight=Decimal(piece['weight']);count=piece['count']
dims=sorted([Decimal(str(x)) for x in dims_to_cm(piece['dims'],piece['dim_unit'])],reverse=True) if piece['dims'] else []
length,second,girth=(dims[0],dims[1],dims[0]+2*(dims[1]+dims[2])) if dims else (None,None,None)
if weight>68 or (dims and (length>270 or girth>419)):
result['notes'].append('该件可能超出承运尺寸或重量,需人工确认是否接收。'+(f'原表超大件若接收的附加费为{huge_fee:g}USD/件。' if huge_fee else ''))
continue
candidates=[]
if weight>max_weight:candidates.append(normal_fee)
if dims and (length>max_length or second>max_second or girth>max_girth) and size_fee is not None:candidates.append(size_fee)
if large_fee is not None and max_weight<weight<68 and dims and 330<girth<419:
candidates.append(large_fee)
elif dims and (length>=243 or girth>=330):
result['notes'].append('该件接近大尺寸档,原表有一处长度条件不清,适用档位需人工确认。')
if candidates:
fee=max(candidates)*count
result['cny']+=fee
result['items'].append(f'超重/超尺寸按最高项{max(candidates):g}元/件×{count}件={fee:g}元')
return result
def charge_details(intent,rate,remote,book):
from .quote_engine import quantity
result={'cny':Decimal(0),'items':[],'notes':[]}
charged=quantity(intent,rate)
if charged is None:return result
if rate['kind']=='parcel' and remote:
if remote['fee'] is not None:
fee=charged*Decimal(remote['fee']);result['cny']+=fee
result['items'].append(f'偏远费{remote["fee"]}元/KG×{charged:g}KG={fee:g}元')
else:result['notes'].append('遥远地址的附加费需单独确认,当前金额未包含该项。')
metal=material_charge(intent,charged,rate['unit'],book)
result['cny']+=metal['cny'];result['items']+=metal['items'];result['notes']+=metal['notes']
if rate['kind']=='parcel':
limits=limit_fees(intent,book)
result['cny']+=limits['cny'];result['items']+=limits['items'];result['notes']+=limits['notes']
if intent.get('address_type')=='non_fba':
result['notes'].append('非亚马逊地址还需核对住宅/商业派送附加费,原表此项需单独询价。')
return result