diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 7a0531b..9afafb7 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.007,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.009,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.006,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.008,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.009,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.025,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.02,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.083,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.02,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.021,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.016,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.026,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.028,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.029,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.027,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.028,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.013,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.015,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.002,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.004,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.003,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.119,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.336,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.022,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.03,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.005,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.008,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.007,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.006,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.01,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.011,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.008,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.006,"Tests\\Feature\\CartTest::test_delete_item":0.008,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.013,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.004,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.008,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.022,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.006,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.006,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.013,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.006,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.008,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.021,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.035,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.002,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.004,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.005,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.006,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.004,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.015,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.014,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.011}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.005,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.017,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.021,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.014,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.013,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.012,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.017,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.018,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.02,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.021,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.043,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.037,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.035,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.194,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.032,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.041,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.035,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.062,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.062,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.056,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.064,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.05,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.019,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.028,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.033,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.006,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.011,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.008,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.163,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.03,"Tests\\Feature\\ExportTest::test_export_pdf":1.831,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.031,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.032,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.009,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.013,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.011,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.009,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.009,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.012,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.01,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.013,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.018,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.008,"Tests\\Feature\\CartTest::test_update_quantity":0.011,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.013,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.011,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.012,"Tests\\Feature\\CartTest::test_delete_item":0.011,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.012,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.022,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.005,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.011,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.002,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.02,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.015,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.021,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.015,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.015,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.026,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.013,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.015,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.013,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.013,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.016,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.016,"Tests\\Feature\\PurchaseGenerateTest::test_generate_uses_min_actual_price_across_types":0.02,"Tests\\Feature\\ProductPriceTest::test_store_without_level_sees_products_without_price":0.012,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.016,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.038,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.003,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.04,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.004,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.004,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.011,"Tests\\Feature\\MiniStoreTest::test_update_info":0.011,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.011,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.012,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.021,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.035,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.034,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.028,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.04}} \ No newline at end of file diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index af26112..0092b22 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -10,6 +10,7 @@ use App\Models\StoreOrderModel; use App\Models\UserModel; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; @@ -74,7 +75,7 @@ class StoreOrderController extends BaseController } /** - * 待汇总预览:聚合所有待汇总订单明细(按商品分组), + * 已接单预览:聚合所有已接单订单明细(按商品分组), * 供生成采购单前确认(C1 前置) */ #[GetRoute('/summary', 'query')] @@ -87,7 +88,7 @@ class StoreOrderController extends BaseController ->selectRaw('SUM(quantity) as total_quantity') ->selectRaw('COUNT(DISTINCT store_id) as store_count') ->whereHas('order', function ($query) { - $query->where('status', StoreOrderModel::STATUS_PENDING); + $query->where('status', StoreOrderModel::STATUS_SUMMARIZED); }) ->groupBy('product_id') ->orderBy('product_id') @@ -117,13 +118,13 @@ class StoreOrderController extends BaseController } /** - * 状态流转(待汇总→配送中→完成;待汇总可取消;已汇总可转配送中) + * 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 */ #[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])] public function status(int $id, Request $request): JsonResponse { $data = $request->validate([ - 'status' => 'required|integer|in:2,3,9', + 'status' => 'required|integer|in:1,3,4,9', ], [ 'status.required' => '目标状态不能为空', 'status.in' => '目标状态值不正确', @@ -135,20 +136,7 @@ class StoreOrderController extends BaseController } $target = (int) $data['status']; - $allowed = match ($order->status) { - StoreOrderModel::STATUS_PENDING => [ - StoreOrderModel::STATUS_DELIVERING, - StoreOrderModel::STATUS_CANCELLED, - ], - StoreOrderModel::STATUS_SUMMARIZED => [ - StoreOrderModel::STATUS_DELIVERING, - ], - StoreOrderModel::STATUS_DELIVERING => [ - StoreOrderModel::STATUS_COMPLETED, - ], - default => [], - }; - if (! in_array($target, $allowed, true)) { + if (! in_array($target, $this->allowedTransitions($order->status), true)) { throw new RepositoryException( '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作' ); @@ -162,6 +150,75 @@ class StoreOrderController extends BaseController return $this->success(); } + /** + * 批量状态流转:先全量校验流转路径,任一订单不允许则整批中止; + * 全部合法时在事务内流转并逐单通知门店 + */ + #[PutRoute(route: '/batchStatus', authorize: 'update')] + public function batchStatus(Request $request): JsonResponse + { + $data = $request->validate([ + 'ids' => 'required|array|min:1', + 'ids.*' => 'integer|exists:store_order,id', + 'status' => 'required|integer|in:1,3,4,9', + ], [ + 'ids.required' => '请选择要流转的订单', + 'ids.min' => '请选择要流转的订单', + 'ids.*.exists' => '订单不存在', + 'status.required' => '目标状态不能为空', + 'status.in' => '目标状态值不正确', + ]); + + $target = (int) $data['status']; + $orders = StoreOrderModel::query()->whereIn('id', $data['ids'])->get(); + + // 全量预校验:任一订单不满足流转条件,整批中止,不做任何修改 + $blocked = []; + foreach ($orders as $order) { + if (! in_array($target, $this->allowedTransitions($order->status), true)) { + $blocked[] = $order->order_no . '(' . (self::STATUS_NAMES[$order->status] ?? $order->status) . ')'; + } + } + if (! empty($blocked)) { + throw new RepositoryException( + '以下订单不允许流转为「' . (self::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked) + ); + } + + DB::transaction(function () use ($orders, $target) { + foreach ($orders as $order) { + $order->status = $target; + $order->save(); + $this->notifyStore($order); + } + }); + + return $this->success(['success' => $orders->count()]); + } + + /** + * 状态流转合法路径(与迁移状态定义一致): + * 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 + * (已接单→采购中 只能经「生成采购单」完成,不在本流转内) + * + * @return int[] + */ + private function allowedTransitions(int $status): array + { + return match ($status) { + StoreOrderModel::STATUS_PENDING => [ + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_CANCELLED, + ], + StoreOrderModel::STATUS_DELIVERING => [ + StoreOrderModel::STATUS_DISTRIBUTION, + StoreOrderModel::STATUS_COMPLETED, + ], + StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED], + default => [], + }; + } + /** * 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改), * 自动重算附加金额与订单总金额 diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index 02e4c3c..8caa3d1 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -78,20 +78,28 @@ class PurchaseOrderController extends BaseController return $this->success(); } - /** C1 按门店订单汇总生成采购单 */ + /** + * C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids), + * 生成后源订单转为「采购中」并回写 purchase_id + */ #[PostRoute('/generate', 'generate')] public function generate(Request $request): JsonResponse { $data = $request->validate([ 'purchase_date' => 'required|date_format:Y-m-d', + 'order_ids' => 'sometimes|array|min:1', + 'order_ids.*' => 'integer|exists:store_order,id', ], [ 'purchase_date.required' => '请选择采购日期', 'purchase_date.date_format' => '采购日期格式为 Y-m-d', + 'order_ids.min' => '请选择要合并的订单', + 'order_ids.*.exists' => '订单不存在', ]); $purchase = app(PurchaseGenerateService::class)->generate( $data['purchase_date'], (int) $request->user()->id, + array_map('intval', $data['order_ids'] ?? []), ); return $this->success( diff --git a/app/Services/PurchaseAllocateService.php b/app/Services/PurchaseAllocateService.php index d3eef4f..b007bc5 100644 --- a/app/Services/PurchaseAllocateService.php +++ b/app/Services/PurchaseAllocateService.php @@ -7,7 +7,6 @@ use App\Models\PurchaseAllocationModel; use App\Models\PurchaseOrderItemModel; use App\Models\PurchaseOrderModel; use App\Models\StoreOrderItemModel; -use App\Models\StoreOrderModel; use Illuminate\Support\Facades\DB; /** @@ -15,7 +14,7 @@ use Illuminate\Support\Facades\DB; * * 流程(事务内): * 1. 采购单须已录入实际金额(存在 amount>0 的明细),否则拒绝 - * 2. 每个采购明细溯源「采购日当天、已汇总订单」中该商品的订货明细 + * 2. 每个采购明细按 purchase_id 溯源该采购单合并的门店订单明细(生成采购单时回写) * 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2) * 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒) * 4. 重复分摊先删旧记录再重建(幂等) @@ -38,11 +37,10 @@ class PurchaseAllocateService throw new RepositoryException('采购单尚未录入实际金额,无法分摊'); } - // 2. 溯源采购日当天「已汇总」订单的订货明细,按商品分组 + // 2. 按 purchase_id 溯源本采购单合并的门店订单明细,按商品分组 $orderItems = StoreOrderItemModel::query() ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') - ->whereDate('store_order.order_date', $purchase->purchase_date) - ->where('store_order.status', StoreOrderModel::STATUS_SUMMARIZED) + ->where('store_order.purchase_id', $purchase->id) ->select('store_order_item.*') ->get() ->groupBy('product_id'); diff --git a/app/Services/PurchaseGenerateService.php b/app/Services/PurchaseGenerateService.php index 8a41b4b..799cd5f 100644 --- a/app/Services/PurchaseGenerateService.php +++ b/app/Services/PurchaseGenerateService.php @@ -14,12 +14,12 @@ use Illuminate\Support\Facades\DB; * C1 订单汇总生成采购单 * * 流程(事务内): - * 1. 行锁查询当日全部「待汇总」订单(无则报错;状态条件天然排除已汇总订单,幂等) + * 1. 行锁「已接单」订单(可传 orderIds 只合并指定订单;状态条件天然排除已归集订单,幂等) * 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商) * 3. 估算单价 = 该商品最低实际等级价(按计价类型换算后取 min,PHP 侧兼容 MySQL/SQLite),amount = quantity × 估算单价 * 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount) * 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号 - * 6. 源订单批量回写 status = 已汇总 + * 6. 源订单批量回写 status = 采购中、purchase_id = 采购单ID(分摊按 purchase_id 溯源) */ class PurchaseGenerateService { @@ -28,28 +28,36 @@ class PurchaseGenerateService } /** - * @param string $date 订货/采购日期(Y-m-d) + * @param string $date 采购日期(Y-m-d) * @param int $operatorId 制单人(后台系统用户ID) + * @param int[] $orderIds 指定合并的门店订单ID(空 = 全部已接单订单) */ - public function generate(string $date, int $operatorId): PurchaseOrderModel + public function generate(string $date, int $operatorId, array $orderIds = []): PurchaseOrderModel { - return DB::transaction(function () use ($date, $operatorId) { - // 1. 行锁当日待汇总订单(并发防护) - $orders = StoreOrderModel::query() - ->whereDate('order_date', $date) - ->where('status', StoreOrderModel::STATUS_PENDING) - ->lockForUpdate() - ->get(); + return DB::transaction(function () use ($date, $operatorId, $orderIds) { + // 1. 行锁已接单订单(并发防护);指定订单时要求全部处于已接单,否则整批拒绝 + $query = StoreOrderModel::query()->lockForUpdate(); + if ($orderIds !== []) { + $orders = $query->whereIn('id', $orderIds)->get(); + $invalid = $orders->where('status', '<>', StoreOrderModel::STATUS_SUMMARIZED); + if ($orders->isEmpty() || $invalid->isNotEmpty()) { + throw new RepositoryException( + '所选订单包含非「已接单」状态,无法生成采购单:' . $invalid->pluck('order_no')->implode('、') + ); + } + } else { + $orders = $query->where('status', StoreOrderModel::STATUS_SUMMARIZED)->get(); + } if ($orders->isEmpty()) { - throw new RepositoryException('当日无待汇总订单'); + throw new RepositoryException('无已接单订单,无法生成采购单'); } // 2. 展开明细按商品聚合 $aggregated = []; - $orderIds = []; + $sourceOrderIds = []; foreach ($orders as $order) { - $orderIds[] = $order->id; + $sourceOrderIds[] = $order->id; foreach ($order->items as $item) { $productId = (int) $item->product_id; if (! isset($aggregated[$productId])) { @@ -69,7 +77,7 @@ class PurchaseGenerateService } if ($aggregated === []) { - throw new RepositoryException('当日待汇总订单均无明细,无法生成采购单'); + throw new RepositoryException('已接单订单均无明细,无法生成采购单'); } $products = ProductModel::withTrashed() @@ -164,9 +172,12 @@ class PurchaseGenerateService ]); } - // 6. 源订单回写「已汇总」 - StoreOrderModel::whereIn('id', $orderIds) - ->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + // 6. 源订单回写「采购中」并关联采购单(分摊按 purchase_id 溯源) + StoreOrderModel::whereIn('id', $sourceOrderIds) + ->update([ + 'status' => StoreOrderModel::STATUS_DELIVERING, + 'purchase_id' => $purchase->id, + ]); return $purchase; }); diff --git a/tests/Feature/AllocationTest.php b/tests/Feature/AllocationTest.php index 066ec72..6b0ffc3 100644 --- a/tests/Feature/AllocationTest.php +++ b/tests/Feature/AllocationTest.php @@ -9,6 +9,7 @@ use App\Models\PurchaseAllocationModel; use App\Models\PurchaseOrderItemModel; use App\Models\PurchaseOrderModel; use App\Models\StoreModel; +use App\Models\StoreOrderModel; use App\Models\UserModel; /** @@ -35,6 +36,9 @@ class AllocationTest extends ProcurementTestCase ->assertJsonPath('success', true); } + // 接单后生成采购单(生成来源为已接单订单) + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); @@ -131,6 +135,8 @@ class AllocationTest extends ProcurementTestCase $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]); + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]); $purchase = PurchaseOrderModel::first(); diff --git a/tests/Feature/ExportTest.php b/tests/Feature/ExportTest.php index 5826c74..b4d8d38 100644 --- a/tests/Feature/ExportTest.php +++ b/tests/Feature/ExportTest.php @@ -9,6 +9,7 @@ use App\Models\ProductModel; use App\Models\ProductPriceModel; use App\Models\PurchaseOrderModel; use App\Models\StoreModel; +use App\Models\StoreOrderModel; use App\Models\UserModel; use Maatwebsite\Excel\Facades\Excel; @@ -39,6 +40,9 @@ class ExportTest extends ProcurementTestCase ['product_id' => $meat->id, 'quantity' => 1], ]])->assertJsonPath('success', true); + // 接单后生成采购单 + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); diff --git a/tests/Feature/PurchaseGenerateTest.php b/tests/Feature/PurchaseGenerateTest.php index 4dd6134..2cdb066 100644 --- a/tests/Feature/PurchaseGenerateTest.php +++ b/tests/Feature/PurchaseGenerateTest.php @@ -16,10 +16,10 @@ use App\Models\UserModel; class PurchaseGenerateTest extends ProcurementTestCase { /** - * 造当日待汇总订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品) + * 造当日已接单订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品),下单后统一接单 * 商品设两个等级价 5.00 / 4.00,估算单价应取最低 4.00 */ - private function seedPendingOrders(): ProductModel + private function seedAcceptedOrders(): ProductModel { $level = CustomerLevelModel::factory()->create(); $levelLow = CustomerLevelModel::factory()->create(); @@ -35,13 +35,16 @@ class PurchaseGenerateTest extends ProcurementTestCase ->assertJsonPath('success', true); } + // 后台接单:待接单 → 已接单(生成采购单的来源状态) + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + return $product; } /** 多门店多订单按商品聚合,估算单价取最低等级价 */ public function test_generate_aggregates_orders_by_product(): void { - $this->seedPendingOrders(); + $this->seedAcceptedOrders(); $admin = $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) @@ -63,33 +66,71 @@ class PurchaseGenerateTest extends ProcurementTestCase $this->assertSame('40.00', (string) $purchase->estimate_amount); } - /** 源订单状态回写为已汇总 */ + /** 源订单状态回写为采购中,并关联 purchase_id */ public function test_generate_writes_back_order_status(): void { - $this->seedPendingOrders(); + $this->seedAcceptedOrders(); $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); - $this->assertSame(0, StoreOrderModel::where('status', StoreOrderModel::STATUS_PENDING)->count()); - $this->assertSame(3, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count()); + $purchase = PurchaseOrderModel::first(); + $this->assertSame(0, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count()); + $this->assertSame(3, StoreOrderModel::where('status', StoreOrderModel::STATUS_DELIVERING)->count()); + $this->assertSame(3, StoreOrderModel::where('purchase_id', $purchase->id)->count()); } - /** 当日无待汇总订单 → 报错 */ - public function test_generate_without_pending_orders_fails(): void + /** 仅有待接单订单时不可生成(待接单不再被归集)→ 报错 */ + public function test_generate_without_accepted_orders_fails(): void { - $this->actingAsSysUser(); + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]); + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) + ->assertJsonPath('success', true); + + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', false); $this->assertSame(0, PurchaseOrderModel::count()); + $this->assertSame(1, StoreOrderModel::where('status', StoreOrderModel::STATUS_PENDING)->count()); } - /** 幂等:已汇总订单不会被重复归集 */ + /** 指定订单合并:仅归集 order_ids 内的已接单订单,混入非已接单则整批拒绝 */ + public function test_generate_with_selected_order_ids(): void + { + $this->seedAcceptedOrders(); + $this->actingAsSysUser(); + + $selected = StoreOrderModel::query()->orderBy('id')->limit(2)->pluck('id')->all(); + + $this->postJson('/purchase/order/generate', [ + 'purchase_date' => now()->toDateString(), + 'order_ids' => $selected, + ])->assertJsonPath('success', true); + + $purchase = PurchaseOrderModel::first(); + $this->assertSame(2, StoreOrderModel::where('purchase_id', $purchase->id)->count()); + $this->assertSame(1, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count(), '未选中的订单保持已接单'); + + // 混入非已接单订单 → 整批拒绝 + $remaining = StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->value('id'); + $done = StoreOrderModel::where('status', StoreOrderModel::STATUS_DELIVERING)->value('id'); + $this->postJson('/purchase/order/generate', [ + 'purchase_date' => now()->toDateString(), + 'order_ids' => [$remaining, $done], + ])->assertJsonPath('success', false); + $this->assertSame(1, PurchaseOrderModel::count(), '整批拒绝,不产生新采购单'); + } + + /** 幂等:已归集订单不会被重复归集 */ public function test_generate_is_idempotent(): void { - $this->seedPendingOrders(); + $this->seedAcceptedOrders(); $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) @@ -128,6 +169,9 @@ class PurchaseGenerateTest extends ProcurementTestCase $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]]) ->assertJsonPath('success', true); + // 接单后生成 + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); diff --git a/tests/Feature/ReconciliationTest.php b/tests/Feature/ReconciliationTest.php index 49eb4a9..ba0caed 100644 --- a/tests/Feature/ReconciliationTest.php +++ b/tests/Feature/ReconciliationTest.php @@ -10,6 +10,7 @@ use App\Models\ReconciliationItemModel; use App\Models\ReconciliationModel; use App\Models\SettlementModel; use App\Models\StoreModel; +use App\Models\StoreOrderModel; use App\Models\SupplierModel; use App\Models\UserModel; @@ -42,6 +43,9 @@ class ReconciliationTest extends ProcurementTestCase ->assertJsonPath('success', true); } + // 接单后生成采购单 + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); diff --git a/web/api/order/store.ts b/web/api/order/store.ts index d59157a..8349878 100644 --- a/web/api/order/store.ts +++ b/web/api/order/store.ts @@ -10,7 +10,7 @@ export async function getStoreOrder(id: number) { }); } -/** 订单状态流转(2配送中 3已完成 9取消) */ +/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成) */ export async function updateOrderStatus(id: number, status: number) { return createAxios({ url: `/order/store/${id}/status`, @@ -19,6 +19,15 @@ export async function updateOrderStatus(id: number, status: number) { }); } +/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */ +export async function batchUpdateOrderStatus(ids: number[], status: number) { + return createAxios<{ success: number }>({ + url: '/order/store/batchStatus', + method: 'put', + data: { ids, status }, + }); +} + /** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */ export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) { return createAxios({ @@ -28,7 +37,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number; }); } -/** 待汇总预览(按商品聚合) */ +/** 已接单预览(按商品聚合,生成采购单前确认) */ export async function getOrderSummary() { return createAxios({ url: '/order/store/summary', diff --git a/web/api/purchase/order.ts b/web/api/purchase/order.ts index 5a934a1..d50977a 100644 --- a/web/api/purchase/order.ts +++ b/web/api/purchase/order.ts @@ -16,12 +16,12 @@ export interface PurchaseItemUpdateParams { remark?: string; } -/** C1 按门店订单汇总生成采购单 */ -export async function generatePurchase(purchase_date: string) { +/** C1 合并「已接单」门店订单生成采购单(order_ids 为空 = 全部已接单;生成后源订单转采购中) */ +export async function generatePurchase(purchase_date: string, order_ids?: number[]) { return createAxios<{ id: number; purchase_no: string }>({ url: '/purchase/order/generate', method: 'post', - data: { purchase_date }, + data: { purchase_date, order_ids }, }); } diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index abe5bbe..151b740 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Button, + DatePicker, Descriptions, Drawer, Form, @@ -8,12 +9,14 @@ import { message, Modal, Popconfirm, + Radio, Space, Table, Tag, Typography, Image, } from 'antd'; +import dayjs from 'dayjs'; import type { TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { @@ -24,7 +27,8 @@ import type { import type IStoreOrder from '@/domain/iStoreOrder.ts'; import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; -import { getStoreOrder, updateOrderContainer, updateOrderStatus } from '@/api/order/store.ts'; +import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts'; +import { generatePurchase } from '@/api/purchase/order.ts'; import { getStoreOptions } from '@/api/customer/store.ts'; import type IStore from '@/domain/iStore.ts'; import AuthButton from '@/components/AuthButton'; @@ -36,17 +40,29 @@ const { Title, Text } = Typography; const CONTAINER_EDITABLE_STATUS = [1, 2, 3]; /** - * 状态流转合法路径:待汇总→配送中/取消;已汇总→配送中;配送中→已完成 + * 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 + * (已接单→采购中 通过「生成采购单」完成,不在流转按钮内) */ const NEXT_STATUS: Record = { 0: [ - { status: 2, label: '开始配送' }, + { status: 1, label: '接单' }, { status: 9, label: '取消订单', danger: true }, ], - 1: [{ status: 2, label: '开始配送' }], - 2: [{ status: 3, label: '完成订单' }], + 2: [ + { status: 3, label: '开始配送' }, + { status: 4, label: '完成订单' }, + ], + 3: [{ status: 4, label: '完成订单' }], }; +/** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */ +const BATCH_TARGET_OPTIONS = [ + { value: 1, label: '接单(待接单 → 已接单)' }, + { value: 3, label: '开始配送(采购中 → 配送中)' }, + { value: 4, label: '完成订单(采购中/配送中 → 已完成)' }, + { value: 9, label: '取消订单(待接单 → 已取消)' }, +]; + /** * 门店订单管理(只读 + 状态流转,订单由小程序端创建) */ @@ -63,6 +79,17 @@ const StoreOrderPage: React.FC = () => { const [containerSaving, setContainerSaving] = useState(false); const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>(); + /** 勾选行(批量流转/生成采购单用) */ + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [batchOpen, setBatchOpen] = useState(false); + const [batchTarget, setBatchTarget] = useState(null); + const [batchLoading, setBatchLoading] = useState(false); + + /** 生成采购单(合并已接单订单,生成后源订单转采购中) */ + const [generateOpen, setGenerateOpen] = useState(false); + const [generateLoading, setGenerateLoading] = useState(false); + const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>(); + useEffect(() => { getStoreOptions().then((res) => setStores(res.data.data ?? [])); }, []); @@ -85,6 +112,40 @@ const StoreOrderPage: React.FC = () => { await tableRef.current?.reload(); }; + /** 批量状态流转:后端全量校验,任一订单不允许流转则整批中止 */ + const handleBatchStatus = async () => { + if (batchTarget === null) { + message.warning('请选择要流转的目标状态'); + return; + } + setBatchLoading(true); + try { + const res = await batchUpdateOrderStatus(selectedRowKeys as number[], batchTarget); + message.success(`已批量流转 ${res.data.data?.success ?? 0} 单`); + setBatchOpen(false); + setBatchTarget(null); + setSelectedRowKeys([]); + await tableRef.current?.reload(); + } finally { + setBatchLoading(false); + } + }; + + /** 生成采购单:勾选时合并所选订单(须均为已接单),未勾选时合并全部已接单订单 */ + const handleGeneratePurchase = async (values: { purchase_date: dayjs.Dayjs }) => { + setGenerateLoading(true); + try { + const orderIds = selectedRowKeys.length > 0 ? selectedRowKeys.map(Number) : undefined; + const res = await generatePurchase(values.purchase_date.format('YYYY-MM-DD'), orderIds); + message.success(`采购单 ${res.data.data?.purchase_no} 已生成,源订单已转为采购中`); + setGenerateOpen(false); + setSelectedRowKeys([]); + await tableRef.current?.reload(); + } finally { + setGenerateLoading(false); + } + }; + const openContainer = (record: IStoreOrder) => { setContainerOrder(record); containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num }); @@ -294,6 +355,20 @@ const StoreOrderPage: React.FC = () => { + { + (NEXT_STATUS[record.status ?? -1] ?? []).map((action) => ( + + handleStatusChange(record.id!, action.status)} + > + + + + )) + } { CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? ( @@ -316,6 +391,28 @@ const StoreOrderPage: React.FC = () => { tableRef, operateShow: false, formProps: false, + rowSelection: { + selectedRowKeys, + onChange: (keys) => setSelectedRowKeys(keys), + }, + actionBarRender: (dom) => [ + dom.search, + + + , + + + , + dom.keywordSearch, + ], }; return ( @@ -323,7 +420,7 @@ const StoreOrderPage: React.FC = () => {
门店订单 - 小程序下单汇总;待汇总订单将进入采购单生成,订单创建与取消在小程序端完成。 + 小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购中可批量流转为配送中或已完成。
{...tableProps} /> @@ -397,6 +494,62 @@ const StoreOrderPage: React.FC = () => { ) : null} + {/* 生成采购单:合并已接单订单 */} + setGenerateOpen(false)} + onOk={() => generateForm.submit()} + confirmLoading={generateLoading} + okText="确认生成" + destroyOnHidden + > +
+ {selectedRowKeys.length > 0 + ? `将合并选中的 ${selectedRowKeys.length} 笔订单生成一张采购单(须均为已接单状态,否则整批中止)。` + : '未勾选订单时,将合并全部「已接单」订单生成一张采购单。'} + 生成后源订单自动转为「采购中」。 +
+
+ + + +
+
+ + {/* 批量状态流转 */} + setBatchOpen(false)} + onOk={handleBatchStatus} + confirmLoading={batchLoading} + okText="确认流转" + destroyOnHidden + > +
+ 已勾选 {selectedRowKeys.length} 笔订单;任一订单不满足流转条件时,本次批量操作将整批中止,不做任何修改。 +
+ setBatchTarget(e.target.value as number)} + > + + {BATCH_TARGET_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + + +
+ {/* 修改周转框/托盘数量 */} { destroyOnHidden >
- 将汇总所选日期全部「待汇总」订单,按商品聚合生成采购单(估算单价取最低等级价)。 + 将合并全部「已接单」门店订单,按商品聚合生成采购单(估算单价取最低等级价),生成后源订单转为「采购中」。
{ title={detail ? `采购单 ${detail.purchase_no}` : '采购单详情'} open={detailOpen} onClose={() => setDetailOpen(false)} - width={1080} + size={1080} loading={detailLoading} > {detail ? (