From 228212f8e3f7685607dc467f947240b7a068a68a Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Fri, 14 Aug 2026 21:28:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phpunit.result.cache | 2 +- app/Exports/PurchaseOrderExport.php | 11 +- .../Controllers/Mini/NoticeController.php | 5 +- .../Order/StoreOrderController.php | 16 +- .../Purchase/PurchaseOrderController.php | 8 +- app/Http/Controllers/Recon/BillController.php | 40 ++-- .../Controllers/Recon/PaymentController.php | 14 ++ app/Models/StoreModel.php | 1 + app/Services/BillGenerateService.php | 9 +- .../2026_07_23_030608_create_store_table.php | 1 + tests/Feature/BillPaymentTest.php | 209 ++++++++++++++++++ tests/Feature/ExportTest.php | 33 +-- tests/Feature/MiniNoticeTest.php | 127 +++++++++++ tests/Feature/StoreOrderTest.php | 64 ++++++ web/api/purchase/order.ts | 7 +- web/domain/iPurchaseOrder.ts | 1 - web/domain/iStore.ts | 2 + web/pages/customer/store.tsx | 10 + web/pages/order/store.tsx | 10 +- web/pages/purchase/order.tsx | 19 +- 20 files changed, 489 insertions(+), 100 deletions(-) create mode 100644 tests/Feature/BillPaymentTest.php create mode 100644 tests/Feature/MiniNoticeTest.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index b3fe115..28244b5 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":8,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":7,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":8,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":8,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":8},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.006,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.007,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.008,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.011,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.01,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.019,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.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.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.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.007,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.006,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.054,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.017,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.017,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.004,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.009,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.011,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.01,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.008,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.006,"Tests\\Feature\\CartTest::test_delete_item":0.01,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.009,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.013,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.002,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.009,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.01,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":0.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.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.006,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.007,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":0.008,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":0.011,"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.008,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.016,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.023,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.003,"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.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.012,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.023,"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.012,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.015,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.014,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.02,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.012,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.011,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.013,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.017,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.019,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.042,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.013,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.023,"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.019,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.022,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.019,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.019,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.02,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.012,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.02,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.019,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.02,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.022,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.021,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.007,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_container_summary":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_container_syncs_order_amounts":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejects_incomplete_or_foreign_orders":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_container_validates_negative_and_duplicate_orders":0.018,"Tests\\Feature\\ProductCategoryTest::test_create_child_category_under_top_level":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.006,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.007,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.005,"Tests\\Feature\\MiniBillTest::test_bill_list_derives_pay_state_and_settlement_date":0.047,"Tests\\Feature\\MiniBillTest::test_bill_list_summary_aggregates_unpaid":0.007,"Tests\\Feature\\MiniBillTest::test_bill_list_payable_filter":0.007,"Tests\\Feature\\MiniBillTest::test_bill_list_filters_by_bill_date_range":0.012,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.009,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.013,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.019,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.008,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.069,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.012,"Tests\\Feature\\BillExportTest::test_export_empty_category_rejected":0.04,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":0.009,"Tests\\Feature\\BillExportTest::test_export_invalid_ids_rejected":0.01,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.016,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.005}} \ No newline at end of file +{"version":2,"defects":{"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":8,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":8,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":7,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":8,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":8,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":8,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":8,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":8,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":8,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":8,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":7,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":7,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":7,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":8,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":7,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":8,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":8,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":7,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":7,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":7,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":5,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":7,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_cost_only_notifies_percent_level_stores":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":7,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":8,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":8,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":7,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":7,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":8,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":8,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":8,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":8,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_order_uses_actual_price_for_percent_row":8,"Tests\\Feature\\ProductCostPriceTest::test_mini_cart_uses_actual_price_for_percent_row":8,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":8,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":8,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":8,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":8,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":7,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":7,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":7,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":7,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":7,"Tests\\Feature\\ExportTest::test_export_pdf":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":7,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":7,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":7,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":7,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":7,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":7,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":7,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":8,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":7,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":8,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":8,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":8,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":7,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":7},"times":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.003,"Tests\\Feature\\ProductPriceTest::test_product_list_returns_price_for_store_level":0.006,"Tests\\Feature\\ProductPriceTest::test_store_without_level_is_rejected":0.007,"Tests\\Feature\\ProductPriceTest::test_batch_price_updates_and_notifies_affected_stores":0.01,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.007,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.012,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.011,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.016,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.017,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.017,"Tests\\Feature\\StoreOrderTest::test_order_data_isolated_between_stores":0.024,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_orders_by_product":0.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_writes_back_order_status":0.032,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.036,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.076,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.028,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.021,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.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.039,"Tests\\Feature\\ExportTest::test_export_category_filters_to_vegetables":0.017,"Tests\\Feature\\ExportTest::test_export_pdf":0.015,"Tests\\Feature\\ExportTest::test_export_invalid_format_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_requires_export_permission":0.017,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\MiniAuthTest::test_login_fails_when_wechat_rejects_code":0.007,"Tests\\Feature\\CartTest::test_add_to_cart_succeeds":0.007,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.006,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.008,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.006,"Tests\\Feature\\CartTest::test_list_with_prices_amounts_and_totals":0.012,"Tests\\Feature\\CartTest::test_list_marks_off_shelf_item_unbuyable":0.011,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.008,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.008,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.007,"Tests\\Feature\\CartTest::test_delete_item":0.009,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_clear_only_own_cart":0.015,"Tests\\Feature\\CartTest::test_unauthenticated_returns_401":0.003,"Tests\\Feature\\CartTest::test_cart_isolated_between_users":0.01,"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.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.01,"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.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_percent_row_update":0.008,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_mixed_row_updates_cost_and_level":0.009,"Tests\\Feature\\ProductCostPriceTest::test_batch_price_empty_row_rejected":0.007,"Tests\\Feature\\ProductCostPriceTest::test_price_matrix_includes_cost_and_percent_columns":0.009,"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.005,"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.009,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.01,"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.006,"Tests\\Feature\\MiniStoreTest::test_requires_bound_store":0.007,"Tests\\Feature\\MiniStoreTest::test_update_info_validates_length":0.005,"Tests\\Feature\\StoreOrderTest::test_container_update_recalculates_amounts":0.012,"Tests\\Feature\\StoreOrderTest::test_container_update_allowed_in_editable_statuses":0.022,"Tests\\Feature\\StoreOrderTest::test_container_update_rejected_in_other_statuses":0.019,"Tests\\Feature\\StoreOrderTest::test_container_update_heals_legacy_product_amount":0.02,"Tests\\Feature\\StoreOrderTest::test_container_update_validates_negative_numbers":0.014,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_accepted_orders_fails":0.023,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.033,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.013,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.017,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.015,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.018,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.02,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.016,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_updates_snapshot_from_product":0.022,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_recalculates_percent_price_with_latest_cost":0.017,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.022,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.024,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.022,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.054,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.023,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.023,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_cost_syncs_all_order_items":0.02,"Tests\\Feature\\PurchaseEditTest::test_update_row_weight_distributed_by_quantity":0.032,"Tests\\Feature\\PurchaseEditTest::test_update_row_requires_at_least_one_field":0.019,"Tests\\Feature\\PurchaseEditTest::test_update_cell_rejects_negative_quantity":0.02,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.037,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.023,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.021,"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.013,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.02,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.019,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.023,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.018,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.028,"Tests\\Feature\\ExportTest::test_export_invalid_type_rejected":0.016,"Tests\\Feature\\ExportTest::test_export_missing_purchase_rejected":0.009,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_container_summary":0.022,"Tests\\Feature\\PurchaseEditTest::test_update_container_syncs_order_amounts":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejected_when_purchase_completed":0.018,"Tests\\Feature\\PurchaseEditTest::test_update_container_rejects_incomplete_or_foreign_orders":0.026,"Tests\\Feature\\PurchaseEditTest::test_update_container_validates_negative_and_duplicate_orders":0.018,"Tests\\Feature\\ProductCategoryTest::test_create_child_category_under_top_level":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_second_level_move_to_another_top":0.007,"Tests\\Feature\\ProductCategoryTest::test_update_parent_with_children_cannot_move_under_category":0.01,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.007,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.008,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_leaf_category_ok":0.007,"Tests\\Feature\\ProductCategoryTest::test_mini_categories_returns_all_enabled_categories":0.006,"Tests\\Feature\\MiniBillTest::test_bill_list_derives_pay_state_and_settlement_date":0.01,"Tests\\Feature\\MiniBillTest::test_bill_list_summary_aggregates_unpaid":0.008,"Tests\\Feature\\MiniBillTest::test_bill_list_payable_filter":0.007,"Tests\\Feature\\MiniBillTest::test_bill_list_filters_by_bill_date_range":0.012,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.008,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.008,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.023,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.029,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.011,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.06,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.011,"Tests\\Feature\\BillExportTest::test_export_empty_category_rejected":0.028,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":0.008,"Tests\\Feature\\BillExportTest::test_export_invalid_ids_rejected":0.011,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.013,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.005,"Tests\\Feature\\MiniNoticeTest::test_notice_list_includes_personal_and_broadcast":0.006,"Tests\\Feature\\MiniNoticeTest::test_broadcast_read_creates_personal_copy":0.008,"Tests\\Feature\\MiniNoticeTest::test_personal_notice_read_marks_directly":0.006,"Tests\\Feature\\MiniNoticeTest::test_other_users_notice_not_found":0.006,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_only_accept_and_cancel":0.027,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_rejects_delivery_and_completion":0.036,"Tests\\Feature\\StoreOrderTest::test_batch_status_only_accept_and_cancel":0.026,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":0.022,"Tests\\Feature\\BillPaymentTest::test_manual_pay_accumulates_store_total_purchase_amount":0.027,"Tests\\Feature\\BillPaymentTest::test_payment_audit_pass_accumulates_store_total_purchase_amount":0.011,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":0.008}} \ No newline at end of file diff --git a/app/Exports/PurchaseOrderExport.php b/app/Exports/PurchaseOrderExport.php index 89a593d..db9cdd6 100644 --- a/app/Exports/PurchaseOrderExport.php +++ b/app/Exports/PurchaseOrderExport.php @@ -33,16 +33,14 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, /** * @param PurchaseOrderModel $purchase 采购单 - * @param string $type all 全品类 / category 仅蔬果分类 */ public function __construct( private readonly PurchaseOrderModel $purchase, - private readonly string $type = 'all', ) { } /** - * 导出行(商品聚合行,按 分类sort → 商品sort 排序;蔬果分类时按顶级分类名过滤) + * 导出行(商品聚合行,按 分类sort → 商品sort 排序) */ public function collection(): Collection { @@ -114,13 +112,6 @@ class PurchaseOrderExport implements FromCollection, WithHeadings, WithMapping, $items = collect($rows); $this->loadLookups($items); - if ($this->type === 'category') { - $items = $items->filter(function (array $row): bool { - $rootName = $this->categoryNames[$row['product_id']] ?? ''; - return str_contains($rootName, '蔬菜') || str_contains($rootName, '水果'); - })->values(); - } - $sort = 1; return $this->items = $items->map(static function (array $row) use (&$sort): array { $row['sort'] = $sort++; diff --git a/app/Http/Controllers/Mini/NoticeController.php b/app/Http/Controllers/Mini/NoticeController.php index f7da20e..69714be 100644 --- a/app/Http/Controllers/Mini/NoticeController.php +++ b/app/Http/Controllers/Mini/NoticeController.php @@ -30,7 +30,10 @@ class NoticeController extends BaseMiniController $readBroadcastIds = NoticeModel::query() ->where('user_id', $user->id) ->whereNotNull('data->broadcast_from') - ->pluck('data->broadcast_from'); + ->pluck('data') + ->map(static fn (?array $data) => $data['broadcast_from'] ?? null) + ->filter() + ->values(); $query = NoticeModel::query()->where(function ($q) use ($user, $readBroadcastIds) { $q->where('user_id', $user->id) diff --git a/app/Http/Controllers/Order/StoreOrderController.php b/app/Http/Controllers/Order/StoreOrderController.php index 89cb66f..7fea0fe 100644 --- a/app/Http/Controllers/Order/StoreOrderController.php +++ b/app/Http/Controllers/Order/StoreOrderController.php @@ -101,10 +101,10 @@ class StoreOrderController extends BaseController public function status(int $id, Request $request): JsonResponse { $data = $request->validate([ - 'status' => 'required|integer|in:1,3,4,9', + 'status' => 'required|integer|in:1,9', ], [ 'status.required' => '目标状态不能为空', - 'status.in' => '目标状态值不正确', + 'status.in' => '目标状态仅支持接单或取消订单', ]); $order = StoreOrderModel::find($id); @@ -136,13 +136,13 @@ class StoreOrderController extends BaseController $data = $request->validate([ 'ids' => 'required|array|min:1', 'ids.*' => 'integer|exists:store_order,id', - 'status' => 'required|integer|in:1,3,4,9', + 'status' => 'required|integer|in:1,9', ], [ 'ids.required' => '请选择要流转的订单', 'ids.min' => '请选择要流转的订单', 'ids.*.exists' => '订单不存在', 'status.required' => '目标状态不能为空', - 'status.in' => '目标状态值不正确', + 'status.in' => '目标状态仅支持接单或取消订单', ]); $target = (int) $data['status']; @@ -196,6 +196,9 @@ class StoreOrderController extends BaseController /** * 状态流转合法路径 * + * 手动流转仅保留「接单」「取消订单」;后续状态由业务链路自动推进: + * 生成采购单 → 采购中;采购单完成 → 配送中;生成账单 → 已完成 + * * @return int[] */ private function allowedTransitions(int $status): array @@ -205,11 +208,6 @@ class StoreOrderController extends BaseController StoreOrderModel::STATUS_SUMMARIZED, StoreOrderModel::STATUS_CANCELLED, ], - StoreOrderModel::STATUS_DELIVERING => [ - StoreOrderModel::STATUS_DISTRIBUTION, - StoreOrderModel::STATUS_COMPLETED, - ], - StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED], default => [], }; } diff --git a/app/Http/Controllers/Purchase/PurchaseOrderController.php b/app/Http/Controllers/Purchase/PurchaseOrderController.php index 231032a..8453f26 100644 --- a/app/Http/Controllers/Purchase/PurchaseOrderController.php +++ b/app/Http/Controllers/Purchase/PurchaseOrderController.php @@ -252,19 +252,15 @@ class PurchaseOrderController extends BaseController * @throws */ #[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])] - public function export(int $id, Request $request): Response + public function export(int $id): Response { $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 Excel::download( - new PurchaseOrderExport($purchase, $type), + new PurchaseOrderExport($purchase), $purchase->purchase_no . '_采购单.xlsx', ); } diff --git a/app/Http/Controllers/Recon/BillController.php b/app/Http/Controllers/Recon/BillController.php index 545ec7e..e66e9ff 100644 --- a/app/Http/Controllers/Recon/BillController.php +++ b/app/Http/Controllers/Recon/BillController.php @@ -5,9 +5,11 @@ namespace App\Http\Controllers\Recon; use App\Exceptions\RepositoryException; use App\Exports\BillExport; use App\Models\BillModel; +use App\Models\StoreModel; use App\Services\BillDetailService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; use Maatwebsite\Excel\Facades\Excel; use Modules\AnnoRoute\Attribute\GetRoute; use Modules\AnnoRoute\Attribute\PutRoute; @@ -119,7 +121,8 @@ class BillController extends BaseController } /** - * 确认收款:线下收款后手动登记付款信息,支付状态置为已支付 + * 确认收款:线下收款后手动登记付款信息,支付状态置为已支付; + * 累加门店总采购金额(只统计商品金额) */ #[PutRoute(route: '/{id}/pay', authorize: 'pay', where: ['id' => '[0-9]+'])] public function pay(int $id, Request $request): JsonResponse @@ -132,20 +135,29 @@ class BillController extends BaseController 'pay_remark.max' => '付款备注超过最大长度', ]); - $bill = BillModel::find($id); - if (empty($bill)) { - throw new RepositoryException('账单不存在'); - } - if ($bill->status === BillModel::STATUS_PAID) { - throw new RepositoryException('账单已支付,请勿重复收款'); - } + return DB::transaction(function () use ($id, $data, $request) { + $bill = BillModel::query()->lockForUpdate()->find($id); + if (empty($bill)) { + throw new RepositoryException('账单不存在'); + } + if ($bill->status === BillModel::STATUS_PAID) { + throw new RepositoryException('账单已支付,请勿重复收款'); + } - $bill->status = BillModel::STATUS_PAID; - $bill->paid_at = $data['paid_at'] ?? now(); - $bill->pay_remark = (string) ($data['pay_remark'] ?? ''); - $bill->paid_operator_id = (int) $request->user()->id; - $bill->save(); + $bill->status = BillModel::STATUS_PAID; + $bill->paid_at = $data['paid_at'] ?? now(); + $bill->pay_remark = (string) ($data['pay_remark'] ?? ''); + $bill->paid_operator_id = (int) $request->user()->id; + $bill->save(); - return $this->success([], '收款已登记'); + // 累加门店总采购金额(只统计商品金额,不含配送费/附加金额) + $store = StoreModel::query()->lockForUpdate()->find($bill->store_id); + if ($store !== null) { + $store->total_purchase_amount = bcadd((string) $store->total_purchase_amount, (string) $bill->product_amount, 2); + $store->save(); + } + + return $this->success([], '收款已登记'); + }); } } diff --git a/app/Http/Controllers/Recon/PaymentController.php b/app/Http/Controllers/Recon/PaymentController.php index 4b23aee..537a4f1 100644 --- a/app/Http/Controllers/Recon/PaymentController.php +++ b/app/Http/Controllers/Recon/PaymentController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Recon; use App\Exceptions\RepositoryException; use App\Models\BillModel; use App\Models\PaymentModel; +use App\Models\StoreModel; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -112,6 +113,19 @@ class PaymentController extends BaseController 'paid_operator_id' => $auditorId, 'pay_remark' => $methodName . '(支付单号 ' . $payment->payment_no . ')', ]); + + // 按门店累加总采购金额(只统计商品金额,不含配送费/附加金额) + foreach ($bills->groupBy('store_id')->sortKeys() as $storeId => $storeBills) { + $amount = '0'; + foreach ($storeBills as $storeBill) { + $amount = bcadd($amount, (string) $storeBill->product_amount, 2); + } + $store = StoreModel::query()->lockForUpdate()->find((int) $storeId); + if ($store !== null) { + $store->total_purchase_amount = bcadd((string) $store->total_purchase_amount, $amount, 2); + $store->save(); + } + } $payment->status = PaymentModel::STATUS_APPROVED; } else { // 拒绝:释放账单,门店可重新发起付款 diff --git a/app/Models/StoreModel.php b/app/Models/StoreModel.php index 3d4ea32..f62211f 100644 --- a/app/Models/StoreModel.php +++ b/app/Models/StoreModel.php @@ -40,6 +40,7 @@ class StoreModel extends Model 'payment_cycle_days' => 'integer', 'pending_box_num' => 'integer', 'pending_tray_num' => 'integer', + 'total_purchase_amount' => 'decimal:2', 'status' => 'integer', ]; diff --git a/app/Services/BillGenerateService.php b/app/Services/BillGenerateService.php index 5454b66..a8cd273 100644 --- a/app/Services/BillGenerateService.php +++ b/app/Services/BillGenerateService.php @@ -19,7 +19,7 @@ use Throwable; * 1. 锁定采购单全部有效订单,按门店分组,校验提交门店完整覆盖 * 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改) * 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照) - * 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联 + * 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联,订单状态置为已完成 */ readonly class BillGenerateService { @@ -111,10 +111,13 @@ readonly class BillGenerateService 'operator_id' => $operatorId, ]); - // 关联该门店在采购单中的全部订单与订单明细到账单 + // 关联该门店在采购单中的全部订单与订单明细到账单,订单转入「已完成」 StoreOrderModel::query() ->whereIn('id', $storeOrders->pluck('id')) - ->update(['bill_id' => $bill->id]); + ->update([ + 'bill_id' => $bill->id, + 'status' => StoreOrderModel::STATUS_COMPLETED, + ]); StoreOrderItemModel::query() ->whereIn('order_id', $storeOrders->pluck('id')) ->update(['bill_id' => $bill->id]); 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 dd551d9..7434ea9 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -62,6 +62,7 @@ return new class extends Migration $table->integer('payment_cycle_days')->default(0)->comment('回款周期(天)'); $table->integer('pending_box_num')->default(0)->comment('待回筐数量(生成账单压筐累加,回筐登记扣减)'); $table->integer('pending_tray_num')->default(0)->comment('待回托盘数量'); + $table->decimal('total_purchase_amount', 12, 2)->default(0)->comment('总采购金额(只统计商品金额,账单支付后累加)'); $table->integer('status')->default(1)->comment('状态(1正常 0停用)'); $table->string('remark', 255)->nullable()->default('')->comment('备注'); $table->timestamps(); diff --git a/tests/Feature/BillPaymentTest.php b/tests/Feature/BillPaymentTest.php new file mode 100644 index 0000000..e475db9 --- /dev/null +++ b/tests/Feature/BillPaymentTest.php @@ -0,0 +1,209 @@ +create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create([ + 'product_id' => $product->id, + 'level_id' => $level->id, + 'price' => '5.00', + ]); + $user = UserModel::factory()->forStore($store->id)->create(); + + $this->actingAsMiniUser($user); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]]) + ->assertJsonPath('success', true); + $order = StoreOrderModel::where('store_id', $store->id)->first(); + + $this->actingAsSysUser(); + // 接单 + $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) + ->assertJsonPath('success', true); + // 生成采购单 → 采购中 + $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) + ->assertJsonPath('success', true); + $purchase = PurchaseOrderModel::first(); + // 完成采购单 → 配送中 + $this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true); + // 生成账单 → 已完成 + $this->postJson("/purchase/order/{$purchase->id}/bill", [ + 'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]], + ])->assertJsonPath('success', true); + + return [$store, $order->fresh(), BillModel::where('store_id', $store->id)->first()]; + } + + /** 造一张指定商品金额的未支付账单(总额=商品金额) */ + private function makeBill(StoreModel $store, string $productAmount, array $attributes = []): BillModel + { + return BillModel::create(array_merge([ + 'bill_no' => 'ZD' . random_int(100000000000, 999999999999), + 'purchase_id' => PurchaseOrderModel::factory()->create()->id, + 'store_id' => $store->id, + 'bill_date' => '2026-08-10', + 'product_amount' => $productAmount, + 'delivery_fee' => '0.00', + 'box_num' => 0, + 'tray_num' => 0, + 'box_price' => '0.00', + 'tray_price' => '0.00', + 'added_amount' => '0.00', + 'total_amount' => $productAmount, + 'status' => BillModel::STATUS_UNPAID, + ], $attributes)); + } + + /** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */ + public function test_order_status_progresses_via_business_chain(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); + ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '5.00']); + $user = UserModel::factory()->forStore($store->id)->create(); + + $this->actingAsMiniUser($user); + $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]]) + ->assertJsonPath('success', true); + $order = StoreOrderModel::where('store_id', $store->id)->first(); + $this->assertSame(StoreOrderModel::STATUS_PENDING, $order->status); + + $this->actingAsSysUser(); + // 接单 → 已接单 + $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) + ->assertJsonPath('success', true); + $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status); + + // 生成采购单 → 采购中 + $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) + ->assertJsonPath('success', true); + $purchase = PurchaseOrderModel::first(); + $order->refresh(); + $this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->status); + $this->assertSame($purchase->id, $order->purchase_id); + + // 完成采购单 → 配送中 + $this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true); + $this->assertSame(StoreOrderModel::STATUS_DISTRIBUTION, $order->fresh()->status); + + // 生成账单 → 已完成 + 回写 bill_id + $this->postJson("/purchase/order/{$purchase->id}/bill", [ + 'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]], + ])->assertJsonPath('success', true); + $order->refresh(); + $bill = BillModel::where('store_id', $store->id)->first(); + $this->assertNotNull($bill); + $this->assertSame(StoreOrderModel::STATUS_COMPLETED, $order->status); + $this->assertSame($bill->id, $order->bill_id); + $this->assertSame('20.00', (string) $bill->product_amount, '5.00 × 4'); + } + + /** 后台手动确认收款:累加门店总采购金额(只统计商品金额),重复收款拒绝不重复累加 */ + public function test_manual_pay_accumulates_store_total_purchase_amount(): void + { + [$store, , $bill] = $this->makeBillViaChain(); + $this->actingAsSysUser(); + + $this->putJson("/recon/bill/{$bill->id}/pay", ['pay_remark' => '线下现金']) + ->assertJsonPath('success', true); + + $store->refresh(); + $this->assertSame('20.00', (string) $store->total_purchase_amount, '只累加商品金额 20.00'); + + // 第二笔账单收款后继续累加 + $bill2 = $this->makeBill($store, '100.00'); + $this->putJson("/recon/bill/{$bill2->id}/pay")->assertJsonPath('success', true); + $this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount); + + // 重复收款拒绝,金额不重复累加 + $this->putJson("/recon/bill/{$bill->id}/pay")->assertJsonPath('success', false); + $this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount); + } + + /** 支付审核通过:关联账单置已支付并累加门店总采购金额 */ + public function test_payment_audit_pass_accumulates_store_total_purchase_amount(): void + { + $store = StoreModel::factory()->create(); + $user = UserModel::factory()->forStore($store->id)->create(); + + $payment = PaymentModel::create([ + 'payment_no' => 'ZF202608140001', + 'store_id' => $store->id, + 'user_id' => $user->id, + 'amount' => '150.00', + 'pay_method' => PaymentModel::METHOD_BANK, + 'voucher_ids' => '', + 'status' => PaymentModel::STATUS_PENDING, + ]); + $bill1 = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]); + $bill2 = $this->makeBill($store, '50.00', ['payment_id' => $payment->id]); + + $this->actingAsSysUser(); + $this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass']) + ->assertJsonPath('success', true); + + $this->assertSame(BillModel::STATUS_PAID, $bill1->fresh()->status); + $this->assertSame(BillModel::STATUS_PAID, $bill2->fresh()->status); + $this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount); + + // 重复审核拒绝,不重复累加 + $this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass']) + ->assertJsonPath('success', false); + $this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount); + } + + /** 审核拒绝:账单释放回待支付,不累加门店总采购金额 */ + public function test_payment_audit_reject_does_not_accumulate(): void + { + $store = StoreModel::factory()->create(); + $user = UserModel::factory()->forStore($store->id)->create(); + + $payment = PaymentModel::create([ + 'payment_no' => 'ZF202608140002', + 'store_id' => $store->id, + 'user_id' => $user->id, + 'amount' => '100.00', + 'pay_method' => PaymentModel::METHOD_WECHAT, + 'voucher_ids' => '', + 'status' => PaymentModel::STATUS_PENDING, + ]); + $bill = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]); + + $this->actingAsSysUser(); + $this->putJson("/recon/payment/{$payment->id}/audit", [ + 'result' => 'reject', + 'audit_remark' => '凭证不清晰', + ])->assertJsonPath('success', true); + + $bill->refresh(); + $this->assertSame(BillModel::STATUS_UNPAID, $bill->status); + $this->assertSame(0, $bill->payment_id, '账单释放可重新付款'); + $this->assertSame('0.00', (string) $store->fresh()->total_purchase_amount); + } +} diff --git a/tests/Feature/ExportTest.php b/tests/Feature/ExportTest.php index be2e5a4..30405e5 100644 --- a/tests/Feature/ExportTest.php +++ b/tests/Feature/ExportTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature; -use App\Exports\PurchaseOrderExport; use App\Models\CustomerLevelModel; use App\Models\ProductCategoryModel; use App\Models\ProductModel; @@ -11,10 +10,9 @@ use App\Models\PurchaseOrderModel; use App\Models\StoreModel; use App\Models\StoreOrderModel; use App\Models\UserModel; -use Maatwebsite\Excel\Facades\Excel; /** - * 采购单导出(仅 Excel 表格):xlsx Content-Type / 蔬果分类过滤 / type 非法拒绝 / + * 采购单导出(仅 Excel 表格,全品类):xlsx Content-Type / * 权限拦截 / 中文文件名 RFC 5987 编码 */ class ExportTest extends ProcurementTestCase @@ -71,35 +69,6 @@ class ExportTest extends ProcurementTestCase $this->assertStringContainsString(rawurlencode('采购单'), $disposition); } - /** 蔬果分类过滤:type=category 仅导出蔬菜/水果顶级分类商品 */ - public function test_export_category_filters_to_vegetables(): void - { - $purchase = $this->buildPurchaseWithItems(); - $this->actingAsSysUser(); - - Excel::fake(); - $this->get("/purchase/order/{$purchase->id}/export?type=category")->assertOk(); - - Excel::assertDownloaded( - $purchase->purchase_no . '_采购单.xlsx', - static function (PurchaseOrderExport $export): bool { - $items = $export->collection(); - // 仅蔬菜商品一行,肉禽被过滤 - return $items->count() === 1; - } - ); - } - - /** type 参数非法 → 拒绝 */ - public function test_export_invalid_type_rejected(): void - { - $purchase = $this->buildPurchaseWithItems(); - $this->actingAsSysUser(); - - $this->get("/purchase/order/{$purchase->id}/export?type=doc") - ->assertJsonPath('success', false); - } - /** 采购单不存在 → 拒绝 */ public function test_export_missing_purchase_rejected(): void { diff --git a/tests/Feature/MiniNoticeTest.php b/tests/Feature/MiniNoticeTest.php new file mode 100644 index 0000000..5f174a8 --- /dev/null +++ b/tests/Feature/MiniNoticeTest.php @@ -0,0 +1,127 @@ +broadcast_from + * (pluck JSON 路径被 Query Builder 当作结果列名读取 stdClass 属性) + */ +class MiniNoticeTest extends ProcurementTestCase +{ + /** + * 造一个绑定门店的小程序用户 + * + * @return array{0: StoreModel, 1: UserModel} + */ + private function makeStoreWithUser(): array + { + $store = StoreModel::factory()->create(); + + return [$store, UserModel::factory()->forStore($store->id)->create()]; + } + + /** 通知列表:本人通知 + 未读广播,unread_count 正确 */ + public function test_notice_list_includes_personal_and_broadcast(): void + { + [, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + NoticeModel::create([ + 'user_id' => NoticeModel::BROADCAST_USER_ID, + 'type' => NoticeModel::TYPE_SYSTEM, + 'title' => '系统公告', + 'content' => '全员可见', + 'is_read' => NoticeModel::UNREAD, + ]); + NoticeModel::create([ + 'user_id' => $user->id, + 'type' => NoticeModel::TYPE_ORDER, + 'title' => '订单状态更新', + 'content' => '您的订单已接单', + 'is_read' => NoticeModel::UNREAD, + ]); + + $data = $this->getJson('/mini/notice')->assertOk()->json('data'); + + $this->assertSame(2, $data['total']); + $this->assertSame(2, $data['unread_count']); + } + + /** 广播标记已读:复制本人已读副本,列表排除原广播(回归:列表不再报错) */ + public function test_broadcast_read_creates_personal_copy(): void + { + [, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + $broadcast = NoticeModel::create([ + 'user_id' => NoticeModel::BROADCAST_USER_ID, + 'type' => NoticeModel::TYPE_SYSTEM, + 'title' => '系统公告', + 'content' => '全员可见', + 'data' => ['foo' => 'bar'], + 'is_read' => NoticeModel::UNREAD, + ]); + + $this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true); + + // 已读副本:本人记录、已读、broadcast_from 记来源且保留原 data + $copy = NoticeModel::where('user_id', $user->id)->first(); + $this->assertNotNull($copy); + $this->assertSame(NoticeModel::READ, $copy->is_read); + $this->assertSame($broadcast->id, $copy->data['broadcast_from']); + $this->assertSame('bar', $copy->data['foo']); + + // 列表:原广播被排除,仅剩已读副本,未读数归零 + $data = $this->getJson('/mini/notice')->assertOk()->json('data'); + $this->assertSame(1, $data['total']); + $this->assertSame(0, $data['unread_count']); + $this->assertSame($copy->id, $data['data'][0]['id']); + + // 重复标记不重复复制 + $this->putJson("/mini/notice/{$broadcast->id}/read")->assertJsonPath('success', true); + $this->assertSame(1, NoticeModel::where('user_id', $user->id)->count()); + } + + /** 个人通知标记已读:直接更新原记录,不产生副本 */ + public function test_personal_notice_read_marks_directly(): void + { + [, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + $notice = NoticeModel::create([ + 'user_id' => $user->id, + 'type' => NoticeModel::TYPE_ORDER, + 'title' => '订单状态更新', + 'content' => '您的订单已接单', + 'is_read' => NoticeModel::UNREAD, + ]); + + $this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', true); + $this->assertSame(NoticeModel::READ, $notice->fresh()->is_read); + $this->assertSame(1, NoticeModel::count(), '个人通知直接更新,不产生副本'); + } + + /** 他人通知不可标记(仅本人通知与广播可读) */ + public function test_other_users_notice_not_found(): void + { + [, $user] = $this->makeStoreWithUser(); + $this->actingAsMiniUser($user); + + [, $other] = $this->makeStoreWithUser(); + $notice = NoticeModel::create([ + 'user_id' => $other->id, + 'type' => NoticeModel::TYPE_ORDER, + 'title' => '订单状态更新', + 'content' => '他人通知', + 'is_read' => NoticeModel::UNREAD, + ]); + + $this->putJson("/mini/notice/{$notice->id}/read")->assertJsonPath('success', false); + $this->assertSame(NoticeModel::UNREAD, $notice->fresh()->is_read); + } +} diff --git a/tests/Feature/StoreOrderTest.php b/tests/Feature/StoreOrderTest.php index dc3187d..8e8fd99 100644 --- a/tests/Feature/StoreOrderTest.php +++ b/tests/Feature/StoreOrderTest.php @@ -257,4 +257,68 @@ class StoreOrderTest extends ProcurementTestCase $this->deleteJson("/order/store/{$order->id}")->assertOk()->assertJsonPath('success', true); $this->deleteJson("/order/store/{$order->id}")->assertJsonPath('success', false); } + + /** 手动流转:仅保留接单与取消(待接单 → 已接单/已取消) */ + public function test_manual_status_flow_only_accept_and_cancel(): void + { + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); + $this->actingAsSysUser(); + + $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) + ->assertJsonPath('success', true); + $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status); + + $order2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_CANCELLED]) + ->assertJsonPath('success', true); + $this->assertSame(StoreOrderModel::STATUS_CANCELLED, $order2->fresh()->status); + } + + /** 手动流转:配送中/已完成等目标状态被拒绝(由采购/账单链路自动推进) */ + public function test_manual_status_flow_rejects_delivery_and_completion(): void + { + foreach ([StoreOrderModel::STATUS_DISTRIBUTION, StoreOrderModel::STATUS_COMPLETED] as $target) { + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_DELIVERING); + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order->id}/status", ['status' => $target]) + ->assertJsonPath('success', false); + $this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->fresh()->status); + } + + // 已接单不能手动转采购中(须经生成采购单) + $order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED); + $this->actingAsSysUser(); + $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_DELIVERING]) + ->assertJsonPath('success', false); + } + + /** 批量流转:仅接单/取消可选,配送/完成目标整批校验拒绝 */ + public function test_batch_status_only_accept_and_cancel(): void + { + $pending1 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); + $pending2 = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING); + $this->actingAsSysUser(); + + // 批量接单 + $this->putJson('/order/store/batchStatus', [ + 'ids' => [$pending1->id, $pending2->id], + 'status' => StoreOrderModel::STATUS_SUMMARIZED, + ])->assertJsonPath('success', true); + $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status); + $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending2->fresh()->status); + + // 批量完成被拒绝(目标状态校验拦截) + $this->putJson('/order/store/batchStatus', [ + 'ids' => [$pending1->id], + 'status' => StoreOrderModel::STATUS_COMPLETED, + ])->assertJsonPath('success', false); + + // 已接单订单不可批量取消(仅待接单可取消),整批中止 + $this->putJson('/order/store/batchStatus', [ + 'ids' => [$pending1->id], + 'status' => StoreOrderModel::STATUS_CANCELLED, + ])->assertJsonPath('success', false); + $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $pending1->fresh()->status); + } } diff --git a/web/api/purchase/order.ts b/web/api/purchase/order.ts index f66a08a..38a9b25 100644 --- a/web/api/purchase/order.ts +++ b/web/api/purchase/order.ts @@ -5,7 +5,6 @@ import type { IPurchaseCell, IPurchaseDetail, IPurchaseStoreSummary, - PurchaseExportType, } from '@/domain/iPurchaseOrder.ts'; /** 订单商品参数修改 */ @@ -103,11 +102,11 @@ export async function generateBill(purchaseId: number, stores: BillGenerateStore }); } -/** C2/C3 导出采购单(Excel 表格):type=all 全品类 / category 仅蔬果分类 */ -export async function exportPurchase(id: number, type: PurchaseExportType = 'all') { +/** 导出采购单(Excel 表格,全品类) */ +export async function exportPurchase(id: number) { return downloadBlob( `/purchase/order/${id}/export`, - { type }, + {}, `采购单_${id}.xlsx`, ); } diff --git a/web/domain/iPurchaseOrder.ts b/web/domain/iPurchaseOrder.ts index 47fa4bf..bfd3a98 100644 --- a/web/domain/iPurchaseOrder.ts +++ b/web/domain/iPurchaseOrder.ts @@ -180,5 +180,4 @@ export const PURCHASE_STATUS_MAP: Record { ), }, + { + title: '总采购金额', + dataIndex: 'total_purchase_amount', + hideInForm: true, + hideInSearch: true, + align: 'center', + render: (_, record) => ( + ¥{record.total_purchase_amount ?? '0.00'} + ), + }, { title: '状态', dataIndex: 'status', diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index 16e22de..9710c2d 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -41,20 +41,16 @@ const { Title, Text } = Typography; /** - * 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成 - * (已接单→采购中 通过「生成采购单」完成,不在流转按钮内) + * 状态流转合法路径:手动流转仅保留「接单」「取消订单」 + * (已接单→采购中 由「生成采购单」推进;采购单完成→配送中、生成账单→已完成 由业务链路自动推进) */ const NEXT_STATUS: Record = { 0: { status: 1, label: '接单' }, - 2: { status: 3, label: '开始配送' }, - 3: { status: 4, label: '完成订单' }, }; /** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */ const BATCH_TARGET_OPTIONS = [ { value: 1, label: '接单(待接单 → 已接单)' }, - { value: 3, label: '开始配送(采购中 → 配送中)' }, - { value: 4, label: '完成订单(采购中/配送中 → 已完成)' }, { value: 9, label: '取消订单(待接单 → 已取消)' }, ]; @@ -411,7 +407,7 @@ const StoreOrderPage: React.FC = () => {
门店订单 - 小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购中可批量流转为配送中或已完成。 + 小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购单完成后订单自动转配送中,生成账单后订单自动完成。
{...tableProps} /> diff --git a/web/pages/purchase/order.tsx b/web/pages/purchase/order.tsx index aa2fd52..5d4e769 100644 --- a/web/pages/purchase/order.tsx +++ b/web/pages/purchase/order.tsx @@ -3,7 +3,6 @@ import { Button, Descriptions, Drawer, - Dropdown, Empty, Form, Image, @@ -833,18 +832,14 @@ const PurchaseOrderPage: React.FC = () => { <> - detail && exportPurchase(detail.purchase.id!, 'all') }, - { key: 'category', label: '仅导出蔬果分类', onClick: () => detail && exportPurchase(detail.purchase.id!, 'category') }, - ], - }} + - + 导出 +