diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 5085fcc..36920d8 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,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":7,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":7,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":8},"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.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.012,"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.009,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.011,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.012,"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.036,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.034,"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.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.058,"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.018,"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.008,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.008,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.007,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.007,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.009,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"Tests\\Feature\\CartTest::test_store_without_level_rejected":0.01,"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.009,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.009,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.008,"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.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.009,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.01,"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.01,"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.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.007,"Tests\\Feature\\ProductPriceTest::test_unbound_user_can_browse_products_without_price_but_cannot_cart":0.011,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.02,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.029,"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.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.021,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.036,"Tests\\Feature\\StoreOrderItemTest::test_place_order_snapshots_full_product_info":0.013,"Tests\\Feature\\StoreOrderItemTest::test_mini_detail_hides_cost_price":0.015,"Tests\\Feature\\StoreOrderItemTest::test_admin_detail_shows_cost_price_and_supplier":0.012,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.013,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.026,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.011,"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.01,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.017,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.024,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.033,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.061,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.02,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.023,"Tests\\Feature\\PurchaseEditTest::test_update_cell_syncs_order_item_and_totals":0.023,"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.036,"Tests\\Feature\\ReconciliationTest::test_build_excludes_unfinished_orders":0.016,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_product_attributes":0.021,"Tests\\Feature\\PurchaseEditTest::test_update_row_syncs_to_product_catalog":0.026,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.02,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.029,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.023,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.025,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.042,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.02,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.032,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.041,"Tests\\Feature\\PurchaseEditTest::test_store_summary_rejects_missing_or_invalid_params":0.043,"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.009,"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.007,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.009,"Tests\\Feature\\ProductCategoryTest::test_create_product_with_parent_category_rejected":0.009,"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.008,"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.011,"Tests\\Feature\\MiniBillTest::test_bill_data_isolated_between_stores":0.009,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.009,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.019,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.02,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.009,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.069,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.011,"Tests\\Feature\\BillExportTest::test_export_empty_category_rejected":0.057,"Tests\\Feature\\BillExportTest::test_export_requires_export_permission":0.01,"Tests\\Feature\\BillExportTest::test_export_invalid_ids_rejected":0.009,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.014,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.006,"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.033,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_rejects_delivery_and_completion":0.028,"Tests\\Feature\\StoreOrderTest::test_batch_status_only_accept_and_cancel":0.024,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":0.025,"Tests\\Feature\\BillPaymentTest::test_manual_pay_accumulates_store_total_purchase_amount":0.032,"Tests\\Feature\\BillPaymentTest::test_payment_audit_pass_accumulates_store_total_purchase_amount":0.012,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":0.009,"Tests\\Feature\\MiniProductTest::test_product_detail_guest_sees_null_price":0.007,"Tests\\Feature\\MiniProductTest::test_product_detail_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_percent_price_calculated_from_cost":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_off_shelf_or_missing_rejected":0.006,"Tests\\Feature\\MiniProductTest::test_product_list_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_null_price":0.005,"Tests\\Feature\\BillPaymentTest::test_admin_order_list_and_detail_show_bill_info":0.031,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":0.013,"Tests\\Feature\\StoreOrderTest::test_order_list_preview_includes_image_spec_and_unit":0.026}} \ 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,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":8,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":7,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":7,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":7,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":7,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":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.021,"Tests\\Feature\\ProductPriceTest::test_price_matrix_structure":0.01,"Tests\\Feature\\StoreOrderTest::test_place_order_snapshots_level_price_and_recalculates":0.018,"Tests\\Feature\\StoreOrderTest::test_client_supplied_amount_is_ignored":0.018,"Tests\\Feature\\StoreOrderTest::test_product_without_level_price_rejected":0.008,"Tests\\Feature\\StoreOrderTest::test_cancel_only_pending_orders":0.01,"Tests\\Feature\\StoreOrderTest::test_summarized_order_cannot_be_cancelled":0.032,"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.028,"Tests\\Feature\\PurchaseGenerateTest::test_generate_without_pending_orders_fails":0.006,"Tests\\Feature\\PurchaseGenerateTest::test_generate_is_idempotent":0.031,"Tests\\Feature\\AllocationTest::test_allocation_conserves_amount_with_tail_correction":0.076,"Tests\\Feature\\AllocationTest::test_allocation_follows_order_ratio":0.022,"Tests\\Feature\\AllocationTest::test_allocation_is_idempotent":0.028,"Tests\\Feature\\AllocationTest::test_allocation_rejected_without_actual_amount":0.021,"Tests\\Feature\\DebugGenerateTest::test_debug_generate":0.047,"Tests\\Feature\\ReconciliationTest::test_build_creates_reconciliation_items":0.019,"Tests\\Feature\\ReconciliationTest::test_build_filters_by_supplier":0.022,"Tests\\Feature\\ReconciliationTest::test_update_item_recalculates_diff_and_header":0.019,"Tests\\Feature\\ReconciliationTest::test_toggle_reconciled_flag":0.02,"Tests\\Feature\\ReconciliationTest::test_settle_creates_settlements_per_store":0.036,"Tests\\Feature\\StatementTest::test_generate_snapshots_payment_cycle":0.011,"Tests\\Feature\\StatementTest::test_statement_isolated_between_stores":0.013,"Tests\\Feature\\StatementTest::test_generate_excludes_cancelled_and_used_items":0.016,"Tests\\Feature\\MiniAuthTest::test_login_creates_user_and_issues_token":0.056,"Tests\\Feature\\MiniAuthTest::test_disabled_user_cannot_login":0.003,"Tests\\Feature\\MiniAuthTest::test_phone_binding_matches_store":0.005,"Tests\\Feature\\MiniAuthTest::test_phone_no_match_stays_pending":0.001,"Tests\\Feature\\MiniAuthTest::test_sys_token_cannot_access_mini":0.006,"Tests\\Feature\\MiniAuthTest::test_mini_token_cannot_access_admin_api":0.004,"Tests\\Feature\\ExportTest::test_export_xlsx_with_encoded_chinese_filename":0.037,"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.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.008,"Tests\\Feature\\CartTest::test_duplicate_add_merges_quantity":0.01,"Tests\\Feature\\CartTest::test_off_shelf_product_rejected":0.006,"Tests\\Feature\\CartTest::test_soft_deleted_product_rejected":0.006,"Tests\\Feature\\CartTest::test_product_without_level_price_rejected":0.007,"Tests\\Feature\\CartTest::test_unbound_user_rejected":0.007,"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.012,"Tests\\Feature\\CartTest::test_empty_cart_returns_empty_items":0.005,"Tests\\Feature\\CartTest::test_update_quantity":0.009,"Tests\\Feature\\CartTest::test_update_other_users_item_rejected":0.01,"Tests\\Feature\\CartTest::test_update_missing_item_rejected":0.011,"Tests\\Feature\\CartTest::test_invalid_quantity_rejected":0.006,"Tests\\Feature\\CartTest::test_delete_item":0.008,"Tests\\Feature\\CartTest::test_delete_other_users_item_rejected":0.011,"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.008,"Tests\\Feature\\DebugCartListTest::test_dump_cart_list_response":0.057,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_fixed_and_percent":0.001,"Tests\\Feature\\ProductCostPriceTest::test_calc_actual_price_edge_cases":0.001,"Tests\\Feature\\ProductCostPriceTest::test_admin_list_contains_cost_price_and_actual_price":0.009,"Tests\\Feature\\ProductCostPriceTest::test_mini_list_returns_actual_price_without_cost_price":0.006,"Tests\\Feature\\ProductCostPriceTest::test_create_product_with_cost_and_percent_pricing":0.011,"Tests\\Feature\\ProductCostPriceTest::test_create_percent_row_without_percent_fails":0.007,"Tests\\Feature\\ProductCostPriceTest::test_update_switches_fixed_to_percent":0.01,"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.011,"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.01,"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.009,"Tests\\Feature\\MiniAuthTest::test_login_issues_token_for_registered_user":0.009,"Tests\\Feature\\MiniAuthTest::test_login_rejects_unregistered_openid":0.002,"Tests\\Feature\\MiniAuthTest::test_register_binds_store_and_issues_token":0.009,"Tests\\Feature\\MiniAuthTest::test_register_rejects_unknown_store":0.002,"Tests\\Feature\\MiniAuthTest::test_register_rejects_duplicate_openid":0.003,"Tests\\Feature\\MiniStoreTest::test_info_returns_bound_store":0.004,"Tests\\Feature\\MiniStoreTest::test_update_info":0.005,"Tests\\Feature\\MiniStoreTest::test_update_info_ignores_readonly_fields":0.008,"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.018,"Tests\\Feature\\PurchaseGenerateTest::test_generate_with_selected_order_ids":0.037,"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.02,"Tests\\Feature\\StoreOrderItemTest::test_update_item_recalculates_order_totals":0.018,"Tests\\Feature\\StoreOrderItemTest::test_update_item_rejected_when_completed_or_cancelled":0.017,"Tests\\Feature\\StoreOrderItemTest::test_update_item_validates_negative_price":0.016,"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.016,"Tests\\Feature\\StoreOrderItemTest::test_sync_item_rejected_when_product_deleted":0.019,"Tests\\Feature\\StoreOrderItemTest::test_order_list_search_by_product_name":0.026,"Tests\\Feature\\StoreOrderTest::test_soft_delete_only_cancelled_orders":0.021,"Tests\\Feature\\StoreOrderTest::test_soft_delete_rejected_for_non_cancelled_orders":0.056,"Tests\\Feature\\StoreOrderTest::test_soft_delete_twice_rejected":0.016,"Tests\\Feature\\PurchaseEditTest::test_detail_returns_store_matrix":0.022,"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.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.02,"Tests\\Feature\\PurchaseGenerateTest::test_generate_aggregates_order_items_into_header":0.043,"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.023,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_returns_items_with_order_info":0.038,"Tests\\Feature\\PurchaseEditTest::test_cell_detail_rejects_missing_params":0.029,"Tests\\Feature\\PurchaseEditTest::test_update_cell_weight_cascades_totals":0.03,"Tests\\Feature\\PurchaseEditTest::test_cell_sync_refreshes_snapshot_and_cascades":0.024,"Tests\\Feature\\PurchaseEditTest::test_edits_rejected_when_purchase_completed":0.034,"Tests\\Feature\\PurchaseEditTest::test_update_row_does_not_touch_manual_weight":0.019,"Tests\\Feature\\PurchaseEditTest::test_store_summary_returns_aggregated_items":0.024,"Tests\\Feature\\PurchaseEditTest::test_store_summary_aggregates_multiple_orders_with_weighted_price":0.019,"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.006,"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.006,"Tests\\Feature\\ProductCategoryTest::test_create_category_under_second_level_rejected":0.019,"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.013,"Tests\\Feature\\ProductCategoryTest::test_update_with_missing_parent_rejected":0.009,"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.008,"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.008,"Tests\\Feature\\MiniBillTest::test_bill_detail_outputs_bill_items_and_orders":0.009,"Tests\\Feature\\StoreOrderTest::test_order_list_outputs_status_name_and_item_preview":0.012,"Tests\\Feature\\StoreOrderTest::test_order_list_filters_by_order_date_range":0.018,"Tests\\Feature\\StoreOrderTest::test_order_list_rejects_invalid_filters":0.009,"Tests\\Feature\\BillExportTest::test_export_merges_items_across_bills":0.057,"Tests\\Feature\\BillExportTest::test_export_filters_by_root_category":0.012,"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.009,"Tests\\Feature\\DebugPermissionTest::test_debug_permission":0.098,"Tests\\Feature\\BillExportTest::test_mini_export_scoped_to_bound_store":0.014,"Tests\\Feature\\BillExportTest::test_mini_export_requires_store_binding":0.006,"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.005,"Tests\\Feature\\MiniNoticeTest::test_other_users_notice_not_found":0.006,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_only_accept_and_cancel":0.02,"Tests\\Feature\\StoreOrderTest::test_manual_status_flow_rejects_delivery_and_completion":0.037,"Tests\\Feature\\StoreOrderTest::test_batch_status_only_accept_and_cancel":0.031,"Tests\\Feature\\BillPaymentTest::test_order_status_progresses_via_business_chain":0.023,"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.012,"Tests\\Feature\\BillPaymentTest::test_payment_audit_reject_does_not_accumulate":0.008,"Tests\\Feature\\MiniProductTest::test_product_detail_guest_sees_null_price":0.008,"Tests\\Feature\\MiniProductTest::test_product_detail_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_percent_price_calculated_from_cost":0.006,"Tests\\Feature\\MiniProductTest::test_product_detail_off_shelf_or_missing_rejected":0.006,"Tests\\Feature\\MiniProductTest::test_product_list_store_user_sees_level_price":0.006,"Tests\\Feature\\MiniProductTest::test_product_list_guest_sees_null_price":0.005,"Tests\\Feature\\BillPaymentTest::test_admin_order_list_and_detail_show_bill_info":0.031,"Tests\\Feature\\BillPaymentTest::test_payment_detail_returns_voucher_urls":0.014,"Tests\\Feature\\StoreOrderTest::test_order_list_preview_includes_image_spec_and_unit":0.019,"Tests\\Feature\\ContainerReturnTest::test_press_containers_adds_amount_and_writes_record":0.021,"Tests\\Feature\\ContainerReturnTest::test_return_containers_deducts_amount":0.02,"Tests\\Feature\\ContainerReturnTest::test_mixed_sign_containers":0.018,"Tests\\Feature\\ContainerReturnTest::test_zero_containers_writes_no_record":0.018,"Tests\\Feature\\ContainerReturnTest::test_non_integer_containers_rejected":0.008,"Tests\\Feature\\ContainerReturnTest::test_manual_create_and_delete_routes_removed":0.006}} \ No newline at end of file diff --git a/app/Http/Controllers/Mini/OrderController.php b/app/Http/Controllers/Mini/OrderController.php index 9c389c7..91e1ce2 100644 --- a/app/Http/Controllers/Mini/OrderController.php +++ b/app/Http/Controllers/Mini/OrderController.php @@ -182,7 +182,7 @@ class OrderController extends BaseMiniController $previewMap = []; $flatItems = []; foreach ($paginator->getCollection() as $order) { - foreach ($order->items as $item) { + foreach ($order->items->take(3) as $item) { $flatItems[] = [ 'order_id' => $order->id, 'product_name' => $item->product_name, diff --git a/app/Http/Controllers/Recon/ContainerReturnController.php b/app/Http/Controllers/Recon/ContainerReturnController.php index 9ae3bb9..3f11b56 100644 --- a/app/Http/Controllers/Recon/ContainerReturnController.php +++ b/app/Http/Controllers/Recon/ContainerReturnController.php @@ -2,124 +2,41 @@ namespace App\Http\Controllers\Recon; -use App\Exceptions\RepositoryException; -use App\Http\Requests\Recon\ContainerReturnFormRequest; use App\Models\ContainerReturnModel; -use App\Models\StoreModel; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; -use Modules\AnnoRoute\Attribute\DeleteRoute; use Modules\AnnoRoute\Attribute\GetRoute; -use Modules\AnnoRoute\Attribute\PostRoute; use Modules\AnnoRoute\Attribute\RequestAttribute; use Modules\Common\Http\Controllers\BaseController; -use Throwable; /** - * 回筐记录(压筐=生成账单自动写入只读;回筐=门店退回手动登记,扣减门店待回数量) + * 压回筐记录(周转筐/托盘跟账单走,生成账单时自动写入完整快照,只读) + * + * 数量正数=压筐(附加金额),负数=回筐(抵扣金额) */ #[RequestAttribute('/recon/container-return', 'recon.containerReturn')] class ContainerReturnController extends BaseController { protected array $searchField = [ 'store_id' => '=', - 'type' => '=', - 'return_date' => 'betweenDate', + 'bill_id' => '=', + 'created_at' => 'betweenDate', ]; - /** 回筐记录列表 */ + /** 压回筐记录列表 */ #[GetRoute(authorize: 'query')] public function query(Request $request): JsonResponse { $params = $request->all(); $pageSize = $params['pageSize'] ?? 10; $data = $this->buildSearch($params, ContainerReturnModel::query()->with([ - 'store:id,name,pending_box_num,pending_tray_num', - 'bill:id,bill_no', + 'store:id,name', + 'bill:id,bill_no,bill_date', 'operator:id,nickname', ])) - ->orderBy('return_date', 'desc') ->orderBy('id', 'desc') ->paginate($pageSize) ->toArray(); return $this->success($data); } - - /** - * 回筐登记:门店退回周转筐/托盘,扣减门店待回数量(超回拒绝) - * @throws Throwable - */ - #[PostRoute(authorize: 'create')] - public function create(ContainerReturnFormRequest $request): JsonResponse - { - $data = $request->validated(); - $boxNum = (int) $data['box_num']; - $trayNum = (int) $data['tray_num']; - if ($boxNum === 0 && $trayNum === 0) { - throw new RepositoryException('周转筐与周转托盘数量不能同时为 0'); - } - - return DB::transaction(function () use ($data, $boxNum, $trayNum, $request) { - $store = StoreModel::query()->lockForUpdate()->find((int) $data['store_id']); - if (empty($store)) { - throw new RepositoryException('门店不存在'); - } - if ($boxNum > (int) $store->pending_box_num) { - throw new RepositoryException( - '回筐数量超过该门店待回筐数量(当前待回 ' . (int) $store->pending_box_num . ' 个)' - ); - } - if ($trayNum > (int) $store->pending_tray_num) { - throw new RepositoryException( - '回托盘数量超过该门店待回托盘数量(当前待回 ' . (int) $store->pending_tray_num . ' 个)' - ); - } - - $store->pending_box_num = (int) $store->pending_box_num - $boxNum; - $store->pending_tray_num = (int) $store->pending_tray_num - $trayNum; - $store->save(); - - ContainerReturnModel::create([ - 'store_id' => $store->id, - 'bill_id' => 0, - 'type' => ContainerReturnModel::TYPE_RETURN, - 'box_num' => $boxNum, - 'tray_num' => $trayNum, - 'return_date' => $data['return_date'], - 'operator_id' => (int) $request->user()->id, - 'remark' => (string) ($data['remark'] ?? ''), - ]); - - return $this->success([], '回筐已登记'); - }); - } - - /** - * 删除回筐记录(仅手动登记的回筐记录可删,删除后恢复门店待回数量;压筐记录随账单生成不允许删除) - * @throws Throwable - */ - #[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])] - public function delete(int $id): JsonResponse - { - $record = ContainerReturnModel::find($id); - if (empty($record)) { - throw new RepositoryException('回筐记录不存在'); - } - if ($record->type !== ContainerReturnModel::TYPE_RETURN) { - throw new RepositoryException('压筐记录由账单生成,不允许删除'); - } - - return DB::transaction(function () use ($record) { - $store = StoreModel::query()->lockForUpdate()->find($record->store_id); - if ($store !== null) { - $store->pending_box_num = (int) $store->pending_box_num + (int) $record->box_num; - $store->pending_tray_num = (int) $store->pending_tray_num + (int) $record->tray_num; - $store->save(); - } - $record->delete(); - - return $this->success([], '回筐记录已删除,门店待回数量已恢复'); - }); - } } diff --git a/app/Http/Requests/Purchase/PurchaseBillGenerateRequest.php b/app/Http/Requests/Purchase/PurchaseBillGenerateRequest.php index e82e70f..be71871 100644 --- a/app/Http/Requests/Purchase/PurchaseBillGenerateRequest.php +++ b/app/Http/Requests/Purchase/PurchaseBillGenerateRequest.php @@ -5,7 +5,8 @@ namespace App\Http\Requests\Purchase; use Modules\Common\Http\Requests\BaseFormRequest; /** - * 采购单生成账单 验证(按门店提交配送费/周转筐/托盘数量,金额由系统汇总不可修改) + * 采购单生成账单 验证(按门店提交配送费/周转筐/托盘数量,金额由系统汇总不可修改; + * 筐/托盘数量正数=压筐附加金额,负数=回筐抵扣金额) */ class PurchaseBillGenerateRequest extends BaseFormRequest { @@ -17,8 +18,8 @@ class PurchaseBillGenerateRequest extends BaseFormRequest 'stores' => 'required|array|min:1', 'stores.*.store_id' => 'required|integer|distinct', 'stores.*.delivery_fee' => 'required|numeric|min:0', - 'stores.*.box_num' => 'required|integer|min:0', - 'stores.*.tray_num' => 'required|integer|min:0', + 'stores.*.box_num' => 'required|integer', + 'stores.*.tray_num' => 'required|integer', ]; } @@ -35,11 +36,9 @@ class PurchaseBillGenerateRequest extends BaseFormRequest 'stores.*.delivery_fee.numeric' => '配送费格式错误', 'stores.*.delivery_fee.min' => '配送费不能小于 0', 'stores.*.box_num.required' => '周转筐数量不能为空', - 'stores.*.box_num.integer' => '周转筐数量必须为整数', - 'stores.*.box_num.min' => '周转筐数量不能小于 0', + 'stores.*.box_num.integer' => '周转筐数量必须为整数(正数=压筐,负数=回筐)', 'stores.*.tray_num.required' => '周转托盘数量不能为空', - 'stores.*.tray_num.integer' => '周转托盘数量必须为整数', - 'stores.*.tray_num.min' => '周转托盘数量不能小于 0', + 'stores.*.tray_num.integer' => '周转托盘数量必须为整数(正数=压筐,负数=回筐)', ]; } } diff --git a/app/Http/Requests/Recon/ContainerReturnFormRequest.php b/app/Http/Requests/Recon/ContainerReturnFormRequest.php deleted file mode 100644 index e25c492..0000000 --- a/app/Http/Requests/Recon/ContainerReturnFormRequest.php +++ /dev/null @@ -1,41 +0,0 @@ - 'required|integer|exists:store,id', - 'box_num' => 'required|integer|min:0', - 'tray_num' => 'required|integer|min:0', - 'return_date' => 'required|date_format:Y-m-d', - 'remark' => 'nullable|string|max:255', - ]; - } - - public function messages(): array - { - return [ - 'store_id.required' => '请选择门店', - 'store_id.exists' => '门店不存在', - 'box_num.required' => '周转筐数量不能为空', - 'box_num.integer' => '周转筐数量必须为整数', - 'box_num.min' => '周转筐数量不能小于 0', - 'tray_num.required' => '周转托盘数量不能为空', - 'tray_num.integer' => '周转托盘数量必须为整数', - 'tray_num.min' => '周转托盘数量不能小于 0', - 'return_date.required' => '请选择退回日期', - 'return_date.date_format' => '退回日期格式为 Y-m-d', - 'remark.max' => '备注超过最大长度', - ]; - } -} diff --git a/app/Models/ContainerReturnModel.php b/app/Models/ContainerReturnModel.php index 5d98ef5..d0f3e79 100644 --- a/app/Models/ContainerReturnModel.php +++ b/app/Models/ContainerReturnModel.php @@ -8,44 +8,36 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Modules\SystemUser\Models\SysUserModel; /** - * 门店回筐记录模型(压筐=生成账单时自动写入并累加门店待回;回筐=门店退回手动登记并扣减待回) + * 门店压回筐记录模型(周转筐/托盘跟账单走,生成账单时写入完整快照) + * + * 数量正数=压筐(附加金额),负数=回筐(抵扣金额);数量为 0 不写记录 */ class ContainerReturnModel extends Model { use HasFactory; - /** 类型:压筐(账单生成压出,系统写入只读) */ - public const int TYPE_PRESS = 1; - /** 类型:回筐(门店退回,后台手动登记) */ - public const int TYPE_RETURN = 2; - - /** 类型中文名 */ - public const array TYPE_NAMES = [ - self::TYPE_PRESS => '压筐', - self::TYPE_RETURN => '回筐', - ]; - protected $table = 'container_return'; protected $primaryKey = 'id'; protected $fillable = [ 'store_id', 'bill_id', - 'type', 'box_num', 'tray_num', - 'return_date', + 'box_price', + 'tray_price', + 'amount', 'operator_id', - 'remark', ]; protected $casts = [ 'store_id' => 'integer', 'bill_id' => 'integer', - 'type' => 'integer', 'box_num' => 'integer', 'tray_num' => 'integer', - 'return_date' => 'date:Y-m-d', + 'box_price' => 'decimal:2', + 'tray_price' => 'decimal:2', + 'amount' => 'decimal:2', 'operator_id' => 'integer', 'created_at' => 'datetime:Y-m-d H:i:s', ]; @@ -59,7 +51,7 @@ class ContainerReturnModel extends Model } /** - * 关联账单(压筐记录) + * 关联账单 */ public function bill(): BelongsTo { diff --git a/app/Models/StoreModel.php b/app/Models/StoreModel.php index f62211f..d7a5bc5 100644 --- a/app/Models/StoreModel.php +++ b/app/Models/StoreModel.php @@ -38,8 +38,6 @@ class StoreModel extends Model protected $casts = [ 'level_id' => 'integer', '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 a8cd273..39d2c89 100644 --- a/app/Services/BillGenerateService.php +++ b/app/Services/BillGenerateService.php @@ -18,8 +18,10 @@ use Throwable; * 流程(事务内): * 1. 锁定采购单全部有效订单,按门店分组,校验提交门店完整覆盖 * 2. 商品金额 = 门店订单商品金额汇总(快照,生成后不可修改) - * 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照) + * 3. 附加金额 = 周转筐数量×筐单价 + 托盘数量×托盘单价(单价取站点配置快照; + * 数量正数=压筐附加金额,负数=回筐抵扣金额) * 4. 总金额 = 商品金额 + 配送费 + 附加金额;回写门店订单 bill_id 完成关联,订单状态置为已完成 + * 5. 压回筐记录:筐/托盘数量非 0 时写入完整快照(数量/单价/金额均可为负) */ readonly class BillGenerateService { @@ -122,22 +124,19 @@ readonly class BillGenerateService ->whereIn('order_id', $storeOrders->pluck('id')) ->update(['bill_id' => $bill->id]); - // 压筐:累加门店待回筐/托盘(行锁防并发),并写入压筐记录 - $store = StoreModel::query()->lockForUpdate()->find((int) $storeId); - if ($store !== null) { - $store->pending_box_num = (int) $store->pending_box_num + (int) $row['box_num']; - $store->pending_tray_num = (int) $store->pending_tray_num + (int) $row['tray_num']; - $store->save(); + // 压回筐记录:数量为 0 不写记录;正数=压筐附加金额,负数=回筐抵扣金额 + if ((int) $row['box_num'] !== 0 || (int) $row['tray_num'] !== 0) { + ContainerReturnModel::create([ + 'store_id' => (int) $storeId, + 'bill_id' => $bill->id, + 'box_num' => (int) $row['box_num'], + 'tray_num' => (int) $row['tray_num'], + 'box_price' => bcadd($boxPrice, '0', 2), + 'tray_price' => bcadd($trayPrice, '0', 2), + 'amount' => $addedAmount, + 'operator_id' => $operatorId, + ]); } - ContainerReturnModel::create([ - 'store_id' => (int) $storeId, - 'bill_id' => $bill->id, - 'type' => ContainerReturnModel::TYPE_PRESS, - 'box_num' => (int) $row['box_num'], - 'tray_num' => (int) $row['tray_num'], - 'return_date' => $billDate, - 'operator_id' => $operatorId, - ]); $bills[] = $bill; } 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 7434ea9..f8344b6 100644 --- a/database/migrations/2026_07_23_030608_create_store_table.php +++ b/database/migrations/2026_07_23_030608_create_store_table.php @@ -60,8 +60,6 @@ return new class extends Migration $table->string('phone', 20)->default('')->comment('联系电话'); $table->string('address', 255)->default('')->comment('门店地址'); $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('备注'); diff --git a/database/migrations/2026_08_14_000001_create_container_return_table.php b/database/migrations/2026_08_14_000001_create_container_return_table.php index 3e7c760..9d75bc1 100644 --- a/database/migrations/2026_08_14_000001_create_container_return_table.php +++ b/database/migrations/2026_08_14_000001_create_container_return_table.php @@ -8,7 +8,8 @@ return new class extends Migration { /** * Run the migrations. - * 门店回筐台账:压筐(生成账单时自动写入)+ 回筐(门店退回,后台手动登记) + * 门店压回筐记录:周转筐/托盘跟账单走,生成账单时写入完整快照; + * 数量正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不写记录 */ public function up(): void { @@ -16,17 +17,17 @@ return new class extends Migration Schema::create('container_return', function (Blueprint $table) { $table->increments('id')->comment('记录ID'); $table->integer('store_id')->comment('门店ID'); - $table->integer('bill_id')->default(0)->comment('关联账单ID(0=手动回筐登记)'); - $table->integer('type')->comment('类型(1压筐-账单生成 2回筐-退回登记)'); - $table->integer('box_num')->default(0)->comment('周转筐数量'); - $table->integer('tray_num')->default(0)->comment('周转托盘数量'); - $table->date('return_date')->comment('记录日期(压筐=账单日期,回筐=退回日期)'); + $table->integer('bill_id')->comment('关联账单ID'); + $table->integer('box_num')->default(0)->comment('压(回)筐数量(正数=压筐,负数=回筐)'); + $table->integer('tray_num')->default(0)->comment('压(回)托盘数量(正数=压筐,负数=回筐)'); + $table->decimal('box_price', 10, 2)->default(0)->comment('周转筐单价(生成账单时快照)'); + $table->decimal('tray_price', 10, 2)->default(0)->comment('周转托盘单价(生成账单时快照)'); + $table->decimal('amount', 10, 2)->default(0)->comment('抵扣(附加)金额 = 筐×筐单价 + 托盘×托盘单价(正=压筐附加,负=回筐抵扣)'); $table->integer('operator_id')->default(0)->comment('操作人(后台系统用户ID)'); - $table->string('remark', 255)->default('')->comment('备注'); $table->timestamps(); - $table->index(['store_id', 'type'], 'container_return_store_type_index'); + $table->index(['store_id'], 'container_return_store_index'); $table->index(['bill_id'], 'container_return_bill_index'); - $table->comment('门店回筐记录表(压筐=生成账单压出,回筐=门店退回登记)'); + $table->comment('门店压回筐记录表(随账单生成:正数=压筐附加金额,负数=回筐抵扣金额)'); }); } } diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index e03d20c..d67b31c 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -223,8 +223,6 @@ class PermissionSeeder extends Seeder 'path' => '/recon/container-return', 'children' => [ ['type' => 'rule', 'key' => 'recon.containerReturn.query', 'name' => '查询'], - ['type' => 'rule', 'key' => 'recon.containerReturn.create', 'name' => '回筐登记'], - ['type' => 'rule', 'key' => 'recon.containerReturn.delete', 'name' => '删除'], ], ], [ diff --git a/tests/Feature/ContainerReturnTest.php b/tests/Feature/ContainerReturnTest.php new file mode 100644 index 0000000..7b10fb7 --- /dev/null +++ b/tests/Feature/ContainerReturnTest.php @@ -0,0 +1,185 @@ +insert([ + 'id' => 100, + 'title' => '业务配置', + 'key' => 'services', + 'remark' => '', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('sys_site_config_items')->insert([ + [ + 'group_id' => 100, 'key' => 'box_amount', 'title' => '周转筐金额', 'describe' => '', + 'values' => '5.00', 'type' => 'InputNumber', 'options' => null, 'props' => null, 'sort' => 0, + 'created_at' => now(), 'updated_at' => now(), + ], + [ + 'group_id' => 100, 'key' => 'tray_amount', 'title' => '周转托盘金额', 'describe' => '', + 'values' => '20.00', 'type' => 'InputNumber', 'options' => null, 'props' => null, 'sort' => 1, + 'created_at' => now(), 'updated_at' => now(), + ], + ]); + SysSiteConfigService::refreshSiteConfig(); + } + + /** + * 完整业务链造账单(商品 5.00 × 4 = 20.00,配送费 0),按指定筐/托盘数量生成 + * + * @return array{0: StoreModel, 1: BillModel} + */ + private function makeBillWithContainers(int $boxNum, int $trayNum): array + { + $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->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' => $boxNum, + 'tray_num' => $trayNum, + ]], + ])->assertJsonPath('success', true); + + return [$store, BillModel::where('store_id', $store->id)->first()]; + } + + /** 压筐(正数):附加金额增加账单总额,写入完整快照记录 */ + public function test_press_containers_adds_amount_and_writes_record(): void + { + $this->seedContainerConfig(); + [$store, $bill] = $this->makeBillWithContainers(3, 2); + + // 附加 = 3×5 + 2×20 = 55;总额 = 20 + 55 = 75 + $this->assertSame('55.00', (string) $bill->added_amount); + $this->assertSame('75.00', (string) $bill->total_amount); + + $record = ContainerReturnModel::where('bill_id', $bill->id)->first(); + $this->assertNotNull($record); + $this->assertSame($store->id, $record->store_id); + $this->assertSame(3, $record->box_num); + $this->assertSame(2, $record->tray_num); + $this->assertSame('5.00', (string) $record->box_price, '筐单价快照'); + $this->assertSame('20.00', (string) $record->tray_price, '托盘单价快照'); + $this->assertSame('55.00', (string) $record->amount); + } + + /** 回筐(负数):抵扣账单金额,记录为负 */ + public function test_return_containers_deducts_amount(): void + { + $this->seedContainerConfig(); + [, $bill] = $this->makeBillWithContainers(-2, 0); + + // 附加 = -2×5 + 0 = -10;总额 = 20 - 10 = 10 + $this->assertSame('-10.00', (string) $bill->added_amount); + $this->assertSame('10.00', (string) $bill->total_amount); + + $record = ContainerReturnModel::where('bill_id', $bill->id)->first(); + $this->assertNotNull($record); + $this->assertSame(-2, $record->box_num); + $this->assertSame(0, $record->tray_num); + $this->assertSame('-10.00', (string) $record->amount); + } + + /** 混合:筐回(负)+ 托盘压(正),金额按代数和计 */ + public function test_mixed_sign_containers(): void + { + $this->seedContainerConfig(); + [, $bill] = $this->makeBillWithContainers(-3, 1); + + // 附加 = -3×5 + 1×20 = 5;总额 = 20 + 5 = 25 + $this->assertSame('5.00', (string) $bill->added_amount); + $this->assertSame('25.00', (string) $bill->total_amount); + + $record = ContainerReturnModel::where('bill_id', $bill->id)->first(); + $this->assertNotNull($record); + $this->assertSame(-3, $record->box_num); + $this->assertSame(1, $record->tray_num); + $this->assertSame('5.00', (string) $record->amount); + } + + /** 数量为 0:不写压回筐记录 */ + public function test_zero_containers_writes_no_record(): void + { + $this->seedContainerConfig(); + [, $bill] = $this->makeBillWithContainers(0, 0); + + $this->assertSame('20.00', (string) $bill->total_amount); + $this->assertSame(0, ContainerReturnModel::where('bill_id', $bill->id)->count()); + } + + /** 筐/托盘数量非整数 → 拒绝 */ + public function test_non_integer_containers_rejected(): void + { + $level = CustomerLevelModel::factory()->create(); + $store = StoreModel::factory()->create(['level_id' => $level->id]); + $this->actingAsSysUser(); + $purchase = PurchaseOrderModel::factory()->create(['status' => PurchaseOrderModel::STATUS_COMPLETED]); + + $this->postJson("/purchase/order/{$purchase->id}/bill", [ + 'stores' => [[ + 'store_id' => $store->id, + 'delivery_fee' => 0, + 'box_num' => 1.5, + 'tray_num' => 0, + ]], + ])->assertJsonPath('success', false); + } + + /** 压回筐记录只读:手动登记/删除端点已下线 */ + public function test_manual_create_and_delete_routes_removed(): void + { + $this->actingAsSysUser(); + + $this->postJson('/recon/container-return', [ + 'store_id' => 1, + 'box_num' => 1, + 'tray_num' => 0, + ])->assertJsonPath('success', false); + $this->deleteJson('/recon/container-return/1')->assertJsonPath('success', false); + } +} diff --git a/web/domain/iContainerReturn.ts b/web/domain/iContainerReturn.ts index 4322adf..a9f4f1d 100644 --- a/web/domain/iContainerReturn.ts +++ b/web/domain/iContainerReturn.ts @@ -1,26 +1,23 @@ -/** 门店回筐记录(压筐=生成账单自动写入只读;回筐=门店退回手动登记) */ +/** 门店压回筐记录(随账单生成:数量正数=压筐附加金额,负数=回筐抵扣金额) */ export default interface IContainerReturn { id?: number; store_id?: number; - /** 关联账单ID(0=手动回筐登记) */ + /** 关联账单ID */ bill_id?: number; - /** 类型:1压筐 2回筐 */ - type?: number; + /** 压(回)筐数量(正数=压筐,负数=回筐) */ box_num?: number; + /** 压(回)托盘数量(正数=压筐,负数=回筐) */ tray_num?: number; - /** 记录日期(压筐=账单日期,回筐=退回日期) */ - return_date?: string; + /** 周转筐单价(生成账单时快照) */ + box_price?: string; + /** 周转托盘单价(生成账单时快照) */ + tray_price?: string; + /** 抵扣(附加)金额(正=压筐附加,负=回筐抵扣) */ + amount?: string; operator_id?: number; - remark?: string; created_at?: string; /** 列表接口附带 */ - store?: { id: number; name: string; pending_box_num?: number; pending_tray_num?: number } | null; - bill?: { id: number; bill_no: string } | null; + store?: { id: number; name: string } | null; + bill?: { id: number; bill_no: string; bill_date?: string } | null; operator?: { id: number; nickname: string } | null; } - -/** 回筐记录类型映射 */ -export const CONTAINER_TYPE_MAP: Record = { - 1: { text: '压筐', color: 'processing' }, - 2: { text: '回筐', color: 'success' }, -}; diff --git a/web/domain/iStore.ts b/web/domain/iStore.ts index 7401aa4..cb5c4a1 100644 --- a/web/domain/iStore.ts +++ b/web/domain/iStore.ts @@ -14,10 +14,6 @@ export default interface IStore { address?: string; /** 回款周期(天) */ payment_cycle_days?: number; - /** 待回筐数量(生成账单压筐累加,回筐登记扣减) */ - pending_box_num?: number; - /** 待回托盘数量 */ - pending_tray_num?: number; /** 总采购金额(只统计商品金额,账单支付后累加) */ total_purchase_amount?: string; status?: number; diff --git a/web/pages/customer/level.tsx b/web/pages/customer/level.tsx index b48f382..fdfc22e 100644 --- a/web/pages/customer/level.tsx +++ b/web/pages/customer/level.tsx @@ -52,8 +52,8 @@ const CustomerLevelPage: React.FC = () => { return ( ); diff --git a/web/pages/customer/store.tsx b/web/pages/customer/store.tsx index 3f48a91..bcb1bd3 100644 --- a/web/pages/customer/store.tsx +++ b/web/pages/customer/store.tsx @@ -86,30 +86,6 @@ const StorePage: React.FC = () => { fieldProps: { min: 0, precision: 0 }, align: 'center', }, - { - title: '待回筐数量', - dataIndex: 'pending_box_num', - hideInForm: true, - hideInSearch: true, - align: 'center', - render: (_, record) => ( - 0 ? 'warning' : undefined}> - {record.pending_box_num ?? 0} - - ), - }, - { - title: '待回托盘数量', - dataIndex: 'pending_tray_num', - hideInForm: true, - hideInSearch: true, - align: 'center', - render: (_, record) => ( - 0 ? 'warning' : undefined}> - {record.pending_tray_num ?? 0} - - ), - }, { title: '总采购金额', dataIndex: 'total_purchase_amount', diff --git a/web/pages/customer/supplier.tsx b/web/pages/customer/supplier.tsx index 02ed25f..d9afecf 100644 --- a/web/pages/customer/supplier.tsx +++ b/web/pages/customer/supplier.tsx @@ -96,6 +96,7 @@ const SupplierPage: React.FC = () => { grid: true, colProps: { span: 12 }, layout: 'vertical', + rowProps: {gutter: 24} }, modalProps: { width: 720 }, }; diff --git a/web/pages/order/store.tsx b/web/pages/order/store.tsx index 0525758..82f9dbc 100644 --- a/web/pages/order/store.tsx +++ b/web/pages/order/store.tsx @@ -329,10 +329,6 @@ const StoreOrderPage: React.FC = () => { {record.bill.pay_state_name ?? BILL_PAY_STATE_MAP[record.bill.pay_state ?? 0]?.text} -
- 账单金额: - {record.bill.total_amount} ¥ -
) : '-' }, @@ -524,17 +520,6 @@ const StoreOrderPage: React.FC = () => { {detail.bill.pay_state_name ?? BILL_PAY_STATE_MAP[detail.bill.pay_state ?? 0]?.text} - ¥{detail.bill.product_amount ?? '-'} - ¥{detail.bill.delivery_fee ?? '-'} - - ¥{detail.bill.added_amount ?? '0.00'} - - (筐{detail.bill.box_num ?? 0}/托盘{detail.bill.tray_num ?? 0}) - - - - ¥{detail.bill.total_amount} - {detail.bill.paid_at ?? '-'} {detail.bill.pay_remark || '-'} diff --git a/web/pages/purchase/order.tsx b/web/pages/purchase/order.tsx index 5d4e769..4444b1f 100644 --- a/web/pages/purchase/order.tsx +++ b/web/pages/purchase/order.tsx @@ -1094,7 +1094,7 @@ const PurchaseOrderPage: React.FC = () => { {billPrepare && ( <>
- 每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费与周转筐/托盘数量。 + 每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费与周转筐/托盘数量(正数=压筐附加金额,负数=回筐抵扣金额)。 {billAllGenerated ? '该采购单已全部生成账单,仅可查看。' : '生成后采购单中的全部订单将关联到对应门店账单。'}
@@ -1105,8 +1105,8 @@ const PurchaseOrderPage: React.FC = () => {
门店
商品金额
配送费(元)
-
周转筐(¥{billPrepare.stores[0]?.box_price ?? '0.00'})
-
周转托盘(¥{billPrepare.stores[0]?.tray_price ?? '0.00'})
+
周转筐(¥{billPrepare.stores[0]?.box_price ?? '0.00'}/个)
+
周转托盘(¥{billPrepare.stores[0]?.tray_price ?? '0.00'}/个)
附加金额
账单总金额
@@ -1148,10 +1148,9 @@ const PurchaseOrderPage: React.FC = () => { > { >
diff --git a/web/pages/recon/container-return.tsx b/web/pages/recon/container-return.tsx index 199a888..7aa0b26 100644 --- a/web/pages/recon/container-return.tsx +++ b/web/pages/recon/container-return.tsx @@ -1,18 +1,40 @@ import React, { useEffect, useState } from 'react'; -import { message, Tag, Typography } from 'antd'; -import dayjs from 'dayjs'; +import { Typography } from 'antd'; import XinTable from '@/components/XinTable'; import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts'; import type IContainerReturn from '@/domain/iContainerReturn.ts'; -import { CONTAINER_TYPE_MAP } from '@/domain/iContainerReturn.ts'; import { getStoreOptions } from '@/api/customer/store.ts'; -import { Create } from '@/api/common/table.ts'; import type IStore from '@/domain/iStore.ts'; const { Title, Text } = Typography; +/** 有符号数量展示:正数=压筐(橙色 +N),负数=回筐(绿色 N),0 置灰 */ +const SignedNum: React.FC<{ value?: number }> = ({ value }) => { + const num = Number(value ?? 0); + if (num > 0) { + return +{num}; + } + if (num < 0) { + return {num}; + } + return 0; +}; + +/** 有符号金额展示:正=压筐附加(橙),负=回筐抵扣(绿) */ +const SignedAmount: React.FC<{ value?: string }> = ({ value }) => { + const num = Number(value ?? 0); + if (num > 0) { + return +¥{num.toFixed(2)}; + } + if (num < 0) { + return -¥{Math.abs(num).toFixed(2)}; + } + return ¥0.00; +}; + /** - * 回筐记录(压筐=生成账单时自动写入,只读;回筐=门店退回手动登记,删除后恢复门店待回数量) + * 压回筐记录(周转筐/托盘跟账单走:生成账单时自动写入完整快照,只读) + * 数量正数=压筐(附加金额),负数=回筐(抵扣金额) */ const ContainerReturnPage: React.FC = () => { const [stores, setStores] = useState([]); @@ -26,7 +48,7 @@ const ContainerReturnPage: React.FC = () => { title: '门店', dataIndex: 'store_id', valueType: 'select', - rules: [{ required: true, message: '请选择门店' }], + hideInForm: true, fieldProps: { options: stores.map((s) => ({ label: s.name, value: s.id })), showSearch: true, @@ -34,81 +56,57 @@ const ContainerReturnPage: React.FC = () => { }, render: (_, record) => record.store?.name ?? `门店#${record.store_id}`, }, - { - title: '类型', - dataIndex: 'type', - valueType: 'select', - hideInForm: true, - align: 'center', - fieldProps: { - options: Object.entries(CONTAINER_TYPE_MAP).map(([value, item]) => ({ - value: Number(value), - label: item.text, - })), - }, - render: (_, record) => { - const item = CONTAINER_TYPE_MAP[record.type ?? 0]; - return {item?.text ?? '-'}; - }, - }, { title: '关联账单', dataIndex: 'bill_id', hideInForm: true, hideInSearch: true, - align: 'center', - render: (_, record) => - record.bill ? {record.bill.bill_no} : '-', + render: (_, record) => record.bill ? ( + <> + {record.bill.bill_no} +
{record.bill.bill_date}
+ + ) : '-', }, { - title: '周转筐数量', + title: '压(回)筐数量', dataIndex: 'box_num', - valueType: 'digit', + hideInForm: true, hideInSearch: true, - initialValue: 0, - rules: [{ required: true, message: '请输入周转筐数量' }], - fieldProps: { min: 0, precision: 0 }, align: 'center', - render: (_, record) => {record.box_num}, + render: (_, record) => , }, { - title: '周转托盘数量', + title: '压(回)托盘数量', dataIndex: 'tray_num', - valueType: 'digit', - hideInSearch: true, - initialValue: 0, - rules: [{ required: true, message: '请输入周转托盘数量' }], - fieldProps: { min: 0, precision: 0 }, - align: 'center', - render: (_, record) => {record.tray_num}, - }, - { - title: '当前待回(筐/托盘)', - key: 'pending', hideInForm: true, hideInSearch: true, align: 'center', - render: (_, record) => - record.store - ? `${record.store.pending_box_num ?? 0} / ${record.store.pending_tray_num ?? 0}` - : '-', + render: (_, record) => , }, { - title: '退回日期', - dataIndex: 'return_date', - valueType: 'dateRange', - hideInTable: true, + title: '筐单价', + dataIndex: 'box_price', hideInForm: true, + hideInSearch: true, align: 'center', + render: (_, record) => `¥${Number(record.box_price ?? 0).toFixed(2)}`, }, { - title: '退回日期', - dataIndex: 'return_date', - valueType: 'date', + title: '托盘单价', + dataIndex: 'tray_price', + hideInForm: true, hideInSearch: true, - initialValue: dayjs(), - rules: [{ required: true, message: '请选择退回日期' }], align: 'center', + render: (_, record) => `¥${Number(record.tray_price ?? 0).toFixed(2)}`, + }, + { + title: '抵扣(附加)金额', + dataIndex: 'amount', + hideInForm: true, + hideInSearch: true, + align: 'center', + render: (_, record) => , }, { title: '操作人', @@ -119,20 +117,20 @@ const ContainerReturnPage: React.FC = () => { render: (_, record) => record.operator?.nickname ?? '-', }, { - title: '登记时间', + title: '记录时间', + dataIndex: 'created_at', + valueType: 'dateRange', + hideInForm: true, + hideInTable: true, + align: 'center', + }, + { + title: '记录时间', dataIndex: 'created_at', hideInForm: true, hideInSearch: true, align: 'center', }, - { - title: '备注', - dataIndex: 'remark', - valueType: 'textarea', - hideInSearch: true, - fieldProps: { rows: 2 }, - render: (v) => v || '-', - }, ]; const tableProps: XinTableProps = { @@ -140,25 +138,11 @@ const ContainerReturnPage: React.FC = () => { columns, rowKey: 'id', accessName: 'recon.containerReturn', - // 压筐记录由账单生成,仅回筐记录可删除;均不允许编辑 + // 记录随账单生成,只读:不新增、不编辑、不删除 + addShow: false, editShow: false, - deleteShow: (record) => record.type === 2, - // 自定义提交:DatePicker 值为 dayjs 对象,格式化为 Y-m-d 后再提交 - handleFinish: async (values) => { - await Create('/recon/container-return', { - ...values, - return_date: dayjs(values.return_date).format('YYYY-MM-DD'), - }); - message.success('回筐已登记'); - return true; - }, - formProps: { - grid: true, - colProps: { span: 12 }, - rowProps: { gutter: 20 }, - layout: 'vertical', - }, - modalProps: { width: 640, title: '回筐登记' }, + deleteShow: false, + formProps: false, }; return ( @@ -166,7 +150,7 @@ const ContainerReturnPage: React.FC = () => {
回筐记录 - 生成账单时自动写入「压筐」记录并累加门店待回数量;门店退回周转筐/托盘时手动新增「回筐」登记并扣减待回,删除登记将恢复待回数量。 + 周转筐/托盘跟账单走:生成账单时按填写数量自动写入;正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不生成记录。
{...tableProps} />