fix: deduct available stock before production yield adjustment
This commit is contained in:
@@ -182,11 +182,13 @@ class CandidateUnavailableError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def production_quantity_for_demand(quantity: int) -> int:
|
||||
"""Cover 5% defects, then round up to a batch of five without float error."""
|
||||
def production_quantity_for_demand(quantity: int, available_inventory: int = 0) -> int:
|
||||
"""Deduct usable finished stock before yield allowance and batch rounding."""
|
||||
if isinstance(quantity, bool) or not isinstance(quantity, int) or quantity <= 0:
|
||||
raise InputError("需求数量必须为正整数。")
|
||||
numerator = quantity * 100
|
||||
if isinstance(available_inventory, bool) or not isinstance(available_inventory, int):
|
||||
raise InputError("可用库存必须为整数。")
|
||||
numerator = max(quantity - max(available_inventory, 0), 0) * 100
|
||||
denominator = (100 - DEFECT_RATE_PERCENT) * PRODUCTION_BATCH_SIZE
|
||||
return ((numerator + denominator - 1) // denominator) * PRODUCTION_BATCH_SIZE
|
||||
|
||||
@@ -830,12 +832,13 @@ def overall_judgment_lines(candidates: list[dict[str, Any]]) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def candidate_report_lines(candidate: dict[str, Any], quantity: int, production_quantity: int) -> list[str]:
|
||||
def candidate_report_lines(candidate: dict[str, Any], quantity: int) -> list[str]:
|
||||
details = candidate["details"]
|
||||
return [
|
||||
f"{candidate['index']}. {candidate['model']}({candidate['type']})",
|
||||
f"- 需求数量:{quantity}台",
|
||||
f"- 排产测算数量:{production_quantity}台",
|
||||
f"- 扣库存后净需求:{details['net_demand']}台",
|
||||
f"- 排产测算数量:{details['production_quantity']}台",
|
||||
f"- {details['prediction']}",
|
||||
f"- 可用库存/覆盖情况:{details['inventory']}",
|
||||
f"- 主导因素:{details['main_factor']}",
|
||||
@@ -906,9 +909,17 @@ def render_record(
|
||||
"",
|
||||
f"**输入型号**:{request.model}",
|
||||
f"**输入数量**:{request.quantity}台",
|
||||
f"**排产测算数量(API查询数量)**:{production_quantity_for_demand(request.quantity)}台",
|
||||
"**数量计算规则**:原始需求÷95%良率,再按5台的倍数向上取整。",
|
||||
"**库存覆盖口径**:按排产测算数量判断。",
|
||||
"**数量计算规则**:各型号先扣可用库存(按100%可用),净需求÷95%良率,再按5台的倍数向上取整;净需求为0时排产为0。",
|
||||
"**库存覆盖口径**:按原始需求数量判断。",
|
||||
"**各型号排产测算**:",
|
||||
*[
|
||||
f"- {item['model']}:净需求{item['details']['net_demand']}台;排产测算数量{item['details']['production_quantity']}台;最终API查询数量{item['details']['api_quantity']}台"
|
||||
for item in candidates
|
||||
],
|
||||
*[
|
||||
f"- {item['model']}:净需求{item['net_demand']}台;排产测算数量{item['production_quantity']}台;交期不可用"
|
||||
for item in unavailable if 'net_demand' in item
|
||||
],
|
||||
f"**生成时间**:{generation_time:%Y-%m-%d %H:%M:%S %z}",
|
||||
f"**规则源实际路径**:`{RULE_SOURCE_DISPLAY}`",
|
||||
f"**接口依据**:`{API_SOURCE_DISPLAY}`",
|
||||
@@ -981,10 +992,49 @@ def write_record(
|
||||
file.write(record)
|
||||
|
||||
|
||||
def query_model_for_demand(request: RequestInput) -> dict[str, Any]:
|
||||
"""Read stock with original demand, then forecast only the production gap.
|
||||
|
||||
The API uses quantity both for its finished-stock shortcut and for production
|
||||
and material requirements. Never accept that shortcut for a partial-stock
|
||||
order, or combine quantities from two different stock snapshots.
|
||||
"""
|
||||
payload = run_controlled_query(request)
|
||||
details = lead_time_details(payload, request.quantity)
|
||||
if not details['has_lead_time']:
|
||||
return details
|
||||
stock = whole_day_count(details['available_inventory'])
|
||||
# A negative balance represents no stock that can cover this new demand.
|
||||
raw_stock = details['available_inventory']
|
||||
if stock is None and isinstance(raw_stock, (int, float)) and not isinstance(raw_stock, bool) and raw_stock < 0:
|
||||
stock = 0 if isinstance(raw_stock, int) or raw_stock.is_integer() else None
|
||||
if stock is None:
|
||||
details.update(has_lead_time=False, unavailable_reason="接口未返回有效整数可用库存,无法计算净需求及排产数量。")
|
||||
return details
|
||||
net_demand = max(request.quantity - stock, 0)
|
||||
planned = production_quantity_for_demand(request.quantity, stock)
|
||||
details.update(net_demand=net_demand, production_quantity=planned, api_quantity=request.quantity)
|
||||
if planned == 0 or planned == request.quantity:
|
||||
return details
|
||||
if stock >= planned:
|
||||
details.update(
|
||||
has_lead_time=False,
|
||||
unavailable_reason=(f"扣库存后净需求{net_demand}台,排产测算数量{planned}台;"
|
||||
"库存仅部分覆盖原需求,但达到排产查询数量,现有接口会误走现货分支;"
|
||||
"需接口支持分别传入原需求和排产数量后测算交期。"),
|
||||
)
|
||||
return details
|
||||
forecast = lead_time_details(run_controlled_query(RequestInput(request.model, planned)), request.quantity)
|
||||
forecast.update(net_demand=net_demand, production_quantity=planned, api_quantity=planned)
|
||||
if forecast['available_inventory'] != raw_stock:
|
||||
forecast.update(has_lead_time=False, unavailable_reason="两次查询的可用库存发生变化,未采用本次交期;请重新查询。")
|
||||
return forecast
|
||||
|
||||
|
||||
def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
if candidate_offset < 0:
|
||||
raise InputError("候选批次位置无效,请重新发起型号和数量查询。")
|
||||
production_quantity = production_quantity_for_demand(request.quantity)
|
||||
production_quantity_for_demand(request.quantity) # Validate before querying.
|
||||
|
||||
all_candidate_specs = generate_candidate_specs(request.model)
|
||||
candidate_specs = all_candidate_specs[
|
||||
@@ -998,18 +1048,14 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
baseline: dict[str, Any] | None = None
|
||||
if is_initial_batch:
|
||||
try:
|
||||
baseline_payload = run_controlled_query(RequestInput(request.model, production_quantity))
|
||||
baseline = query_model_for_demand(request)
|
||||
except CandidateUnavailableError as exc:
|
||||
raise RuntimeError(f"原型号未生成候选结果。\n原因:{exc}") from exc
|
||||
baseline = lead_time_details(
|
||||
baseline_payload,
|
||||
production_quantity,
|
||||
)
|
||||
if not baseline["has_lead_time"]:
|
||||
raise RuntimeError(
|
||||
"原型号接口未返回有效工作日交期和最终交货日期,未生成候选结果。\n"
|
||||
"原型号未生成可用交期结果,未生成候选结果。\n"
|
||||
f"原因:{baseline['unavailable_reason']}\n"
|
||||
"处理建议:请接口维护人员核对上述字段及服务端计算结果后重试。"
|
||||
"处理建议:请接口维护人员核对上述原因、库存口径及服务端计算结果后重试。"
|
||||
)
|
||||
candidates.append(
|
||||
{
|
||||
@@ -1028,15 +1074,15 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
|
||||
def query_candidate(spec: dict[str, str]) -> tuple[dict[str, str], dict[str, Any] | None]:
|
||||
try:
|
||||
payload = run_controlled_query(RequestInput(spec["model"], production_quantity))
|
||||
details = query_model_for_demand(RequestInput(spec["model"], request.quantity))
|
||||
except CandidateUnavailableError as exc:
|
||||
return {**spec, "unavailable_reason": str(exc)}, None
|
||||
details = lead_time_details(
|
||||
payload,
|
||||
production_quantity,
|
||||
)
|
||||
if not details["has_lead_time"]:
|
||||
return {**spec, "unavailable_reason": details["unavailable_reason"]}, None
|
||||
return {
|
||||
**spec,
|
||||
**{key: details[key] for key in ('net_demand', 'production_quantity') if key in details},
|
||||
"unavailable_reason": details["unavailable_reason"],
|
||||
}, None
|
||||
return spec, details
|
||||
|
||||
if candidate_specs:
|
||||
@@ -1104,8 +1150,8 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
),
|
||||
"",
|
||||
f"输入需求:{request.model},{request.quantity}台",
|
||||
f"排产测算数量:{production_quantity}台(不良率{DEFECT_RATE_PERCENT}%,按{PRODUCTION_BATCH_SIZE}台的倍数向上取整)",
|
||||
"库存覆盖口径:按排产测算数量判断",
|
||||
f"排产测算规则:各型号先扣可用库存,再除以{100 - DEFECT_RATE_PERCENT}%合格率并向上取{PRODUCTION_BATCH_SIZE}台的倍数;库存足量时排产为0",
|
||||
"库存覆盖口径:按原始需求数量判断(可用库存按100%可用)",
|
||||
f"数据时间戳:{data_timestamp}",
|
||||
"状态:试运行版输出 / 内部候选",
|
||||
"判断等级:AM516算法预测参考,需人工复核",
|
||||
@@ -1116,7 +1162,7 @@ def build_report(request: RequestInput, candidate_offset: int = 0) -> str:
|
||||
"",
|
||||
]
|
||||
for candidate_item in candidates:
|
||||
report_lines.extend(candidate_report_lines(candidate_item, request.quantity, production_quantity))
|
||||
report_lines.extend(candidate_report_lines(candidate_item, request.quantity))
|
||||
report_lines.append("不可用候选:")
|
||||
if unavailable:
|
||||
for item in unavailable:
|
||||
|
||||
Reference in New Issue
Block a user