diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 4a6ef2b..409721b 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.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 +{"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},"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.007,"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.01,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.008,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.007,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.011,"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.022,"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.018,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.021,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.02,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.022,"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.017,"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.003,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.061,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.018,"Tests\\Feature\\ExportTest::test_export_pdf":1.295,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.017,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.017,"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.045,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.009,"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.009,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.009,"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.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.014,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.003,"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.011,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"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.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.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.01,"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.009,"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.008,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.011,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"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.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.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.016,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.017,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.016,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.012,"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.013,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.016,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.015,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.014,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.014,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.012,"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.055,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.012,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.026,"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.019,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.027,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.021,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.02,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.018,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.025,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.019}} \ No newline at end of file diff --git a/app/Exports/PurchaseOrderExport.php b/app/Exports/PurchaseOrderExport.php index 6eaaac8..67ec5c7 100644 --- a/app/Exports/PurchaseOrderExport.php +++ b/app/Exports/PurchaseOrderExport.php @@ -8,7 +8,6 @@ use App\Models\PurchaseOrderModel; use App\Models\StoreModel; use App\Models\StoreOrderItemModel; use App\Models\SupplierModel; -use App\Services\PurchaseEditService; use Illuminate\Support\Collection; use Maatwebsite\Excel\Concerns\FromCollection; use Maatwebsite\Excel\Concerns\WithHeadings; @@ -75,7 +74,7 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, foreach ($orderItems->groupBy('product_id') as $productId => $group) { $first = $group->first(); $product = $products->get((int) $productId); - $unitCost = PurchaseEditService::unitCost((string) ($first->cost_price ?? '0'), (string) $first->product_spec); + $unitCost = $first->cost_price ?? '0'; $quantity = '0'; $weight = '0'; @@ -84,11 +83,8 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, foreach ($group as $item) { $quantity = bcadd($quantity, (string) $item->quantity, 2); $weight = bcadd($weight, (string) $item->weight, 3); - $amount = bcadd($amount, PurchaseEditService::costAmount( - (string) $item->quantity, - (string) $item->weight, - $unitCost - ), 2); + // 金额 = 数量 × 每包成本价(单价/包规不参与金额计算) + $amount = bcadd($amount, bcmul((string) $item->quantity, (string) $item->cost_price, 2), 2); $storeQuantities[(int) $item->store_id] = bcadd( $storeQuantities[(int) $item->store_id] ?? '0', (string) $item->quantity, diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index 4acfa7d..7bc28bf 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -18,13 +18,13 @@ use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; /** - * 小程序门店订单(下单 / 历史 / 详情 / 取消 / 周期汇总) + * 小程序门店订单 */ #[RequestAttribute('/mini', 'mini', authGuard: 'users')] class OrderController extends BaseMiniController { /** - * 下单:逐行取当前门店等级价快照,服务端重算 amount 与 total(不接受前端金额) + * 下单 */ #[PostRoute('/order', authorize: true)] public function store(MiniOrderRequest $request): JsonResponse diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index 38961d2..5295e6f 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -4,11 +4,9 @@ namespace App\Http\Controllers\Order; use App\Exceptions\RepositoryException; use App\Models\NoticeModel; -use App\Models\ProductModel; -use App\Models\ProductPriceModel; -use App\Models\StoreOrderItemModel; use App\Models\StoreOrderModel; use App\Models\UserModel; +use App\Services\ItemImageResolver; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -17,7 +15,6 @@ use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\Common\Http\Controllers\BaseController; -use Modules\SystemTool\Models\SysFileModel; /** * 门店订单管理(订单只读 + 状态管理 + 明细修改/同步;创建/取消在小程序端) @@ -25,24 +22,6 @@ use Modules\SystemTool\Models\SysFileModel; #[RequestAttribute('/order/store', 'order.store')] class StoreOrderController extends BaseController { - /** 状态中文名(通知文案用) */ - private const array STATUS_NAMES = [ - StoreOrderModel::STATUS_PENDING => '待接单', - StoreOrderModel::STATUS_SUMMARIZED => '已接单', - StoreOrderModel::STATUS_DELIVERING => '采购中', - StoreOrderModel::STATUS_DISTRIBUTION => '配送中', - StoreOrderModel::STATUS_COMPLETED => '已完成', - StoreOrderModel::STATUS_CANCELLED => '已取消', - ]; - - /** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */ - private const array ITEM_EDITABLE_STATUS = [ - StoreOrderModel::STATUS_PENDING, - StoreOrderModel::STATUS_SUMMARIZED, - StoreOrderModel::STATUS_DELIVERING, - StoreOrderModel::STATUS_DISTRIBUTION, - ]; - protected array $searchField = [ 'store_id' => '=', 'status' => '=', @@ -84,7 +63,7 @@ class StoreOrderController extends BaseController // 明细封面图:从快照 image_ids 批量解析(不再关联商品档案表) foreach ($data['data'] as &$order) { - $this->resolveItemImages($order['items']); + app(ItemImageResolver::class)->resolve($order['items']); } unset($order); @@ -99,48 +78,7 @@ class StoreOrderController extends BaseController return $this->success($data); } - /** - * 已接单预览:聚合所有已接单订单明细(按商品分组), - * 供生成采购单前确认(C1 前置);单位取明细快照 - */ - #[GetRoute('/summary', 'query')] - public function summary(): JsonResponse - { - $rows = StoreOrderItemModel::query() - ->select('product_id') - ->selectRaw('MAX(product_name) as product_name') - ->selectRaw('MAX(product_spec) as product_spec') - ->selectRaw('MAX(unit) as unit') - ->selectRaw('SUM(quantity) as total_quantity') - ->selectRaw('COUNT(DISTINCT store_id) as store_count') - ->whereHas('order', function ($query) { - $query->where('status', StoreOrderModel::STATUS_SUMMARIZED); - }) - ->groupBy('product_id') - ->orderBy('product_id') - ->get() - ->toArray(); - - // 快照无单位的历史明细兜底:从商品档案(含已下架/软删除)补齐 - $emptyUnitProductIds = array_column(array_filter( - $rows, - static fn (array $row) => ($row['unit'] ?? '') === '' - ), 'product_id'); - if ($emptyUnitProductIds !== []) { - $units = ProductModel::withTrashed() - ->whereIn('id', $emptyUnitProductIds) - ->pluck('unit', 'id'); - foreach ($rows as &$row) { - if (($row['unit'] ?? '') === '') { - $row['unit'] = $units[$row['product_id']] ?? ''; - } - } - } - - return $this->success($rows); - } - - /** 订单详情:订单头 + 明细(含商品快照、首图;后台侧成本价可见、附供应商名) */ + /** 订单详情 */ #[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])] public function detail(int $id): JsonResponse { @@ -158,7 +96,7 @@ class StoreOrderController extends BaseController $data = $order->toArray(); // 明细首图 + 附加金额(与列表接口一致) - $this->resolveItemImages($data['items']); + app(ItemImageResolver::class)->resolve($data['items']); $boxAmount = site_config('services.box_amount'); $trayAmount = site_config('services.tray_amount'); $data['box_price'] = number_format($boxAmount, 2); @@ -170,160 +108,7 @@ class StoreOrderController extends BaseController } /** - * 明细首图解析:从快照 image_ids 批量解析文件 URL,未找到时置空字符串(列表/详情共用) - * - * @param array> $items 订单明细数组(引用修改) - */ - private function resolveItemImages(array &$items): void - { - $fileIds = []; - foreach ($items as $line) { - foreach ((array) ($line['image_ids'] ?? []) as $fileId) { - if ($fileId !== '' && $fileId !== null) { - $fileIds[] = (int) $fileId; - } - } - } - $fileUrls = []; - if ($fileIds !== []) { - foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) { - $fileUrls[$file->id] = $file->preview_url; - } - } - foreach ($items as &$product) { - $product['image'] = ''; - foreach ((array) ($product['image_ids'] ?? []) as $fileId) { - if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) { - $product['image'] = $fileUrls[(int) $fileId]; - break; - } - } - unset($product['image_ids']); - } - } - - /** - * 修改订单明细(商品快照 + 订货量/重量),事务内重算单品金额与订单总价。 - * 可改字段:供应商、品名、规格、单位、单价、成本价、订货量、重量 - */ - #[PutRoute(route: '/item/{id}', authorize: 'update', where: ['id' => '[0-9]+'])] - public function updateItem(int $id, Request $request): JsonResponse - { - $data = $request->validate([ - 'supplier_id' => 'required|integer|min:0', - 'product_name' => 'required|string|max:100', - 'product_spec' => 'nullable|string|max:100', - 'unit' => 'required|string|max:20', - 'price' => 'required|numeric|min:0', - 'cost_price' => 'required|numeric|min:0', - 'quantity' => 'required|integer|min:1', - 'weight' => 'required|numeric|min:0', - 'remark' => 'nullable|string' - ], [ - 'supplier_id.required' => '供应商不能为空', - 'supplier_id.integer' => '供应商ID必须为整数', - 'supplier_id.min' => '供应商ID不正确', - 'product_name.required' => '品名不能为空', - 'product_name.max' => '品名最长 100 个字符', - 'product_spec.max' => '规格最长 100 个字符', - 'unit.required' => '计价单位不能为空', - 'unit.max' => '计价单位最长 20 个字符', - 'price.required' => '单价不能为空', - 'price.numeric' => '单价必须为数字', - 'price.min' => '单价不能小于 0', - 'cost_price.required' => '成本价不能为空', - 'cost_price.numeric' => '成本价必须为数字', - 'cost_price.min' => '成本价不能小于 0', - 'quantity.required' => '订货量不能为空', - 'quantity.integer' => '订货量必须为整数', - 'quantity.min' => '订货量必须大于 0', - 'weight.required' => '重量不能为空', - 'weight.numeric' => '重量必须为数字', - 'weight.min' => '重量不能小于 0', - ]); - - return DB::transaction(function () use ($id, $data) { - $item = StoreOrderItemModel::query()->lockForUpdate()->find($id); - if (empty($item)) { - throw new RepositoryException('订单明细不存在'); - } - $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); - if (empty($order)) { - throw new RepositoryException('订单不存在'); - } - $this->assertItemEditable($order); - - $item->fill($data); - $item->product_spec = (string) ($data['product_spec'] ?? ''); - $item->amount = bcmul( - bcadd((string) $data['price'], '0', 2), - (string) $data['quantity'], - 2 - ); - $item->save(); - - $this->recalculateOrderTotals($order); - - return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray()); - }); - } - - /** - * 一键同步明细商品快照:按商品ID同步最新商品档案的 - * 供应商、品名、规格、单位、成本价;单价按门店当前等级价重算 - * (未设置等级价时保留原单价),并重算订单总价 - */ - #[PutRoute(route: '/item/{id}/sync', authorize: 'update', where: ['id' => '[0-9]+'])] - public function syncItem(int $id): JsonResponse - { - return DB::transaction(function () use ($id) { - $item = StoreOrderItemModel::query()->lockForUpdate()->find($id); - if (empty($item)) { - throw new RepositoryException('订单明细不存在'); - } - $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); - if (empty($order)) { - throw new RepositoryException('订单不存在'); - } - $this->assertItemEditable($order); - - // 软删除商品无法同步(下架商品仍可同步最新档案) - $product = ProductModel::find($item->product_id); - if (empty($product)) { - throw new RepositoryException('商品不存在或已被删除,无法同步'); - } - - $item->supplier_id = (int) $product->supplier_id; - $item->product_name = $product->name; - $item->product_spec = (string) $product->spec; - $item->unit = (string) $product->unit; - $item->cost_price = $product->cost_price; - - // 单价:按订货门店当前客户等级价重算(百分比计价按最新成本价换算) - $levelId = (int) ($order->store->level_id ?? 0); - $priceRow = ProductPriceModel::query() - ->forProductLevel((int) $item->product_id, $levelId) - ->first(['price', 'price_type', 'percent']); - if ($priceRow !== null) { - $item->price = ProductPriceModel::calcActualPrice( - (int) $priceRow->price_type, - $priceRow->price, - $priceRow->percent, - $product->cost_price, - ); - } - - $item->amount = bcmul((string) $item->price, (string) $item->quantity, 2); - $item->save(); - - $this->recalculateOrderTotals($order); - - return $this->success($item->load('supplier:id,name')->makeVisible('cost_price')->toArray()); - }); - } - - /** - * 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 + * 状态流转 */ #[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])] public function status(int $id, Request $request): JsonResponse @@ -343,7 +128,7 @@ class StoreOrderController extends BaseController $target = (int) $data['status']; if (! in_array($target, $this->allowedTransitions($order->status), true)) { throw new RepositoryException( - '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作' + '订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作' ); } @@ -356,8 +141,7 @@ class StoreOrderController extends BaseController } /** - * 批量状态流转:先全量校验流转路径,任一订单不允许则整批中止; - * 全部合法时在事务内流转并逐单通知门店 + * 批量状态流转 */ #[PutRoute(route: '/batchStatus', authorize: 'update')] public function batchStatus(Request $request): JsonResponse @@ -381,12 +165,12 @@ class StoreOrderController extends BaseController $blocked = []; foreach ($orders as $order) { if (! in_array($target, $this->allowedTransitions($order->status), true)) { - $blocked[] = $order->order_no . '(' . (self::STATUS_NAMES[$order->status] ?? $order->status) . ')'; + $blocked[] = $order->order_no . '(' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . ')'; } } if (! empty($blocked)) { throw new RepositoryException( - '以下订单不允许流转为「' . (self::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked) + '以下订单不允许流转为「' . (StoreOrderModel::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked) ); } @@ -402,7 +186,7 @@ class StoreOrderController extends BaseController } /** - * 删除订单(软删除):仅已取消订单允许删除;删除后后台列表/详情、小程序端均不可见 + * 删除订单(软删除) */ #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] public function delete(int $id): JsonResponse @@ -413,7 +197,7 @@ class StoreOrderController extends BaseController } if ($order->status !== StoreOrderModel::STATUS_CANCELLED) { throw new RepositoryException( - '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除' + '订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,仅已取消订单可删除' ); } @@ -423,31 +207,7 @@ class StoreOrderController extends BaseController } /** - * 状态流转合法路径(与迁移状态定义一致): - * 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 - * (已接单→采购中 只能经「生成采购单」完成,不在本流转内) - * - * @return int[] - */ - private function allowedTransitions(int $status): array - { - return match ($status) { - StoreOrderModel::STATUS_PENDING => [ - StoreOrderModel::STATUS_SUMMARIZED, - StoreOrderModel::STATUS_CANCELLED, - ], - StoreOrderModel::STATUS_DELIVERING => [ - StoreOrderModel::STATUS_DISTRIBUTION, - StoreOrderModel::STATUS_COMPLETED, - ], - StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED], - default => [], - }; - } - - /** - * 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改), - * 自动重算附加金额与订单总金额 + * 修改周转框/周转托盘数量 */ #[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])] public function container(int $id, Request $request): JsonResponse @@ -476,7 +236,7 @@ class StoreOrderController extends BaseController ]; if (! in_array($order->status, $editable, true)) { throw new RepositoryException( - '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量' + '订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量' ); } @@ -501,36 +261,24 @@ class StoreOrderController extends BaseController } /** - * 明细编辑状态校验:已完成/已取消订单锁定 + * 状态流转合法路径 + * + * @return int[] */ - private function assertItemEditable(StoreOrderModel $order): void + private function allowedTransitions(int $status): array { - if (! in_array($order->status, self::ITEM_EDITABLE_STATUS, true)) { - throw new RepositoryException( - '订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改商品明细' - ); - } - } - - /** - * 重算订单汇总:明细金额合计 → 商品总金额;订货量/重量合计 → 订货总量/总重量; - * 订单总金额 = 商品总金额 + 附加金额(恒成立) - */ - private function recalculateOrderTotals(StoreOrderModel $order): void - { - $totals = StoreOrderItemModel::query() - ->where('order_id', $order->id) - ->selectRaw('COALESCE(SUM(quantity), 0) as total_quantity') - ->selectRaw('COALESCE(SUM(weight), 0) as total_weight') - ->selectRaw('COALESCE(SUM(amount), 0) as product_amount') - ->first(); - - $productAmount = bcadd((string) $totals->product_amount, '0', 2); - $order->total_quantity = (int) $totals->total_quantity; - $order->total_weight = bcadd((string) $totals->total_weight, '0', 3); - $order->product_amount = $productAmount; - $order->total_amount = bcadd($productAmount, (string) $order->added_amount, 2); - $order->save(); + return match ($status) { + StoreOrderModel::STATUS_PENDING => [ + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_CANCELLED, + ], + StoreOrderModel::STATUS_DELIVERING => [ + StoreOrderModel::STATUS_DISTRIBUTION, + StoreOrderModel::STATUS_COMPLETED, + ], + StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED], + default => [], + }; } /** @@ -543,7 +291,7 @@ class StoreOrderController extends BaseController ->where('status', UserModel::STATUS_NORMAL) ->pluck('id'); - $statusName = self::STATUS_NAMES[$order->status] ?? (string) $order->status; + $statusName = StoreOrderModel::STATUS_NAMES[$order->status] ?? (string) $order->status; foreach ($userIds as $userId) { NoticeModel::create([ 'user_id' => $userId, diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index ab3d57b..2ec64fc 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -9,17 +9,17 @@ use App\Models\ProductModel; use App\Models\PurchaseOrderModel; use App\Models\StoreModel; use App\Models\StoreOrderItemModel; -use App\Services\ExportService; -use App\Services\PurchaseEditService; +use App\Models\StoreOrderModel; +use App\Services\ItemImageResolver; use App\Services\PurchaseGenerateService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\PostRoute; use Modules\AnnoRoute\Attribute\PutRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\Common\Http\Controllers\BaseController; -use Symfony\Component\HttpFoundation\Response; use Throwable; /** @@ -50,8 +50,7 @@ class PurchaseOrderController extends BaseController } /** - * 采购单详情:头 + 明细矩阵(商品行 × 门店列) - * 行 = 商品(按分类 sort → 商品 sort 排序),列 = 商品信息 + 每个门店一格(数量/金额) + * 采购单详情 */ #[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])] public function detail(int $id): JsonResponse @@ -163,8 +162,7 @@ class PurchaseOrderController extends BaseController } /** - * C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids), - * 生成后源订单转为「采购中」并回写 purchase_id(订单与订货明细同步归集) + * 生成采购单 */ #[PostRoute('/generate', 'generate')] public function generate(Request $request): JsonResponse @@ -192,29 +190,77 @@ class PurchaseOrderController extends BaseController ); } - /** C2/C3 导出采购单:?type=all|category & format=xlsx|pdf */ - #[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])] - public function export(int $id, Request $request): Response + /** + * 单元格下钻:采购单中某门店某商品的全部订货明细 + */ + #[GetRoute(route: '/{id}/cell', authorize: 'query', where: ['id' => '[0-9]+'])] + public function cell(int $id, Request $request): JsonResponse { + $productId = (int) $request->query('product_id', 0); + $storeId = (int) $request->query('store_id', 0); + if ($productId <= 0 || $storeId <= 0) { + throw new RepositoryException('缺少商品或门店参数'); + } $purchase = PurchaseOrderModel::find($id); if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } - $type = (string) $request->query('type', 'all'); - if (! in_array($type, ['all', 'category'], true)) { - throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)'); - } - return app(ExportService::class)->download( - 'purchase', - $purchase, - (string) $request->query('format', ExportService::FORMAT_XLSX), - type: $type, - ); + $items = StoreOrderItemModel::query() + ->join('store_order', 'store_order.id', '=', 'store_order_item.order_id') + ->where('store_order_item.purchase_id', $purchase->id) + ->where('store_order_item.product_id', $productId) + ->where('store_order_item.store_id', $storeId) + ->whereNull('store_order.deleted_at') + ->with('supplier:id,name') + ->select( + 'store_order_item.*', + 'store_order.order_no', + 'store_order.order_date', + 'store_order.status as order_status', + ) + ->orderBy('store_order.id') + ->get() + ->makeVisible('cost_price'); + + $rows = []; + foreach ($items as $item) { + $rows[] = [ + 'id' => $item->id, + 'order_id' => $item->order_id, + 'order_no' => $item->order_no, + 'order_date' => (string) $item->order_date, + 'order_status' => (int) $item->order_status, + 'product_name' => $item->product_name, + 'product_spec' => $item->product_spec, + 'unit' => $item->unit, + 'supplier_id' => (int) $item->supplier_id, + 'supplier' => $item->supplier ? ['id' => $item->supplier->id, 'name' => $item->supplier->name] : null, + 'price' => (string) $item->price, + 'cost_price' => (string) $item->cost_price, + 'quantity' => (int) $item->quantity, + 'weight' => (string) $item->weight, + 'amount' => (string) $item->amount, + 'remark' => (string) $item->remark, + 'image_ids' => $item->image_ids, + 'editable' => $purchase->status === PurchaseOrderModel::STATUS_PENDING + && in_array((int) $item->order_status, StoreOrderItemModel::ITEM_EDITABLE_STATUS, true), + ]; + } + app(ItemImageResolver::class)->resolve($rows); + + $store = StoreModel::withTrashed()->find($storeId); + $product = ProductModel::withTrashed()->find($productId); + + return $this->success([ + 'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null, + 'product' => $product ? ['id' => $product->id, 'name' => $product->name] : null, + 'items' => $rows, + ]); } /** - * 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细; + * 商品行修改:品名/供应商/包规/单位/成本 * @throws Throwable */ #[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])] @@ -224,25 +270,77 @@ class PurchaseOrderController extends BaseController if (empty($purchase)) { throw new RepositoryException('采购单不存在'); } + if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { + throw new RepositoryException('采购单已完成,不允许修改明细'); + } + $validated = $request->validated(); - $attrs = []; - foreach (['product_name', 'supplier_id', 'product_spec', 'unit', 'cost_price'] as $field) { - if (isset($validated[$field])) { - $attrs[$field] = $validated[$field]; + $items = StoreOrderItemModel::query() + ->where('purchase_id', $purchase->id) + ->where('product_id', $productId) + ->lockForUpdate() + ->get(); + if ($items->isEmpty()) { + throw new RepositoryException('该采购单下无此商品的订货明细'); + } + foreach ($items as $item) { + $item->fill($validated)->save(); + } + + // 重算采购单成本 + $query = StoreOrderItemModel::query()->where('purchase_id', $purchase->id); + $purchase->total_quantity = $query->sum('quantity'); + $purchase->estimate_amount = $query->sum(DB::raw('quantity * cost_price')); + $purchase->save(); + + return $this->success(['count' => $items->count()], '已同步 ' . $items->count() . ' 条订货明细'); + } + + /** + * 门店订单明细修改(订货量、重量、单价),自动重算价格 + * @throws Throwable + */ + #[PutRoute(route: '/cell/{itemId}', authorize: 'update', where: ['itemId' => '[0-9]+'])] + public function cellUpdate(int $itemId, PurchaseCellUpdateRequest $request): JsonResponse + { + $validated = $request->validated(); + + return DB::transaction(function () use ($itemId, $validated) { + $item = StoreOrderItemModel::query()->lockForUpdate()->find($itemId); + if (empty($item) || (int) $item->purchase_id === 0) { + throw new RepositoryException('采购单明细不存在'); + } + $purchase = PurchaseOrderModel::find((int) $item->purchase_id); + if (empty($purchase)) { + throw new RepositoryException('采购单不存在'); + } + if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) { + throw new RepositoryException('采购单已完成,不允许修改明细'); } - } - if ($attrs === []) { - throw new RepositoryException('品名/供应商/包规/单位/成本至少填写一项'); - } - $count = app(PurchaseEditService::class)->updateRow( - $purchase, - $productId, - $attrs, - (bool) ($validated['sync_product'] ?? false), - ); + $item->quantity = $validated['quantity']; + $item->price = $validated['price']; + if (array_key_exists('weight', $validated)) { + $item->weight = bcadd((string) $validated['weight'], '0', 3); + } + $item->amount = bcmul($validated['quantity'], $validated['price'], 3); + $item->save(); - return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细'); + // 重算订单金额 + $order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id); + $order->product_amount = $order->items()->sum('amount'); + $order->total_weight = $order->items()->sum('weight'); + $order->total_amount = bcadd($order->product_amount, $order->added_amount, 2); + $order->save(); + + // 重算采购单重量 + $purchase->total_weight = StoreOrderItemModel::query() + ->where('purchase_id', $purchase->id) + ->sum('weight'); + $purchase->save(); + + return $this->success(); + }); } } diff --git a/app/Http/Requests/Purchase/PurchaseCellUpdateRequest.php b/app/Http/Requests/Purchase/PurchaseCellUpdateRequest.php index a725e37..23f4cc6 100644 --- a/app/Http/Requests/Purchase/PurchaseCellUpdateRequest.php +++ b/app/Http/Requests/Purchase/PurchaseCellUpdateRequest.php @@ -15,6 +15,7 @@ class PurchaseCellUpdateRequest extends BaseFormRequest { return [ 'quantity' => 'required|integer|min:0', + 'price' => 'required|numeric|min:0', 'weight' => 'nullable|numeric|min:0', ]; } @@ -25,6 +26,9 @@ class PurchaseCellUpdateRequest extends BaseFormRequest 'quantity.required' => '数量不能为空', 'quantity.integer' => '数量必须为整数', 'quantity.min' => '数量不能小于 0', + 'price.required' => '单价不能为空', + 'price.numeric' => '单价必须为数字', + 'price.min' => '单价不能小于 0', 'weight.numeric' => '实际称重必须为数字', 'weight.min' => '实际称重不能小于 0', ]; diff --git a/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php b/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php index 6abf7e9..00d7a84 100644 --- a/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php +++ b/app/Http/Requests/Purchase/PurchaseRowUpdateRequest.php @@ -5,8 +5,7 @@ namespace App\Http\Requests\Purchase; use Modules\Common\Http\Requests\BaseFormRequest; /** - * 采购明细商品行修改 验证(C4;品名/供应商/包规/单位/成本/称重至少一项, - * 提交后一键同步该商品在本采购单下的全部订货明细) + * 采购明细商品行修改 */ class PurchaseRowUpdateRequest extends BaseFormRequest { @@ -15,12 +14,11 @@ 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', - 'sync_product' => 'nullable|boolean', + 'product_name' => 'required|string|max:100', + 'supplier_id' => 'required|integer|exists:supplier,id', + 'product_spec' => 'required|string|max:100', + 'unit' => 'required|string|max:20', + 'cost_price' => 'required|numeric|min:0', ]; } @@ -28,11 +26,20 @@ class PurchaseRowUpdateRequest extends BaseFormRequest { return [ 'product_name.max' => '品名不能超过 100 字', + 'product_name.required' => '品名不能为空', + 'product_name.string' => '品名必须为字符串', + 'supplier_id.supplier_id' => '供应商不能为空', + 'supplier_id.integer' => '供应商格式错误', 'supplier_id.exists' => '供应商不存在', + 'product_spec.required' => '包规不能为空', + 'product_spec.string' => '包规必须为字符串', 'product_spec.max' => '包规不能超过 100 字', + 'unit.required' => '单位不能为空', + 'unit.string' => '单位必须为字符串', 'unit.max' => '单位不能超过 20 字', - 'cost_price.numeric' => '采购成本必须为数字', - 'cost_price.min' => '采购成本不能小于 0', + 'cost_price.required' => '成本不能为空', + 'cost_price.numeric' => '成本必须为数字', + 'cost_price.min' => '成本不能小于 0', ]; } } diff --git a/app/Models/StoreOrderItemModel.php b/app/Models/StoreOrderItemModel.php index 4d87b35..442fe1c 100644 --- a/app/Models/StoreOrderItemModel.php +++ b/app/Models/StoreOrderItemModel.php @@ -60,6 +60,14 @@ class StoreOrderItemModel extends Model */ protected $hidden = ['cost_price']; + /** 明细可编辑状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */ + public const array ITEM_EDITABLE_STATUS = [ + StoreOrderModel::STATUS_PENDING, + StoreOrderModel::STATUS_SUMMARIZED, + StoreOrderModel::STATUS_DELIVERING, + StoreOrderModel::STATUS_DISTRIBUTION, + ]; + /** * 商品图片ID(逗号分隔字符串 ↔ 数组) */ diff --git a/app/Models/StoreOrderModel.php b/app/Models/StoreOrderModel.php index 8d08af7..d38ca51 100644 --- a/app/Models/StoreOrderModel.php +++ b/app/Models/StoreOrderModel.php @@ -28,6 +28,16 @@ class StoreOrderModel extends Model /** 状态:已取消 */ public const int STATUS_CANCELLED = 9; + /** 状态中文名(后台文案/订单通知用) */ + public const array STATUS_NAMES = [ + self::STATUS_PENDING => '待接单', + self::STATUS_SUMMARIZED => '已接单', + self::STATUS_DELIVERING => '采购中', + self::STATUS_DISTRIBUTION => '配送中', + self::STATUS_COMPLETED => '已完成', + self::STATUS_CANCELLED => '已取消', + ]; + protected $table = 'store_order'; protected $primaryKey = 'id'; diff --git a/app/Services/ItemImageResolver.php b/app/Services/ItemImageResolver.php new file mode 100644 index 0000000..9657ceb --- /dev/null +++ b/app/Services/ItemImageResolver.php @@ -0,0 +1,42 @@ +> $items 明细数组(引用修改:写入 image,移除 image_ids) + */ + public function resolve(array &$items): void + { + $fileIds = []; + foreach ($items as $line) { + foreach ((array) ($line['image_ids'] ?? []) as $fileId) { + if ($fileId !== '' && $fileId !== null) { + $fileIds[] = (int) $fileId; + } + } + } + $fileUrls = []; + if ($fileIds !== []) { + foreach (SysFileModel::query()->whereIn('id', array_unique($fileIds))->get() as $file) { + $fileUrls[$file->id] = $file->preview_url; + } + } + foreach ($items as &$product) { + $product['image'] = ''; + foreach ((array) ($product['image_ids'] ?? []) as $fileId) { + if ($fileId !== '' && $fileId !== null && isset($fileUrls[(int) $fileId])) { + $product['image'] = $fileUrls[(int) $fileId]; + break; + } + } + unset($product['image_ids']); + } + } +} diff --git a/app/Services/PurchaseEditService.php b/app/Services/PurchaseEditService.php deleted file mode 100644 index c6b8df2..0000000 --- a/app/Services/PurchaseEditService.php +++ /dev/null @@ -1,138 +0,0 @@ -where('purchase_id', $purchase->id) - ->where('product_id', $productId) - ->lockForUpdate() - ->get(); - if ($items->isEmpty()) { - throw new RepositoryException('该采购单下无此商品的订货明细'); - } - - $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->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(); - } - } - - $this->recalculatePurchaseTotals($purchase); - - return $items->count(); - }); - } - - /** - * 重算采购金额汇总 - */ - public function recalculatePurchaseTotals(PurchaseOrderModel $purchase): void - { - $items = StoreOrderItemModel::query() - ->where('purchase_id', $purchase->id) - ->get(['quantity', 'cost_price']); - - $totalQuantity = 0; - $estimate = '0'; - foreach ($items as $item) { - $totalQuantity += $item->quantity; - $item_estimate = bcmul($item->cost_price, $item->quantity, 2); - $estimate = bcadd($estimate, $item_estimate, 2); - } - - $purchase->total_quantity = $totalQuantity; - $purchase->estimate_amount = $estimate; - $purchase->save(); - } - - /** - * 采购成本金额:称重>0 按 称重×单价,否则按 数量×单价 - */ - public static function costAmount(string $quantity, string $weight, string $unitCost): string - { - return (float) $weight > 0 - ? bcmul($weight, $unitCost, 2) - : bcmul($quantity, $unitCost, 2); - } - - /** - * 单价 = 成本 / 包规数值(spec 解析不出正数时按 1 处理,即单价=成本) - */ - public static function unitCost(string $costPrice, string $spec): string - { - if (preg_match('/\d+(?:\.\d+)?/', $spec, $matches) === 1 && (float) $matches[0] > 0) { - return bcdiv($costPrice, $matches[0], 2); - } - - return bcadd($costPrice, '0', 2); - } -} diff --git a/app/Services/PurchaseGenerateService.php b/app/Services/PurchaseGenerateService.php index 66ec60b..611fa5c 100644 --- a/app/Services/PurchaseGenerateService.php +++ b/app/Services/PurchaseGenerateService.php @@ -58,8 +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, bcmul($item->quantity, (string) $item->cost_price, 2), 2), + static fn (string $carry, $item): string => bcadd($carry, (string) $item->amount, 2), '0' ); diff --git a/app/Services/ReconciliationBuildService.php b/app/Services/ReconciliationBuildService.php index ff3a965..18a7196 100644 --- a/app/Services/ReconciliationBuildService.php +++ b/app/Services/ReconciliationBuildService.php @@ -17,8 +17,8 @@ use Illuminate\Support\Facades\DB; * 流程(事务内,可重复 build:先清后建): * 1. 按周期 + 品类(含子孙分类)+ 供应商拉取「已完成」门店订单的订货明细 * 2. 每条订货明细 → 一条对账明细: - * publish_amount = 订货金额(order_item.amount) - * actual_amount = 采购成本(称重>0 ? 称重×单价 : 数量×单价,单价 = 成本/包规) + * publish_amount = 订货金额(order_item.amount = 每包等级价×数量) + * actual_amount = 采购成本(数量 × 每包成本价;单价/包规不参与金额计算) * diff = publish − actual,冗余 product_name / store_id * 3. 汇总写回头的 publish/actual/diff_amount,status → 对账中 */ @@ -64,15 +64,7 @@ class ReconciliationBuildService $now = now(); foreach ($orderItems as $orderItem) { $publish = (string) $orderItem->amount; - $unitCost = PurchaseEditService::unitCost( - (string) ($orderItem->cost_price ?? '0'), - (string) $orderItem->product_spec - ); - $actual = PurchaseEditService::costAmount( - (string) $orderItem->quantity, - (string) $orderItem->weight, - $unitCost - ); + $actual = bcmul((string) $orderItem->quantity, (string) ($orderItem->cost_price ?? '0'), 2); $publishTotal = bcadd($publishTotal, $publish, 2); $actualTotal = bcadd($actualTotal, $actual, 2); diff --git a/database/migrations/2026_07_23_030610_create_store_order_table.php b/database/migrations/2026_07_23_030610_create_store_order_table.php index 3b6eccb..26bb23c 100644 --- a/database/migrations/2026_07_23_030610_create_store_order_table.php +++ b/database/migrations/2026_07_23_030610_create_store_order_table.php @@ -59,7 +59,7 @@ return new class extends Migration $table->decimal('weight', 10, 3)->default(0)->comment('重量'); $table->decimal('amount', 10, 2)->default(0)->comment('单品金额'); $table->decimal('cost_price', 10, 2)->default(0)->comment('成本价'); - $table->string('remark', 255)->default('')->comment('门店下单备注'); + $table->string('remark', 255)->default('')->nullable()->comment('门店下单备注'); $table->timestamps(); $table->index(['order_id'], 'store_order_item_order_index'); $table->index(['purchase_id'], 'store_order_item_purchase_index'); diff --git a/tests/Feature/PurchaseEditTest.php b/tests/Feature/PurchaseEditTest.php index 00189b1..2724dbf 100644 --- a/tests/Feature/PurchaseEditTest.php +++ b/tests/Feature/PurchaseEditTest.php @@ -13,8 +13,8 @@ use App\Models\SupplierModel; use App\Models\UserModel; /** - * C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格数量、行级成本/称重, - * 修改同步订货明细并重算订单/采购单汇总 + * C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本, + * 修改同步订货明细并重算订单/采购单汇总(金额口径:数量(包) × 每包价格,单价/包规不参与金额计算) */ class PurchaseEditTest extends ProcurementTestCase { @@ -101,9 +101,10 @@ class PurchaseEditTest extends ProcurementTestCase $purchase = $purchase->fresh(); $this->assertSame('8.00', (string) $purchase->total_quantity, '5+3'); $this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30'); + $this->assertSame('160.00', (string) $purchase->actual_amount, '8 包 × 每包成本 20.00'); } - /** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×单价 重算 */ + /** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×每包成本 重算 */ public function test_update_row_cost_syncs_all_order_items(): void { [$purchase, $product] = $this->buildPurchase(); @@ -116,33 +117,20 @@ class PurchaseEditTest extends ProcurementTestCase $this->assertSame(2, StoreOrderItemModel::where('cost_price', '30.00')->count()); $purchase = $purchase->fresh(); - // 单价 = 30 ÷ 10 = 3.00,实际金额 = 5 × 3.00 - $this->assertSame('15.00', (string) $purchase->actual_amount); + $this->assertSame('150.00', (string) $purchase->actual_amount, '5 包 × 每包成本 30.00'); $this->assertSame('50.00', (string) $purchase->estimate_amount, '订货金额不受成本修改影响'); } - /** 行级称重修改 → 按各店数量比例分摊写入明细(尾差修正守恒),实际金额按 称重×单价 计 */ - public function test_update_row_weight_distributed_by_quantity(): void + /** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细(重量为手动录入,不受行修改影响) */ + public function test_update_row_does_not_touch_manual_weight(): void { - [$purchase, $product, $stores] = $this->buildPurchase(); + [$purchase, $product] = $this->buildPurchase(); $this->actingAsSysUser(); - $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['weight' => 10]) + $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30]) ->assertJsonPath('success', true); - $items = StoreOrderItemModel::query()->orderBy('store_id')->get()->keyBy('store_id'); - $this->assertSame('4.000', (string) $items[$stores[0]->id]->weight, '10 × 2/5'); - $this->assertSame('6.000', (string) $items[$stores[1]->id]->weight, '10 × 3/5'); - $this->assertSame( - '10.000', - bcadd((string) $items[$stores[0]->id]->weight, (string) $items[$stores[1]->id]->weight, 3), - '分摊守恒' - ); - - $purchase = $purchase->fresh(); - $this->assertSame('10.000', (string) $purchase->total_weight); - // 称重>0 → 金额按 称重×单价:10 × 2.00 - $this->assertSame('20.00', (string) $purchase->actual_amount); + $this->assertSame(0, StoreOrderItemModel::where('weight', '<>', 0)->count(), '重量保持手动录入值,不自动计算'); } /** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细 */ @@ -175,8 +163,7 @@ class PurchaseEditTest extends ProcurementTestCase $product = $product->fresh(); $this->assertNotSame('优选土豆', $product->name); - // 新单价 = 25 ÷ 5 = 5.00,实际金额 = 5 件 × 5.00 - $this->assertSame('25.00', (string) $purchase->fresh()->actual_amount); + $this->assertSame('125.00', (string) $purchase->fresh()->actual_amount, '5 包 × 每包成本 25.00'); } /** sync_product=true:订货明细与商品档案同步更新;软删除商品拒绝 */ @@ -231,4 +218,145 @@ class PurchaseEditTest extends ProcurementTestCase $this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1]) ->assertJsonPath('success', false); } + + /** 单元格下钻:返回该门店该商品的全部订货明细(订单号/状态/快照/可编辑标记),其他门店无明细 */ + public function test_cell_detail_returns_items_with_order_info(): void + { + [$purchase, $product, $stores] = $this->buildPurchase(); + $this->actingAsSysUser(); + + $response = $this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}") + ->assertOk() + ->assertJsonPath('success', true); + + $data = $response->json('data'); + $this->assertSame($stores[0]->name, $data['store']['name']); + $this->assertSame($product->name, $data['product']['name']); + $this->assertCount(1, $data['items']); + + $item = $data['items'][0]; + $this->assertArrayHasKey('order_no', $item); + $this->assertSame(2, $item['quantity']); + $this->assertSame('20.00', (string) $item['amount']); + $this->assertSame(StoreOrderModel::STATUS_DELIVERING, $item['order_status'], '生成采购单后源订单为采购中'); + $this->assertTrue($item['editable']); + + // 其他门店 → 另一条明细 + $this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[1]->id}") + ->assertOk() + ->assertJsonPath('data.items.0.quantity', 3); + } + + /** 单元格下钻参数校验:缺少商品/门店参数拒绝 */ + public function test_cell_detail_rejects_missing_params(): void + { + [$purchase] = $this->buildPurchase(); + $this->actingAsSysUser(); + + $this->getJson("/purchase/order/{$purchase->id}/cell") + ->assertJsonPath('success', false); + } + + /** 单元格称重修改 → 明细/订货单/采购单重量与金额汇总级联重算 */ + public function test_update_cell_weight_cascades_totals(): void + { + [$purchase, , $stores] = $this->buildPurchase(); + $this->actingAsSysUser(); + + $item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first(); + $this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'weight' => 4.5]) + ->assertJsonPath('success', true) + ->assertJsonPath('data.amount', 50); + + $item = $item->fresh(); + $this->assertSame('4.500', (string) $item->weight); + $this->assertSame('50.00', (string) $item->amount, '5 × 订货单价 10.00'); + + $order = StoreOrderModel::find($item->order_id); + $this->assertSame('4.500', (string) $order->total_weight); + $this->assertSame('50.00', (string) $order->product_amount); + + $purchase = $purchase->fresh(); + $this->assertSame('4.500', (string) $purchase->total_weight); + $this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30 订货金额'); + $this->assertSame('160.00', (string) $purchase->actual_amount, '8 包 × 每包成本 20.00(称重仅参考,不参与金额)'); + } + + /** 单元格一键同步:按商品ID同步档案 + 等级价重算,订货单与采购单汇总级联 */ + public function test_cell_sync_refreshes_snapshot_and_cascades(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create([ + 'status' => ProductModel::STATUS_ON, + 'cost_price' => '10.00', + 'spec' => '1斤/袋', + 'unit' => '斤', + ]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price_type' => ProductPriceModel::PRICE_TYPE_PERCENT, + 'percent' => 30, + ]); + + $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]]) + ->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(); + $item = StoreOrderItemModel::first(); + $this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%'); + $this->assertSame('26.00', (string) $purchase->estimate_amount); + + // 成本上调后同步:单价按最新成本重算,金额与两级汇总级联 + $product->update(['cost_price' => '20.00']); + + $this->putJson("/purchase/order/cell/{$item->id}/sync") + ->assertJsonPath('success', true) + ->assertJsonPath('data.amount', 52); + + $item->refresh(); + $this->assertSame('20.00', (string) $item->cost_price); + $this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00'); + $this->assertSame('52.00', (string) $item->amount, '26.00 × 2'); + + $order = StoreOrderModel::first(); + $this->assertSame('52.00', (string) $order->product_amount); + $this->assertSame('52.00', (string) $order->total_amount); + + $purchase = $purchase->fresh(); + $this->assertSame('52.00', (string) $purchase->estimate_amount); + $this->assertSame('40.00', (string) $purchase->actual_amount, '2 包 × 每包成本 20.00'); + } + + /** 采购单已完成:单元格编辑/同步、行修改均拒绝,明细不变 */ + public function test_edits_rejected_when_purchase_completed(): void + { + [$purchase, $product, $stores] = $this->buildPurchase(); + $item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first(); + $purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]); + + $this->actingAsSysUser(); + $this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5]) + ->assertJsonPath('success', false) + ->assertJsonPath('msg', '采购单已完成,不允许修改明细'); + $this->putJson("/purchase/order/cell/{$item->id}/sync") + ->assertJsonPath('success', false); + $this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30]) + ->assertJsonPath('success', false); + + $this->assertSame(2, $item->fresh()->quantity, '被拒绝后明细不变'); + $this->assertSame('20.00', (string) $item->fresh()->cost_price); + + // 下钻明细同步标记不可编辑 + $this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}") + ->assertOk() + ->assertJsonPath('data.items.0.editable', false); + } } diff --git a/web/api/order/store.ts b/web/api/order/store.ts index 562b1c2..df453c4 100644 --- a/web/api/order/store.ts +++ b/web/api/order/store.ts @@ -1,8 +1,7 @@ import createAxios from '@/utils/request'; import type IStoreOrder from '@/domain/iStoreOrder.ts'; -import type { IOrderSummaryRow, IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts'; -/** 订单详情(头 + 明细) */ +/** 订单详情 */ export async function getStoreOrder(id: number) { return createAxios({ url: `/order/store/${id}`, @@ -10,7 +9,7 @@ export async function getStoreOrder(id: number) { }); } -/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成) */ +/** 订单状态流转 */ export async function updateOrderStatus(id: number, status: number) { return createAxios({ url: `/order/store/${id}/status`, @@ -19,7 +18,7 @@ export async function updateOrderStatus(id: number, status: number) { }); } -/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */ +/** 批量订单状态流转 */ export async function batchUpdateOrderStatus(ids: number[], status: number) { return createAxios<{ success: number }>({ url: '/order/store/batchStatus', @@ -37,32 +36,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number; }); } -/** 已接单预览(按商品聚合,生成采购单前确认) */ -export async function getOrderSummary() { - return createAxios({ - url: '/order/store/summary', - method: 'get', - }); -} - -/** 修改订单明细(供应商/品名/规格/单位/单价/成本价/订货量/重量,后端重算订单总价) */ -export async function updateOrderItem(itemId: number, data: IStoreOrderItemUpdate) { - return createAxios({ - url: `/order/store/item/${itemId}`, - method: 'put', - data, - }); -} - -/** 一键同步明细商品快照为最新商品档案信息(供应商/品名/规格/单位/单价/成本价) */ -export async function syncOrderItem(itemId: number) { - return createAxios({ - url: `/order/store/item/${itemId}/sync`, - method: 'put', - }); -} - -/** 删除订单(软删除,仅已取消订单允许) */ +/** 删除订单 */ export async function deleteStoreOrder(id: number) { return createAxios({ url: `/order/store/${id}`, diff --git a/web/api/purchase/order.ts b/web/api/purchase/order.ts index e967434..9fc8a2b 100644 --- a/web/api/purchase/order.ts +++ b/web/api/purchase/order.ts @@ -1,12 +1,27 @@ import createAxios from '@/utils/request'; import type { - ExportFormat, + IPurchaseCell, IPurchaseDetail, - PurchaseExportType, } from '@/domain/iPurchaseOrder.ts'; -import { downloadBlob } from '@/api/common/download.ts'; -/** C1 合并「已接单」门店订单生成采购单(order_ids 为空 = 全部已接单;生成后源订单转采购中) */ +/** 订单商品参数修改 */ +export interface PurchaseRowUpdateParams { + product_name: string; + supplier_id: number; + product_spec: string; + unit: string; + cost_price: number; +} + +/** 修改门店订单明细参数 */ +export interface PurchaseCellUpdateParams { + quantity: number; + price: number; + weight?: number; +} + + +/** 生成采购单 */ export async function generatePurchase(purchase_date: string, order_ids?: number[]) { return createAxios<{ id: number; purchase_no: string }>({ url: '/purchase/order/generate', @@ -15,7 +30,7 @@ export async function generatePurchase(purchase_date: string, order_ids?: number }); } -/** 采购单详情(商品行 × 门店列矩阵) */ +/** 采购单详情 */ export async function getPurchaseDetail(id: number) { return createAxios({ url: `/purchase/order/${id}`, @@ -23,36 +38,29 @@ export async function getPurchaseDetail(id: number) { }); } -/** C2/C3 导出采购单(blob 下载) */ -export async function exportPurchase(id: number, type: PurchaseExportType, format: ExportFormat) { - return downloadBlob( - `/purchase/order/${id}/export`, - { type, format }, - `采购单_${id}.${format}` - ); -} - -/** 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 商品行修改 */ -export async function updatePurchaseRow( - purchaseId: number, - productId: number, - data: PurchaseRowUpdateParams, -) { +/** 商品行修改 */ +export async function updatePurchaseRow(purchaseId: number, productId: number, data: PurchaseRowUpdateParams) { return createAxios<{ count: number }>({ url: `/purchase/order/${purchaseId}/row/${productId}`, method: 'put', data, }); } + +/** 单元格下钻:采购单中某门店某商品的全部订货明细 */ +export async function getPurchaseCell(purchaseId: number, productId: number, storeId: number) { + return createAxios({ + url: `/purchase/order/${purchaseId}/cell`, + method: 'get', + params: { product_id: productId, store_id: storeId }, + }); +} + +/** 门店订单明细修改(订货量、重量、单价),自动重算价格 */ +export async function updatePurchaseCellItem(itemId: number, data: PurchaseCellUpdateParams) { + return createAxios({ + url: `/purchase/order/cell/${itemId}`, + method: 'put', + data, + }); +} diff --git a/web/domain/iPurchaseOrder.ts b/web/domain/iPurchaseOrder.ts index 63e0d0b..10d5035 100644 --- a/web/domain/iPurchaseOrder.ts +++ b/web/domain/iPurchaseOrder.ts @@ -50,6 +50,40 @@ export interface IPurchaseDetail { items: IPurchaseDetailRow[]; } +/** 单元格下钻明细行(溯源订货单明细,附订单号/状态/可编辑标记) */ +export interface IPurchaseCellItem { + id: number; + order_id: number; + order_no: string; + order_date: string; + /** 订货单状态:0待接单 1已接单 2采购中 3配送中 4已完成 9已取消 */ + order_status: number; + product_name: string; + product_spec: string; + unit: string; + supplier_id: number; + supplier?: { id: number; name: string } | null; + /** 等级单价 */ + price: string; + cost_price: string; + quantity: number; + weight: string; + /** 金额 = 数量×单价(整数金额返回 number) */ + amount: number | string; + remark: string; + /** 首图 */ + image?: string; + /** 是否可编辑/同步(采购单进行中且订货单未锁定) */ + editable: boolean; +} + +/** 单元格下钻数据(门店 + 商品 + 全部订货明细) */ +export interface IPurchaseCell { + store: { id: number; name: string } | null; + product: { id: number; name: string } | null; + items: IPurchaseCellItem[]; +} + export const PURCHASE_STATUS_MAP: Record = { 0: { text: '进行中', color: 'processing' }, 3: { text: '已完成', color: 'success' }, diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index f3d80c2..24009d0 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -6,13 +6,11 @@ import { Drawer, Empty, Form, - Input, InputNumber, message, Modal, Popconfirm, Radio, - Select, Space, Tag, Typography, @@ -26,33 +24,25 @@ import type { XinTableProps, } from '@/components/XinTable/typings.ts'; import type IStoreOrder from '@/domain/iStoreOrder.ts'; -import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus, - updateOrderItem, - syncOrderItem, deleteStoreOrder, } from '@/api/order/store.ts'; import { generatePurchase } from '@/api/purchase/order.ts'; import { getStoreOptions } from '@/api/customer/store.ts'; -import { getSupplierOptions } from '@/api/customer/supplier.ts'; import type IStore from '@/domain/iStore.ts'; -import type ISupplier from '@/domain/iSupplier.ts'; import AuthButton from '@/components/AuthButton'; -import { DeleteOutlined, EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons'; -import TextArea from "antd/es/input/TextArea"; +import { DeleteOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons'; const { Title, Text } = Typography; /** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */ const CONTAINER_EDITABLE_STATUS = [1, 2, 3]; -/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */ -const ITEM_EDITABLE_STATUS = [0, 1, 2, 3]; /** * 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 @@ -99,18 +89,9 @@ const StoreOrderPage: React.FC = () => { const [generateLoading, setGenerateLoading] = useState(false); const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>(); - /** 供应商选项(明细编辑用) */ - const [suppliers, setSuppliers] = useState([]); - - /** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */ - const [itemEditOpen, setItemEditOpen] = useState(false); - const [itemEditTarget, setItemEditTarget] = useState(null); - const [itemSaving, setItemSaving] = useState(false); - const [itemForm] = Form.useForm(); useEffect(() => { getStoreOptions().then((res) => setStores(res.data.data ?? [])); - getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); }, []); const openDetail = async (id: number) => { @@ -184,47 +165,6 @@ const StoreOrderPage: React.FC = () => { } }; - /** 打开明细编辑弹窗(商品快照 + 订货量/重量) */ - const openItemEdit = (item: IStoreOrderItem) => { - setItemEditTarget(item); - itemForm.setFieldsValue({ - supplier_id: item.supplier_id ?? 0, - product_name: item.product_name ?? '', - product_spec: item.product_spec ?? '', - unit: item.unit ?? '', - price: Number(item.price ?? 0), - cost_price: Number(item.cost_price ?? 0), - quantity: Number(item.quantity ?? 0), - weight: Number(item.weight ?? 0), - remark: item.remark ?? '', - }); - setItemEditOpen(true); - }; - - /** 保存明细修改:后端重算单品金额与订单总价 */ - const handleItemSave = async (values: IStoreOrderItemUpdate) => { - if (!itemEditTarget?.id || !detail?.id) return; - setItemSaving(true); - try { - await updateOrderItem(itemEditTarget.id, values); - message.success('明细已更新,订单总价已重算'); - setItemEditOpen(false); - await openDetail(detail.id); - await tableRef.current?.reload(); - } finally { - setItemSaving(false); - } - }; - - /** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */ - const handleItemSync = async (item: IStoreOrderItem) => { - if (!item.id || !detail?.id) return; - await syncOrderItem(item.id); - message.success('已同步最新商品信息,订单总价已重算'); - await openDetail(detail.id); - await tableRef.current?.reload(); - }; - /** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */ const handleDelete = async (id: number) => { await deleteStoreOrder(id); @@ -240,9 +180,6 @@ const StoreOrderPage: React.FC = () => { + watchTrayNum * Number(containerOrder?.tray_price ?? 0); const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded; - /** 明细行编辑/同步按钮(仅未完成、未取消订单可见) */ - const itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1); - const columns: XinTableColumn[] = [ { title: '订单号', @@ -622,8 +559,8 @@ const StoreOrderPage: React.FC = () => {
供应商
单价
订货量
-
金额
- {itemEditable ?
操作
: null} +
订货金额
+
重量
{(detail.items ?? []).map((item) => (
@@ -645,9 +582,8 @@ const StoreOrderPage: React.FC = () => {
{item.product_name}
- {item.product_spec || '-'} / {item.unit || '-'} · 成本价 ¥ - {item.cost_price ?? '0.00'} - {Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''} + {item.product_spec || '-'} {item.unit || '-'} +
成本:¥{item.cost_price ?? '0.00'}
{item.remark ? (
备注:{item.remark}
@@ -660,42 +596,18 @@ const StoreOrderPage: React.FC = () => {
¥{item.amount}
- {itemEditable ? ( -
- - - - - - handleItemSync(item)} - > - - - - -
- ) : null} +
+ {item.weight ?? '-'} 斤 +
))} - {(detail.items ?? []).length === 0 ? ( + {(detail.items ?? []).length === 0 && ( - ) : null} + )}
{/* 附加信息 */} @@ -828,87 +740,6 @@ const StoreOrderPage: React.FC = () => { - - {/* 编辑商品明细(保存后后端重算单品金额与订单总价) */} - setItemEditOpen(false)} - onOk={() => itemForm.submit()} - confirmLoading={itemSaving} - okText="保存" - width={640} - destroyOnHidden - > -
- 订单 {detail?.order_no};修改保存后,系统将自动重算单品金额与订单总价。 -
-
-
- - - - - - - - - - - - - - - - - - - - - - -