logistics/logisticsClass/logisticsTail_UK.py

489 lines
21 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.

""" 英国尾端物流模块实现类"""
import math
from pathlib import Path
import re
import pandas
from logisticsClass.logisticsBaseClass import LogisticsType, TailLogistics
class ZGDPDLogistics_UK(TailLogistics):
# 实重计费
country_code = 'UK'
country = 'United Kingdom'
company = '智谷-DPD'
currency = 'GBP'
def __init__(self):
super().__init__()
self.base_fee = 3.8
self.oversize = 26
self.fuel_rate = 0.2 # BT开头的有20%燃油费
self.remote_fee = 35
def calculate_fee(self, packages, postcode):
detail_amount = {
"base":0.00,
"oversize":0.00,
"remote":0.00,
"fuel":0.00,
"tail_amount":0.00
}
# zone = self.is_remote(postcode)
# if zone == 2:
# return detail_amount
if self.is_remote(postcode):
detail_amount['remote'] = self.remote_fee
for package in packages:
if package.weight >= 40000 or package.fst_size >= 175 or package.girth>=339:
detail_amount['tail_amount'] =99999
return detail_amount
if package.weight > 30000 or package.fst_size >= 100 or package.sed_size >=60:
detail_amount['oversize'] += self.oversize
detail_amount['base'] += self.base_fee
detail_amount['tail_amount'] = detail_amount['base'] + detail_amount['oversize'] + detail_amount['remote']
# if postcode.startswith('BT'):
# detail_amount['fuel'] = detail_amount['tail_amount'] * self.fuel_rate
# detail_amount['tail_amount'] += detail_amount['fuel']
return detail_amount
def is_remote(self,postcode):
"""判断是否偏远,1偏远0非偏远"""
# 先判断邮编是否合法
# if not re.match(r'^[A-Z]{1,2}[0-9]{1,2}[A-Z]?\s?[0-9][ABD-HJLNP-UW-Z]{2}$', postcode):
# print("邮编不合法")
# return 2
postcode = postcode.split()[0].upper()
if postcode[0:2] not in ['BT','IM','JE','ZE','GY','HS','PO','IV','KA','KW','PH','PA']:
return 0
remote_postcodes = ["GY1-9","HS1-9","PO30-41","IM1-9","JE1-4","ZE1-3","BT1-71",
"BT86-88","BT74-82","BT92-97","IV1-28","IV36","IV40-56","PA20-38",
"PA41-48","IV63","KA27-28","KW1-17","PH19-26","PH31-44"]
postcodelist = []
# 解析并直接展开
for postcodes in remote_postcodes:
if "-" in postcodes: # 如果包含范围
prefix = ''.join(filter(str.isalpha, postcodes)) # 提取字母前缀
start, end = map(int, (''.join(filter(str.isdigit, part)) for part in postcodes.split("-")))
postcodelist.extend([f"{prefix}{i}" for i in range(start, end + 1)])
else:
postcodelist.append(postcodes) # 没有范围,直接添加
return 1 if postcode in postcodelist else 0
class SAIRDPDLogistics_UK(TailLogistics):
# 实重计费
country_code = 'UK'
country = 'United Kingdom'
company = 'SAIR-DPD'
currency = 'GBP'
parent_current_directory = Path(__file__).parent.parent
price_path = parent_current_directory.joinpath("data")
_price_files = price_path.joinpath("智谷物流费.xlsx")
dpd_zone = None
def __new__(cls):
"""实现单例模式,只加载一次文件"""
if cls.dpd_zone is None:
# 取AB两列即可
cls.dpd_zone = pandas.read_excel(cls._price_files,sheet_name="DPD-SAIR分区",usecols='A:B')
return super().__new__(cls)
def __init__(self):
super().__init__()
self.base = {
'A': 3.5,
'B': 10,
'C': 12,
'D': 16
}
self.transportation = 0.11 # 交通费每单0.11
self.remote_fee = 0.95 # 偏远费
self.fuel_rate = 0.05 # 燃油费率
def is_remote(self,postcode):
"""根据邮编分区,返回分区"""
postcode_prefix = postcode.split()[0].upper()
postcode_prefix = str(postcode_prefix)
# 取前缀的字母
letters = ''.join(re.findall(r'[A-Za-z]', postcode.split()[0])).upper()
letters=str(letters)
# 先匹配字母,字母能找到就直接用,字母对应的找不到,再匹配带上数字的
zone_df = self.dpd_zone[self.dpd_zone['邮编']== letters]
if zone_df.empty:
zone_df = self.dpd_zone[self.dpd_zone['邮编']== postcode_prefix]
if not zone_df.empty:
return zone_df['分区'].values[0]
return "A"
def calculate_fee(self, packages, postcode):
detail_amount = {
"base":0.00,
"transportation":0.00,
"remote":0.00,
"fuel":0.00,
"tail_amount":0.00
}
zone = self.is_remote(postcode)
if zone == "不在配送范围内":
detail_amount['tail_amount'] = 99999
return detail_amount
base_fee = self.base[zone]
postcode_prefix = postcode.split()[0].upper()
postcode_prefix = str(postcode_prefix)
if postcode_prefix in ['E1','EC1','EC2','EC3','EC4','NW1','SE1','SE11','SW1','SW3','SW7','W1','W10','W11','W2','W8','WC1','WC2']:
detail_amount['remote'] = self.remote_fee
for package in packages:
if package.weight > 32000 or package.fst_size > 230:
detail_amount['tail_amount'] =99999
return detail_amount
detail_amount["base"] += base_fee
detail_amount['transportation']=self.transportation
detail_amount['tail_amount'] = (detail_amount['base'] + detail_amount['transportation'] + detail_amount['remote'])*(1+self.fuel_rate)
detail_amount['fuel']=self.fuel_rate*(detail_amount['base'] + detail_amount['transportation'] + detail_amount['remote'])
return detail_amount
class bigLogistics_UK(TailLogistics):
# 计费重5000取大
country_code = 'UK'
country = 'United Kingdom'
company = '智谷-大件'
currency = 'GBP'
def __init__(self):
super().__init__()
self.base_fee:float = 8
self.oversize:float = 0
self.congestion = 3
def calculate_fee(self, packages, postcode):
detail_amount = {
"base":0.00,
"oversize":0.00,
"congestion":0.00, # 伦敦中心地区有拥堵费
"tail_amount":0.00}
# 0正常1收拥堵费,2不派送
is_remote = self.is_remote(postcode)
if is_remote==2:
detail_amount['tail_amount'] =99999
return detail_amount
elif is_remote==1:
detail_amount['congestion'] = self.congestion
for package in packages:
# 计费重
volume_weight = package.get_volume_weight(5000)
bill_weight = math.ceil(max(package.weight/1000,volume_weight))
if package.weight/1000 > 150 or volume_weight > 600 or package.fst_size > 300:
detail_amount['tail_amount'] =99999
return detail_amount
# 大包 计费重 20以内-->820,50-->0.5*(计费重-20)+8 , 50以上-->50+0.5*(计费重-50)
if bill_weight < 50 and package.fst_size < 300:
detail_amount['base'] += 8
if bill_weight > 20:
detail_amount['oversize'] += 0.5 * (bill_weight -20)
else:
detail_amount['base'] += 50
if bill_weight > 50:
detail_amount['oversize'] += 0.5 * (bill_weight -50)
detail_amount['tail_amount'] = detail_amount['base'] + detail_amount['oversize'] + detail_amount['congestion']
return detail_amount
def is_remote(self,postcode):
"""判断邮编情况,0正常1收拥堵费,2不派送"""
# 先判断邮编是否合法
# if not re.match(r'^[A-Z]{1,2}[0-9]{1,2}[A-Z]?\s?[0-9][ABD-HJLNP-UW-Z]{2}$', postcode):
# print("邮编不合法")
# return 2
postcode = postcode.strip().upper()
# 先判断london中心区
congestion = ['E','EC','N','NW','SE','SW','W','WC']
for c in congestion:
pattern = rf"^{c}\d.*"
if re.match(pattern, postcode):
return 1
# prefixes = ["BS","AL","B","BA","BB","BD","BH","BL","BN","BR","CB","CH","CM","CR","CT","CV","CW","DA","DE","DN","DT","DY","EN","FY","GL","GU","HA","HD","HG","HP","HU","HX","IG","L","LA","LE","LN","LS","LU","M","ME","MK","NG","NN","OL","OX","PE","PO1-24","PR","RG","RH","RM","S","SG","SK","SL","SM","SO","SP","SS","ST","TF","TN","TW","UB","WA","WD","WF","WN","WR","WS","WV","YO","KT","OX","PR","RG","RM","S","SG","SK","SL","SM","SN","ST","TF","TW","UB","WA","WD","WF","WN","WR","WS","WV",]
# for prefix in prefixes:
# if "-" in prefix:
# # 处理范围格式前缀 (如 "PO1-24")
# po_post = []
# prefix_code = ''.join(filter(str.isalpha, prefix)) # 提取字母前缀
# start, end = map(int, (''.join(filter(str.isdigit, part)) for part in prefix.split("-")))
# po_post.extend([f"{prefix_code}{i}" for i in range(start, end + 1)])
# if postcode.split()[0] in po_post:
# return 0
# else:
# # 精确匹配单一前缀(后面必须是数字或空格)
# pattern = rf"^{prefix}\d.*"
# if re.match(pattern, postcode):
# return 0
return 0
class KPZGLogistics_UK(TailLogistics):
# 实重
country_code = 'UK'
country = 'United Kingdom'
company = '卡派-ZG'
currency = 'GBP'
logistics_type = LogisticsType.LTL
def __init__(self):
super().__init__()
self.base_dice = {
55: ['RG7-8', 'RG14', 'RG17-29'],
65: ['B','CV','DY','NN','WS','WV'],
75: ['AL', 'BA', 'BB', 'BD', 'BL', 'BS', 'CB', 'CF', 'CH', 'CM', 'CW', 'DE', 'DN', 'FY',
'GL', 'HD', 'HG', 'HP', 'H', 'HU', 'L', 'LA1-9', 'LE', 'NG', 'NN', 'NP', 'OL',
'OX', 'PE1-19', 'PE26-29', 'PE34', 'PE38-99', 'PR', 'S', 'SG', 'SK', 'SN', 'ST',
'TF', 'WA', 'WD', 'WF', 'WN', 'WR', 'M', 'BR', 'HA','CO', 'DH', 'DL', 'IP', 'NE', 'NR', 'PE20-25', 'PE30-32', 'PE33', 'PE35-37',
'PO1-29', 'RH', 'SO', 'SP', 'SR', 'SS', 'TA', 'TS', 'YO', 'UB', 'KT', 'HR','DA','EN',
'IG','RG1-7','RG9-13','RG15-16','RG30-99','RM','SL'],
78: ['BH','DT','GU1-24','GU26-99','ME','TW','BN','EX','LD','TN','TQ'],
80: ['CR','SM'],
95: ['E','EC','N','NW','SE','SW','W','WC','CA','CT','DG','EH','FK','G','KA1-26','KA29-99','KY','LA10-99','LL','ML','PA1-19','PL','SA','SY','TD','TR'],
125:['DD','PH1-7','PH14'],
140:['IV1-3','IV30','IV36','AB10-16','AB21-25'],
150:['HS1-2','IV20-29','IV37-99','PH15-26','KW1-14','PH8-13','PO30-41'],
160:['HS3-99','KA27-28','PA20-33','PA35-59','PA62-75','PA79-99','PH27-41','PH45-50'],
200:['KW15-17','PA34','PA60-61','PA76-78','PH42-44', 'ZE' ],
}
def calculate_fee(self, packages, postcode):
detail_amount = {
"base":0.00,
"oversize":0.00,
"tail_amount":0.00}
postcode_prefix = postcode.split()[0].upper()
letters = ''.join([char for char in postcode_prefix if char.isalpha()])
numbers = ''.join([char for char in postcode_prefix if char.isdigit()])
if numbers=='':
detail_amount['tail_amount'] = 99999
return detail_amount
for price,codes in self.base_dice.items():
for code in codes:
if letters == code:
detail_amount['base'] += price
break
elif code.startswith(letters) and '-' in code:
start, end = map(int, (''.join(filter(str.isdigit, part)) for part in code.split("-")))
if int(numbers) in range(start, end + 1):
detail_amount['base'] += price
break
else:
code_numbers = ''.join([char for char in code if char.isdigit()])
if code_numbers=='':
continue
if int(numbers) == int(code_numbers):
detail_amount['base'] += price
break
if detail_amount['base']>0:
break
# 处理超尺寸问题
if detail_amount['base']==0:
detail_amount['tail_amount'] = 99999
return detail_amount
# 计算托盘数 初始为1个
nember = 0
for package in packages:
nember =nember + (int(package.fst_size/180)+int(package.sed_size/120)+int(package.weight/800000))
detail_amount['oversize'] = detail_amount['base'] * nember
detail_amount['tail_amount'] = detail_amount['base'] + detail_amount['oversize']
return detail_amount
class KPNVlogistics_UK(TailLogistics):
country_code = 'UK'
country = 'United Kingdom'
company = '卡派-NV'
currency = 'GBP'
logistics_type = LogisticsType.LTL
parent_current_directory = Path(__file__).parent.parent
price_path = parent_current_directory.joinpath("data")
_price_files = price_path.joinpath("英国卡派.xlsx")
ltl_cost = None
ltl_zone = None
def __new__(cls):
"""实现单例模式,只加载一次文件"""
if cls.ltl_cost is None or cls.ltl_zone is None:
cls.ltl_cost = pandas.read_excel(cls._price_files,sheet_name="NV运费")
cls.ltl_zone = pandas.read_excel(cls._price_files,sheet_name="NV分区")
return super().__new__(cls)
def __init__(self):
super().__init__()
self.base_fee = 0
self.fuel_rate = 0.1
def is_remote(self,postcode):
"""根据邮编分区,返回分区"""
postcode_prefix = postcode.split()[0].upper()
postcode_prefix = str(postcode_prefix)
zone_df = self.ltl_zone[self.ltl_zone['邮编']== postcode_prefix]
if not zone_df.empty:
return zone_df['区域'].values[0]
return "不在配送范围内"
def calculate_fee(self, packages, postcode):
detail_amount = {
"base":0.00,
"fuel":0.00,
"tail_amount":0.00
}
zone = self.is_remote(postcode)
if zone == "不在配送范围内":
detail_amount['tail_amount'] = 99999
return detail_amount
for package in packages:
tuopan = math.ceil(package.fst_size/120)
tuopan = min(tuopan, 7)
base_df = self.ltl_cost[(self.ltl_cost['分区']==zone)&(self.ltl_cost['托盘']==tuopan)]
if base_df.empty:
detail_amount['tail_amount'] = 99999
return detail_amount
self.base_fee = base_df['运费'].values[0]
price = self.base_fee * tuopan/len(packages)
detail_amount['base'] += price
detail_amount['fuel'] = detail_amount['base'] * self.fuel_rate
detail_amount['tail_amount'] = detail_amount['base']+detail_amount['fuel']
return detail_amount
class ZGbigLogistics_UK(TailLogistics):
country_code = 'UK'
country = 'United Kingdom'
company = '海GB-大件' # 分区的智谷大件
currency = 'GBP'
# logistics_type = LogisticsType.LTL
parent_current_directory = Path(__file__).parent.parent
price_path = parent_current_directory.joinpath("data")
_price_files = price_path.joinpath("智谷物流费.xlsx")
xl_zone = None
def __new__(cls):
"""实现单例模式,只加载一次文件"""
if cls.xl_zone is None:
# 取AB两列即可
cls.xl_zone = pandas.read_excel(cls._price_files,sheet_name="大件分区",usecols='A:B')
return super().__new__(cls)
def __init__(self):
super().__init__()
def is_remote(self,postcode):
"""根据邮编分区,返回分区"""
postcode_prefix = postcode.split()[0].upper()
postcode_prefix = str(postcode_prefix)
# 取前缀的字母
letters = ''.join(re.findall(r'[A-Za-z]', postcode.split()[0])).upper()
letters=str(letters)
# 先匹配字母,字母能找到就直接用,字母对应的找不到,再匹配带上数字的
zone_df = self.xl_zone[self.xl_zone['邮编']== letters]
if zone_df.empty:
zone_df = self.xl_zone[self.xl_zone['邮编']== postcode_prefix]
if not zone_df.empty:
return zone_df['分区'].values[0]
return "不在配送范围内"
def calculate_fee(self, packages, postcode):
"""5000计费重 一票一件
单个包裹实重上限150kg
体积重上限600Kg
尺寸上限: 300*180*180cm
"""
detail_amount = {
"base":0.00,
"fuel":0.00,
"tail_amount":0.00
}
zone = self.is_remote(postcode)
if zone == "不在配送范围内":
detail_amount['tail_amount'] = 99999
return detail_amount
for package in packages:
volume_weight = package.get_volume_weight(5000)
if package.weight>=150000 or volume_weight>=600 or package.fst_size>300 or package.sed_size>180:
detail_amount['tail_amount'] = 99999
return detail_amount
bill_weight = max(package.weight/1000, volume_weight)
detail_amount['base'] += self.get_shipping_cost(zone, bill_weight)
detail_amount['tail_amount'] = detail_amount['base']
return detail_amount
def get_shipping_cost(self, zone, weight):
"""获取运费"""
price_table = {
'A': [14, 19, 25, 50, 65, 85, 100, 120],
'B': [19, 24, 30, 55, 70, 90, 105, 125],
'C': [27, 30, 35, 60, 80, 95, 115, 135],
'D': [36, 40, 45, 70, 90, 105, 125, 145]
}
weight_breaks = [30, 40, 50, 70, 90, 110, 130, 150]
overweight_rate = 0.5 # 续重费率 GBP/KG
prices = price_table[zone]
# 150KG以内的标准费用
for i, break_point in enumerate(weight_breaks):
if weight < break_point:
return prices[i]
# 超过150KG的部分基础费用 + 续重费用
if weight >= 150:
base_cost = prices[-1] # 150KG对应的基础费用120, 125, 135, 145
overweight = weight - 150
return base_cost + overweight * overweight_rate
return prices[-1] # 默认返回最后一个价格
# BJS-SAIR
# class BJSbigLogistics_UK(TailLogistics):
# country_code = 'UK'
# country = 'United Kingdom'
# company = 'BJS-SAIR'
# currency = 'GBP'
# logistics_type = LogisticsType.LTL
# parent_current_directory = Path(__file__).parent.parent
# price_path = parent_current_directory.joinpath("data")
# _price_files = price_path.joinpath("")
# xl_zone = None
# def __new__(cls):
# """实现单例模式,只加载一次文件"""
# if cls.xl_zone is None:
# # 取AB两列即可
# cls.xl_zone = pandas.read_excel(cls._price_files,sheet_name="BJS分区",usecols='A:B')
# return super().__new__(cls)
# def __init__(self):
# super().__init__()
# def is_remote(self,postcode):
# """根据邮编分区,返回分区"""
# postcode_prefix = postcode.split()[0].upper()
# postcode_prefix = str(postcode_prefix)
# # 取前缀的字母
# letters = ''.join(re.findall(r'[A-Za-z]', postcode.split()[0])).upper()
# letters=str(letters)
# # 先匹配字母,字母能找到就直接用,字母对应的找不到,再匹配带上数字的
# zone_df = self.xl_zone[self.xl_zone['邮编']== letters]
# if zone_df.empty:
# zone_df = self.xl_zone[self.xl_zone['邮编']== postcode_prefix]
# if not zone_df.empty:
# return zone_df['分区'].values[0]
# return "不在配送范围内"
# def calculate_fee(self, packages, postcode):
# """5000计费重 一票一件
# 单个包裹实重上限150kg
# 体积重上限600Kg
# 尺寸上限: 300*180*180cm
# """
# detail_amount = {
# "base":0.00,
# "fuel":0.00,
# "tail_amount":0.00
# }
# zone = self.is_remote(postcode)
# if zone == "不在配送范围内":
# detail_amount['tail_amount'] = 99999
# return detail_amount
# for package in packages:
# volume_weight = package.get_volume_weight(5000)
# if package.weight>=150000 or volume_weight>=600 or package.fst_size>300 or package.sed_size>180:
# detail_amount['tail_amount'] = 99999
# return detail_amount
# bill_weight = max(package.weight/1000, volume_weight)
# detail_amount['base'] += self.get_shipping_cost(zone, bill_weight)
# detail_amount['tail_amount'] = detail_amount['base']
# return detail_amount
if __name__ == '__main__':
# # 关闭渠道
bigLogistics_UK.active = True