2026-04-30 23:46:43 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
美国售价模型 V2 (shopfad)
|
|
|
|
|
|
通过输入多包裹体积和采购价格,快速计算售价
|
2026-04-30 23:47:01 +08:00
|
|
|
|
规则版本: 2604
|
2026-04-30 23:46:43 +08:00
|
|
|
|
"""
|
2026-04-30 23:47:01 +08:00
|
|
|
|
import sys
|
|
|
|
|
|
sys.path.append(r'D:\workspace\dags\logistics')
|
|
|
|
|
|
|
2026-04-30 23:46:43 +08:00
|
|
|
|
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)
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 2604版本价格表 (表名按需修改)
|
|
|
|
|
|
self.sell_uandf_2604 = pd.read_sql("SELECT * FROM sell_uandf_2604", c.con)
|
|
|
|
|
|
self.sell_fedex_price_ref_2604 = pd.read_sql("SELECT * FROM sell_fedex_price_ref_2604", c.con)
|
|
|
|
|
|
self.sell_fedex_order_ref_2604 = pd.read_sql("SELECT * FROM sell_fedex_order_ref_2604", c.con)
|
|
|
|
|
|
# 物流比例限制表 (表名按需修改)
|
|
|
|
|
|
self.logistics_ratio_limit = pd.read_sql("SELECT * FROM logistics_ratio_limit", c.con)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# ========== 包裹计费类型判断 ==========
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
def get_package_types(self, packages):
|
|
|
|
|
|
"""
|
|
|
|
|
|
USPS:默认
|
|
|
|
|
|
UandF:最长边>50 或 次长边>40 或 最短边>30 或 实重>2718 或 体积重>4500
|
|
|
|
|
|
FEDEX:最长边>264 或 实重>67000 或 体积重>95000 或 围长>391
|
|
|
|
|
|
卡派:同FEDEX条件(或该SKU的其他包裹为卡派类型)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
"""
|
2026-04-30 23:47:01 +08:00
|
|
|
|
types = []
|
2026-04-30 23:46:43 +08:00
|
|
|
|
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]
|
2026-04-30 23:47:01 +08:00
|
|
|
|
vol_weight = l * w * h / 6
|
|
|
|
|
|
girth = fst + sed * 2 + trd * 2
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
pkg_type = 'USPS'
|
|
|
|
|
|
if fst > 50 or sed > 40 or trd > 30 or wt > 2718 or vol_weight > 4500:
|
|
|
|
|
|
pkg_type = 'UandF'
|
|
|
|
|
|
if fst > 264 or wt > 67000 or vol_weight > 95000 or girth > 391:
|
|
|
|
|
|
pkg_type = 'FEDEX'
|
|
|
|
|
|
types.append(pkg_type)
|
|
|
|
|
|
|
|
|
|
|
|
# FEDEX升级为卡派(同SKU联动)
|
|
|
|
|
|
if 'FEDEX' in types:
|
|
|
|
|
|
types = ['卡派' if t == 'FEDEX' else t for t in types]
|
|
|
|
|
|
|
|
|
|
|
|
return types
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 快递费计算 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _lookup_express_base_fee(self, pkg_type, lbs_weight, oz_weight, profit_rate, fuel_rate):
|
|
|
|
|
|
"""
|
|
|
|
|
|
查表获取单个包裹的快递基础计费
|
|
|
|
|
|
USPS/UandF: 直接查表
|
|
|
|
|
|
FEDEX: FEDEX售价参考表 * (1+快递燃油费率) + 2.9
|
|
|
|
|
|
"""
|
|
|
|
|
|
if pkg_type == 'USPS':
|
|
|
|
|
|
row = self.sell_usps[self.sell_usps['oz'] == oz_weight]
|
|
|
|
|
|
if len(row) > 0:
|
|
|
|
|
|
return row['最终费用_v2'].iloc[0] / profit_rate, 'USPS'
|
|
|
|
|
|
else:
|
|
|
|
|
|
return self._lookup_express_base_fee('UandF', lbs_weight, oz_weight, profit_rate, fuel_rate)
|
|
|
|
|
|
|
|
|
|
|
|
if pkg_type == 'UandF':
|
|
|
|
|
|
row = self.sell_uandf_2604[self.sell_uandf_2604['lbs'] == lbs_weight]
|
|
|
|
|
|
if len(row) > 0:
|
|
|
|
|
|
return row['加权价格_v2'].iloc[0] / profit_rate, 'UandF'
|
|
|
|
|
|
else:
|
|
|
|
|
|
return self._lookup_express_base_fee('FEDEX', lbs_weight, oz_weight, profit_rate, fuel_rate)
|
|
|
|
|
|
|
|
|
|
|
|
if pkg_type in ('FEDEX', '卡派'):
|
|
|
|
|
|
row = self.sell_fedex_price_ref_2604[self.sell_fedex_price_ref_2604['lbs'] == lbs_weight]
|
|
|
|
|
|
if len(row) > 0:
|
|
|
|
|
|
return row['售价尾端价格_v4'].iloc[0] * (1 + fuel_rate) + 2.9, 'FEDEX'
|
|
|
|
|
|
else:
|
|
|
|
|
|
return 99999, 'FEDEX'
|
|
|
|
|
|
|
|
|
|
|
|
return 0, pkg_type
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_express_fee(self, packages, types, profit_rate, fuel_rate):
|
|
|
|
|
|
"""
|
|
|
|
|
|
快递费 = Σ(单包裹快递基础计费 + 大包裹费 + 超重费 + 超尺寸费)
|
|
|
|
|
|
大包裹费: 最长边>238 或 围长>315 → 109.4
|
|
|
|
|
|
超重费: 实重>21000 → 6.1
|
|
|
|
|
|
超尺寸费: 最长边>116 或 围长>251 → 7.4
|
|
|
|
|
|
"""
|
|
|
|
|
|
total_fee = 0
|
|
|
|
|
|
for i, pkg in enumerate(packages):
|
2026-04-30 23:46:43 +08:00
|
|
|
|
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
|
2026-04-30 23:47:01 +08:00
|
|
|
|
girth = fst + sed * 2 + trd * 2
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
lbs_weight = math.ceil(max(volume / 8500 / 0.453, wt / 453))
|
2026-04-30 23:46:43 +08:00
|
|
|
|
oz_weight = math.ceil(wt / 28)
|
|
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
pkg_type = types[i]
|
|
|
|
|
|
base_fee, actual_type = self._lookup_express_base_fee(
|
|
|
|
|
|
pkg_type, lbs_weight, oz_weight, profit_rate, fuel_rate
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
large_pkg_fee = 0
|
|
|
|
|
|
overweight_fee = 0
|
|
|
|
|
|
oversize_fee = 0
|
|
|
|
|
|
|
|
|
|
|
|
if actual_type in ('FEDEX', '卡派'):
|
|
|
|
|
|
if fst > 238 or girth > 315:
|
|
|
|
|
|
large_pkg_fee = 109.4
|
|
|
|
|
|
if wt > 21000:
|
|
|
|
|
|
overweight_fee = 6.1
|
|
|
|
|
|
if fst > 116 or girth > 251:
|
|
|
|
|
|
oversize_fee = 7.4
|
|
|
|
|
|
|
|
|
|
|
|
total_fee += base_fee + large_pkg_fee + overweight_fee + oversize_fee
|
|
|
|
|
|
|
|
|
|
|
|
return total_fee
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 卡派费计算 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_ltl_fee(self, packages, profit_rate):
|
|
|
|
|
|
"""
|
|
|
|
|
|
卡派费 = 卡派基础费 + 超长费 + 超重费
|
|
|
|
|
|
合并体积 = ceil(长x宽x高/1000000 x 35.3) (立方英尺)
|
|
|
|
|
|
"""
|
|
|
|
|
|
total_cubic_feet = 0
|
|
|
|
|
|
over_length_fee = 0
|
|
|
|
|
|
over_weight_fee = 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 = sizes[0]
|
|
|
|
|
|
volume = l * w * h
|
|
|
|
|
|
|
2026-04-30 23:46:43 +08:00
|
|
|
|
cubic_feet = math.ceil(volume / 1000000 * 35.3147)
|
2026-04-30 23:47:01 +08:00
|
|
|
|
total_cubic_feet += cubic_feet
|
|
|
|
|
|
|
|
|
|
|
|
# 超长费: 最长边>250
|
2026-04-30 23:46:43 +08:00
|
|
|
|
if fst > 250:
|
2026-04-30 23:47:01 +08:00
|
|
|
|
over_length_fee = max(over_length_fee, round(130 / profit_rate - 118, 1))
|
|
|
|
|
|
# 超重费
|
2026-04-30 23:46:43 +08:00
|
|
|
|
if 130000 <= wt < 157000:
|
2026-04-30 23:47:01 +08:00
|
|
|
|
over_weight_fee = max(over_weight_fee, round(130 / profit_rate - 118, 1))
|
|
|
|
|
|
elif 111000 <= wt < 130000:
|
|
|
|
|
|
over_weight_fee = max(over_weight_fee, round(80 / profit_rate - 78, 1))
|
|
|
|
|
|
|
|
|
|
|
|
# 卡派基础费
|
|
|
|
|
|
v = total_cubic_feet
|
|
|
|
|
|
if v < 25:
|
|
|
|
|
|
base_fee = round(198 / profit_rate / 2, 2)
|
|
|
|
|
|
elif v < 35:
|
|
|
|
|
|
base_fee = round(231 / profit_rate / 2, 2)
|
|
|
|
|
|
else:
|
|
|
|
|
|
base_fee = round(max(231, 6.6 * v) / profit_rate / 2, 2)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
ltl_fee = base_fee + over_length_fee + over_weight_fee
|
|
|
|
|
|
return ltl_fee, total_cubic_feet
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 初始前端展示物流费 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_display_logistics_fee(self, packages, types, total_cubic_feet):
|
|
|
|
|
|
"""
|
|
|
|
|
|
卡派: v<25→90, v<35→90+(v-25)*3, 其他→99+(v-35)*5
|
|
|
|
|
|
快递: FEDEX订单参考表查表
|
|
|
|
|
|
"""
|
|
|
|
|
|
has_ltl = '卡派' in types
|
|
|
|
|
|
|
|
|
|
|
|
if has_ltl:
|
|
|
|
|
|
v = total_cubic_feet
|
|
|
|
|
|
if v < 25:
|
|
|
|
|
|
display = 90
|
|
|
|
|
|
elif v < 35:
|
|
|
|
|
|
display = 90 + (v - 25) * 3
|
|
|
|
|
|
else:
|
|
|
|
|
|
display = 99 + (v - 35) * 5
|
|
|
|
|
|
print(f"[卡派展示物流费] 合并体积={v} cu ft, 展示费={display}")
|
|
|
|
|
|
return display
|
2026-04-30 23:46:43 +08:00
|
|
|
|
else:
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 快递: FEDEX订单参考表查表
|
|
|
|
|
|
total = 0
|
|
|
|
|
|
for pkg in packages:
|
|
|
|
|
|
l, w, h, wt = pkg['length_cm'], pkg['width_cm'], pkg['height_cm'], pkg['weight_g']
|
|
|
|
|
|
volume = l * w * h
|
|
|
|
|
|
lbs_weight = math.ceil(max(volume / 8500 / 0.453, wt / 453))
|
|
|
|
|
|
row = self.sell_fedex_order_ref_2604[
|
|
|
|
|
|
self.sell_fedex_order_ref_2604['lbs'] == lbs_weight
|
|
|
|
|
|
]
|
|
|
|
|
|
if len(row) > 0:
|
|
|
|
|
|
total += row['售价尾端价格_v3'].iloc[0]
|
|
|
|
|
|
return total
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 物流比例限制查表 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _lookup_logistics_limit(self, category, price):
|
|
|
|
|
|
"""通过条目和价格查表得到建议物流比例上限和建议物流绝对上限"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
rows = self.logistics_ratio_limit[
|
|
|
|
|
|
(self.logistics_ratio_limit['条目'] == category) &
|
|
|
|
|
|
(self.logistics_ratio_limit['价格'] <= price)
|
|
|
|
|
|
]
|
|
|
|
|
|
if len(rows) > 0:
|
|
|
|
|
|
row = rows.iloc[-1]
|
|
|
|
|
|
return row['建议物流比例上限'], row['建议物流绝对上限']
|
|
|
|
|
|
return 1.0, float('inf')
|
|
|
|
|
|
except:
|
|
|
|
|
|
return 1.0, float('inf')
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 统一尾端费用接口 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def get_tail_fee_info(self, packages, profit_rate=0.359, fuel_rate=0):
|
|
|
|
|
|
"""
|
|
|
|
|
|
尾端费用 = min(快递费, 卡派费)
|
|
|
|
|
|
存在卡派类型包裹则强制使用卡派
|
|
|
|
|
|
"""
|
|
|
|
|
|
types = self.get_package_types(packages)
|
|
|
|
|
|
has_ltl = '卡派' in types
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
express_fee = self._calc_express_fee(packages, types, profit_rate, fuel_rate)
|
|
|
|
|
|
ltl_fee, total_cubic_feet = self._calc_ltl_fee(packages, profit_rate)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
if has_ltl:
|
|
|
|
|
|
tail_fee = ltl_fee
|
2026-04-30 23:46:43 +08:00
|
|
|
|
else:
|
2026-04-30 23:47:01 +08:00
|
|
|
|
tail_fee = min(express_fee, ltl_fee)
|
|
|
|
|
|
|
|
|
|
|
|
display_fee = self._calc_display_logistics_fee(packages, types, total_cubic_feet)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'tail_fee': tail_fee,
|
|
|
|
|
|
'display_logistics_fee': display_fee,
|
|
|
|
|
|
'express_fee': express_fee,
|
|
|
|
|
|
'ltl_fee': ltl_fee,
|
|
|
|
|
|
'types': types,
|
|
|
|
|
|
'has_ltl': has_ltl,
|
|
|
|
|
|
'total_cubic_feet': total_cubic_feet
|
|
|
|
|
|
}
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class USSellPrice:
|
|
|
|
|
|
"""
|
|
|
|
|
|
美国售价计算器 V2 (shopfad)
|
2026-04-30 23:47:01 +08:00
|
|
|
|
规则版本: 2604
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
|
|
|
|
|
使用示例:
|
|
|
|
|
|
calculator = USSellPrice()
|
|
|
|
|
|
result = calculator.calculate(
|
|
|
|
|
|
packages=[{'length_cm': 30, 'width_cm': 20, 'height_cm': 10, 'weight_g': 500}],
|
|
|
|
|
|
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,
|
|
|
|
|
|
'fuel_rate': 0,
|
|
|
|
|
|
'extra_rate': 0
|
|
|
|
|
|
},
|
2026-04-30 23:47:01 +08:00
|
|
|
|
'shopfad2': {
|
2026-04-30 23:46:43 +08:00
|
|
|
|
'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,
|
|
|
|
|
|
'fuel_rate': 0,
|
|
|
|
|
|
'extra_rate': 0
|
|
|
|
|
|
},
|
2026-04-30 23:47:01 +08:00
|
|
|
|
'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,
|
|
|
|
|
|
'fuel_rate': 0,
|
|
|
|
|
|
'extra_rate': 0
|
|
|
|
|
|
},
|
|
|
|
|
|
'shopfad4': {
|
2026-04-30 23:46:43 +08:00
|
|
|
|
'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,
|
|
|
|
|
|
'fuel_rate': 0,
|
|
|
|
|
|
'extra_rate': 0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.logistics = USLogisticsPrice()
|
|
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
def calculate(self, packages, purchase_price_cny, preset='shopfad', config=None, category=''):
|
2026-04-30 23:46:43 +08:00
|
|
|
|
"""
|
|
|
|
|
|
计算售价
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
2026-04-30 23:47:01 +08:00
|
|
|
|
packages: 包裹列表 [{'length_cm', 'width_cm', 'height_cm', 'weight_g'}]
|
2026-04-30 23:46:43 +08:00
|
|
|
|
purchase_price_cny: 采购价(CNY)
|
|
|
|
|
|
preset: 预设配置名称
|
|
|
|
|
|
config: 自定义配置 (优先使用)
|
2026-04-30 23:47:01 +08:00
|
|
|
|
category: 条目 (用于物流比例限制查表)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
"""
|
|
|
|
|
|
cfg = config or self.PRESETS.get(preset, self.PRESETS['shopfad'])
|
|
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 1. 获取尾端费用
|
2026-04-30 23:46:43 +08:00
|
|
|
|
fuel_rate = cfg.get('fuel_rate', 0)
|
2026-04-30 23:47:01 +08:00
|
|
|
|
tail_info = self.logistics.get_tail_fee_info(packages, cfg['profit_rate'], fuel_rate)
|
|
|
|
|
|
tail_fee = tail_info['tail_fee']
|
|
|
|
|
|
display_fee = tail_info['display_logistics_fee']
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 2. 判断SPU运输方式
|
|
|
|
|
|
has_ltl = tail_info['has_ltl']
|
|
|
|
|
|
shipping_type = '海运' if has_ltl else '空运'
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 3. 汇总体积重 (长x宽x高/6)
|
|
|
|
|
|
total_volume_weight = sum(
|
|
|
|
|
|
p['length_cm'] * p['width_cm'] * p['height_cm'] / 6 for p in packages
|
|
|
|
|
|
)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
tax_amount = purchase_price_cny * cfg['tax_rate'] / cfg['exchange_rate']
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 4. 根据运输方式计算调整前售价
|
|
|
|
|
|
if shipping_type == '海运':
|
|
|
|
|
|
pre_adjust_price = self._calc_ocean_price(
|
|
|
|
|
|
total_volume_weight, purchase_price_cny, tax_amount, tail_fee, cfg
|
|
|
|
|
|
)
|
|
|
|
|
|
first_cny = cfg['ocean_first_cny'] / 1000 * total_volume_weight
|
|
|
|
|
|
first_usd = cfg['ocean_first_usd'] / 1000 * total_volume_weight
|
|
|
|
|
|
else:
|
|
|
|
|
|
pre_adjust_price = self._calc_air_price(
|
|
|
|
|
|
packages, purchase_price_cny, tax_amount, tail_fee, cfg
|
|
|
|
|
|
)
|
|
|
|
|
|
first_cny, first_usd = self._calc_air_first_leg(packages, cfg)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 5. 最终修正 (仅海运)
|
|
|
|
|
|
shared_tail_fee = 0
|
|
|
|
|
|
final_price = pre_adjust_price
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
if shipping_type == '海运':
|
|
|
|
|
|
final_price, shared_tail_fee = self._apply_ocean_adjustment(
|
|
|
|
|
|
pre_adjust_price, display_fee, category
|
|
|
|
|
|
)
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# 头程费用 / 总物流费用
|
|
|
|
|
|
head_fee = first_cny / cfg['exchange_rate'] + first_usd
|
|
|
|
|
|
total_logistics_fee = head_fee + tail_fee
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-30 23:47:01 +08:00
|
|
|
|
'sell_price_usd': round(final_price, 2),
|
|
|
|
|
|
'sell_price_cny': round(final_price * cfg['exchange_rate'], 2),
|
2026-04-30 23:46:43 +08:00
|
|
|
|
'purchase_price_cny': purchase_price_cny,
|
2026-04-30 23:47:01 +08:00
|
|
|
|
'express_fee': round(tail_fee, 2),
|
2026-04-30 23:46:43 +08:00
|
|
|
|
'first_cny': round(first_cny, 2),
|
|
|
|
|
|
'first_usd': round(first_usd, 2),
|
|
|
|
|
|
'tax_amount': round(tax_amount, 2),
|
|
|
|
|
|
'shipping_type': shipping_type,
|
2026-04-30 23:47:01 +08:00
|
|
|
|
'preset': preset,
|
|
|
|
|
|
'pre_adjust_price': round(pre_adjust_price, 2),
|
|
|
|
|
|
'display_logistics_fee': round(display_fee, 2),
|
|
|
|
|
|
'shared_tail_fee': round(shared_tail_fee, 2),
|
|
|
|
|
|
'head_fee': round(head_fee, 2),
|
|
|
|
|
|
'total_logistics_fee': round(total_logistics_fee, 2),
|
|
|
|
|
|
'package_types': tail_info['types'],
|
2026-04-30 23:46:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 23:47:01 +08:00
|
|
|
|
# ========== 海运计费 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_ocean_price(self, total_volume_weight, purchase_price_cny, tax_amount, tail_fee, cfg):
|
|
|
|
|
|
"""
|
|
|
|
|
|
调整前售价 = (((海运头程CNY总价 + 采购价格) / 汇率 + 海运头程USD总价)
|
|
|
|
|
|
+ 采购价格 x 税金系数 / 汇率) / 利润系数 + 尾端费用
|
|
|
|
|
|
"""
|
|
|
|
|
|
ocean_first_cny_total = cfg['ocean_first_cny'] / 1000 * total_volume_weight
|
|
|
|
|
|
ocean_first_usd_total = cfg['ocean_first_usd'] / 1000 * total_volume_weight
|
|
|
|
|
|
|
|
|
|
|
|
pre_adjust = (
|
|
|
|
|
|
((ocean_first_cny_total + purchase_price_cny) / cfg['exchange_rate']
|
|
|
|
|
|
+ ocean_first_usd_total + tax_amount) / cfg['profit_rate'] + tail_fee
|
|
|
|
|
|
)
|
|
|
|
|
|
return pre_adjust
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 空运计费 ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_air_price(self, packages, purchase_price_cny, tax_amount, tail_fee, cfg):
|
|
|
|
|
|
"""
|
|
|
|
|
|
调整前售价 = (采购价格/汇率/利润系数
|
|
|
|
|
|
+ ((空运头程CNY总价/汇率 + 空运头程USD总价 + 税金) / 利润系数 + 尾端费用)
|
|
|
|
|
|
x 空运分配比例) x (1 + 附加费率)
|
|
|
|
|
|
"""
|
|
|
|
|
|
air_cny_total = 0
|
|
|
|
|
|
air_usd_total = 0
|
|
|
|
|
|
|
|
|
|
|
|
for pkg in packages:
|
|
|
|
|
|
volume = pkg['length_cm'] * pkg['width_cm'] * pkg['height_cm']
|
|
|
|
|
|
volume_weight = volume / 6
|
|
|
|
|
|
density = pkg['weight_g'] / (volume / 1000) if volume > 0 else 0
|
|
|
|
|
|
type_weight = min(337, max(37, math.ceil(density)))
|
|
|
|
|
|
|
|
|
|
|
|
air_cny_unit = cfg['air_cny_type'] / 1000 * type_weight + cfg['air_first_fix'] / 1000
|
|
|
|
|
|
air_cny_total += air_cny_unit * volume_weight
|
|
|
|
|
|
air_usd_total += cfg['air_first_usd'] / 1000 * volume_weight
|
|
|
|
|
|
|
|
|
|
|
|
purchase_usd = purchase_price_cny / cfg['exchange_rate']
|
|
|
|
|
|
|
|
|
|
|
|
pre_adjust = (
|
|
|
|
|
|
purchase_usd / cfg['profit_rate']
|
|
|
|
|
|
+ ((air_cny_total / cfg['exchange_rate'] + air_usd_total + tax_amount)
|
|
|
|
|
|
/ cfg['profit_rate'] + tail_fee) * cfg['air_rate']
|
|
|
|
|
|
) * (1 + cfg.get('extra_rate', 0))
|
|
|
|
|
|
|
|
|
|
|
|
return pre_adjust
|
|
|
|
|
|
|
|
|
|
|
|
def _calc_air_first_leg(self, packages, cfg):
|
|
|
|
|
|
"""计算空运头程费用 (CNY/USD)"""
|
|
|
|
|
|
first_cny = 0
|
|
|
|
|
|
first_usd = 0
|
|
|
|
|
|
for pkg in packages:
|
|
|
|
|
|
volume = pkg['length_cm'] * pkg['width_cm'] * pkg['height_cm']
|
|
|
|
|
|
volume_weight = volume / 6
|
|
|
|
|
|
density = pkg['weight_g'] / (volume / 1000) if volume > 0 else 0
|
|
|
|
|
|
type_weight = min(337, max(37, math.ceil(density)))
|
|
|
|
|
|
|
|
|
|
|
|
air_cny_unit = cfg['air_cny_type'] / 1000 * type_weight + cfg['air_first_fix'] / 1000
|
|
|
|
|
|
first_cny += air_cny_unit * volume_weight
|
|
|
|
|
|
first_usd += cfg['air_first_usd'] / 1000 * volume_weight
|
|
|
|
|
|
return first_cny, first_usd
|
|
|
|
|
|
|
|
|
|
|
|
# ========== 最终修正 (海运) ==========
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_ocean_adjustment(self, pre_adjust_price, display_fee, category):
|
|
|
|
|
|
"""
|
|
|
|
|
|
前端展示物流费 = min(初始前端展示物流费, 调整前售价×比例上限, 绝对上限)
|
|
|
|
|
|
if min == 初始前端展示物流费: 分摊尾端=0, 最终售价=调整前售价
|
|
|
|
|
|
else: 分摊尾端=初始前端展示物流费-min, 最终售价=调整前售价+分摊尾端
|
|
|
|
|
|
"""
|
|
|
|
|
|
total = display_fee + pre_adjust_price
|
|
|
|
|
|
if total <= 0:
|
|
|
|
|
|
return pre_adjust_price, 0
|
|
|
|
|
|
|
|
|
|
|
|
ratio_limit, abs_limit = self.logistics._lookup_logistics_limit(category, pre_adjust_price)
|
|
|
|
|
|
actual_display_fee = min(display_fee, pre_adjust_price * ratio_limit, abs_limit)
|
|
|
|
|
|
|
|
|
|
|
|
if actual_display_fee == display_fee:
|
|
|
|
|
|
return pre_adjust_price, 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
shared_tail = display_fee - actual_display_fee
|
|
|
|
|
|
final_price = pre_adjust_price + shared_tail
|
|
|
|
|
|
return final_price, shared_tail
|
|
|
|
|
|
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
|
|
|
|
|
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']}")
|
2026-04-30 23:47:01 +08:00
|
|
|
|
print(f" 运输方式: {result['shipping_type']}")
|
|
|
|
|
|
print(f" 包裹类型: {result['package_types']}")
|
2026-04-30 23:46:43 +08:00
|
|
|
|
|
|
|
|
|
|
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']}")
|
2026-04-30 23:47:01 +08:00
|
|
|
|
print(f" 运输方式: {result['shipping_type']}")
|
|
|
|
|
|
print(f" 包裹类型: {result['package_types']}")
|
|
|
|
|
|
print(f" 尾端费用: ${result['express_fee']}")
|
2026-04-30 23:46:43 +08:00
|
|
|
|
print(f" 头程CNY: {result['first_cny']}, 头程USD: ${result['first_usd']}")
|
2026-04-30 23:47:01 +08:00
|
|
|
|
print(f" 总物流: ${result['total_logistics_fee']}")
|