fix: pass original demand to delivery prediction API
This commit is contained in:
@@ -404,18 +404,7 @@ class TriggerGateTests(unittest.TestCase):
|
||||
self.assertNotIn(key, bridge._candidate_continuations)
|
||||
|
||||
|
||||
class ProductionQuantityTests(unittest.TestCase):
|
||||
def test_defect_allowance_precedes_batch_rounding_at_exact_boundaries(self) -> None:
|
||||
for demand, expected in ((1, 5), (4, 5), (5, 10), (10, 15), (19, 20), (20, 25), (25, 30), (95, 100), (190, 200)):
|
||||
with self.subTest(demand=demand):
|
||||
self.assertEqual(bridge.production_quantity_for_demand(demand), expected)
|
||||
# Integer arithmetic must also cover quantities beyond float precision.
|
||||
demand = 10**18 + 19
|
||||
planned = bridge.production_quantity_for_demand(demand)
|
||||
self.assertEqual(planned % 5, 0)
|
||||
self.assertGreaterEqual(planned * 95, demand * 100)
|
||||
self.assertLess((planned - 5) * 95, demand * 100)
|
||||
|
||||
class OriginalDemandQueryTests(unittest.TestCase):
|
||||
def test_invalid_demand_stops_before_api_or_recording(self) -> None:
|
||||
for value in (0, -1, True, 5.0, "5"):
|
||||
with (
|
||||
@@ -424,58 +413,65 @@ class ProductionQuantityTests(unittest.TestCase):
|
||||
mock.patch.object(bridge, "write_record") as record,
|
||||
):
|
||||
with self.assertRaises(bridge.InputError):
|
||||
bridge.build_report(bridge.RequestInput("eRob70H50I-FS-18CN[V5]", value))
|
||||
bridge.build_report(bridge.RequestInput("eRob70F50I-FHM-18ET[V4]", value))
|
||||
query.assert_not_called()
|
||||
record.assert_not_called()
|
||||
|
||||
def test_stock_is_deducted_before_yield_and_batching(self) -> None:
|
||||
for demand, stock, expected in ((24, 6, 20), (24, 0, 30), (24, 24, 0), (24, 30, 0), (25, 6, 20), (5, 1, 5), (95, 0, 100), (24, -2, 30)):
|
||||
with self.subTest(demand=demand, stock=stock):
|
||||
self.assertEqual(bridge.production_quantity_for_demand(demand, stock), expected)
|
||||
def test_partial_full_and_zero_stock_do_not_change_api_quantity(self) -> None:
|
||||
for demand, stock in ((12, 10), (8, 5), (24, 6), (24, 0), (24, 24), (24, 30), (24, -2), (5, 1)):
|
||||
payload = successful_payload()
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = stock
|
||||
request = bridge.RequestInput('eRob70F50I-FHM-18ET[V4]', demand)
|
||||
with (
|
||||
self.subTest(demand=demand, stock=stock),
|
||||
mock.patch.object(bridge, 'run_controlled_query', return_value=payload) as query,
|
||||
):
|
||||
details = bridge.query_model_for_demand(request)
|
||||
query.assert_called_once_with(request)
|
||||
self.assertTrue(details['has_lead_time'])
|
||||
self.assertEqual(details['api_quantity'], demand)
|
||||
|
||||
def test_initial_and_continuation_query_net_production_per_model(self) -> None:
|
||||
request = bridge.RequestInput(model="eRob70H50I-FS-18CN[V5]", quantity=24)
|
||||
def test_screenshot_regression_passes_twelve_to_controlled_script(self) -> None:
|
||||
request = bridge.RequestInput('eRob70F50I-FHM-18ET[V4]', 12)
|
||||
payload = successful_payload()
|
||||
payload["data"]["库存信息"]["可用库存(扣除了待产数量)"] = 6
|
||||
completed = SimpleNamespace(stdout=bridge.json.dumps(payload), stderr="", returncode=0)
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = 10
|
||||
payload['data']['交期汇总'].update(AM516工作日交期=8, AM516最终交货日期='2026-10-27', AM516主导因素='生产')
|
||||
completed = SimpleNamespace(stdout=bridge.json.dumps(payload), stderr='', returncode=0)
|
||||
with (
|
||||
mock.patch.object(bridge.subprocess, 'run', return_value=completed) as run,
|
||||
mock.patch.object(bridge, 'write_record') as record,
|
||||
):
|
||||
report = bridge.build_report(request)
|
||||
self.assertEqual(run.call_count, 5)
|
||||
for call in run.call_args_list:
|
||||
command = call.args[0]
|
||||
self.assertEqual(command[command.index('--quantity') + 1], '12')
|
||||
self.assertIn('整机可用库存10台(部分覆盖,仍需补齐2台)', report)
|
||||
self.assertIn('8个工作日,最终交货日期:2026-10-27', report)
|
||||
self.assertNotIn('误走现货分支', report)
|
||||
self.assertNotIn('排产测算数量', report)
|
||||
recorded = bridge.render_record(*record.call_args.args)
|
||||
self.assertIn('**输入数量**:12台', recorded)
|
||||
self.assertEqual(recorded.count('最终API查询数量12台'), 5)
|
||||
self.assertNotIn('净需求', recorded)
|
||||
|
||||
def test_initial_and_continuation_preserve_original_demand(self) -> None:
|
||||
request = bridge.RequestInput('eRob70H50I-FS-18CN[V5]', 24)
|
||||
for offset, model_count in ((0, 5), (bridge.CANDIDATE_BATCH_SIZE, 4)):
|
||||
with (
|
||||
self.subTest(offset=offset),
|
||||
mock.patch.object(bridge.subprocess, "run", return_value=completed) as run,
|
||||
mock.patch.object(bridge, "write_record") as record,
|
||||
mock.patch.object(bridge, 'run_controlled_query', return_value=successful_payload()) as query,
|
||||
mock.patch.object(bridge, 'write_record'),
|
||||
):
|
||||
report = bridge.build_report(request, candidate_offset=offset)
|
||||
by_model = {}
|
||||
for call in run.call_args_list:
|
||||
command = call.args[0]
|
||||
by_model.setdefault(command[command.index("--product") + 1], []).append(command[command.index("--quantity") + 1])
|
||||
self.assertEqual(len(by_model), model_count)
|
||||
self.assertTrue(all(quantities == ["24", "20"] for quantities in by_model.values()))
|
||||
self.assertEqual(report.count("- 扣库存后净需求:18台"), model_count)
|
||||
self.assertEqual(report.count("- 排产测算数量:20台"), model_count)
|
||||
self.assertIn("部分覆盖,仍需补齐18台", report)
|
||||
recorded = bridge.render_record(*record.call_args.args)
|
||||
self.assertIn("**输入数量**:24台", recorded)
|
||||
self.assertEqual(recorded.count("排产测算数量20台;最终API查询数量20台"), model_count)
|
||||
bridge.build_report(request, candidate_offset=offset)
|
||||
self.assertEqual(query.call_count, model_count)
|
||||
self.assertEqual(len({call.args[0].model for call in query.call_args_list}), model_count)
|
||||
self.assertTrue(all(call.args[0].quantity == 24 for call in query.call_args_list))
|
||||
|
||||
def test_stock_covering_original_demand_needs_no_production_query(self) -> None:
|
||||
payload = successful_payload()
|
||||
payload["data"]["库存信息"]["可用库存(扣除了待产数量)"] = 7
|
||||
with (
|
||||
mock.patch.object(bridge, "run_controlled_query", return_value=payload) as query,
|
||||
mock.patch.object(bridge, "write_record"),
|
||||
):
|
||||
report = bridge.build_report(bridge.RequestInput("eRob70H50I-BHM-18CTC[V5]", 5))
|
||||
self.assertTrue(all(call.args[0].quantity == 5 for call in query.call_args_list))
|
||||
self.assertIn("库存覆盖口径:按原始需求数量判断", report)
|
||||
self.assertIn("整机可用库存7台(完全覆盖)", report)
|
||||
self.assertIn("- 排产测算数量:0台", report)
|
||||
self.assertIn("是否完全覆盖数量:是", report)
|
||||
|
||||
def test_each_candidate_uses_its_own_stock(self) -> None:
|
||||
request = bridge.RequestInput("eRob70H50I-FS-18CN[V5]", 24)
|
||||
def test_each_candidate_displays_its_own_stock(self) -> None:
|
||||
request = bridge.RequestInput('eRob70H50I-FS-18CN[V5]', 24)
|
||||
models = [request.model, *[s['model'] for s in bridge.generate_candidate_specs(request.model)[:4]]]
|
||||
stocks = dict(zip(models, [6, 0, 24, 5, 30]))
|
||||
stocks = dict(zip(models, [6, 0, 24, 20, 30]))
|
||||
def query(r):
|
||||
payload = successful_payload()
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = stocks[r.model]
|
||||
@@ -485,12 +481,13 @@ class ProductionQuantityTests(unittest.TestCase):
|
||||
mock.patch.object(bridge, 'write_record') as record,
|
||||
):
|
||||
bridge.build_report(request)
|
||||
candidates = record.call_args.args[1]
|
||||
self.assertEqual([c['details']['production_quantity'] for c in candidates], [20, 30, 0, 20, 0])
|
||||
for model, expected in zip(models, [[24, 20], [24, 30], [24], [24, 20], [24]]):
|
||||
self.assertEqual([c.args[0].quantity for c in run.call_args_list if c.args[0].model == model], expected)
|
||||
self.assertEqual(run.call_count, len(models))
|
||||
for candidate in record.call_args.args[1]:
|
||||
self.assertEqual(candidate['details']['available_inventory'], stocks[candidate['model']])
|
||||
self.assertEqual(candidate['details']['api_quantity'], 24)
|
||||
self.assertEqual(candidate['details']['full_coverage'], stocks[candidate['model']] >= 24)
|
||||
|
||||
def test_missing_or_invalid_stock_cannot_be_treated_as_zero(self) -> None:
|
||||
def test_missing_or_invalid_stock_is_unknown_without_overriding_api_forecast(self) -> None:
|
||||
for stock in (None, True, '6', 1.5, float('nan'), float('inf')):
|
||||
payload = successful_payload()
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = stock
|
||||
@@ -499,52 +496,32 @@ class ProductionQuantityTests(unittest.TestCase):
|
||||
mock.patch.object(bridge, 'run_controlled_query', return_value=payload) as query,
|
||||
):
|
||||
details = bridge.query_model_for_demand(bridge.RequestInput('model', 24))
|
||||
self.assertFalse(details['has_lead_time'])
|
||||
self.assertIn('无法计算净需求', details['unavailable_reason'])
|
||||
self.assertTrue(details['has_lead_time'])
|
||||
self.assertIsNone(details['available_inventory'])
|
||||
self.assertIsNone(details['full_coverage'])
|
||||
self.assertIn('需人工复核', details['inventory'])
|
||||
query.assert_called_once()
|
||||
|
||||
def test_partial_stock_cannot_trigger_false_stock_delivery(self) -> None:
|
||||
def test_forecast_preserves_api_date_and_materials(self) -> None:
|
||||
payload = successful_payload()
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = 20
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = 10
|
||||
payload['data']['交期汇总'].update(AM516工作日交期=8, AM516最终交货日期='2026-10-27')
|
||||
payload['data']['采购信息']['缺料明细'] = [{'物料名称': '测算物料'}]
|
||||
with mock.patch.object(bridge, 'run_controlled_query', return_value=payload) as query:
|
||||
details = bridge.query_model_for_demand(bridge.RequestInput('model', 24))
|
||||
self.assertEqual(details['net_demand'], 4)
|
||||
self.assertEqual(details['production_quantity'], 5)
|
||||
self.assertFalse(details['has_lead_time'])
|
||||
self.assertIn('误走现货分支', details['unavailable_reason'])
|
||||
details = bridge.query_model_for_demand(bridge.RequestInput('model', 12))
|
||||
query.assert_called_once()
|
||||
|
||||
def test_stock_change_between_queries_invalidates_forecast(self) -> None:
|
||||
first, second = successful_payload(), successful_payload()
|
||||
first['data']['库存信息']['可用库存(扣除了待产数量)'] = 6
|
||||
second['data']['库存信息']['可用库存(扣除了待产数量)'] = 7
|
||||
with mock.patch.object(bridge, 'run_controlled_query', side_effect=[first, second]):
|
||||
details = bridge.query_model_for_demand(bridge.RequestInput('model', 24))
|
||||
self.assertFalse(details['has_lead_time'])
|
||||
self.assertIn('库存发生变化', details['unavailable_reason'])
|
||||
|
||||
def test_forecast_uses_production_response_date_and_materials(self) -> None:
|
||||
first, second = successful_payload(), successful_payload()
|
||||
for payload in (first, second):
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = 6
|
||||
second['data']['交期汇总']['AM516工作日交期'] = 8
|
||||
second['data']['交期汇总']['AM516最终交货日期'] = '2026-10-22'
|
||||
second['data']['采购信息']['缺料明细'] = [{'物料名称': '测算物料'}]
|
||||
with mock.patch.object(bridge, 'run_controlled_query', side_effect=[first, second]):
|
||||
details = bridge.query_model_for_demand(bridge.RequestInput('model', 24))
|
||||
self.assertTrue(details['has_lead_time'])
|
||||
self.assertEqual(details['prediction_value'], '8个工作日,最终交货日期:2026-10-22')
|
||||
self.assertEqual(details['prediction_value'], '8个工作日,最终交货日期:2026-10-27')
|
||||
self.assertIn('- 缺料物料明细:测算物料', details['evidence_lines'])
|
||||
|
||||
def test_unsupported_baseline_stops_before_candidate_queries_and_record(self) -> None:
|
||||
def test_missing_api_date_still_stops_before_candidates_and_record(self) -> None:
|
||||
payload = successful_payload()
|
||||
payload['data']['库存信息']['可用库存(扣除了待产数量)'] = 20
|
||||
del payload['data']['交期汇总']['AM516最终交货日期']
|
||||
with (
|
||||
mock.patch.object(bridge, 'run_controlled_query', return_value=payload) as query,
|
||||
mock.patch.object(bridge, 'write_record') as record,
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, '净需求4台,排产测算数量5台'):
|
||||
bridge.build_report(bridge.RequestInput('eRob70H50I-FS-18CN[V5]', 24))
|
||||
with self.assertRaisesRegex(RuntimeError, '原型号未生成可用交期结果'):
|
||||
bridge.build_report(bridge.RequestInput('eRob70F50I-FHM-18ET[V4]', 12))
|
||||
query.assert_called_once()
|
||||
record.assert_not_called()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user