426 lines
20 KiB
Python
426 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||
import math
|
||
from pathlib import Path
|
||
|
||
import pandas as pd
|
||
|
||
from utils.gtools import DBconnect
|
||
|
||
|
||
# 根据SPU判断空运或海运,1为海运
|
||
def spu_head_type(base_df: pd.DataFrame):
|
||
df = base_df.copy(deep=True)
|
||
spu_type = {} # 字典的键为SPU,值为一个元组(start, end),表示该SPU在原始DataFrame中的索引范围
|
||
last_spu = None
|
||
spu_start_index = None
|
||
|
||
for i, row in df.iterrows():
|
||
current_spu = row['SPU']
|
||
if current_spu != last_spu:
|
||
spu_start_index = i
|
||
last_spu = current_spu
|
||
else:
|
||
# 如果是同一个SPU的连续行,则更新该SPU组的结束索引
|
||
if current_spu in spu_type:
|
||
spu_type[current_spu][1] = i
|
||
else:
|
||
# 如果SPU组的起始索引存在,则初始化一个新的SPU组,并更新结束索引
|
||
spu_type[current_spu] = [spu_start_index, i]
|
||
if i == len(df) - 1 or current_spu != df.iloc[i + 1]['SPU']:
|
||
if current_spu not in spu_type:
|
||
spu_type[current_spu] = [i, i]
|
||
# 单独处理最后一行
|
||
if last_spu and last_spu not in spu_type:
|
||
spu_type[last_spu] = [spu_start_index, spu_start_index]
|
||
|
||
spu_flag_values = {}
|
||
for spu, index_range in spu_type.items():
|
||
# 获取每个 SPU 组的起始索引和结束索引
|
||
start_index = index_range[0]
|
||
end_index = index_range[1]
|
||
sub_df = df.loc[start_index:end_index]
|
||
flag = 0
|
||
for index, row in sub_df.iterrows():
|
||
item_list = [
|
||
{key: float(value) for key, value in package.items()}
|
||
for package in row['packages'].values()
|
||
]
|
||
for item in item_list:
|
||
bag_list = [item['长'], item['宽'], item['高']]
|
||
sizes = sorted(bag_list, reverse=True)
|
||
volume_weight = (sizes[0] * sizes[1] * sizes[2]) / 6000
|
||
if sizes[0] > 50 or sizes[1] > 40 or sizes[2] > 30 or item['重量'] > 2718 or volume_weight > 4.5:
|
||
flag = 1
|
||
break
|
||
if flag:
|
||
break
|
||
spu_flag_values[spu] = flag
|
||
|
||
df.loc[start_index:end_index, 'flag'] = flag
|
||
return df
|
||
|
||
|
||
class SellPriceBase:
|
||
parent_current_directory = Path(__file__).parent.parent
|
||
sell_usps = None
|
||
sell_uandf = None
|
||
sell_fedex = None
|
||
_instance = None
|
||
def __new__(cls, *args, **kwargs):
|
||
"""实现单例模式,只加载一次文件"""
|
||
if cls._instance is None:
|
||
cls._instance = super().__new__(cls)
|
||
with DBconnect() as db:
|
||
if cls.sell_usps is None: # 快递费usps小件
|
||
cls.sell_usps = pd.read_sql("select * from sell_usps", db.eng)
|
||
if cls.sell_uandf is None: # 快递费uandf小件
|
||
cls.sell_uandf = pd.read_sql("select * from sell_uandf", db.eng)
|
||
if cls.sell_fedex is None: # 快递费fedex
|
||
cls.sell_fedex = pd.read_sql("select * from sell_fedex", db.eng)
|
||
return cls._instance
|
||
return cls._instance
|
||
def __init__(self, packages, purchase_price, shipping_type, ocean_first_cny, ocean_first_usd, air_first_usd,
|
||
air_cny_type, air_first_fix, exchange_rate, profit_rate, air_rate,tax_rate):
|
||
self.packages = packages # 单sku包裹数据
|
||
self.purchase_price = purchase_price # 采购价/成本价
|
||
self.ocean_first_cny = ocean_first_cny # 海运头程单价CNY
|
||
self.ocean_first_usd = ocean_first_usd # 海运头程单价USD
|
||
self.air_first_usd = air_first_usd # 空运头程单价USD
|
||
self.air_cny_type = air_cny_type # 空运头程货型单价CNY
|
||
self.air_first_fix = air_first_fix # 空运头程固定单价CNY
|
||
self.exchange_rate = exchange_rate # 汇率
|
||
self.profit_rate = profit_rate # 利润系数
|
||
self.air_rate = air_rate # 空运分配占比
|
||
self.shipping_type = shipping_type # 1为海运0为空运,由spu定好的
|
||
self.tax_rate = tax_rate # 税率
|
||
|
||
# 计算快递费用
|
||
def cal_express_fee(self):
|
||
head_type = 0 # 默认为USPS
|
||
index = 0 # 标记卡派和不可售
|
||
for package in self.packages:
|
||
volume_weight = package.get_volume_weight(6000)
|
||
if package.weight > 420:
|
||
head_type = 1 # UandF
|
||
if package.fst_size > 50 or package.sed_size > 40 or package.trd_size > 30 or package.weight > 2718 or volume_weight > 3624:
|
||
head_type = 2 # FEDEX
|
||
if package.fst_size > 264 or package.weight > 67000 or volume_weight > 95000 or package.girth > 391:
|
||
head_type = 3 # 卡派
|
||
index = 1
|
||
if package.fst_size > 310:
|
||
head_type = 4 # 不可售
|
||
index = 1
|
||
|
||
lbs_weight_sum = 0
|
||
express_base_fee1 = 0
|
||
sku_express_fee = 0 # 单产品快递基础费
|
||
for package in self.packages:
|
||
oz_weight = math.ceil(package.weight / 28)
|
||
lbs_weight = math.ceil(max(package.get_volume_weight(8500) / 0.453, package.weight / 453))
|
||
lbs_weight_sum += lbs_weight
|
||
express_base_fee = 0
|
||
oversize_charge = 0
|
||
ahs_weight = 0
|
||
ahs_dimension = 0
|
||
if head_type == 0:
|
||
try:
|
||
express_base_fee = self.sell_usps[self.sell_usps['oz'] == oz_weight]['最终费用_v1'].iloc[0] / self.profit_rate
|
||
except:
|
||
head_type = 1
|
||
# USPSA2/FEDEXA1
|
||
if head_type == 1:
|
||
try:
|
||
express_base_fee = self.sell_uandf[self.sell_uandf['lbs'] == lbs_weight]['加权价格_v1'].iloc[0] / self.profit_rate
|
||
except:
|
||
head_type = 2
|
||
# FEDEX
|
||
if head_type == 2:
|
||
try:
|
||
express_base_fee = self.sell_fedex[self.sell_fedex['lbs'] == lbs_weight]['售价尾端价格_v1'].iloc[0]
|
||
except:
|
||
express_base_fee = 99999
|
||
head_type = 3
|
||
|
||
# 额外费用计算
|
||
if package.fst_size > 238 or package.girth > 315:
|
||
# 如果包裹符合大包裹
|
||
oversize_charge = 109.4
|
||
elif package.weight > 21000:
|
||
# 注意是个负数
|
||
ahs_weight = -5.6
|
||
elif package.weight > 116 or package.girth > 251:
|
||
# 虽然是0,但是保留这一条逻辑
|
||
ahs_dimension = 0
|
||
|
||
express_price = (express_base_fee + oversize_charge + ahs_weight + ahs_dimension)
|
||
express_base_fee1 += express_base_fee
|
||
sku_express_fee += express_price
|
||
# 卡派
|
||
if head_type in (3, 4):
|
||
continue
|
||
|
||
# 计算合并的卡派价格
|
||
ltl_over_weightfee = 0
|
||
sku_total_cubic_feet = 0 # 库存总体立方英尺
|
||
ltlover_size = 0
|
||
for package in self.packages:
|
||
cubic_feet = math.ceil(package.volume / 1000000 * 35.3147) # 1立方米 = 35.3147立方英尺
|
||
sku_total_cubic_feet += cubic_feet
|
||
# 额外费用
|
||
if package.fst_size > 250:
|
||
ltlover_size = round(110 / self.profit_rate - 118, 1) # 订单物流放118,其他在售价里
|
||
if 111000 <= package.weight < 130000:
|
||
ltl_over_weightfee = max(ltl_over_weightfee, round(70 / self.profit_rate - 78, 1)) # 订单物流放78,其他在售价里
|
||
if 130000 <= package.weight < 157000:
|
||
ltl_over_weightfee = max(ltl_over_weightfee, round(110 / self.profit_rate - 118, 1)) # 订单物流放118,其他在售价里
|
||
# 基础费用
|
||
if sku_total_cubic_feet < 25:
|
||
ltl_base_fee1 = round(163 / self.profit_rate / 2, 2)
|
||
elif sku_total_cubic_feet < 35:
|
||
ltl_base_fee1 = round(180 / self.profit_rate / 2, 2)
|
||
else:
|
||
# 大于一个立方的(35立方英尺) 按照mei立方英尺*5美金
|
||
# 最低为190美金
|
||
ltl_base_fee1 = round(max(190, 5 * sku_total_cubic_feet) / self.profit_rate / 2, 2)
|
||
ltl_base_fee = ltl_base_fee1 * 1.025 # 2.5%平摊偏远附加费成本
|
||
# 最后卡派费用等于基础费用+ 超长+ 超重费
|
||
ltl_fee = ltl_base_fee + ltlover_size + ltl_over_weightfee
|
||
|
||
# 最后根据卡派费用和快递费用,取小值
|
||
if index == 0: # 该sku没有卡派和不可售包裹
|
||
sku_total_express_fee = min(ltl_fee, sku_express_fee)
|
||
else:
|
||
sku_total_express_fee = ltl_fee
|
||
return sku_total_express_fee
|
||
|
||
def cal_sell_price(self):
|
||
sell_price = 0
|
||
express = self.cal_express_fee()
|
||
if self.shipping_type == 0: # 空运
|
||
for package in self.packages:
|
||
volume_weight = package.get_volume_weight(6000)
|
||
type_weight = min(337, max(37, package.density)) # 货型 = 密度?
|
||
air_first_cny = (self.air_cny_type * type_weight + self.air_first_fix)
|
||
air_first_cny_total = air_first_cny * volume_weight
|
||
air_first_usd_total = self.air_first_usd * volume_weight
|
||
sell_price += (self.purchase_price / self.exchange_rate / self.profit_rate +
|
||
((air_first_cny_total / self.exchange_rate + air_first_usd_total) /
|
||
self.profit_rate + express) * self.air_rate)
|
||
|
||
else: # 海运 or 不可售
|
||
volume_weight = sum([package.get_volume_weight(6000) for package in self.packages])
|
||
ocean_first_cny_total = self.ocean_first_cny * volume_weight
|
||
ocean_first_usd_total = self.ocean_first_usd * volume_weight
|
||
sell_price = ((ocean_first_cny_total + self.purchase_price) /
|
||
self.exchange_rate + ocean_first_usd_total) / self.profit_rate + express
|
||
return sell_price
|
||
|
||
|
||
# 计算快递费用
|
||
def cal_express_fee_2025(self):
|
||
head_type = 0 # 默认为USPS
|
||
index = 0 # 标记卡派和不可售
|
||
for package in self.packages:
|
||
volume_weight = package.get_volume_weight(6000)
|
||
if package.weight > 420:
|
||
head_type = 1 # UandF
|
||
if package.fst_size > 50 or package.sed_size > 40 or package.trd_size > 30 or package.weight > 2718 or volume_weight > 3624:
|
||
head_type = 2 # FEDEX
|
||
if package.fst_size > 264 or package.weight > 67000 or volume_weight > 95000 or package.girth > 391:
|
||
head_type = 3 # 卡派
|
||
index = 1
|
||
if package.fst_size > 310:
|
||
head_type = 4 # 不可售
|
||
index = 1
|
||
|
||
lbs_weight_sum = 0
|
||
express_base_fee1 = 0
|
||
sku_express_fee = 0 # 单产品快递基础费
|
||
for package in self.packages:
|
||
oz_weight = math.ceil(package.weight / 28)
|
||
lbs_weight = math.ceil(max(package.get_volume_weight(8500) / 0.453, package.weight / 453))
|
||
lbs_weight_sum += lbs_weight
|
||
express_base_fee = 0
|
||
oversize_charge = 0
|
||
ahs_weight = 0
|
||
ahs_dimension = 0
|
||
if head_type == 0:
|
||
try:
|
||
express_base_fee = self.sell_usps[self.sell_usps['oz'] == oz_weight]['最终费用_v2'].iloc[0] / self.profit_rate
|
||
except:
|
||
head_type = 1
|
||
# USPSA2/FEDEXA1
|
||
if head_type == 1:
|
||
try:
|
||
express_base_fee = self.sell_uandf[self.sell_uandf['lbs'] == lbs_weight]['加权价格_v2'].iloc[0] / self.profit_rate
|
||
except:
|
||
head_type = 2
|
||
# FEDEX
|
||
if head_type == 2:
|
||
try:
|
||
express_base_fee = self.sell_fedex[self.sell_fedex['lbs'] == lbs_weight]['售价尾端价格_v2'].iloc[0]
|
||
except:
|
||
express_base_fee = 99999
|
||
head_type = 3
|
||
|
||
# 额外费用计算
|
||
if package.fst_size > 238 or package.girth > 315:
|
||
# 如果包裹符合大包裹
|
||
oversize_charge = 109.4
|
||
elif package.weight > 21000:
|
||
# 注意是个负数
|
||
ahs_weight = 6.1
|
||
elif package.length > 116 or package.girth > 251:
|
||
# 虽然是0,但是保留这一条逻辑
|
||
ahs_dimension = 7.4
|
||
|
||
express_price = (express_base_fee + oversize_charge + ahs_weight + ahs_dimension)
|
||
express_base_fee1 += express_base_fee
|
||
sku_express_fee += express_price
|
||
# 卡派
|
||
if head_type in (3, 4):
|
||
continue
|
||
|
||
# 计算合并的卡派价格
|
||
ltl_over_weightfee = 0
|
||
sku_total_cubic_feet = 0 # 库存总体立方英尺
|
||
ltlover_size = 0
|
||
for package in self.packages:
|
||
cubic_feet = math.ceil(package.volume / 1000000 * 35.3147) # 1立方米 = 35.3147立方英尺
|
||
sku_total_cubic_feet += cubic_feet
|
||
# 额外费用
|
||
if package.fst_size > 250:
|
||
ltlover_size = round(110 / self.profit_rate - 118, 1) # 订单物流放118,其他在售价里
|
||
if 111000 <= package.weight < 130000:
|
||
ltl_over_weightfee = max(ltl_over_weightfee, round(70 / self.profit_rate - 78, 1)) # 订单物流放78,其他在售价里
|
||
if 130000 <= package.weight < 157000:
|
||
ltl_over_weightfee = max(ltl_over_weightfee, round(110 / self.profit_rate - 118, 1)) # 订单物流放118,其他在售价里
|
||
# 基础费用
|
||
if sku_total_cubic_feet < 25:
|
||
ltl_base_fee1 = round(163 / self.profit_rate / 2, 2)
|
||
elif sku_total_cubic_feet < 35:
|
||
ltl_base_fee1 = round(180 / self.profit_rate / 2, 2)
|
||
else:
|
||
# 大于一个立方的(35立方英尺) 按照mei立方英尺*5美金
|
||
# 最低为190美金
|
||
ltl_base_fee1 = round(max(190, 5 * sku_total_cubic_feet) / self.profit_rate / 2, 2)
|
||
ltl_base_fee = ltl_base_fee1 * 1.025 # 2.5%平摊偏远附加费成本
|
||
# 最后卡派费用等于基础费用+ 超长+ 超重费
|
||
ltl_fee = ltl_base_fee + ltlover_size + ltl_over_weightfee
|
||
|
||
# 最后根据卡派费用和快递费用,取小值
|
||
if index == 0: # 该sku没有卡派和不可售包裹
|
||
sku_total_express_fee = min(ltl_fee, sku_express_fee)
|
||
else:
|
||
sku_total_express_fee = ltl_fee
|
||
return sku_total_express_fee
|
||
|
||
def cal_sell_price_2025(self):
|
||
sell_price = 0
|
||
express = self.cal_express_fee_2025()
|
||
if self.shipping_type == 0: # 空运
|
||
for package in self.packages:
|
||
volume_weight = package.get_volume_weight(6000)
|
||
type_weight = min(337, max(37, package.density)) # 货型 = 密度?
|
||
air_first_cny = (self.air_cny_type * type_weight + self.air_first_fix)
|
||
air_first_cny_total = air_first_cny * volume_weight
|
||
air_first_usd_total = self.air_first_usd * volume_weight
|
||
sell_price += (self.purchase_price / self.exchange_rate / self.profit_rate +
|
||
(((air_first_cny_total / self.exchange_rate + air_first_usd_total)+
|
||
self.purchase_price*self.tax_rate/self.exchange_rate)/
|
||
self.profit_rate + express) * self.air_rate)
|
||
|
||
else: # 海运 or 不可售
|
||
volume_weight = sum([package.get_volume_weight(6000) for package in self.packages])
|
||
ocean_first_cny_total = self.ocean_first_cny * volume_weight
|
||
ocean_first_usd_total = self.ocean_first_usd * volume_weight
|
||
sell_price = (((ocean_first_cny_total + self.purchase_price) /
|
||
self.exchange_rate + ocean_first_usd_total)
|
||
+ self.purchase_price*self.tax_rate/self.exchange_rate) / self.profit_rate + express
|
||
|
||
return (sell_price,express,ocean_first_cny_total,ocean_first_usd_total)
|
||
|
||
@classmethod
|
||
def litfad(cls, packages, purchase_price, shipping_type):
|
||
return cls(
|
||
packages, # 单sku包裹数据
|
||
purchase_price, # 采购价/成本价
|
||
shipping_type, # 1为海运0为空运,由spu定好的
|
||
ocean_first_cny=1.077, #1.077, # 海运头程单价CNY
|
||
ocean_first_usd=1.06, #1.06, # 海运头程单价USD
|
||
air_first_usd=0.65, # 空运头程单价USD
|
||
air_cny_type=0.093, # 空运头程货型单价CNY
|
||
air_first_fix=27.7, # 空运头程固定单价CNY
|
||
exchange_rate=7, # 汇率
|
||
profit_rate=0.45, #0.45, # 利润系数
|
||
air_rate=0.7, # 空运分配占比
|
||
tax_rate = 0 # 税率
|
||
)
|
||
@classmethod
|
||
def litfad_2025(cls, packages, purchase_price, shipping_type):
|
||
return cls(
|
||
packages, # 单sku包裹数据
|
||
purchase_price, # 采购价/成本价
|
||
shipping_type, # 1为海运0为空运,由spu定好的
|
||
ocean_first_cny=1, #1.077, # 海运头程单价CNY
|
||
ocean_first_usd=1, #1.06, # 海运头程单价USD
|
||
air_first_usd=0.65, # 空运头程单价USD
|
||
air_cny_type=0.093, # 空运头程货型单价CNY
|
||
air_first_fix=22.7, # 空运头程固定单价CNY
|
||
exchange_rate=6.5, # 汇率
|
||
profit_rate=0.359, #0.45, # 利润系数
|
||
air_rate=0.7, # 空运分配占比
|
||
tax_rate = 0.145 # 税率
|
||
)
|
||
|
||
@classmethod
|
||
def runnup(cls, packages, purchase_price, shipping_type):
|
||
return cls(
|
||
packages, # 单sku包裹数据
|
||
purchase_price, # 采购价/成本价
|
||
shipping_type, # 1为海运0为空运,由spu定好的
|
||
ocean_first_cny=1.077, # 海运头程单价CNY
|
||
ocean_first_usd=1.06, # 海运头程单价USD
|
||
air_first_usd=0.65, # 空运头程单价USD
|
||
air_cny_type=0.093, # 空运头程货型单价CNY
|
||
air_first_fix=27.7, # 空运头程固定单价CNY
|
||
exchange_rate=6, # 汇率
|
||
profit_rate=0.45, # 利润系数
|
||
air_rate=0.7, # 空运分配占比
|
||
tax_rate = 0.145 # 税率
|
||
)
|
||
|
||
@classmethod
|
||
def lakiq(cls, packages, purchase_price, shipping_type):
|
||
return cls(
|
||
packages, # 单sku包裹数据
|
||
purchase_price, # 采购价/成本价
|
||
shipping_type, # 1为海运0为空运,由spu定好的
|
||
ocean_first_cny=1.1, # 海运头程单价CNY
|
||
ocean_first_usd=1.1, # 海运头程单价USD
|
||
air_first_usd=0.65, # 空运头程单价USD
|
||
air_cny_type=0.093, # 空运头程货型单价CNY
|
||
air_first_fix=27.7, # 空运头程固定单价CNY
|
||
exchange_rate=6, # 汇率
|
||
profit_rate=0.45, # 利润系数
|
||
air_rate=0.7, # 空运分配占比
|
||
tax_rate = 0.145 # 税率
|
||
)
|
||
|
||
@classmethod
|
||
def kwoking(cls, packages, purchase_price, shipping_type):
|
||
return cls(
|
||
packages, # 单sku包裹数据
|
||
purchase_price, # 采购价/成本价
|
||
shipping_type, # 1为海运0为空运,由spu定好的
|
||
ocean_first_cny=1.08, # 海运头程单价CNY
|
||
ocean_first_usd=1.07, # 海运头程单价USD
|
||
air_first_usd=0.65, # 空运头程单价USD
|
||
air_cny_type=0.093, # 空运头程货型单价CNY
|
||
air_first_fix=27.7, # 空运头程固定单价CNY
|
||
exchange_rate=6, # 汇率
|
||
profit_rate=0.45, # 利润系数
|
||
air_rate=0.7, # 空运分配占比
|
||
tax_rate = 0.145 # 税率
|
||
)
|