319 lines
11 KiB
Python
319 lines
11 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
美国售价模型 V2 (shopfad)
|
|||
|
|
通过输入多包裹体积和采购价格,快速计算售价
|
|||
|
|
"""
|
|||
|
|
import math
|
|||
|
|
from utils.gtools import MySQLconnect
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
|
|||
|
|
class USLogisticsPrice:
|
|||
|
|
"""美国物流价格查询"""
|
|||
|
|
|
|||
|
|
_instance = None
|
|||
|
|
|
|||
|
|
def __new__(cls):
|
|||
|
|
if cls._instance is None:
|
|||
|
|
cls._instance = super().__new__(cls)
|
|||
|
|
cls._instance._init_data()
|
|||
|
|
return cls._instance
|
|||
|
|
|
|||
|
|
def _init_data(self):
|
|||
|
|
"""从数据库加载价格数据"""
|
|||
|
|
conn = MySQLconnect('logistics')
|
|||
|
|
with conn as c:
|
|||
|
|
self.sell_usps = pd.read_sql("SELECT * FROM sell_usps", c.con)
|
|||
|
|
self.sell_uandf = pd.read_sql("SELECT * FROM sell_uandf", c.con)
|
|||
|
|
self.sell_fedex = pd.read_sql("SELECT * FROM sell_fedex", c.con)
|
|||
|
|
|
|||
|
|
def get_express_fee(self, packages, profit_rate=0.359):
|
|||
|
|
"""
|
|||
|
|
计算快递费用 (尾程) - 支持多包裹
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
packages: 包裹列表, 每个元素为 dict:
|
|||
|
|
{'length_cm', 'width_cm', 'height_cm', 'weight_g'}
|
|||
|
|
profit_rate: 利润系数
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
float: 快递费用 (USD)
|
|||
|
|
"""
|
|||
|
|
# 先遍历所有包裹确定物流类型(取最严格的)
|
|||
|
|
head_type = 0
|
|||
|
|
index = 0
|
|||
|
|
for pkg in packages:
|
|||
|
|
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
|||
|
|
sizes = sorted([l, w, h], reverse=True)
|
|||
|
|
fst, sed, trd = sizes[0], sizes[1], sizes[2]
|
|||
|
|
vol_6000 = l * w * h / 6000
|
|||
|
|
|
|||
|
|
if wt > 420:
|
|||
|
|
head_type = max(head_type, 1)
|
|||
|
|
if fst > 50 or sed > 40 or trd > 30 or wt > 2718 or vol_6000 > 3624:
|
|||
|
|
head_type = max(head_type, 2)
|
|||
|
|
if fst > 264 or wt > 67000 or vol_6000 > 95000 or (fst + sed * 2 + trd * 2) > 391:
|
|||
|
|
head_type = max(head_type, 3)
|
|||
|
|
index = 1
|
|||
|
|
if fst > 310:
|
|||
|
|
head_type = max(head_type, 4)
|
|||
|
|
index = 1
|
|||
|
|
|
|||
|
|
# 逐包裹计算快递费
|
|||
|
|
sku_express_fee = 0
|
|||
|
|
sku_total_cubic_feet = 0
|
|||
|
|
ltl_over_size = 0
|
|||
|
|
ltl_over_weightfee = 0
|
|||
|
|
|
|||
|
|
for pkg in packages:
|
|||
|
|
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
|||
|
|
sizes = sorted([l, w, h], reverse=True)
|
|||
|
|
fst, sed, trd = sizes[0], sizes[1], sizes[2]
|
|||
|
|
volume = l * w * h
|
|||
|
|
vol_8500 = volume / 8500
|
|||
|
|
vol_6000 = volume / 6000
|
|||
|
|
|
|||
|
|
lbs_weight = math.ceil(max(vol_8500 / 0.453, wt / 453))
|
|||
|
|
oz_weight = math.ceil(wt / 28)
|
|||
|
|
|
|||
|
|
express_base_fee = 0
|
|||
|
|
oversize_charge = 0
|
|||
|
|
ahs_weight = 0
|
|||
|
|
ahs_dimension = 0
|
|||
|
|
|
|||
|
|
if head_type == 0:
|
|||
|
|
try:
|
|||
|
|
row = self.sell_usps[self.sell_usps['oz'] == oz_weight]
|
|||
|
|
if len(row) > 0:
|
|||
|
|
express_base_fee = row['最终费用_v2'].iloc[0] / profit_rate
|
|||
|
|
else:
|
|||
|
|
head_type = 1
|
|||
|
|
except:
|
|||
|
|
head_type = 1
|
|||
|
|
|
|||
|
|
if head_type == 1:
|
|||
|
|
try:
|
|||
|
|
row = self.sell_uandf[self.sell_uandf['lbs'] == lbs_weight]
|
|||
|
|
if len(row) > 0:
|
|||
|
|
express_base_fee = row['加权价格_v2'].iloc[0] / profit_rate
|
|||
|
|
else:
|
|||
|
|
head_type = 2
|
|||
|
|
except:
|
|||
|
|
head_type = 2
|
|||
|
|
|
|||
|
|
if head_type == 2:
|
|||
|
|
try:
|
|||
|
|
row = self.sell_fedex[self.sell_fedex['lbs'] == lbs_weight]
|
|||
|
|
if len(row) > 0:
|
|||
|
|
express_base_fee = row['售价尾端价格_v3'].iloc[0]
|
|||
|
|
else:
|
|||
|
|
express_base_fee = 99999
|
|||
|
|
head_type = 3
|
|||
|
|
if fst > 238 or (fst + sed * 2 + trd * 2) > 315:
|
|||
|
|
oversize_charge = 109.4
|
|||
|
|
elif wt > 21000:
|
|||
|
|
ahs_weight = 6.1
|
|||
|
|
except:
|
|||
|
|
head_type = 3
|
|||
|
|
|
|||
|
|
sku_express_fee += express_base_fee + oversize_charge + ahs_weight + ahs_dimension
|
|||
|
|
|
|||
|
|
# 卡派计算
|
|||
|
|
cubic_feet = math.ceil(volume / 1000000 * 35.3147)
|
|||
|
|
sku_total_cubic_feet += cubic_feet
|
|||
|
|
if fst > 250:
|
|||
|
|
ltl_over_size = max(ltl_over_size, round(130 / profit_rate - 118, 1))
|
|||
|
|
if 111000 <= wt < 130000:
|
|||
|
|
ltl_over_weightfee = max(ltl_over_weightfee, round(80 / profit_rate - 78, 1))
|
|||
|
|
if 130000 <= wt < 157000:
|
|||
|
|
ltl_over_weightfee = max(ltl_over_weightfee, round(130 / profit_rate - 118, 1))
|
|||
|
|
|
|||
|
|
# 卡派基础费用
|
|||
|
|
if sku_total_cubic_feet < 25:
|
|||
|
|
ltl_base_fee1 = round(198 / profit_rate / 2, 2)
|
|||
|
|
elif sku_total_cubic_feet < 35:
|
|||
|
|
ltl_base_fee1 = round(231 / profit_rate / 2, 2)
|
|||
|
|
else:
|
|||
|
|
ltl_base_fee1 = round(max(231, 6.6 * sku_total_cubic_feet) / profit_rate / 2, 2)
|
|||
|
|
|
|||
|
|
ltl_base_fee = ltl_base_fee1 * 1.025
|
|||
|
|
ltl_fee = ltl_base_fee + ltl_over_size + ltl_over_weightfee
|
|||
|
|
|
|||
|
|
if index == 0:
|
|||
|
|
return min(ltl_fee, sku_express_fee)
|
|||
|
|
else:
|
|||
|
|
return ltl_fee
|
|||
|
|
|
|||
|
|
|
|||
|
|
class USSellPrice:
|
|||
|
|
"""
|
|||
|
|
美国售价计算器 V2 (shopfad)
|
|||
|
|
|
|||
|
|
使用示例:
|
|||
|
|
calculator = USSellPrice()
|
|||
|
|
|
|||
|
|
# 单包裹
|
|||
|
|
result = calculator.calculate(
|
|||
|
|
packages=[{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500}],
|
|||
|
|
purchase_price_cny=100,
|
|||
|
|
preset='shopfad'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 多包裹
|
|||
|
|
result = calculator.calculate(
|
|||
|
|
packages=[
|
|||
|
|
{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500},
|
|||
|
|
{'length_cm': 40, 'width_cm': 30, 'height_cm': 15, 'weight_g': 1000},
|
|||
|
|
],
|
|||
|
|
purchase_price_cny=100,
|
|||
|
|
preset='shopfad'
|
|||
|
|
)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
PRESETS = {
|
|||
|
|
'shopfad': {
|
|||
|
|
'ocean_first_cny': 0.5,
|
|||
|
|
'ocean_first_usd': 0.7,
|
|||
|
|
'air_first_usd': 0.65,
|
|||
|
|
'air_cny_type': 0.093,
|
|||
|
|
'air_first_fix': 22.7,
|
|||
|
|
'exchange_rate': 6.5,
|
|||
|
|
'profit_rate': 0.38,
|
|||
|
|
'air_rate': 0.7,
|
|||
|
|
'tax_rate': 0.1,
|
|||
|
|
'shipping_type': 1,
|
|||
|
|
'fuel_rate': 0,
|
|||
|
|
'extra_rate': 0
|
|||
|
|
},
|
|||
|
|
'shopfad2': {
|
|||
|
|
'ocean_first_cny': 1,
|
|||
|
|
'ocean_first_usd': 1,
|
|||
|
|
'air_first_usd': 0.65,
|
|||
|
|
'air_cny_type': 0.093,
|
|||
|
|
'air_first_fix': 22.7,
|
|||
|
|
'exchange_rate': 6.5,
|
|||
|
|
'profit_rate': 0.38,
|
|||
|
|
'air_rate': 0.7,
|
|||
|
|
'tax_rate': 0.145,
|
|||
|
|
'shipping_type': 1,
|
|||
|
|
'fuel_rate': 0,
|
|||
|
|
'extra_rate': 0
|
|||
|
|
},
|
|||
|
|
'shopfad3': {
|
|||
|
|
'ocean_first_cny': 0.67,
|
|||
|
|
'ocean_first_usd': 0.7,
|
|||
|
|
'air_first_usd': 0.65,
|
|||
|
|
'air_cny_type': 0.093,
|
|||
|
|
'air_first_fix': 22.7,
|
|||
|
|
'exchange_rate': 6.5,
|
|||
|
|
'profit_rate': 0.3,
|
|||
|
|
'air_rate': 0.7,
|
|||
|
|
'tax_rate': 0.1,
|
|||
|
|
'shipping_type': 1,
|
|||
|
|
'fuel_rate': 0,
|
|||
|
|
'extra_rate': 0
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self.logistics = USLogisticsPrice()
|
|||
|
|
|
|||
|
|
def calculate(self, packages, purchase_price_cny, preset='shopfad', config=None):
|
|||
|
|
"""
|
|||
|
|
计算售价
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
packages: 包裹列表, 每个元素为 dict:
|
|||
|
|
{'length_cm', 'width_cm', 'height_cm', 'weight_g'}
|
|||
|
|
purchase_price_cny: 采购价(CNY)
|
|||
|
|
preset: 预设配置名称
|
|||
|
|
config: 自定义配置 (优先使用)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
dict: 包含详细费用信息
|
|||
|
|
"""
|
|||
|
|
cfg = config or self.PRESETS.get(preset, self.PRESETS['shopfad'])
|
|||
|
|
|
|||
|
|
# 汇总体积重
|
|||
|
|
total_volume_weight = sum(
|
|||
|
|
p['length_cm'] * p['width_cm'] * p['height_cm'] / 6000 for p in packages
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 尾程快递费
|
|||
|
|
fuel_rate = cfg.get('fuel_rate', 0)
|
|||
|
|
extra_rate = cfg.get('extra_rate', 0)
|
|||
|
|
express_fee = self.logistics.get_express_fee(packages, cfg['profit_rate'])
|
|||
|
|
express_fee = express_fee * (1 + fuel_rate) * (1 + extra_rate)
|
|||
|
|
|
|||
|
|
purchase_price_usd = purchase_price_cny / cfg['exchange_rate']
|
|||
|
|
tax_amount = purchase_price_cny * cfg['tax_rate'] / cfg['exchange_rate']
|
|||
|
|
|
|||
|
|
if cfg['shipping_type'] == 0: # 空运
|
|||
|
|
sell_price = 0
|
|||
|
|
first_cny = 0
|
|||
|
|
first_usd = 0
|
|||
|
|
for pkg in packages:
|
|||
|
|
volume = pkg['length_cm'] * pkg['width_cm'] * pkg['height_cm']
|
|||
|
|
volume_weight = volume / 6000
|
|||
|
|
density = pkg['weight_g'] / (volume / 1000) if volume > 0 else 0
|
|||
|
|
type_weight = min(337, max(37, density))
|
|||
|
|
|
|||
|
|
air_first_cny = cfg['air_cny_type'] * type_weight + cfg['air_first_fix']
|
|||
|
|
air_first_cny_total = air_first_cny * volume_weight
|
|||
|
|
air_first_usd_total = cfg['air_first_usd'] * volume_weight
|
|||
|
|
|
|||
|
|
sell_price += (purchase_price_usd / cfg['profit_rate'] +
|
|||
|
|
((air_first_cny_total / cfg['exchange_rate'] + air_first_usd_total +
|
|||
|
|
tax_amount) / cfg['profit_rate'] + express_fee) * cfg['air_rate'])
|
|||
|
|
first_cny += air_first_cny_total
|
|||
|
|
first_usd += air_first_usd_total
|
|||
|
|
shipping_type = '空运'
|
|||
|
|
|
|||
|
|
else: # 海运
|
|||
|
|
ocean_first_cny_total = cfg['ocean_first_cny'] * total_volume_weight
|
|||
|
|
ocean_first_usd_total = cfg['ocean_first_usd'] * total_volume_weight
|
|||
|
|
|
|||
|
|
sell_price = ((ocean_first_cny_total + purchase_price_cny) /
|
|||
|
|
cfg['exchange_rate'] + ocean_first_usd_total + tax_amount) / cfg['profit_rate'] + express_fee
|
|||
|
|
|
|||
|
|
first_cny = ocean_first_cny_total
|
|||
|
|
first_usd = ocean_first_usd_total
|
|||
|
|
shipping_type = '海运'
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
'sell_price_usd': round(sell_price, 2),
|
|||
|
|
'sell_price_cny': round(sell_price * cfg['exchange_rate'], 2),
|
|||
|
|
'purchase_price_cny': purchase_price_cny,
|
|||
|
|
'express_fee': round(express_fee, 2),
|
|||
|
|
'first_cny': round(first_cny, 2),
|
|||
|
|
'first_usd': round(first_usd, 2),
|
|||
|
|
'tax_amount': round(tax_amount, 2),
|
|||
|
|
'shipping_type': shipping_type,
|
|||
|
|
'preset': preset
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
calc = USSellPrice()
|
|||
|
|
|
|||
|
|
print("=== 单包裹测试 ===")
|
|||
|
|
result = calc.calculate(
|
|||
|
|
packages=[{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500}],
|
|||
|
|
purchase_price_cny=100,
|
|||
|
|
preset='shopfad'
|
|||
|
|
)
|
|||
|
|
print(f" 30x20x10cm 500g => ${result['sell_price_usd']}")
|
|||
|
|
|
|||
|
|
print("\n=== 多包裹测试 ===")
|
|||
|
|
result = calc.calculate(
|
|||
|
|
packages=[
|
|||
|
|
{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500},
|
|||
|
|
{'length_cm': 40, 'width_cm': 30, 'height_cm': 15, 'weight_g': 1000},
|
|||
|
|
],
|
|||
|
|
purchase_price_cny=200,
|
|||
|
|
preset='shopfad'
|
|||
|
|
)
|
|||
|
|
print(f" 两件包裹 => ${result['sell_price_usd']}")
|
|||
|
|
print(f" 快递费: ${result['express_fee']}")
|
|||
|
|
print(f" 头程CNY: {result['first_cny']}, 头程USD: ${result['first_usd']}")
|