489 lines
16 KiB
Python
Executable File
489 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
import argparse
|
||
import subprocess
|
||
import sys
|
||
|
||
|
||
def build_sql(days: int, lead_days: int, warn_days: int, limit: int, include_safe: bool) -> str:
|
||
include_safe_bit = 1 if include_safe else 0
|
||
return f"""
|
||
USE [JSDataMiddlePlatform];
|
||
SET NOCOUNT ON;
|
||
|
||
DECLARE @lookback_days int = {days};
|
||
DECLARE @lead_days int = {lead_days};
|
||
DECLARE @warn_days int = {warn_days};
|
||
DECLARE @limit int = {limit};
|
||
DECLARE @include_safe bit = {include_safe_bit};
|
||
DECLARE @start_time datetime2 = DATEADD(day, -@lookback_days, GETDATE());
|
||
DECLARE @end_time datetime2 = GETDATE();
|
||
|
||
IF OBJECT_ID('tempdb..#ship_base') IS NOT NULL
|
||
DROP TABLE #ship_base;
|
||
|
||
SELECT
|
||
NULLIF(a.undefined10, '') AS sku_id,
|
||
NULLIF(a.undefined2, '') AS order_id,
|
||
NULLIF(a.undefined12, '') AS product_id,
|
||
ISNULL(NULLIF(a.undefined8, ''), 'unknown') AS shop_name,
|
||
CASE
|
||
WHEN CHARINDEX('-', ISNULL(NULLIF(a.undefined8, ''), '')) > 0
|
||
THEN LEFT(ISNULL(NULLIF(a.undefined8, ''), ''), CHARINDEX('-', ISNULL(NULLIF(a.undefined8, ''), '')) - 1)
|
||
ELSE ISNULL(NULLIF(a.undefined8, ''), '未知渠道')
|
||
END AS shop_channel,
|
||
ISNULL(NULLIF(a.undefined66, ''), '') AS ship_warehouse_name,
|
||
COALESCE(
|
||
TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 120),
|
||
TRY_CONVERT(datetime2, REPLACE(NULLIF(a.undefined6, ''), '/', '-'), 120),
|
||
TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 111),
|
||
TRY_CONVERT(datetime2, a.createdAt)
|
||
) AS ship_dt,
|
||
COALESCE(
|
||
TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined30, '')),
|
||
TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined20, '')),
|
||
CAST(0 AS decimal(18, 2))
|
||
) AS shipped_qty
|
||
INTO #ship_base
|
||
FROM dbo.jst_goods_profit_kpi_items_afs AS a WITH (NOLOCK)
|
||
WHERE ISNULL(NULLIF(a.undefined10, ''), '') <> ''
|
||
AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%配件%'
|
||
AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%特殊单%';
|
||
|
||
IF OBJECT_ID('tempdb..#ship_stats') IS NOT NULL
|
||
DROP TABLE #ship_stats;
|
||
|
||
SELECT
|
||
sb.sku_id,
|
||
COUNT(DISTINCT sb.order_id) AS order_count_30d,
|
||
COUNT(DISTINCT CASE WHEN ISNULL(sb.product_id, '') <> '' THEN sb.product_id END) AS product_id_count_30d,
|
||
COUNT(DISTINCT sb.shop_name) AS shop_count_30d,
|
||
SUM(CASE WHEN sb.ship_dt >= DATEADD(day, -7, @end_time) THEN ISNULL(sb.shipped_qty, 0) ELSE 0 END) AS shipped_qty_7d,
|
||
SUM(CASE WHEN sb.ship_dt >= DATEADD(day, -30, @end_time) THEN ISNULL(sb.shipped_qty, 0) ELSE 0 END) AS shipped_qty_30d,
|
||
MAX(sb.ship_dt) AS last_ship_time
|
||
INTO #ship_stats
|
||
FROM #ship_base AS sb
|
||
WHERE sb.ship_dt >= @start_time
|
||
AND sb.ship_dt < @end_time
|
||
GROUP BY sb.sku_id;
|
||
|
||
IF OBJECT_ID('tempdb..#sku_ref') IS NOT NULL
|
||
DROP TABLE #sku_ref;
|
||
|
||
SELECT
|
||
s.sku_id,
|
||
s.name AS sku_name,
|
||
s.other_1 AS spu,
|
||
s.i_id AS style_code,
|
||
s.category,
|
||
s.item_type,
|
||
s.brand,
|
||
s.supplier_name,
|
||
s.other_3 AS default_warehouse,
|
||
s.other_5 AS sku_status,
|
||
s.cost_price,
|
||
s.other_10 AS board_material,
|
||
s.modified
|
||
INTO #sku_ref
|
||
FROM dbo.jst_sku AS s WITH (NOLOCK)
|
||
WHERE NULLIF(s.sku_id, '') IN (SELECT sku_id FROM #ship_stats);
|
||
|
||
IF OBJECT_ID('tempdb..#inv_latest') IS NOT NULL
|
||
DROP TABLE #inv_latest;
|
||
|
||
WITH inv_rank AS (
|
||
SELECT
|
||
NULLIF(i.sku_id, '') AS sku_id,
|
||
TRY_CONVERT(decimal(18, 2), i.qty) AS stock_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.order_lock) AS order_lock_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.pick_lock) AS pick_lock_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.lock_qty) AS lock_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.virtual_qty) AS virtual_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.defective_qty) AS defective_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.purchase_qty) AS purchase_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.return_qty) AS return_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.in_qty) AS in_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.min_qty) AS min_qty,
|
||
TRY_CONVERT(decimal(18, 2), i.max_qty) AS max_qty,
|
||
i.wms_co_id,
|
||
i.name AS inventory_note,
|
||
COALESCE(i.updateTime, i.modified, i.createTime) AS inventory_snapshot_time,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY NULLIF(i.sku_id, '')
|
||
ORDER BY COALESCE(i.updateTime, i.modified, i.createTime) DESC, i.Id DESC
|
||
) AS rn
|
||
FROM dbo.jst_inventoryInfo AS i WITH (NOLOCK)
|
||
WHERE NULLIF(i.sku_id, '') IN (SELECT sku_id FROM #ship_stats)
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
stock_qty,
|
||
order_lock_qty,
|
||
pick_lock_qty,
|
||
lock_qty,
|
||
virtual_qty,
|
||
defective_qty,
|
||
purchase_qty,
|
||
return_qty,
|
||
in_qty,
|
||
min_qty,
|
||
max_qty,
|
||
wms_co_id,
|
||
inventory_note,
|
||
inventory_snapshot_time,
|
||
CASE
|
||
WHEN ISNULL(stock_qty, 0) - ISNULL(order_lock_qty, 0) - ISNULL(pick_lock_qty, 0) - ISNULL(lock_qty, 0) < 0
|
||
THEN CAST(0 AS decimal(18, 2))
|
||
ELSE ISNULL(stock_qty, 0) - ISNULL(order_lock_qty, 0) - ISNULL(pick_lock_qty, 0) - ISNULL(lock_qty, 0)
|
||
END AS available_qty
|
||
INTO #inv_latest
|
||
FROM inv_rank
|
||
WHERE rn = 1;
|
||
|
||
IF OBJECT_ID('tempdb..#sku_owner') IS NOT NULL
|
||
DROP TABLE #sku_owner;
|
||
|
||
SELECT
|
||
sr.sku_id,
|
||
COALESCE(
|
||
NULLIF(pc_exact.purchaser, ''),
|
||
NULLIF(pc_style.purchaser, ''),
|
||
NULLIF(pc_base.purchaser, ''),
|
||
'未分配'
|
||
) AS purchaser_name,
|
||
COALESCE(
|
||
NULLIF(po_exact.oper, ''),
|
||
NULLIF(po_style.oper, ''),
|
||
NULLIF(po_base.oper, ''),
|
||
NULLIF(pc_exact.oper, ''),
|
||
NULLIF(pc_style.oper, ''),
|
||
NULLIF(pc_base.oper, ''),
|
||
'未分配'
|
||
) AS spu_oper_name
|
||
INTO #sku_owner
|
||
FROM #sku_ref AS sr
|
||
OUTER APPLY (
|
||
SELECT TOP 1 c.purchaser, c.oper
|
||
FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK)
|
||
WHERE NULLIF(c.spu, '') = NULLIF(sr.spu, '')
|
||
ORDER BY c.Id DESC
|
||
) AS pc_exact
|
||
OUTER APPLY (
|
||
SELECT TOP 1 c.purchaser, c.oper
|
||
FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK)
|
||
WHERE NULLIF(c.spu, '') = NULLIF(sr.style_code, '')
|
||
ORDER BY c.Id DESC
|
||
) AS pc_style
|
||
OUTER APPLY (
|
||
SELECT TOP 1 c.purchaser, c.oper
|
||
FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK)
|
||
WHERE NULLIF(c.spu, '') = CASE
|
||
WHEN CHARINDEX('K', ISNULL(sr.spu, '')) > 0
|
||
THEN LEFT(sr.spu, CHARINDEX('K', sr.spu) - 1)
|
||
ELSE NULL
|
||
END
|
||
ORDER BY c.Id DESC
|
||
) AS pc_base
|
||
OUTER APPLY (
|
||
SELECT TOP 1 o.oper
|
||
FROM dbo.op_operManage AS o WITH (NOLOCK)
|
||
WHERE NULLIF(o.spu, '') = NULLIF(sr.spu, '')
|
||
ORDER BY o.Id DESC
|
||
) AS po_exact
|
||
OUTER APPLY (
|
||
SELECT TOP 1 o.oper
|
||
FROM dbo.op_operManage AS o WITH (NOLOCK)
|
||
WHERE NULLIF(o.spu, '') = NULLIF(sr.style_code, '')
|
||
ORDER BY o.Id DESC
|
||
) AS po_style
|
||
OUTER APPLY (
|
||
SELECT TOP 1 o.oper
|
||
FROM dbo.op_operManage AS o WITH (NOLOCK)
|
||
WHERE NULLIF(o.spu, '') = CASE
|
||
WHEN CHARINDEX('K', ISNULL(sr.spu, '')) > 0
|
||
THEN LEFT(sr.spu, CHARINDEX('K', sr.spu) - 1)
|
||
ELSE NULL
|
||
END
|
||
ORDER BY o.Id DESC
|
||
) AS po_base;
|
||
|
||
IF OBJECT_ID('tempdb..#operator_top') IS NOT NULL
|
||
DROP TABLE #operator_top;
|
||
|
||
WITH oper_rank AS (
|
||
SELECT
|
||
sb.sku_id,
|
||
NULLIF(t.oper, '') AS oper_name,
|
||
SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY sb.sku_id
|
||
ORDER BY SUM(ISNULL(sb.shipped_qty, 0)) DESC, NULLIF(t.oper, '') ASC
|
||
) AS rn
|
||
FROM #ship_base AS sb
|
||
INNER JOIN dbo.op_TargetManagement AS t WITH (NOLOCK)
|
||
ON NULLIF(t.shopproid, '') = NULLIF(sb.product_id, '')
|
||
AND ISNULL(NULLIF(t.shopname, ''), '') = ISNULL(NULLIF(sb.shop_name, ''), '')
|
||
WHERE sb.ship_dt >= @start_time
|
||
AND sb.ship_dt < @end_time
|
||
AND ISNULL(NULLIF(t.oper, ''), '') <> ''
|
||
GROUP BY sb.sku_id, NULLIF(t.oper, '')
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
oper_name AS operator_name
|
||
INTO #operator_top
|
||
FROM oper_rank
|
||
WHERE rn = 1;
|
||
|
||
IF OBJECT_ID('tempdb..#channel_top') IS NOT NULL
|
||
DROP TABLE #channel_top;
|
||
|
||
WITH channel_rank AS (
|
||
SELECT
|
||
sb.sku_id,
|
||
sb.shop_channel,
|
||
SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY sb.sku_id
|
||
ORDER BY SUM(ISNULL(sb.shipped_qty, 0)) DESC, sb.shop_channel ASC
|
||
) AS rn
|
||
FROM #ship_base AS sb
|
||
WHERE sb.ship_dt >= @start_time
|
||
AND sb.ship_dt < @end_time
|
||
GROUP BY sb.sku_id, sb.shop_channel
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
shop_channel AS top_shop_channel,
|
||
shipped_qty_sum_30d AS top_channel_shipped_qty_30d
|
||
INTO #channel_top
|
||
FROM channel_rank
|
||
WHERE rn = 1;
|
||
|
||
IF OBJECT_ID('tempdb..#shop_top') IS NOT NULL
|
||
DROP TABLE #shop_top;
|
||
|
||
WITH shop_rank AS (
|
||
SELECT
|
||
sb.sku_id,
|
||
sb.shop_name,
|
||
SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY sb.sku_id
|
||
ORDER BY SUM(ISNULL(sb.shipped_qty, 0)) DESC, sb.shop_name ASC
|
||
) AS rn
|
||
FROM #ship_base AS sb
|
||
WHERE sb.ship_dt >= @start_time
|
||
AND sb.ship_dt < @end_time
|
||
GROUP BY sb.sku_id, sb.shop_name
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
shop_name AS top_shop_name,
|
||
shipped_qty_sum_30d AS top_shop_shipped_qty_30d
|
||
INTO #shop_top
|
||
FROM shop_rank
|
||
WHERE rn = 1;
|
||
|
||
IF OBJECT_ID('tempdb..#warehouse_top') IS NOT NULL
|
||
DROP TABLE #warehouse_top;
|
||
|
||
WITH wh_rank AS (
|
||
SELECT
|
||
sb.sku_id,
|
||
sb.ship_warehouse_name,
|
||
SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY sb.sku_id
|
||
ORDER BY SUM(ISNULL(sb.shipped_qty, 0)) DESC, sb.ship_warehouse_name ASC
|
||
) AS rn
|
||
FROM #ship_base AS sb
|
||
WHERE sb.ship_dt >= @start_time
|
||
AND sb.ship_dt < @end_time
|
||
AND ISNULL(NULLIF(sb.ship_warehouse_name, ''), '') <> ''
|
||
GROUP BY sb.sku_id, sb.ship_warehouse_name
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
ship_warehouse_name AS top_warehouse_name
|
||
INTO #warehouse_top
|
||
FROM wh_rank
|
||
WHERE rn = 1;
|
||
|
||
IF OBJECT_ID('tempdb..#risk_eval') IS NOT NULL
|
||
DROP TABLE #risk_eval;
|
||
|
||
SELECT
|
||
sr.sku_id,
|
||
sr.sku_name,
|
||
sr.spu,
|
||
sr.category,
|
||
sr.item_type,
|
||
sr.brand,
|
||
sr.cost_price,
|
||
sr.board_material,
|
||
sr.sku_status,
|
||
COALESCE(NULLIF(sr.supplier_name, ''), '未标注') AS factory_name,
|
||
COALESCE(NULLIF(ow.purchaser_name, ''), '未分配') AS purchaser_name,
|
||
COALESCE(NULLIF(ot.operator_name, ''), NULLIF(ow.spu_oper_name, ''), '未分配') AS operator_name,
|
||
COALESCE(NULLIF(wt.top_warehouse_name, ''), NULLIF(sr.default_warehouse, ''), NULLIF(inv.inventory_note, ''), '未标注') AS warehouse_name,
|
||
ct.top_shop_channel,
|
||
stp.top_shop_name,
|
||
ISNULL(ss.product_id_count_30d, 0) AS product_id_count_30d,
|
||
ISNULL(ss.shop_count_30d, 0) AS shop_count_30d,
|
||
ISNULL(ss.order_count_30d, 0) AS order_count_30d,
|
||
ISNULL(ss.shipped_qty_7d, 0) AS shipped_qty_7d,
|
||
ISNULL(ss.shipped_qty_30d, 0) AS shipped_qty_30d,
|
||
ss.last_ship_time,
|
||
inv.stock_qty,
|
||
inv.order_lock_qty,
|
||
inv.pick_lock_qty,
|
||
inv.lock_qty,
|
||
inv.available_qty,
|
||
inv.virtual_qty,
|
||
inv.defective_qty,
|
||
inv.purchase_qty,
|
||
inv.return_qty,
|
||
inv.in_qty,
|
||
inv.min_qty,
|
||
inv.max_qty,
|
||
inv.inventory_snapshot_time,
|
||
CAST(ISNULL(ss.shipped_qty_7d, 0) / 7.0 AS decimal(18, 2)) AS avg_daily_shipped_7d,
|
||
CAST(ISNULL(ss.shipped_qty_30d, 0) / 30.0 AS decimal(18, 2)) AS avg_daily_shipped_30d,
|
||
CAST(
|
||
CASE
|
||
WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN NULL
|
||
ELSE inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)
|
||
END AS decimal(18, 2)
|
||
) AS estimated_stockout_days,
|
||
@lead_days AS replenishment_lead_days,
|
||
CAST(
|
||
CASE
|
||
WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN 0
|
||
WHEN inv.available_qty >= (ISNULL(ss.shipped_qty_30d, 0) / 30.0) * @lead_days THEN 0
|
||
ELSE ((ISNULL(ss.shipped_qty_30d, 0) / 30.0) * @lead_days) - inv.available_qty
|
||
END AS decimal(18, 2)
|
||
) AS suggest_replenish_qty_to_lead_days,
|
||
CASE
|
||
WHEN inv.sku_id IS NULL THEN '无库存快照'
|
||
WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN '近30天无发货'
|
||
WHEN inv.available_qty <= 0 THEN '缺货/可用库存<=0'
|
||
WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= @lead_days THEN '补货紧急(<=10天)'
|
||
WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= @warn_days THEN '补货预警(10-15天)'
|
||
ELSE '库存安全'
|
||
END AS stock_risk_level,
|
||
CASE
|
||
WHEN inv.sku_id IS NULL THEN 1
|
||
WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN 0
|
||
WHEN inv.available_qty <= 0 THEN 1
|
||
WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= @warn_days THEN 1
|
||
ELSE 0
|
||
END AS has_stock_risk
|
||
INTO #risk_eval
|
||
FROM #ship_stats AS ss
|
||
LEFT JOIN #sku_ref AS sr
|
||
ON sr.sku_id = ss.sku_id
|
||
LEFT JOIN #inv_latest AS inv
|
||
ON inv.sku_id = ss.sku_id
|
||
LEFT JOIN #sku_owner AS ow
|
||
ON ow.sku_id = ss.sku_id
|
||
LEFT JOIN #operator_top AS ot
|
||
ON ot.sku_id = ss.sku_id
|
||
LEFT JOIN #channel_top AS ct
|
||
ON ct.sku_id = ss.sku_id
|
||
LEFT JOIN #shop_top AS stp
|
||
ON stp.sku_id = ss.sku_id
|
||
LEFT JOIN #warehouse_top AS wt
|
||
ON wt.sku_id = ss.sku_id;
|
||
|
||
-- 1) 汇总:近30天在售SKU + 缺货风险SKU数量
|
||
SELECT
|
||
COUNT(*) AS sold_sku_count_30d,
|
||
SUM(CASE WHEN has_stock_risk = 1 THEN 1 ELSE 0 END) AS stock_risk_sku_count,
|
||
SUM(CASE WHEN stock_risk_level LIKE '补货紧急%' OR stock_risk_level LIKE '缺货%' THEN 1 ELSE 0 END) AS urgent_sku_count,
|
||
SUM(CASE WHEN stock_risk_level LIKE '补货预警%' THEN 1 ELSE 0 END) AS warning_sku_count,
|
||
@lookback_days AS lookback_days,
|
||
@lead_days AS replenishment_lead_days,
|
||
@warn_days AS warning_days
|
||
FROM #risk_eval;
|
||
|
||
-- 2) 明细:全量SKU风险清单(默认只显示风险项)
|
||
SELECT TOP (@limit)
|
||
sku_id,
|
||
sku_name,
|
||
spu,
|
||
category,
|
||
item_type,
|
||
brand,
|
||
sku_status,
|
||
cost_price,
|
||
board_material,
|
||
factory_name,
|
||
purchaser_name,
|
||
operator_name,
|
||
warehouse_name,
|
||
top_shop_channel,
|
||
top_shop_name,
|
||
product_id_count_30d,
|
||
shop_count_30d,
|
||
order_count_30d,
|
||
shipped_qty_7d,
|
||
shipped_qty_30d,
|
||
avg_daily_shipped_7d,
|
||
avg_daily_shipped_30d,
|
||
stock_qty,
|
||
order_lock_qty,
|
||
pick_lock_qty,
|
||
lock_qty,
|
||
available_qty,
|
||
estimated_stockout_days,
|
||
replenishment_lead_days,
|
||
suggest_replenish_qty_to_lead_days,
|
||
stock_risk_level,
|
||
inventory_snapshot_time,
|
||
last_ship_time
|
||
FROM #risk_eval
|
||
WHERE @include_safe = 1
|
||
OR has_stock_risk = 1
|
||
ORDER BY
|
||
CASE
|
||
WHEN stock_risk_level LIKE '缺货%' THEN 1
|
||
WHEN stock_risk_level LIKE '补货紧急%' THEN 2
|
||
WHEN stock_risk_level LIKE '补货预警%' THEN 3
|
||
WHEN stock_risk_level = '无库存快照' THEN 4
|
||
ELSE 9
|
||
END,
|
||
estimated_stockout_days ASC,
|
||
shipped_qty_30d DESC,
|
||
sku_id ASC;
|
||
"""
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="All-SKU stock risk alert report")
|
||
parser.add_argument("--days", type=int, default=30, help="lookback window days, default 30")
|
||
parser.add_argument("--lead-days", type=int, default=10, help="replenishment lead time days")
|
||
parser.add_argument("--warn-days", type=int, default=15, help="warning line days (must >= lead-days)")
|
||
parser.add_argument("--limit", type=int, default=500, help="max rows for detail result set")
|
||
parser.add_argument("--include-safe", action="store_true", help="include safe SKUs in detail output")
|
||
args = parser.parse_args()
|
||
|
||
days = max(7, min(args.days, 180))
|
||
lead_days = max(1, min(args.lead_days, 60))
|
||
warn_days = max(lead_days, min(args.warn_days, 90))
|
||
limit = max(1, min(args.limit, 5000))
|
||
|
||
sql = build_sql(days, lead_days, warn_days, limit, args.include_safe)
|
||
cmd = ["company-db", "query", "--sql", sql]
|
||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||
if proc.returncode != 0:
|
||
err = (proc.stderr or proc.stdout or "company-db failed").strip()
|
||
print(err, file=sys.stderr)
|
||
return proc.returncode
|
||
|
||
print(proc.stdout.rstrip())
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|