# 计划预测 - 计算列逻辑详解 > 最后更新:2026-06-23(第二版,精确到代码级细节) > > 本文档按代码执行路径逐步记录计划预测页面所有计算列的逻辑,不省略任何细节。 --- ## 一、数据源说明 ### 1.1 销量数据 → `daily_outbound` 表 取**最新日期**: ```python latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d'] # → latest_str = 格式化后的日期字符串 ``` 以最新日期为基准日,逐天向前构造日期列表: ```python dates_3 = [latest-0d, latest-1d, latest-2d] # 3天 dates_7 = [latest-0d, latest-1d, latest-2d, latest-3d, latest-4d, latest-5d, latest-6d] # 7天 dates_14 = [latest-0d .. latest-13d] # 14天 dates_30 = [latest-0d .. latest-29d] # 30天 dates_60 = [latest-0d .. latest-59d] # 60天 ``` SQL 查询原始数据: ```sql SELECT 货品编号, 日期, SUM(货品数量) AS 日销量 FROM daily_outbound WHERE 日期 >= dates_60[-1] GROUP BY 货品编号, 日期 ``` 数据整理为:`{product_code: {date_string: qty}}` - `qty = int(r['日销量']) if r['日销量'] else 0` ### 1.2 库存数据 → `inventory_detail` 表 取**最新库存日期**: ```python latest = query("SELECT MAX(data_date) AS d FROM inventory_detail")[0]['d'] ``` **库存**(排除 15 号仓库): ```sql SELECT product_code, SUM(available_qty) AS total_qty FROM inventory_detail WHERE data_date = '{latest}' AND warehouse_code != '15' GROUP BY product_code ``` → `inv_map[pc] = int(r['total_qty']) if r['total_qty'] else 0` **加工厂库存**(仅 15 号仓库): ```sql SELECT product_code, SUM(available_qty) AS total_qty FROM inventory_detail WHERE data_date = '{latest}' AND warehouse_code = '15' GROUP BY product_code ``` → `inv15_map[pc] = int(r['total_qty']) if r['total_qty'] else 0` **生产在途 + 采购在途**(所有仓库合计): ```sql SELECT product_code, SUM(production_in_transit) AS prod_in_transit, SUM(purchase_in_transit) AS purch_in_transit FROM inventory_detail WHERE data_date = '{latest}' GROUP BY product_code ``` → `transit_map[pc] = {'生产在途': int(prod_in_transit) or 0, '采购在途': int(purch_in_transit) or 0}` ### 1.3 预测配置 → `sales_predict_config` 表 ```sql SELECT * FROM sales_predict_config ORDER BY id ``` → `sp_map = {r['trend_type']: r for r in ...}` 每条记录字段:`trend_type`, `avg_3d`, `avg_7d`, `avg_14d`, `adjust_factor` ### 1.4 全局配置 → `global_config` 表 ```sql SELECT config_key, config_value FROM global_config ``` → `gc = {'safety_stock_days': '7', 'suggested_return_days': '15', ...}` 默认值(数据库无数据时): ```python gc = { 'safety_stock_days': '7', 'suggested_return_days': '15', } ``` --- ## 二、销量统计列 ### 2.1 昨日销量 ```python # 取 latest_str(最新日期)当天该产品的出货量 yesterday_qty = dmap.get(latest_str, 0) ``` **类型**:整数(`int`,但最终存储时未额外转换,直接存 Python int) ### 2.2 3天平均销量 ```python qty_3 = [dmap.get(d, 0) for d in dates_3] # 列表,长度3 avg_3 = int(round(sum(qty_3) / 3)) ``` **类型**:`int`,先 `round` 再 `int` ### 2.3 7天平均销量 ```python qty_7 = [dmap.get(d, 0) for d in dates_7] # 列表,长度7 avg_7 = int(round(sum(qty_7) / 7)) ``` ### 2.4 14天平均销量 ```python qty_14 = [dmap.get(d, 0) for d in dates_14] # 列表,长度14 avg_14 = int(round(sum(qty_14) / 14)) ``` ### 2.5 30天平均销量 ```python qty_30 = [dmap.get(d, 0) for d in dates_30] # 列表,长度30 avg_30 = int(round(sum(qty_30) / 30)) ``` ### 2.6 60天平均销量 ```python qty_60 = [dmap.get(d, 0) for d in dates_60] # 列表,长度60 avg_60 = int(round(sum(qty_60) / 60)) ``` ### 2.7 3天最高销量 ```python max_3 = max(qty_3) ``` ### 2.8 7天最高销量 ```python max_7 = max(qty_7) ``` ### 2.9 14天最高销量 ```python max_14 = max(qty_14) ``` ### 2.10 30天最高销量 ```python max_30 = max(qty_30) ``` ### 2.11 14天最低销量 ```python min_14 = min(qty_14) ``` ### 2.12 14天标准差 ```python def _calc_stddev(qty_list): n = len(qty_list) if n < 2: return 0 avg = sum(qty_list) / n variance = sum((x - avg) ** 2 for x in qty_list) / n return math.sqrt(variance) std_14 = int(round(_calc_stddev(qty_14))) ``` **公式**: ``` avg = sum(qty_14) / 14 variance = sum((x - avg)² for x in qty_14) / 14 # 总体标准差(分母n,而非n-1) std = sqrt(variance) ``` **类型**:输出为 `int(round(...))` ### 2.13 最低销量偏差率 ```python base = avg_3 if avg_3 != 0 else 1 最低销量偏差率 = round((min_14 - avg_3) / base, 4) ``` **公式**:`(14天最低销量 - 3天平均销量) / max(3天平均销量, 1)`,保留4位小数 - 注意:`base` 用的是 `avg_3`(int),如果 `avg_3 == 0` 则 `base = 1` - 不是用 `max(avg_3, 1)` 而是条件赋值 - 由于此时 `avg_3` 是 int,`min_14` 也是 int,除法结果可能是负数 ### 2.14 最高销量偏差率 ```python 最高销量偏差率 = round((max_14 - avg_3) / base, 4) ``` **公式**:`(14天最高销量 - 3天平均销量) / max(3天平均销量, 1)`,保留4位小数 ### 2.15 极值判断 ```python 极小值 = 0.7 极大值 = 1 if 最低销量偏差率 <= 极小值 and 最高销量偏差率 >= 极大值: 极值判断 = '极大/极小' elif 最低销量偏差率 < 极小值: 极值判断 = '极小' elif 最高销量偏差率 > 极大值: 极值判断 = '极大' else: 极值判断 = '正常' ``` **判定细节**: - `<= 0.7`(含等于)算极小;`< 0.7`(不含等于)才触发"极小" - `>= 1.0`(含等于)算极大;`> 1.0`(不含等于)才触发"极大" - 同时满足两个条件 → `极大/极小` - 优先级:`极大/极小` > `极小` > `极大` > `正常` ### 2.16 14天波动率 ```python avg_base = avg_14 if avg_14 != 0 else 1 波动率 = round(std_14 / avg_base, 4) ``` **公式**:`14天标准差 / max(14天平均销量, 1)`,保留4位小数 ### 2.17 波动风险 ```python if 波动率 <= 0.5: 波动风险 = 'X' elif 波动率 <= 1: 波动风险 = 'Y' else: 波动风险 = 'Z' ``` **分级**: | 条件 | 结果 | |------|------| | 波动率 ≤ 0.5 | `X` | | 0.5 < 波动率 ≤ 1.0 | `Y` | | 波动率 > 1.0 | `Z` | --- ## 三、销售趋势(10 种) ### 函数签名 ```python _determine_trend(avg_3, avg_7, avg_14) ``` ### 调用方式 ```python # 有销量数据时 row['销售趋势'] = _determine_trend(s['3天平均销量'], s['7天平均销量'], s['14天平均销量']) # 无销量数据时 row['销售趋势'] = _determine_trend(0, 0, 0) ``` ### 完整逻辑(按代码执行顺序判定) ```python def _determine_trend(avg_3, avg_7, avg_14): a3, a7, a14 = float(avg_3), float(avg_7), float(avg_14) # 规则1:全部为0 if a3 == 0 and a7 == 0 and a14 == 0: return '近期无销售' # 防除零 base_7 = a7 if a7 != 0 else 1 base_14 = a14 if a14 != 0 else 1 # 计算比值和差值 ratio_3_7 = a3 / base_7 # 3天/7天 比值 ratio_7_14 = a7 / base_14 # 7天/14天 比值 diff_3_7 = abs(a3 - a7) # 3天与7天的绝对差值 diff_7_14 = abs(a7 - a14) # 7天与14天的绝对差值 # 规则2:开始上升 if a3 > a7 and 0.95 <= ratio_7_14 <= 1.05: return '开始上升' # 规则3:开始下跌 if a3 < a7 and 0.95 <= ratio_7_14 <= 1.05: return '开始下跌' # 规则4:平稳趋势(差值均 ≤ 20) if diff_3_7 <= 20 and diff_7_14 <= 20: return '平稳趋势' # 规则5:上升趋稳(3天≈7天 且 7天>14天) if 0.95 <= ratio_3_7 <= 1.05 and a7 > a14: return '上升趋稳' # 规则6:下跌趋稳(3天≈7天 且 7天<14天) if 0.95 <= ratio_3_7 <= 1.05 and a7 < a14: return '下跌趋稳' # 规则7:持续上升(3天>7天>14天) if a3 > a7 and a7 > a14: return '持续上升' # 规则8:急跌(3天<7天 且 7天>14天) if a3 < a7 and a7 > a14: return '急跌' # 规则9:持续下跌(3天<7天<14天) if a3 < a7 and a7 < a14: return '持续下跌' # 规则10:急增(3天>7天 且 7天<14天) if a3 > a7 and a7 < a14: return '急增' # 兜底 return '平稳趋势' ``` **注意**: - 所有输入先用 `float()` 转换 - `0.95 <= ratio <= 1.05` 表示波动在 ±5% 以内视为"≈" - `diff <= 20` 表示绝对差值 ≤ 20 视为"平稳" - 规则有先后顺序:前三条(近期无销售/开始上升/开始下跌)优先匹配,然后才是平稳,最后是各趋势 --- ## 四、产品分级 ### 函数签名 ```python get_product_grade(monthly_sales, product_name='', product_type='') ``` ### 调用方式 ```python row['产品分级'] = get_product_grade( row.get('30天平均销量', 0) * 30, # 月销量 = 30天平均销量 × 30 row.get('product_name', ''), row.get('产品种类', row.get('product_type', '')) ) ``` ### 完整逻辑 ```python def get_product_grade(monthly_sales, product_name='', product_type=''): # 前置过滤1:赠品或含"晒图卡" → 不参与评级 if product_type == '赠品' or '晒图卡' in (product_name or ''): return '不参与评级' # 前置过滤2:小样 → 最高C级 if product_type == '小样': return 'C级' # 类型转换 try: ms = float(monthly_sales) except: return 'C级' # 销量分级 if ms >= 10000: return 'S级' elif ms >= 5000: return 'A级' elif ms >= 3000: return 'B级' else: return 'C级' ``` **注意**: - `monthly_sales` 已在外层乘以 30:`row.get('30天平均销量', 0) * 30`,所以 `ms` 是月销量估算值 - `product_type` 是数据库字段(不是"产品种类"),但外层调用传了 `row.get('产品种类', row.get('product_type', ''))`,优先用"产品种类",兜底用数据库字段 --- ## 五、是否新品 ### 函数签名 ```python is_new_product(created_at_str) ``` ### 调用方式 ```python row['是否新品'] = is_new_product(row.get('创建时间', '')) ``` ### 完整逻辑 ```python def is_new_product(created_at_str): if not created_at_str: return '返单品' try: if isinstance(created_at_str, str): created = datetime.strptime(created_at_str[:10], '%Y-%m-%d') else: created = created_at_str if hasattr(created, 'strftime'): five_months_ago = datetime.now() - relativedelta(months=5) return '新品' if created >= five_months_ago else '返单品' return '返单品' except: return '返单品' ``` **逻辑**:创建时间 ≥ 当前时间往前推5个月 → `新品`,否则 → `返单品` - 无创建时间或解析失败都返回 `返单品` --- ## 六、产品种类 ### 函数签名 ```python get_product_type_cached(name, brand, db_product_type=None) ``` ### 调用方式 ```python row['产品种类'] = get_product_type_cached( row.get('product_name', ''), row.get('brand', ''), row.get('product_type') ) ``` ### 完整逻辑 ```python PACKAGE_KEYWORDS = { '瓶子', '泵头', '软管', '标签', '标贴', '盖子', '彩盒', '收缩膜', '阀门', '瓶标', '喷枪', '海绵头', '内料', '膜袋', '唇膏管', '面膜布', '滴管', '说明书', '卡托', '中盒', '展示盒', '底托', '次抛管', '旋盖', '瓶体', '礼袋', '管子', '条码', '瓶盖', '丝带', '按摩棒', '刮板', '刮痧板', '拉菲草', '膜纸', '硅胶刷', '刮勺', '加工', '勺子', '瓶身', '喷雾头', '瓶贴', '上药器', '内袋', '套膜', '胶头', '塑封膜', '翻盖', '贴纸', '珍珠棉', '洗眼杯', '棉棒', } def _get_product_type(name, brand, db_product_type=None): if not name: return '其他' # 1. 数据库标记为赠品 if db_product_type and db_product_type == '赠品': return '赠品' # 2. 名称含包材关键词 for kw in PACKAGE_KEYWORDS: if kw in name: return '包材' # 3. 名称含"小样" if '小样' in name: return '小样' # 4. 有品牌且不为空/无 if brand and brand.strip() and brand.strip() != '无': return '成品' # 5. 名称含"赠品" if '赠品' in name: return '赠品' # 6. 数据库标记为成品 if db_product_type and db_product_type in ('成品',): return db_product_type # 7. 兜底 return '其他' ``` **判定优先级**:赠品(数据库) > 包材(关键词) > 小样(关键词) > 成品(有品牌) > 赠品(关键词) > 成品(数据库兜底) > 其他 --- ## 七、日销量预测 ### 函数签名 ```python compute_daily_forecast(row, sales_predict_map) ``` ### 调用方式 ```python row['日销量预测'] = compute_daily_forecast(row, sp_map) # sp_map = {r['trend_type']: r for r in sales_predict_config_cached()} ``` ### 完整逻辑 ```python def compute_daily_forecast(row, sales_predict_map): avg3 = float(row.get('3天平均销量', 0) or 0) avg7 = float(row.get('7天平均销量', 0) or 0) avg14 = float(row.get('14天平均销量', 0) or 0) max30 = float(row.get('30天最高销量', 0) or 0) trend = row.get('销售趋势', '平稳趋势') # 如果全部为0 → 无法预测 if avg3 == 0 and avg7 == 0 and avg14 == 0 and max30 == 0: return 0 # 根据趋势类型获取预测系数 cfg = sales_predict_map.get(trend, {}) w3 = float(cfg.get('avg_3d', 0.5)) # 3天权重,默认0.5 w7 = float(cfg.get('avg_7d', 0.3)) # 7天权重,默认0.3 w14 = float(cfg.get('avg_14d', 0.2)) # 14天权重,默认0.2 adj = float(cfg.get('adjust_factor', 1.0)) # 调节系数,默认1.0 # 加权计算 forecast = (avg3 * w3 + avg7 * w7 + avg14 * w14) * adj return round(forecast) ``` **注意**: - 所有取值用了 `row.get(..., 0) or 0` —— 如果值为 `None` 或 `0` 或 `''`,都统一变 `0` - 查找配置时用 `trend`(即"销售趋势"的10种值之一)匹配 `sales_predict_config.trend_type` - 找不到趋势配置时使用默认值(`w3=0.5, w7=0.3, w14=0.2, adj=1.0`) - 最后结果 `round(forecast)` 取整 --- ## 八、支撑天数列(5 列) ### 全部在 `_calc_support_days(row, gc)` 中计算 ### 前置处理 ```python safety_days = int(gc.get('safety_stock_days', '7')) return_days = int(gc.get('suggested_return_days', '15')) forecast_val = row.get('日销量预测', 0) fcast = float(forecast_val) if forecast_val not in ('近期无销售', 0, '0') else 0 inv_val = float(row.get('库存', 0) or 0) inv15_val = float(row.get('加工厂库存', 0) or 0) prod_transit = float(row.get('生产在途', 0) or 0) purch_transit = float(row.get('采购在途', 0) or 0) ``` **注意 `fcast` 的取值逻辑**: ```python fcast = float(forecast_val) if forecast_val not in ('近期无销售', 0, '0') else 0 ``` - 如果 `日销量预测` 的值是字符串 `'近期无销售'`(虽然在预测函数中不会出现,但代码做了兼容),则视为 0 - 如果值是 `0` 或 `'0'`,也视为 0 ### 8.1 库存可支撑天数 ```python if fcast == 0: row['库存可支撑天数'] = 999 else: row['库存可支撑天数'] = round(inv_val / fcast, 1) if fcast > 0 else 999 ``` **公式**:`库存 / 日销量预测`,保留 1 位小数 **无销量时**:999 ### 8.2 库存工厂可支撑天数(前端显示为"现货采购在途") ```python if fcast == 0: row['库存工厂可支撑天数'] = 999 else: row['库存工厂可支撑天数'] = round(purch_transit / fcast, 1) if fcast > 0 else 999 ``` **公式**:`采购在途 / 日销量预测`,保留 1 位小数 ### 8.3 库存工厂待生产可支撑天数(前端显示为"定制品在途") ```python if fcast == 0: row['库存工厂待生产可支撑天数'] = 999 else: row['库存工厂待生产可支撑天数'] = round(prod_transit / fcast, 1) if fcast > 0 else 999 ``` **公式**:`生产在途 / 日销量预测`,保留 1 位小数 ### 8.4 加工厂库存支撑天数 ```python if fcast == 0: row['加工厂库存支撑天数'] = 999 else: row['加工厂库存支撑天数'] = round(inv15_val / fcast, 1) if fcast > 0 else 999 ``` **公式**:`加工厂库存 / 日销量预测`,保留 1 位小数 ### 8.5 总支撑天数 ```python if fcast == 0: row['总支撑天数'] = 999 else: total_inv = prod_transit + inv15_val row['总支撑天数'] = round(total_inv / fcast, 1) if fcast > 0 else 999 ``` **公式**:`(生产在途 + 加工厂库存) / 日销量预测`,保留 1 位小数 --- ## 九、库存健康指数 ```python sup_days = row.get('库存可支撑天数', 999) s1 = sup_days if isinstance(sup_days, (int, float)) and sup_days != 999 else None lead_h = float(row.get('生产货期', 0) or 0) logis_h = float(row.get('物流时间', 0) or 0) if s1 is not None and (lead_h + logis_h) > 0: health = s1 / (lead_h + logis_h) row['库存健康指数'] = round(min(health, 999), 1) else: row['库存健康指数'] = 999 ``` **逻辑细节**: - `s1` 只有当 `库存可支撑天数` 是数字类型且不等于 999 时才有效 - 即:有销量(支撑天数不为 999)时才计算健康指数 - 公式:`库存可支撑天数 / (生产货期 + 物流时间)` - 上限 999:`min(health, 999)` - 保留 1 位小数:`round(..., 1)` - 分母为 0 时(货期+物流时间为 0)→ 返回 999 --- ## 十、风险把控 ```python created = row.get('创建时间', '') sup_days_val = row.get('库存可支撑天数', 999) try: sup_days_val = float(sup_days_val) except: sup_days_val = 999 days_since_created = 999 try: if created: if isinstance(created, str): created_dt = datetime.strptime(created[:10], '%Y-%m-%d') else: created_dt = created days_since_created = (datetime.now() - created_dt).days except: pass if days_since_created < 90: row['风险把控'] = '新品' elif days_since_created > 180 and sup_days_val > 240: row['风险把控'] = '需启动清库存' elif days_since_created > 90 and sup_days_val > 90: row['风险把控'] = '呆滞品' else: row['风险把控'] = '正常' ``` **判定表**(按代码执行顺序): | 条件 | 结果 | 说明 | |------|------|------| | 创建天数 < 90 | `新品` | 优先匹配 | | 创建天数 > 180 **且** 库存可支撑天数 > 240 | `需启动清库存` | 超半年库存过多 | | 创建天数 > 90 **且** 库存可支撑天数 > 90 | `呆滞品` | 超过3个月且库存能撑3个月 | | 其他 | `正常` | | **注意**: - `days_since_created` 默认 999,如果创建时间解析失败将保持 999 - 第一个条件 `< 90` 优先于后面的条件(新品的判断级别最高) - `days_since_created > 180` 是严格大于 180(不含等于) - `sup_days_val > 240` 和 `> 90` 都是严格大于(不含等于) --- ## 十一、建议下单数量 & 实际下单数量 ### 11.1 建议下单数量 ```python if fcast == 0 and inv_val > 0: row['建议下单数量'] = 0 else: row['建议下单数量'] = int(fcast * (safety_days + lead_h + logis_h)) ``` **公式**:`int(日销量预测 × (安全库存天数 + 生产货期 + 物流时间))` **边界条件**: - 如果日销量预测为 0 但库存 > 0 → 结果为 0(有库存但无销量,不下单) - 如果日销量预测为 0 且库存也为 0 → 进入 `else` 分支,结果 = `int(0 * ...)` = 0 - 如果日销量预测 > 0 → 正常计算并用 `int()` 截断小数 ### 11.2 实际下单数量 ```python if row['建议下单数量'] == 0: row['实际下单数量'] = 0 else: row['实际下单数量'] = ((row['建议下单数量'] + 999) // 1000) * 1000 ``` **公式**:`((建议下单数量 + 999) // 1000) * 1000` **效果**:**按 1000 向上取整** - 1 → 1000 - 999 → 1000 - 1000 → 1000(1000+999=1999//1000=1 → 1×1000=1000) - 1001 → 2000 - 1234 → 2000 ### 11.3 正式下单数量 - **非自动计算列** - 用户手动编辑输入 - 编辑交互:回车+确认 → 保存到后端 | 取消/Esc/失焦 → 恢复编辑前原值 --- ## 十二、建议回货数量 ```python total_transit = prod_transit + purch_transit if fcast > 0 and total_transit > 0: calc_val = fcast * return_days - inv_val round_up = int(calc_val * 10 + 0.99999) / 10 if round_up < 0: round_up = 0 row['建议回货数量'] = round_up if round_up <= total_transit else total_transit if row['建议回货数量'] < 0: row['建议回货数量'] = 0 else: row['建议回货数量'] = 0 ``` **公式**: ``` calc_val = 日销量预测 × 建议回货天数 - 库存 round_up = ceil_1(calc_val) # 向上取整到1位小数(通过 int(x * 10 + 0.99999) / 10 实现) 最终值 = min(round_up, 总在途) 最终值 = max(最终值, 0) ``` **注意**: - `return_days` 来自全局配置 `suggested_return_days`(默认 15 天) - 向上取整方式:`int(calc_val * 10 + 0.99999) / 10`,等价于 `math.ceil(calc_val * 10) / 10` - 上限为 `total_transit`(生产在途 + 采购在途的总和) - 下限为 0 - 仅当 `fcast > 0 and total_transit > 0` 时才计算,否则返回 0 --- ## 十三、本地数据库字段 以下列直接从 `products` 表(或关联 `suppliers` 表)读取,不经计算: ```sql SELECT p.product_code, p.product_name, p.brand, p.product_type, p.specification, p.cost_price, p.lead_time, -- 对应"生产货期" p.logistics_time, -- 对应"物流时间" p.min_order_qty, -- 对应"最低起订量" p.created_at, -- 对应"创建时间" COALESCE(s.supplier_name, p.supplier_name_match) AS "供应商名称", p.procurement_follower, -- 对应"采购跟单" p.procurement_planner, -- 对应"采购计划" p.operator, -- 对应"运营专员" p.developer, -- 对应"开发专员" p.platform, -- 对应"归属平台" p.category1, -- 对应"一级类目" p.category2, -- 对应"二级类目" p.category3, -- 对应"三级类目" p.bom -- 对应"物料清单" FROM products p LEFT JOIN suppliers s ON p.supplier_code = s.supplier_code ``` --- ## 十四、注入流程(代码执行路径) 数据从 SQL 取出后,按以下顺序注入每一行: ```python _enrich_row_with_planning_data(row, sales, inv_map, inv15_map, transit_map, sp_map, gc): ① row['库存'] = inv_map.get(pc, 0) ② row['加工厂库存'] = inv15_map.get(pc, 0) t = transit_map.get(pc, {}) ③ row['生产在途'] = t.get('生产在途', 0) ④ row['采购在途'] = t.get('采购在途', 0) ⑤ 销量数据注入(如果 pc in sales): - 昨日销量, 3/7/14/30/60天平均销量 - 3/7/14/30天最高销量, 14天最低销量 - 14天标准差, 最低/最高销量偏差率 - 极值判断, 14天波动率, 波动风险 否则全部赋 0 ⑥ row['销售趋势'] = _determine_trend(avg3, avg7, avg14) ⑦ row['产品分级'] = get_product_grade(30天平均*30, product_name, 产品种类/产品类型) ⑧ row['是否新品'] = is_new_product(创建时间) ⑨ row['日销量预测'] = compute_daily_forecast(row, sp_map) ⑩ _calc_support_days(row, gc): - 库存可支撑天数 / 库存工厂可支撑天数 / 库存工厂待生产可支撑天数 - 加工厂库存支撑天数 / 总支撑天数 - 库存健康指数 - 风险把控 - 建议下单数量 / 实际下单数量 - 建议回货数量 ``` --- ## 附录:依赖关系图 ``` [daily_outbound 表] [inventory_detail 表] │ │ ├─ 昨日销量 ├─ 库存(≠15号仓) ├─ 3/7/14/30/60天平均 ├─ 加工厂库存(15号仓) ├─ 3/7/14/30天最高 ├─ 生产在途 ├─ 14天最低 └─ 采购在途 ├─ 14天标准差 ──→ 14天波动率 ──→ 波动风险 │ ├─ 最低/最高偏差率 ──→ 极值判断 ▼ └─ [平均值] ──→ 销售趋势 [products 表] │ ├─ 生产货期/物流时间 ▼ ├─ 创建时间 ──→ 是否新品 [sales_predict_config] │ └─→ 风险把控 ├─ avg_3d/w7/w14 └─ 品牌/产品类型 ──→ 产品分级 └─ adjust_factor │ │ ▼ ▼ [产品种类判定] [日销量预测] (关键词/品牌规则) │ ┌────────────────────┼────────────────────┐ ▼ ▼ ▼ [支撑天数×5] [库存健康指数] [全局配置] ├─ 库存可支撑天数 = 支撑天数 ├─ safety_stock_days ├─ 库存工厂可支撑 ÷ (货期+物流) ├─ suggested_return_days ├─ 库存工厂待生产可支撑 │ ├─ 加工厂库存支撑 ▼ └─ 总支撑天数 [下单数量] ├─ 建议下单数量 ├─ 实际下单数量(千位取整) └─ 建议回货数量 ```