diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 1231bd0..e900824 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,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7},"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.01,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.008,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.011,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.017,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.014,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.019,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.076,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.028,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.021,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.02,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.019,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.021,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.016,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"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.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.063,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.02,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.016,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.003,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.046,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.01,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.008,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.01,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.011,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.01,"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.01,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.015,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.009,"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.01,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.007,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.009,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.026,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.01,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.008,"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.014,"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.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.009,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.009,"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.005,"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.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.005,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.016,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.018,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.021,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.008,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.013,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.024,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.016,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.031,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.066,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.012,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.064,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.017,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.017,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.018,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.017,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.024,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.02,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.013,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.019,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.02,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.006}} \ 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,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":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.011,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"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.018,"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.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"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.019,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.076,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.028,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.021,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.003,"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.005,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.076,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.021,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.016,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.003,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.046,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.01,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.01,"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.01,"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.014,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.009,"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.007,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.009,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.007,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.007,"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.014,"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.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.01,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.003,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.011,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.003,"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.005,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.011,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.021,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.01,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.009,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.012,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.011,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.013,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.01,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.017,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.019,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.042,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.012,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.02,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.022,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.018,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.011,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.017,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.02,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.019,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.005,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_container_summary":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_container_syncs_order_amounts":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejects_incomplete_or_foreign_orders":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_container_validates_negative_and_duplicate_orders":0.018}} \ No newline at end of file diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index f1a880f..c411433 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -207,60 +207,6 @@ class StoreOrderController extends BaseController return $this->success(); } - /** - * 修改周转框/周转托盘数量 - */ - #[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])] - public function container(int $id, Request $request): JsonResponse - { - $data = $request->validate([ - 'box_num' => 'required|integer|min:0', - 'tray_num' => 'required|integer|min:0', - ], [ - 'box_num.required' => '周转框数量不能为空', - 'box_num.integer' => '周转框数量必须为整数', - 'box_num.min' => '周转框数量不能小于 0', - 'tray_num.required' => '周转托盘数量不能为空', - 'tray_num.integer' => '周转托盘数量必须为整数', - 'tray_num.min' => '周转托盘数量不能小于 0', - ]); - - $order = StoreOrderModel::find($id); - if (empty($order)) { - throw new RepositoryException('订单不存在'); - } - - $editable = [ - StoreOrderModel::STATUS_SUMMARIZED, - StoreOrderModel::STATUS_DELIVERING, - StoreOrderModel::STATUS_DISTRIBUTION, - ]; - if (! in_array($order->status, $editable, true)) { - throw new RepositoryException( - '订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量' - ); - } - - $boxPrice = (float) site_config('services.box_amount', 0); - $trayPrice = (float) site_config('services.tray_amount', 0); - $addedAmount = round($data['box_num'] * $boxPrice + $data['tray_num'] * $trayPrice, 2); - - // 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立 - $productAmount = (float) $order->product_amount; - if ($productAmount <= 0 && (float) $order->total_amount > 0) { - $productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2); - } - - $order->box_num = (int) $data['box_num']; - $order->tray_num = (int) $data['tray_num']; - $order->product_amount = $productAmount; - $order->added_amount = $addedAmount; - $order->total_amount = round($productAmount + $addedAmount, 2); - $order->save(); - - return $this->success(); - } - /** * 状态流转合法路径 * diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index 3d95a66..7de4277 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Purchase; use App\Exceptions\RepositoryException; use App\Exports\PurchaseOrderExport; use App\Http\Requests\Purchase\PurchaseCellUpdateRequest; +use App\Http\Requests\Purchase\PurchaseContainerUpdateRequest; use App\Http\Requests\Purchase\PurchaseRowUpdateRequest; use App\Models\ProductModel; use App\Models\PurchaseOrderModel; @@ -13,6 +14,7 @@ use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; use App\Services\ItemImageResolver; use App\Services\PurchaseGenerateService; +use App\Services\StoreOrderContainerService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -133,6 +135,45 @@ class PurchaseOrderController extends BaseController [$a['category_sort'], $a['product_sort'], $a['product_id']] <=> [$b['category_sort'], $b['product_sort'], $b['product_id']]); + // 周转框/托盘合并记录:按门店聚合全部订单(附底层订单明细,供采购单完成前修改) + $storeOrders = StoreOrderModel::query() + ->where('purchase_id', $purchase->id) + ->whereNull('deleted_at') + ->orderBy('id') + ->get(['id', 'order_no', 'store_id', 'box_num', 'tray_num']); + + $boxPrice = (float) site_config('services.box_amount', 0); + $trayPrice = (float) site_config('services.tray_amount', 0); + $storeSort = $stores->pluck('id')->flip(); + $containers = []; + foreach ($storeOrders->groupBy('store_id') as $storeId => $orders) { + $boxNum = (int) $orders->sum('box_num'); + $trayNum = (int) $orders->sum('tray_num'); + $containers[] = [ + 'store_id' => (int) $storeId, + 'store_name' => $stores->firstWhere('id', (int) $storeId)['name'] ?? '门店#' . $storeId, + 'box_num' => $boxNum, + 'tray_num' => $trayNum, + 'box_price' => number_format($boxPrice, 2), + 'tray_price' => number_format($trayPrice, 2), + 'added_amount' => number_format($boxNum * $boxPrice + $trayNum * $trayPrice, 2), + 'order_count' => $orders->count(), + 'orders' => $orders->map(static fn (StoreOrderModel $order) => [ + 'order_id' => $order->id, + 'order_no' => $order->order_no, + 'box_num' => (int) $order->box_num, + 'tray_num' => (int) $order->tray_num, + ])->values()->toArray(), + 'store_sort' => (int) ($storeSort[$storeId] ?? 9999), + ]; + } + usort($containers, static fn (array $a, array $b): int => + [$a['store_sort'], $a['store_id']] <=> [$b['store_sort'], $b['store_id']]); + $containers = array_map(static function (array $row): array { + unset($row['store_sort']); + return $row; + }, $containers); + return $this->success([ 'purchase' => $purchase->toArray(), 'stores' => $stores->toArray(), @@ -140,6 +181,7 @@ class PurchaseOrderController extends BaseController unset($row['category_sort'], $row['product_sort']); return $row; }, $rows), + 'containers' => $containers, ]); } @@ -395,6 +437,74 @@ class PurchaseOrderController extends BaseController ]); } + /** + * 周转框/托盘合并记录修改:按门店覆盖全部订单逐笔更新(仅采购单进行中可改) + * @throws Throwable + */ + #[PutRoute(route: '/{id}/container/{storeId}', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+'])] + public function updateContainer(int $id, int $storeId, PurchaseContainerUpdateRequest $request): JsonResponse + { + $purchase = PurchaseOrderModel::find($id); + if (empty($purchase)) { + throw new RepositoryException('采购单不存在'); + } + if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { + throw new RepositoryException('采购单已完成,不允许修改周转框/托盘'); + } + + $submitted = $request->validated()['orders']; + + return DB::transaction(function () use ($purchase, $storeId, $submitted) { + $orders = StoreOrderModel::query() + ->where('purchase_id', $purchase->id) + ->where('store_id', $storeId) + ->whereNull('deleted_at') + ->lockForUpdate() + ->get(); + + if ($orders->isEmpty()) { + throw new RepositoryException('该采购单下无此门店的订单'); + } + + // 合并记录修改必须覆盖该门店全部订单,避免只改部分造成汇总偏差 + $actualIds = $orders->pluck('id')->map(static fn ($orderId) => (int) $orderId)->all(); + $submittedIds = array_map(static fn (array $row) => (int) $row['order_id'], $submitted); + $missing = array_diff($actualIds, $submittedIds); + if ($missing !== []) { + throw new RepositoryException('提交不完整,缺少订单:' . implode('、', $missing)); + } + if (array_diff($submittedIds, $actualIds) !== []) { + throw new RepositoryException('包含不属于该采购单该门店的订单'); + } + + foreach ($orders as $order) { + if (! in_array($order->status, [ + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_DELIVERING, + StoreOrderModel::STATUS_DISTRIBUTION, + ], true)) { + throw new RepositoryException( + '订单 ' . $order->order_no . ' 当前状态为「' + . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) + . '」,不允许修改周转框/托盘数量' + ); + } + } + + $ordersById = $orders->keyBy('id'); + $containerService = app(StoreOrderContainerService::class); + foreach ($submitted as $row) { + $containerService->update( + $ordersById->get($row['order_id']), + (int) $row['box_num'], + (int) $row['tray_num'], + ); + } + + return $this->success(); + }); + } + /** * 商品行修改:品名/供应商/包规/单位/成本 * @throws Throwable diff --git a/app/Http/Requests/Purchase/PurchaseContainerUpdateRequest.php b/app/Http/Requests/Purchase/PurchaseContainerUpdateRequest.php new file mode 100644 index 0000000..63acda2 --- /dev/null +++ b/app/Http/Requests/Purchase/PurchaseContainerUpdateRequest.php @@ -0,0 +1,41 @@ + 'required|array|min:1', + 'orders.*.order_id' => 'required|integer|distinct', + 'orders.*.box_num' => 'required|integer|min:0', + 'orders.*.tray_num' => 'required|integer|min:0', + ]; + } + + public function messages(): array + { + return [ + 'orders.required' => '请提交门店订单周转框/托盘数量', + 'orders.array' => '订单数据格式错误', + 'orders.min' => '请提交门店订单周转框/托盘数量', + 'orders.*.order_id.required' => '订单ID不能为空', + 'orders.*.order_id.integer' => '订单ID格式错误', + 'orders.*.order_id.distinct' => '存在重复订单', + 'orders.*.box_num.required' => '周转框数量不能为空', + 'orders.*.box_num.integer' => '周转框数量必须为整数', + 'orders.*.box_num.min' => '周转框数量不能小于 0', + 'orders.*.tray_num.required' => '周转托盘数量不能为空', + 'orders.*.tray_num.integer' => '周转托盘数量必须为整数', + 'orders.*.tray_num.min' => '周转托盘数量不能小于 0', + ]; + } +} diff --git a/app/Services/PurchaseGenerateService.php b/app/Services/PurchaseGenerateService.php index 611fa5c..b319bd7 100644 --- a/app/Services/PurchaseGenerateService.php +++ b/app/Services/PurchaseGenerateService.php @@ -58,9 +58,9 @@ readonly class PurchaseGenerateService static fn (string $carry, $item): string => bcadd($carry, (string) $item->quantity, 2), '0' ); - // 预估金额 = Σ订货金额(明细金额 price×quantity),与采购单修改重算口径一致 + // 预估成本 $estimateAmount = $items->reduce( - static fn (string $carry, $item): string => bcadd($carry, (string) $item->amount, 2), + static fn (string $carry, $item): string => bcadd($carry, bcmul($item->quantity, $item->cost_price, 2), 2), '0' ); diff --git a/app/Services/StoreOrderContainerService.php b/app/Services/StoreOrderContainerService.php new file mode 100644 index 0000000..26bc045 --- /dev/null +++ b/app/Services/StoreOrderContainerService.php @@ -0,0 +1,34 @@ +product_amount; + if ($productAmount <= 0 && (float) $order->total_amount > 0) { + $productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2); + } + + $order->box_num = $boxNum; + $order->tray_num = $trayNum; + $order->product_amount = $productAmount; + $order->added_amount = $addedAmount; + $order->total_amount = round($productAmount + $addedAmount, 2); + $order->save(); + } +} diff --git a/tests/Feature/PurchaseEditTest.php b/tests/Feature/PurchaseEditTest.php index 7857186..341499c 100644 --- a/tests/Feature/PurchaseEditTest.php +++ b/tests/Feature/PurchaseEditTest.php @@ -11,6 +11,9 @@ use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; use App\Models\SupplierModel; use App\Models\UserModel; +use Illuminate\Support\Facades\Cache; +use Modules\SystemTool\Models\SysSiteConfigGroupModel; +use Modules\SystemTool\Models\SysSiteConfigItemsModel; /** * C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本, @@ -52,6 +55,23 @@ class PurchaseEditTest extends ProcurementTestCase return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]]; } + /** 播种站点配置:周转框单价 2.00、周转托盘单价 10.00 */ + private function seedContainerConfig(): void + { + $group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']); + foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) { + SysSiteConfigItemsModel::create([ + 'group_id' => $group->id, + 'key' => $key, + 'title' => $title, + 'type' => 'InputNumber', + 'values' => $value, + 'sort' => 0, + ]); + } + Cache::forget('site_config'); + } + /** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */ public function test_detail_returns_store_matrix(): void { @@ -440,4 +460,148 @@ class PurchaseEditTest extends ProcurementTestCase ->assertOk() ->assertJsonPath('data.items.0.editable', false); } + + /** 详情返回周转框/托盘合并记录:按门店聚合全部订单(含底层订单明细与单价) */ + public function test_detail_returns_container_summary(): void + { + $this->seedContainerConfig(); + [$purchase, , $stores] = $this->buildPurchase(); + + StoreOrderModel::where('store_id', $stores[0]->id)->update(['box_num' => 2, 'tray_num' => 1]); + StoreOrderModel::where('store_id', $stores[1]->id)->update(['box_num' => 3, 'tray_num' => 0]); + + $this->actingAsSysUser(); + $containers = $this->getJson("/purchase/order/{$purchase->id}") + ->assertOk() + ->assertJsonPath('success', true) + ->json('data.containers'); + $this->assertCount(2, $containers); + $this->assertSame($stores[0]->id, $containers[0]['store_id'], '与门店列同序'); + + $row = $containers[0]; + $this->assertSame($stores[0]->name, $row['store_name']); + $this->assertSame(2, $row['box_num']); + $this->assertSame(1, $row['tray_num']); + $this->assertSame('2.00', $row['box_price']); + $this->assertSame('10.00', $row['tray_price']); + $this->assertSame('14.00', $row['added_amount'], '2×2.00 + 1×10.00'); + $this->assertSame(1, $row['order_count']); + $this->assertCount(1, $row['orders']); + + $order = $row['orders'][0]; + $this->assertStringStartsWith('SO', $order['order_no']); + $this->assertSame(2, $order['box_num']); + $this->assertSame(1, $order['tray_num']); + + $this->assertSame(3, $containers[1]['box_num']); + $this->assertSame('6.00', $containers[1]['added_amount']); + } + + /** 合并记录修改:逐笔更新订单数量并重算附加金额与订单总金额,其他门店不受影响 */ + public function test_update_container_syncs_order_amounts(): void + { + $this->seedContainerConfig(); + [$purchase, , $stores] = $this->buildPurchase(); + $order = StoreOrderModel::where('store_id', $stores[0]->id)->first(); + $this->assertSame('20.00', (string) $order->product_amount, '下单时应写入商品金额'); + + $this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [ + 'orders' => [['order_id' => $order->id, 'box_num' => 3, 'tray_num' => 1]], + ])->assertOk()->assertJsonPath('success', true); + + $order->refresh(); + $this->assertSame(3, $order->box_num); + $this->assertSame(1, $order->tray_num); + $this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00'); + $this->assertSame('20.00', (string) $order->product_amount); + $this->assertSame('36.00', (string) $order->total_amount, '20.00 + 16.00'); + + $other = StoreOrderModel::where('store_id', $stores[1]->id)->first(); + $this->assertSame(0, $other->box_num, '其他门店订单不受影响'); + $this->assertSame('30.00', (string) $other->total_amount); + } + + /** 采购单已完成:合并记录修改拒绝,数量与金额不变 */ + public function test_update_container_rejected_when_purchase_completed(): void + { + $this->seedContainerConfig(); + [$purchase, , $stores] = $this->buildPurchase(); + $order = StoreOrderModel::where('store_id', $stores[0]->id)->first(); + $purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]); + + $this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [ + 'orders' => [['order_id' => $order->id, 'box_num' => 5, 'tray_num' => 5]], + ])->assertJsonPath('success', false) + ->assertJsonPath('msg', '采购单已完成,不允许修改周转框/托盘'); + + $this->assertSame(0, $order->fresh()->box_num, '被拒绝后数量不变'); + } + + /** 提交必须覆盖该门店全部订单;跨门店/跨采购单订单拒绝 */ + public function test_update_container_rejects_incomplete_or_foreign_orders(): void + { + $this->seedContainerConfig(); + $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' => 10.00]); + + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + foreach ([1, 2] as $qty) { + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]]) + ->assertJsonPath('success', true); + } + StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]); + + $this->actingAsSysUser(); + $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) + ->assertJsonPath('success', true); + $purchase = PurchaseOrderModel::first(); + $orders = StoreOrderModel::where('purchase_id', $purchase->id)->orderBy('id')->get(); + $this->assertCount(2, $orders, '同一门店两笔订单'); + + // 只提交一笔 → 缺少另一笔,整批拒绝 + $this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [ + 'orders' => [['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0]], + ])->assertJsonPath('success', false) + ->assertJsonPath('msg', '提交不完整,缺少订单:' . $orders[1]->id); + + // 混入其他门店订单(同一采购单另一门店)→ 拒绝 + $otherStore = StoreModel::factory()->create(['level_id' => $level->id]); + $this->actingAsMiniUser(UserModel::factory()->forStore($otherStore->id)->create()); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) + ->assertJsonPath('success', true); + $foreign = StoreOrderModel::where('store_id', $otherStore->id)->first(); + $this->actingAsSysUser(); + $this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [ + 'orders' => [ + ['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0], + ['order_id' => $orders[1]->id, 'box_num' => 1, 'tray_num' => 0], + ['order_id' => $foreign->id, 'box_num' => 1, 'tray_num' => 0], + ], + ])->assertJsonPath('success', false) + ->assertJsonPath('msg', '包含不属于该采购单该门店的订单'); + + $this->assertSame(0, $orders[0]->fresh()->box_num, '被拒绝后数量不变'); + } + + /** 数量为负/重复订单时校验失败 */ + public function test_update_container_validates_negative_and_duplicate_orders(): void + { + $this->seedContainerConfig(); + [$purchase, , $stores] = $this->buildPurchase(); + $order = StoreOrderModel::where('store_id', $stores[0]->id)->first(); + + $this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [ + 'orders' => [['order_id' => $order->id, 'box_num' => -1, 'tray_num' => 0]], + ])->assertJsonPath('success', false) + ->assertJsonPath('msg', '周转框数量不能小于 0'); + + $this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [ + 'orders' => [ + ['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0], + ['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0], + ], + ])->assertJsonPath('success', false); + } } diff --git a/tests/Feature/PurchaseGenerateTest.php b/tests/Feature/PurchaseGenerateTest.php index 960673c..1e468c1 100644 --- a/tests/Feature/PurchaseGenerateTest.php +++ b/tests/Feature/PurchaseGenerateTest.php @@ -19,7 +19,7 @@ class PurchaseGenerateTest extends ProcurementTestCase { /** * 造当日已接单订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品),下单后统一接单 - * 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价) + * 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价),每包成本 6.00 */ private function seedAcceptedOrders(): ProductModel { @@ -27,7 +27,7 @@ class PurchaseGenerateTest extends ProcurementTestCase $levelLow = CustomerLevelModel::factory()->create(); $storeA = StoreModel::factory()->create(['level_id' => $level->id]); $storeB = StoreModel::factory()->create(['level_id' => $level->id]); - $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '6.00']); ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]); ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $levelLow->id, 'price' => 4.00]); @@ -43,7 +43,7 @@ class PurchaseGenerateTest extends ProcurementTestCase return $product; } - /** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ订货金额 */ + /** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ数量×成本价(预估成本) */ public function test_generate_aggregates_order_items_into_header(): void { $this->seedAcceptedOrders(); @@ -61,7 +61,7 @@ class PurchaseGenerateTest extends ProcurementTestCase $this->assertSame(PurchaseOrderModel::STATUS_PENDING, $purchase->status); $this->assertSame('10.00', (string) $purchase->total_quantity, '3+3+4'); - $this->assertSame('50.00', (string) $purchase->estimate_amount, '10 件 × 等级价 5.00'); + $this->assertSame('60.00', (string) $purchase->estimate_amount, '10 件 × 每包成本 6.00'); $this->assertSame('0.00', (string) $purchase->actual_amount); } diff --git a/tests/Feature/StoreOrderTest.php b/tests/Feature/StoreOrderTest.php index 18d6602..d968109 100644 --- a/tests/Feature/StoreOrderTest.php +++ b/tests/Feature/StoreOrderTest.php @@ -8,9 +8,6 @@ use App\Models\ProductPriceModel; use App\Models\StoreModel; use App\Models\StoreOrderModel; use App\Models\UserModel; -use Illuminate\Support\Facades\Cache; -use Modules\SystemTool\Models\SysSiteConfigGroupModel; -use Modules\SystemTool\Models\SysSiteConfigItemsModel; /** * 小程序下单:等级价快照、服务端重算总价、取消限制、门店数据隔离 @@ -133,23 +130,6 @@ class StoreOrderTest extends ProcurementTestCase $this->getJson('/mini/order')->assertJsonPath('data.total', 0); } - /** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00) */ - private function seedContainerConfig(): void - { - $group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']); - foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) { - SysSiteConfigItemsModel::create([ - 'group_id' => $group->id, - 'key' => $key, - 'title' => $title, - 'type' => 'InputNumber', - 'values' => $value, - 'sort' => 0, - ]); - } - Cache::forget('site_config'); - } - /** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */ private function makeOrderWithStatus(int $status): StoreOrderModel { @@ -163,94 +143,6 @@ class StoreOrderTest extends ProcurementTestCase return $order; } - /** 修改周转框/托盘:自动重算附加金额与订单总金额 */ - public function test_container_update_recalculates_amounts(): void - { - $this->seedContainerConfig(); - $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); - $this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额'); - - $this->actingAsSysUser(); - $this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1]) - ->assertOk()->assertJsonPath('success', true); - - $order->refresh(); - $this->assertSame(3, $order->box_num); - $this->assertSame(1, $order->tray_num); - $this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00'); - $this->assertSame('10.00', (string) $order->product_amount); - $this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00'); - } - - /** 已接单、采购中、配送中均可修改 */ - public function test_container_update_allowed_in_editable_statuses(): void - { - $this->seedContainerConfig(); - $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); - $this->actingAsSysUser(); - - foreach ([ - StoreOrderModel::STATUS_SUMMARIZED, - StoreOrderModel::STATUS_DELIVERING, - StoreOrderModel::STATUS_DISTRIBUTION, - ] as $status) { - $order->update(['status' => $status]); - $this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0]) - ->assertOk()->assertJsonPath('success', true); - $this->assertSame('2.00', (string) $order->fresh()->added_amount); - } - } - - /** 待接单、已完成、已取消不允许修改 */ - public function test_container_update_rejected_in_other_statuses(): void - { - $this->seedContainerConfig(); - $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); - $this->actingAsSysUser(); - - foreach ([ - StoreOrderModel::STATUS_PENDING, - StoreOrderModel::STATUS_COMPLETED, - StoreOrderModel::STATUS_CANCELLED, - ] as $status) { - $order->update(['status' => $status]); - $this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5]) - ->assertOk()->assertJsonPath('success', false); - } - $order->refresh(); - $this->assertSame(0, $order->box_num, '被拒绝后数量不变'); - $this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变'); - } - - /** 历史订单未写商品金额时按「总额 - 附加」反推回写 */ - public function test_container_update_heals_legacy_product_amount(): void - { - $this->seedContainerConfig(); - $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); - $order->update(['product_amount' => 0]); // 模拟历史数据 - - $this->actingAsSysUser(); - $this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0]) - ->assertOk()->assertJsonPath('success', true); - - $order->refresh(); - $this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额'); - $this->assertSame('4.00', (string) $order->added_amount, '2×2.00'); - $this->assertSame('14.00', (string) $order->total_amount); - } - - /** 数量为负时校验失败 */ - public function test_container_update_validates_negative_numbers(): void - { - $this->seedContainerConfig(); - $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); - - $this->actingAsSysUser(); - $this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0]) - ->assertJsonPath('success', false) - ->assertJsonPath('msg', '周转框数量不能小于 0'); - } - /** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */ public function test_soft_delete_only_cancelled_orders(): void { diff --git a/web/api/order/store.ts b/web/api/order/store.ts index df453c4..93711d3 100644 --- a/web/api/order/store.ts +++ b/web/api/order/store.ts @@ -27,15 +27,6 @@ export async function batchUpdateOrderStatus(ids: number[], status: number) { }); } -/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */ -export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) { - return createAxios({ - url: `/order/store/${id}/container`, - method: 'put', - data, - }); -} - /** 删除订单 */ export async function deleteStoreOrder(id: number) { return createAxios({ diff --git a/web/api/purchase/order.ts b/web/api/purchase/order.ts index d6cddd3..d4fff14 100644 --- a/web/api/purchase/order.ts +++ b/web/api/purchase/order.ts @@ -23,6 +23,13 @@ export interface PurchaseCellUpdateParams { weight?: number; } +/** 采购单周转框/托盘合并记录修改:单笔订单的框/托盘数量 */ +export interface PurchaseContainerOrderParams { + order_id: number; + box_num: number; + tray_num: number; +} + /** 生成采购单 */ export async function generatePurchase(purchase_date: string, order_ids?: number[]) { @@ -77,6 +84,19 @@ export async function getPurchaseStoreSummary(purchaseId: number, storeId: numbe }); } +/** 采购单周转框/托盘合并记录修改:按门店覆盖全部订单逐笔更新(自动重算附加金额与订单总金额) */ +export async function updatePurchaseContainer( + purchaseId: number, + storeId: number, + orders: PurchaseContainerOrderParams[], +) { + return createAxios({ + url: `/purchase/order/${purchaseId}/container/${storeId}`, + method: 'put', + data: { orders }, + }); +} + /** C2/C3 导出采购单(Excel 表格):type=all 全品类 / category 仅蔬果分类 */ export async function exportPurchase(id: number, type: PurchaseExportType = 'all') { return downloadBlob( diff --git a/web/domain/iPurchaseOrder.ts b/web/domain/iPurchaseOrder.ts index fb86298..7506cc5 100644 --- a/web/domain/iPurchaseOrder.ts +++ b/web/domain/iPurchaseOrder.ts @@ -48,6 +48,34 @@ export interface IPurchaseDetail { purchase: IPurchaseOrder; stores: { id: number; name: string }[]; items: IPurchaseDetailRow[]; + /** 周转框/托盘合并记录(按门店聚合) */ + containers: IPurchaseContainerStore[]; +} + +/** 合并记录中的底层门店订单(订单号 + 框/托盘数量) */ +export interface IPurchaseContainerOrder { + order_id: number; + order_no: string; + box_num: number; + tray_num: number; +} + +/** 门店周转框/托盘合并记录(该门店在本采购单下全部订单的合计) */ +export interface IPurchaseContainerStore { + store_id: number; + store_name: string; + /** 周转框合计 */ + box_num: number; + /** 周转托盘合计 */ + tray_num: number; + /** 周转框单价(站点配置) */ + box_price: string; + /** 周转托盘单价(站点配置) */ + tray_price: string; + /** 附加金额合计 = 框合计×框单价 + 托盘合计×托盘单价 */ + added_amount: string; + order_count: number; + orders: IPurchaseContainerOrder[]; } /** 单元格下钻明细行(溯源订货单明细,附订单号/状态/可编辑标记) */ diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index 47eee5f..0ac2454 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -6,7 +6,6 @@ import { Drawer, Empty, Form, - InputNumber, message, Modal, Popconfirm, @@ -27,7 +26,6 @@ import type IStoreOrder from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; import { getStoreOrder, - updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus, deleteStoreOrder, @@ -36,14 +34,11 @@ 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'; -import { DeleteOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons'; +import { DeleteOutlined, UnorderedListOutlined } from '@ant-design/icons'; import {PURCHASE_STATUS_MAP} from "@/domain/iPurchaseOrder.ts"; const { Title, Text } = Typography; -/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */ -const CONTAINER_EDITABLE_STATUS = [1, 2, 3]; - /** * 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 @@ -74,11 +69,6 @@ const StoreOrderPage: React.FC = () => { const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); - const [containerOpen, setContainerOpen] = useState(false); - const [containerOrder, setContainerOrder] = useState(null); - const [containerSaving, setContainerSaving] = useState(false); - const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>(); - /** 勾选行(批量流转/生成采购单用) */ const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchOpen, setBatchOpen] = useState(false); @@ -147,25 +137,6 @@ const StoreOrderPage: React.FC = () => { } }; - const openContainer = (record: IStoreOrder) => { - setContainerOrder(record); - containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num }); - setContainerOpen(true); - }; - - const handleContainerSave = async (values: { box_num: number; tray_num: number }) => { - if (!containerOrder?.id) return; - setContainerSaving(true); - try { - await updateOrderContainer(containerOrder.id, values); - message.success('周转框/托盘数量已更新'); - setContainerOpen(false); - await tableRef.current?.reload(); - } finally { - setContainerSaving(false); - } - }; - /** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */ const handleDelete = async (id: number) => { await deleteStoreOrder(id); @@ -174,13 +145,6 @@ const StoreOrderPage: React.FC = () => { await tableRef.current?.reload(); }; - // 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额 - const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0); - const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0); - const previewAdded = watchBoxNum * Number(containerOrder?.box_price ?? 0) - + watchTrayNum * Number(containerOrder?.tray_price ?? 0); - const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded; - const columns: XinTableColumn[] = [ { title: '订单号', @@ -387,15 +351,6 @@ const StoreOrderPage: React.FC = () => { icon={} onClick={() => openDetail(record.id!)} /> - { CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) && ( - - - - )} { NEXT_STATUS[detail.status!] && ( { - - {/* 修改周转框/托盘数量 */} - setContainerOpen(false)} - onOk={() => containerForm.submit()} - confirmLoading={containerSaving} - okText="保存" - destroyOnHidden - > -
- 订单 {containerOrder?.order_no},保存后将按单价自动重算附加金额与订单总金额。 -
-
- - - - - - -
- -
- 商品总金额:¥{containerOrder?.product_amount ?? '0.00'} -
-
- 附加总金额: - ¥{previewAdded.toFixed(2)} -
-
- 订单总金额: - ¥{previewTotal.toFixed(2)} -
-
-
); }; diff --git a/web/pages/purchase/order.tsx b/web/pages/purchase/order.tsx index f6e5083..cf651c7 100644 --- a/web/pages/purchase/order.tsx +++ b/web/pages/purchase/order.tsx @@ -32,6 +32,7 @@ import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts'; import type { IPurchaseCell, IPurchaseCellItem, + IPurchaseContainerStore, IPurchaseDetail, IPurchaseDetailRow, IPurchaseStoreItem, @@ -44,8 +45,9 @@ import { getPurchaseCell, getPurchaseDetail, getPurchaseStoreSummary, - type PurchaseCellUpdateParams, type PurchaseRowUpdateParams, + type PurchaseCellUpdateParams, type PurchaseContainerOrderParams, type PurchaseRowUpdateParams, updatePurchaseCellItem, + updatePurchaseContainer, updatePurchaseRow, } from '@/api/purchase/order.ts'; import { Update } from '@/api/common/table.ts'; @@ -98,6 +100,26 @@ const PurchaseOrderPage: React.FC = () => { const [storeSummary, setStoreSummary] = useState(null); const [storeLoading, setStoreLoading] = useState(false); + // 周转框/托盘合并记录修改(按门店覆盖全部订单逐笔修改) + const [containerOpen, setContainerOpen] = useState(false); + const [containerStore, setContainerStore] = useState(null); + const [containerSaving, setContainerSaving] = useState(false); + const [containerForm] = Form.useForm<{ orders: PurchaseContainerOrderParams[] }>(); + + // 弹窗内实时预览:合并合计 = Σ 各订单框/托盘数量,附加金额 = 合计 × 单价 + const watchContainerOrders = Form.useWatch('orders', containerForm) ?? []; + const previewContainerBox = watchContainerOrders.reduce( + (sum, order) => sum + Number(order?.box_num ?? 0), + 0, + ); + const previewContainerTray = watchContainerOrders.reduce( + (sum, order) => sum + Number(order?.tray_num ?? 0), + 0, + ); + const previewContainerAdded = + previewContainerBox * Number(containerStore?.box_price ?? 0) + + previewContainerTray * Number(containerStore?.tray_price ?? 0); + useEffect(() => { getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); }, []); @@ -242,6 +264,37 @@ const PurchaseOrderPage: React.FC = () => { } }; + /** 打开周转框/托盘合并记录修改(初始化该门店全部订单的当前数量) */ + const openContainerEdit = (store: IPurchaseContainerStore) => { + setContainerStore(store); + containerForm.setFieldsValue({ + orders: store.orders.map((order) => ({ + order_id: order.order_id, + box_num: order.box_num, + tray_num: order.tray_num, + })), + }); + setContainerOpen(true); + }; + + /** 提交合并记录修改:逐笔更新订单,自动重算附加金额与订单总金额 */ + const handleContainerSave = async (values: { orders: PurchaseContainerOrderParams[] }) => { + if (!detail || !containerStore) { + return; + } + setContainerSaving(true); + try { + await updatePurchaseContainer(detail.purchase.id!, containerStore.store_id, values.orders); + message.success('周转框/托盘已更新,订单金额已重算'); + setContainerOpen(false); + setContainerStore(null); + await loadDetail(detail.purchase.id!); + await tableRef.current?.reload(); + } finally { + setContainerSaving(false); + } + }; + const handleComplete = async (id: number) => { setCompleting(true); try { @@ -420,6 +473,61 @@ const PurchaseOrderPage: React.FC = () => { ); }; + /** 周转框/托盘合并记录列:门店/订单数/框合计/托盘合计/附加金额(合计可点击下钻逐订单修改) */ + const containerColumns: TableProps['columns'] = [ + { title: '门店', dataIndex: 'store_name', width: 180, align: 'center' }, + { title: '订单数', dataIndex: 'order_count', width: 90, align: 'center' }, + { + title: '周转框合计', + dataIndex: 'box_num', + width: 110, + align: 'center', + render: (_, row) => ( + openContainerEdit(row)}>{row.box_num} + ), + }, + { + title: '周转托盘合计', + dataIndex: 'tray_num', + width: 120, + align: 'center', + render: (_, row) => ( + openContainerEdit(row)}>{row.tray_num} + ), + }, + { + title: '附加金额', + dataIndex: 'added_amount', + width: 110, + align: 'center', + render: (_, row) => ¥{row.added_amount}, + }, + ]; + + /** 周转框/托盘合并记录合计行 */ + const renderContainerSummary = () => { + const containers = detail?.containers ?? []; + const totalBox = containers.reduce((sum, row) => sum + row.box_num, 0); + const totalTray = containers.reduce((sum, row) => sum + row.tray_num, 0); + const totalAdded = containers.reduce((sum, row) => sum + Number(row.added_amount), 0); + return ( + + + 合计 + + + {totalBox} + + + {totalTray} + + + ¥{totalAdded.toFixed(2)} + + + ); + }; + const columns: XinTableColumn[] = [ { title: '采购单号', @@ -588,6 +696,7 @@ const PurchaseOrderPage: React.FC = () => { items={[ { key: 'items', label: '商品明细' }, { key: 'stores', label: '门店购买详情' }, + { key: 'containers', label: '周转框/托盘' }, ]} /> @@ -627,6 +736,24 @@ const PurchaseOrderPage: React.FC = () => { )} + ) : detailTab === 'containers' ? ( + detail.containers.length > 0 ? ( + + rowKey="store_id" + size="small" + bordered + columns={containerColumns} + dataSource={detail.containers} + pagination={false} + summary={renderContainerSummary} + /> + ) : ( + + ) ) : ( <> @@ -877,6 +1004,101 @@ const PurchaseOrderPage: React.FC = () => { + + {/* 周转框/托盘合并记录下钻:按门店展示全部订单逐笔修改(采购单已完成则只读) */} + { + setContainerOpen(false); + setContainerStore(null); + }} + onOk={() => containerForm.submit()} + confirmLoading={containerSaving} + okText="保存" + okButtonProps={{ disabled: detail?.purchase.status !== 0 }} + width={760} + destroyOnHidden + > +
+ 该门店在本采购单下的全部订单({containerStore?.order_count ?? 0} 笔): + {detail?.purchase.status === 0 + ? '逐笔修改后保存,系统将自动重算每笔订单的附加金额与订单总金额。' + : '采购单已完成,仅可查看。'} +
+
+ + {(fields) => ( +
+
+
订单号
+
+ 周转框(单价 ¥{containerStore?.box_price ?? '0.00'}) +
+
+ 周转托盘(单价 ¥{containerStore?.tray_price ?? '0.00'}) +
+
+ {fields.map((field) => { + const order = containerStore?.orders[field.name]; + return ( +
+
+ {order?.order_no ?? '-'} +
+ + + + + + + +
+ ); + })} +
+ )} +
+
+ +
+ 周转框合计: + {previewContainerBox} + 周转托盘合计: + {previewContainerTray} +
+
+ 附加金额合计: + ¥{previewContainerAdded.toFixed(2)} +
+
+
); };