diff --git a/.phpunit.result.cache b/.phpunit.result.cache index bec3c52..4a6ef2b 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},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.004,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.018,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.024,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.016,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.015,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.013,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.011,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.028,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.021,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.027,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.041,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.054,"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.046,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.061,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.043,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.052,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.043,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.026,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.021,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.041,"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.01,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.009,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.077,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.023,"Tests\\Feature\\ExportTest::test_export_pdf":1.46,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.021,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.02,"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.063,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.016,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.008,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.008,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.014,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.011,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.013,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.016,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.006,"Tests\\Feature\\CartTest::test_update_quantity":0.012,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.022,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.018,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.015,"Tests\\Feature\\CartTest::test_delete_item":0.009,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.013,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.021,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.003,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.013,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.002,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.002,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.018,"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.019,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.021,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.017,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.021,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.015,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.02,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.014,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.012,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.019,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.022,"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.015,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.023,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.017,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.004,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.021,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.004,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.006,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.008,"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.012,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.026,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.032,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.032,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.028,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.021,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.026,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.045,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.021,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.023,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.022,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.031,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.026,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.019,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.02,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.033,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.025,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.026,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.043,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.098,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.028,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.062,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.053,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.052,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.05,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.046,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.04,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.039,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.042}} \ 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},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.002,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.008,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.011,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.009,"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.014,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.012,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.012,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.015,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.024,"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.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.031,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.026,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.027,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.033,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.012,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.016,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.02,"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.057,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.309,"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.048,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.021,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.007,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.01,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.011,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.007,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.011,"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.01,"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.009,"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.013,"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.009,"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.01,"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.007,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.01,"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.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info":0.006,"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.006,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.013,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.021,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.023,"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.022,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.008,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.011,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.02,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.022,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.019,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.019,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.016,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.016,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.026,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.033,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.066,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.02,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.025,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.029,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.027,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.034,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.03,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.024,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.044,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.037}} \ No newline at end of file diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index 6ad4eaf..7f35a92 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -238,7 +238,8 @@ class PurchaseOrderController extends BaseController } /** - * C4 商品行修改:采购成本同步该商品全部订货明细;实际称重按数量比例分摊(尾差修正守恒) + * C4 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细; + * 实际称重按数量比例分摊(尾差修正守恒) */ #[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])] public function updateRow(int $id, int $productId, PurchaseRowUpdateRequest $request): JsonResponse @@ -248,13 +249,25 @@ class PurchaseOrderController extends BaseController throw new RepositoryException('采购单不存在'); } $validated = $request->validated(); - $costPrice = isset($validated['cost_price']) ? (float) $validated['cost_price'] : null; + + $attrs = []; + foreach (['product_name', 'supplier_id', 'product_spec', 'unit', 'cost_price'] as $field) { + if (isset($validated[$field])) { + $attrs[$field] = $validated[$field]; + } + } $weight = isset($validated['weight']) ? (float) $validated['weight'] : null; - if ($costPrice === null && $weight === null) { - throw new RepositoryException('采购成本与实际称重至少填写一项'); + if ($attrs === [] && $weight === null) { + throw new RepositoryException('品名/供应商/包规/单位/成本/实际称重至少填写一项'); } - $count = app(PurchaseEditService::class)->updateRow($purchase, $productId, $costPrice, $weight); + $count = app(PurchaseEditService::class)->updateRow( + $purchase, + $productId, + $attrs, + $weight, + (bool) ($validated['sync_product'] ?? false), + ); return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细'); } diff --git a/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php b/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php index 9df9396..4a6a534 100644 --- a/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php +++ b/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php @@ -5,7 +5,8 @@ namespace App\Http\Requests\Purchase; use Modules\Common\Http\Requests\BaseFormRequest; /** - * 采购明细商品行修改 验证(C4;成本/称重至少填一项,同步到该商品全部订货明细) + * 采购明细商品行修改 验证(C4;品名/供应商/包规/单位/成本/称重至少一项, + * 提交后一键同步该商品在本采购单下的全部订货明细) */ class PurchaseRowUpdateRequest extends BaseFormRequest { @@ -14,14 +15,23 @@ class PurchaseRowUpdateRequest extends BaseFormRequest public function rules(): array { return [ + 'product_name' => 'nullable|string|max:100', + 'supplier_id' => 'nullable|integer|exists:supplier,id', + 'product_spec' => 'nullable|string|max:100', + 'unit' => 'nullable|string|max:20', 'cost_price' => 'nullable|numeric|min:0', 'weight' => 'nullable|numeric|min:0', + 'sync_product' => 'nullable|boolean', ]; } public function messages(): array { return [ + 'product_name.max' => '品名不能超过 100 字', + 'supplier_id.exists' => '供应商不存在', + 'product_spec.max' => '包规不能超过 100 字', + 'unit.max' => '单位不能超过 20 字', 'cost_price.numeric' => '采购成本必须为数字', 'cost_price.min' => '采购成本不能小于 0', 'weight.numeric' => '实际称重必须为数字', diff --git a/app/Services/PurchaseEditService.php b/app/Services/PurchaseEditService.php index 2122a12..170d7d7 100644 --- a/app/Services/PurchaseEditService.php +++ b/app/Services/PurchaseEditService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Exceptions\RepositoryException; +use App\Models\ProductModel; use App\Models\PurchaseOrderModel; use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; @@ -46,14 +47,16 @@ class PurchaseEditService } /** - * 商品行修改:采购成本(写入该商品全部订货明细)/ 实际称重(按数量比例分摊,尾差修正守恒) + * 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细; + * 实际称重按数量比例分摊(尾差修正守恒); + * syncProduct = true 时同步更新商品档案(product 表) * - * @param float|null $costPrice 采购成本(每包规),NULL 不修改 + * @param array{product_name?: string, supplier_id?: int, product_spec?: string, unit?: string, cost_price?: float} $attrs 行属性(仅同步传入键) * @param float|null $weight 行实际称重合计,NULL 不修改 */ - public function updateRow(PurchaseOrderModel $purchase, int $productId, ?float $costPrice, ?float $weight): int + public function updateRow(PurchaseOrderModel $purchase, int $productId, array $attrs, ?float $weight, bool $syncProduct = false): int { - return DB::transaction(function () use ($purchase, $productId, $costPrice, $weight) { + return DB::transaction(function () use ($purchase, $productId, $attrs, $weight, $syncProduct) { $items = StoreOrderItemModel::query() ->where('purchase_id', $purchase->id) ->where('product_id', $productId) @@ -63,11 +66,50 @@ class PurchaseEditService throw new RepositoryException('该采购单下无此商品的订货明细'); } - if ($costPrice !== null) { - $cost = bcadd((string) $costPrice, '0', 2); + $sync = []; + if (isset($attrs['product_name'])) { + $sync['product_name'] = $attrs['product_name']; + } + if (isset($attrs['supplier_id'])) { + $sync['supplier_id'] = (int) $attrs['supplier_id']; + } + if (isset($attrs['product_spec'])) { + $sync['product_spec'] = $attrs['product_spec']; + } + if (isset($attrs['unit'])) { + $sync['unit'] = $attrs['unit']; + } + if (isset($attrs['cost_price'])) { + $sync['cost_price'] = bcadd((string) $attrs['cost_price'], '0', 2); + } + if ($sync !== []) { foreach ($items as $item) { - $item->cost_price = $cost; - $item->save(); + $item->fill($sync)->save(); + } + + // 同步至商品档案(软删除商品不同步,直接报错回滚) + if ($syncProduct) { + $product = ProductModel::withTrashed()->find($productId); + if ($product === null || $product->trashed()) { + throw new RepositoryException('商品档案不存在或已删除,无法同步至商品'); + } + $productAttrs = []; + if (isset($sync['product_name'])) { + $productAttrs['name'] = $sync['product_name']; + } + if (isset($sync['supplier_id'])) { + $productAttrs['supplier_id'] = $sync['supplier_id']; + } + if (isset($sync['product_spec'])) { + $productAttrs['spec'] = $sync['product_spec']; + } + if (isset($sync['unit'])) { + $productAttrs['unit'] = $sync['unit']; + } + if (isset($sync['cost_price'])) { + $productAttrs['cost_price'] = $sync['cost_price']; + } + $product->fill($productAttrs)->save(); } } diff --git a/database/migrations/2026_07_23_030608_create_store_table.php b/database/migrations/2026_07_23_030608_create_store_table.php index 5ecbd53..5d7b3c1 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -61,7 +61,7 @@ return new class extends Migration $table->string('address', 255)->default('')->comment('门店地址'); $table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); - $table->string('remark', 255)->default('')->comment('备注'); + $table->string('remark', 255)->nullable()->default('')->comment('备注'); $table->timestamps(); $table->softDeletes(); $table->index(['level_id', 'status'], 'store_level_status_index'); diff --git a/tests/Feature/PurchaseEditTest.php b/tests/Feature/PurchaseEditTest.php index 8b17793..00189b1 100644 --- a/tests/Feature/PurchaseEditTest.php +++ b/tests/Feature/PurchaseEditTest.php @@ -9,6 +9,7 @@ use App\Models\PurchaseOrderModel; use App\Models\StoreModel; use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; +use App\Models\SupplierModel; use App\Models\UserModel; /** @@ -51,7 +52,7 @@ class PurchaseEditTest extends ProcurementTestCase return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]]; } - /** 详情返回「商品行 × 门店列」矩阵:单价 = 成本/包规,单元格金额 = 数量×单价 */ + /** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */ public function test_detail_returns_store_matrix(): void { [$purchase, $product, $stores] = $this->buildPurchase(); @@ -69,16 +70,12 @@ class PurchaseEditTest extends ProcurementTestCase $this->assertSame($product->id, $row['product_id']); $this->assertSame('10斤/箱', $row['product_spec']); $this->assertSame('斤', $row['unit']); - $this->assertEquals(20.0, $row['cost_price']); - $this->assertEquals(2.0, $row['unit_cost'], '单价 = 20 ÷ 10'); - $this->assertEquals(5.0, $row['quantity'], '2+3'); - $this->assertEquals(10.0, $row['amount'], '5 × 2.00'); + $this->assertSame('20.00', (string) $row['cost_price']); + $this->assertSame(5, $row['quantity'], '2+3'); - $cells = collect($row['cells'])->keyBy('store_id'); - $this->assertSame(2, $cells[$stores[0]->id]['quantity']); - $this->assertEquals(4.0, $cells[$stores[0]->id]['amount'], '2 × 2.00'); - $this->assertSame(3, $cells[$stores[1]->id]['quantity']); - $this->assertEquals(6.0, $cells[$stores[1]->id]['amount'], '3 × 2.00'); + $cells = $row['cells']; + $this->assertSame(2, $cells[$stores[0]->id]); + $this->assertSame(3, $cells[$stores[1]->id]); } /** 门店单元格数量修改 → 明细金额重算(数量×单价),订单与采购单汇总同步 */ @@ -148,7 +145,73 @@ class PurchaseEditTest extends ProcurementTestCase $this->assertSame('20.00', (string) $purchase->actual_amount); } - /** 行级修改:成本与称重至少一项 */ + /** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细 */ + public function test_update_row_syncs_product_attributes(): void + { + [$purchase, $product] = $this->buildPurchase(); + $supplier = SupplierModel::factory()->create(); + $this->actingAsSysUser(); + + $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [ + 'product_name' => '优选土豆', + 'supplier_id' => $supplier->id, + 'product_spec' => '5斤/袋', + 'unit' => '袋', + 'cost_price' => 25, + ])->assertJsonPath('success', true) + ->assertJsonPath('data.count', 2); + + $items = StoreOrderItemModel::all(); + $this->assertCount(2, $items); + foreach ($items as $item) { + $this->assertSame('优选土豆', $item->product_name); + $this->assertSame($supplier->id, $item->supplier_id); + $this->assertSame('5斤/袋', $item->product_spec); + $this->assertSame('袋', $item->unit); + $this->assertSame('25.00', (string) $item->cost_price); + } + + // 未传 sync_product:商品档案保持原值 + $product = $product->fresh(); + $this->assertNotSame('优选土豆', $product->name); + + // 新单价 = 25 ÷ 5 = 5.00,实际金额 = 5 件 × 5.00 + $this->assertSame('25.00', (string) $purchase->fresh()->actual_amount); + } + + /** sync_product=true:订货明细与商品档案同步更新;软删除商品拒绝 */ + public function test_update_row_syncs_to_product_catalog(): void + { + [$purchase, $product] = $this->buildPurchase(); + $supplier = SupplierModel::factory()->create(); + $this->actingAsSysUser(); + + $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [ + 'product_name' => '优选土豆', + 'supplier_id' => $supplier->id, + 'product_spec' => '5斤/袋', + 'unit' => '袋', + 'cost_price' => 25, + 'sync_product' => true, + ])->assertJsonPath('success', true); + + $product = $product->fresh(); + $this->assertSame('优选土豆', $product->name); + $this->assertSame($supplier->id, $product->supplier_id); + $this->assertSame('5斤/袋', $product->spec); + $this->assertSame('袋', $product->unit); + $this->assertSame('25.00', (string) $product->cost_price); + + // 软删除商品 → 拒绝并回滚(订货明细不同步) + $product->delete(); + $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [ + 'product_name' => '再次改名', + 'sync_product' => true, + ])->assertJsonPath('success', false); + $this->assertSame('优选土豆', StoreOrderItemModel::first()->product_name, '回滚后明细保持原值'); + } + + /** 行级修改:属性与称重至少一项 */ public function test_update_row_requires_at_least_one_field(): void { [$purchase, $product] = $this->buildPurchase(); diff --git a/web/api/purchase/order.ts b/web/api/purchase/order.ts index be31dfc..e967434 100644 --- a/web/api/purchase/order.ts +++ b/web/api/purchase/order.ts @@ -32,23 +32,23 @@ export async function exportPurchase(id: number, type: PurchaseExportType, forma ); } -/** C4 门店单元格修改(数量/称重,同步订货明细并重算汇总) */ -export async function updatePurchaseCell( - orderItemId: number, - data: { quantity: number; weight?: number }, -) { - return createAxios<{ quantity: number; weight: number; amount: number }>({ - url: `/purchase/order/cell/${orderItemId}`, - method: 'put', - data, - }); +/** C4 商品行修改参数(品名/供应商/包规/单位/成本/称重,一键同步该商品全部订货明细;sync_product 追加同步商品档案) */ +export interface PurchaseRowUpdateParams { + product_name?: string; + supplier_id?: number; + product_spec?: string; + unit?: string; + cost_price?: number; + weight?: number; + /** true = 同时同步至商品档案(product 表) */ + sync_product?: boolean; } -/** C4 商品行修改(成本/实际称重,同步该商品全部订货明细) */ +/** C4 商品行修改 */ export async function updatePurchaseRow( purchaseId: number, productId: number, - data: { cost_price?: number; weight?: number }, + data: PurchaseRowUpdateParams, ) { return createAxios<{ count: number }>({ url: `/purchase/order/${purchaseId}/row/${productId}`, diff --git a/web/pages/purchase/order.tsx b/web/pages/purchase/order.tsx index b2c02b2..fed76be 100644 --- a/web/pages/purchase/order.tsx +++ b/web/pages/purchase/order.tsx @@ -1,17 +1,20 @@ -import React, { useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Button, Descriptions, Drawer, - Dropdown, + Form, + Input, InputNumber, message, + Modal, Popconfirm, + Select, Table, Tag, Typography, } from 'antd'; -import { CheckOutlined, DownloadOutlined } from '@ant-design/icons'; +import { CheckOutlined, EditOutlined } from '@ant-design/icons'; import type { TableProps } from 'antd'; import XinTable from '@/components/XinTable'; import type { @@ -26,21 +29,31 @@ import type { } from '@/domain/iPurchaseOrder.ts'; import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts'; import { - exportPurchase, getPurchaseDetail, + updatePurchaseRow, } from '@/api/purchase/order.ts'; import { Update } from '@/api/common/table.ts'; +import { getSupplierOptions } from '@/api/customer/supplier.ts'; +import type ISupplier from '@/domain/iSupplier.ts'; import AuthButton from '@/components/AuthButton'; const { Title, Text } = Typography; -/** 行草稿:成本/称重 + 各门店单元格数量 */ -interface RowDraft { - cost_price?: number; - weight?: number; - cells: Record; +/** 行修改表单:品名/供应商/包规/单位/成本 */ +interface RowEditForm { + product_name: string; + supplier_id?: number; + product_spec: string; + unit: string; + cost_price: number; } +/** 单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径) */ +const calcUnitCost = (cost: number, spec: string): number => { + const pack = parseFloat(spec); + return Number.isFinite(pack) && pack > 0 ? cost / pack : cost; +}; + /** * 采购单管理(C1 在门店订单页合并生成 / C2-C3 导出 / C4 明细矩阵修改) * 采购单无独立明细表,明细直接溯源门店订货明细:商品行 × 门店列 @@ -52,16 +65,24 @@ const PurchaseOrderPage: React.FC = () => { const [detailOpen, setDetailOpen] = useState(false); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); - const [drafts, setDrafts] = useState>({}); - const [savingProductId, setSavingProductId] = useState(null); const [completing, setCompleting] = useState(false); + // 行修改弹窗 + const [editingRow, setEditingRow] = useState(null); + const [rowSaving, setRowSaving] = useState(false); + const [syncTarget, setSyncTarget] = useState<'purchase' | 'product'>('purchase'); + const [editForm] = Form.useForm(); + const [suppliers, setSuppliers] = useState([]); + + useEffect(() => { + getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); + }, []); + const loadDetail = async (id: number) => { setDetailLoading(true); try { const res = await getPurchaseDetail(id); setDetail(res.data.data ?? null); - setDrafts({}); } finally { setDetailLoading(false); } @@ -72,65 +93,39 @@ const PurchaseOrderPage: React.FC = () => { await loadDetail(id); }; - const getDraft = (productId: number): RowDraft => drafts[productId] ?? { cells: {} }; - - const patchDraft = (productId: number, patch: Partial) => { - setDrafts((prev) => { - const current = prev[productId] ?? { cells: {} }; - return { - ...prev, - [productId]: { ...current, ...patch, cells: { ...current.cells, ...patch.cells } }, - }; + const openEdit = (row: IPurchaseDetailRow) => { + setEditingRow(row); + editForm.setFieldsValue({ + product_name: row.product_name, + supplier_id: row.supplier_id > 0 ? row.supplier_id : undefined, + product_spec: row.product_spec, + unit: row.unit, + cost_price: Number(row.cost_price), }); }; - /** 草稿成本(未修改取原值) */ - const draftCost = (row: IPurchaseDetailRow): number => - getDraft(row.product_id).cost_price ?? row.cost_price; - - const isRowDirty = (row: IPurchaseDetailRow): boolean => { - // const draft = getDraft(row.product_id); - // if (draft.cost_price !== undefined && draft.cost_price !== row.cost_price) { - // return true; - // } - // if (draft.weight !== undefined && draft.weight !== row.weight) { - // return true; - // } - // return row.cells.some( - // (cell) => draft.cells[cell.order_item_id] !== undefined - // && draft.cells[cell.order_item_id] !== cell.quantity, - // ); - }; - - /** 保存一行:先落各门店单元格数量,再落行级成本/称重,最后刷新 */ - const saveRow = async (row: IPurchaseDetailRow) => { - if (!detail || !isRowDirty(row)) { + /** 提交行修改:同步该商品全部订货明细;syncTarget=product 时追加同步商品档案 */ + const handleEditSave = async (values: RowEditForm) => { + if (!detail || !editingRow) { return; } - const draft = getDraft(row.product_id); - setSavingProductId(row.product_id); + setRowSaving(true); try { - // for (const cell of row.cells) { - // const qty = draft.cells[cell.order_item_id]; - // if (qty !== undefined && qty !== cell.quantity) { - // await updatePurchaseCell(cell.order_item_id, { quantity: qty }); - // } - // } - // const rowPatch: { cost_price?: number; weight?: number } = {}; - // if (draft.cost_price !== undefined && draft.cost_price !== row.cost_price) { - // rowPatch.cost_price = draft.cost_price; - // } - // if (draft.weight !== undefined && draft.weight !== row.weight) { - // rowPatch.weight = draft.weight; - // } - // if (Object.keys(rowPatch).length > 0) { - // await updatePurchaseRow(detail.purchase.id!, row.product_id, rowPatch); - // } - message.success('已保存并同步订货明细'); + const res = await updatePurchaseRow(detail.purchase.id!, editingRow.product_id, { + ...values, + supplier_id: values.supplier_id ?? 0, + sync_product: syncTarget === 'product', + }); + message.success( + syncTarget === 'product' + ? `已同步 ${res.data.data?.count ?? 0} 条订货明细,并更新商品档案` + : `已同步 ${res.data.data?.count ?? 0} 条订货明细`, + ); + setEditingRow(null); await loadDetail(detail.purchase.id!); await tableRef.current?.reload(); } finally { - setSavingProductId(null); + setRowSaving(false); } }; @@ -149,7 +144,7 @@ const PurchaseOrderPage: React.FC = () => { } }; - /** 明细矩阵列:品名/供应商/包规/单位/成本/单价 + 每门店一组(数量/金额)+ 合计 + 操作 */ + /** 明细矩阵列:品名/供应商/单价/包规/单位/成本 + 每门店一列(数量)+ 合计 + 操作 */ const buildItemColumns = (): TableProps['columns'] => { const fixed: NonNullable['columns']> = [ { title: '品名', dataIndex: 'product_name', width: 120, align: 'center' }, @@ -163,27 +158,23 @@ const PurchaseOrderPage: React.FC = () => { { title: '单价', key: 'unit_cost', - width: 80, + width: 90, align: 'center', - render: (_, row) => `¥${(Number(row.cost_price) / Number(row.product_spec)).toFixed(2)}`, + render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`, }, { title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' }, { title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' }, - { title: '成本', dataIndex: 'cost_price', width: 110, align: 'center', render: (v) => v || '-' } + { title: '成本', dataIndex: 'cost_price', width: 90, align: 'center', render: (v) => `¥${Number(v).toFixed(2)}` }, ]; const storeColumns = (detail?.stores ?? []).map((store) => ({ - title: store.name, + title: {store.name}, key: `store-${store.id}`, + width: 100, align: 'center' as const, render: (_: unknown, row: IPurchaseDetailRow) => { - const cell = row.cells[store.id]; - if (!cell) { - return -; - } - return ( - { row.cells[store.id] } - ); + const quantity = row.cells[store.id]; + return quantity !== undefined ? {quantity} : -; }, })); @@ -193,21 +184,21 @@ const PurchaseOrderPage: React.FC = () => { key: 'total_quantity', width: 90, align: 'center', - render: (_, row) => Object.values(row.cells).reduce((a, b) => a + b, 0), + render: (_, row) => {row.quantity}, }, { title: '操作', key: 'action', - width: 80, + width: 90, fixed: 'right', + align: 'center', render: (_, row) => ( @@ -219,6 +210,39 @@ const PurchaseOrderPage: React.FC = () => { return [...fixed, ...storeColumns, ...tail]; }; + /** 底部统计行:按门店统计金额(Σ 门店数量 × 行单价)+ 合计 */ + const renderSummary = () => { + const stores = detail?.stores ?? []; + const items = detail?.items ?? []; + const storeTotals = stores.map((store) => + items.reduce( + (sum, row) => sum + (row.cells[store.id] ?? 0) * Number(row.cost_price), + 0, + ), + ); + const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0); + const totalAmount = storeTotals.reduce((sum, amount) => sum + amount, 0); + + return ( + + + 成本统计(按门店) + + {storeTotals.map((amount, index) => ( + + ¥{amount.toFixed(2)} + + ))} + + {totalQuantity} + + + ¥{totalAmount.toFixed(2)} + + + ); + }; + const columns: XinTableColumn[] = [ { title: '采购单号', @@ -286,21 +310,7 @@ const PurchaseOrderPage: React.FC = () => { const operateRender: XinTableProps['operateRender'] = (record) => [ , - - exportPurchase(record.id!, 'all', 'xlsx') }, - { key: 'all-pdf', label: '全品类 PDF', onClick: () => exportPurchase(record.id!, 'all', 'pdf') }, - { key: 'category-xlsx', label: '蔬果分类 Excel', onClick: () => exportPurchase(record.id!, 'category', 'xlsx') }, - { key: 'category-pdf', label: '蔬果分类 PDF', onClick: () => exportPurchase(record.id!, 'category', 'pdf') }, - ], - }} - > - ]; const tableProps: XinTableProps = { @@ -318,7 +328,7 @@ const PurchaseOrderPage: React.FC = () => {
采购单 - 在「门店订单」页勾选已接单订单合并生成;明细按商品行 × 门店列展示,修改数量/成本/称重后点行内「保存」同步订货明细。 + 在「门店订单」页勾选已接单订单合并生成采购单;采购单支持修改订单信息。
{...tableProps} /> @@ -330,25 +340,22 @@ const PurchaseOrderPage: React.FC = () => { onClose={() => setDetailOpen(false)} size={1200} loading={detailLoading} + extra={detail?.purchase.status === 0 && ( + + + + + + )} > - {detail ? ( + {detail && ( <> - - - - -
- ) : undefined - } - > + + 采购单信息 + + {detail.purchase.purchase_date} @@ -363,9 +370,9 @@ const PurchaseOrderPage: React.FC = () => { {detail.purchase.total_weight} -
- 单价 = 成本 ÷ 包规;门店金额 = 数量 × 单价;行级「实际称重」保存时按各店数量比例分摊到订货明细(称重>0 时金额按称重×单价计)。 -
+ + 商品明细 + rowKey="product_id" size="small" @@ -374,10 +381,79 @@ const PurchaseOrderPage: React.FC = () => { dataSource={detail.items} pagination={false} scroll={{ x: 'max-content' }} + summary={renderSummary} /> - ) : null} + )} + + {/* 行修改:品名/供应商/包规/单位/成本,同步采购单明细 / 追加同步商品档案 */} + setEditingRow(null)} + destroyOnHidden + footer={[ + , + , + , + ]} + > +
+ 「保存」仅修改本采购单中该商品的所有订单项;「保存并同步」在此基础上同时更新商品档案(后续新订单生效)。 +
+
+ + + + + + + + + + + + +
+
); };