修复BUG

This commit is contained in:
liu
2026-08-27 14:20:09 +08:00
parent 513cc568a9
commit 52dc6efadf
203 changed files with 576 additions and 1130 deletions
+57
View File
@@ -72,6 +72,63 @@ class BillPaymentTest extends ProcurementTestCase
], $attributes));
}
/** 生成账单:按门店保存售后说明与备注(可选,不参与金额计算) */
public function test_generate_bill_saves_after_sale_and_remark(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$this->actingAsMiniStore($store);
$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]);
$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,
'after_sale' => '白菜烂叶 2 斤已协商',
'remark' => '下次配送顺带回收筐',
]],
])->assertJsonPath('success', true);
$bill = BillModel::where('store_id', $store->id)->first();
$this->assertSame('白菜烂叶 2 斤已协商', $bill->after_sale);
$this->assertSame('下次配送顺带回收筐', $bill->remark);
$this->assertSame('20.00', (string) $bill->total_amount, '售后/备注不影响金额');
// 不传售后/备注时默认空字符串
$store2 = StoreModel::factory()->create(['level_id' => $level->id]);
$this->actingAsMiniStore($store2);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
->assertJsonPath('success', true);
$order2 = StoreOrderModel::where('store_id', $store2->id)->first();
$this->actingAsSysUser();
$this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase2 = PurchaseOrderModel::latest('id')->first();
$this->putJson("/purchase/order/{$purchase2->id}/finish")->assertJsonPath('success', true);
$this->postJson("/purchase/order/{$purchase2->id}/bill", [
'stores' => [['store_id' => $store2->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]],
])->assertJsonPath('success', true);
$bill2 = BillModel::where('store_id', $store2->id)->first();
$this->assertSame('', (string) $bill2->after_sale);
$this->assertSame('', (string) $bill2->remark);
}
/** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */
public function test_order_status_progresses_via_business_chain(): void
{
+31
View File
@@ -265,6 +265,37 @@ class CartTest extends ProcurementTestCase
$this->getJson('/mini/cart')->assertStatus(401);
}
/** 下单成功后自动清空购物车中已下单的商品(未下单商品保留) */
public function test_place_order_clears_ordered_items_from_cart(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$p1 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$p2 = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '3.00']);
$this->actingAsMiniStore($store);
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 2]);
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 1]);
$this->assertSame(2, CartModel::where('store_id', $store->id)->count());
// 下单两个商品 → 购物车清空
$this->postJson('/mini/order', ['items' => [
['product_id' => $p1->id, 'quantity' => 2],
['product_id' => $p2->id, 'quantity' => 1],
]])->assertOk()->assertJsonPath('success', true);
$this->assertSame(0, CartModel::where('store_id', $store->id)->count(), '下单后购物车应自动清空');
// 部分下单:仅清除已下单商品,未下单商品保留
$this->postJson('/mini/cart', ['product_id' => $p1->id, 'quantity' => 2]);
$this->postJson('/mini/cart', ['product_id' => $p2->id, 'quantity' => 1]);
$this->postJson('/mini/order', ['items' => [['product_id' => $p1->id, 'quantity' => 2]]])
->assertOk()->assertJsonPath('success', true);
$remaining = CartModel::where('store_id', $store->id)->get();
$this->assertSame(1, $remaining->count(), '未下单商品应保留在购物车');
$this->assertSame($p2->id, $remaining->first()->product_id);
}
/** 跨门店列表隔离:B 店看不到 A 店的购物车 */
public function test_cart_isolated_between_stores(): void
{
+85 -19
View File
@@ -137,7 +137,7 @@ class ExportTest extends ProcurementTestCase
return [PurchaseOrderModel::first(), $veg, $meat, $storeA, $storeB, $supplierA, $supplierB];
}
/** 商品明细导出:含系统全部商品行(无订货数量 0)+ 行列合计 + 参考零售价列 */
/** 商品明细导出:含系统全部商品行(无订货数量 0)+ 行列合计 + 价列 */
public function test_export_lists_all_catalog_products_with_totals_row(): void
{
[$purchase, $veg, $meat, $storeA, $storeB] = $this->buildPurchaseWithSuppliers();
@@ -156,12 +156,12 @@ class ExportTest extends ProcurementTestCase
if ($rows->count() !== 8) {
return false;
}
// 列头含市场/参考零售价与门店列(市场列在供应商后,门店列从索引 12 起)
// 列头含市场/价与门店列(市场列在供应商后,门店列从索引 12 起)
$header = $rows[3];
if ($header[4] !== '市场' || $header[8] !== '参考零售价' || $header[12] !== $storeA->name || $header[13] !== $storeB->name) {
if ($header[4] !== '市场' || $header[8] !== '价' || $header[12] !== $storeA->name || $header[13] !== $storeB->name) {
return false;
}
// 蔬菜行:市场 新发地、数量 2+3=5、金额 25、参考零售价 5÷10=0.50、门店列 2/3
// 蔬菜行:市场 新发地、数量 2+3=5、金额 25、价 5÷10=0.50、门店列 2/3
$vegRow = $rows->firstWhere(2, $veg->name);
if ($vegRow === null
|| $vegRow[4] !== '新发地'
@@ -170,7 +170,7 @@ class ExportTest extends ProcurementTestCase
|| (float) $vegRow[12] !== 2.0 || (float) $vegRow[13] !== 3.0) {
return false;
}
// 无订货商品行:数量 0、参考零售价留空、门店列 0
// 无订货商品行:数量 0、价留空、门店列 0
$extraRow = $rows->firstWhere(2, $extra->name);
if ($extraRow === null
|| (float) $extraRow[9] !== 0.0 || $extraRow[8] !== ''
@@ -277,10 +277,10 @@ class ExportTest extends ProcurementTestCase
->assertJsonPath('msg', '该门店在此采购单中无采购商品');
}
/** 供应商采购明细导出:供应商合并一个 XLSX(工作表名=供应商名),按供应商聚合 */
/** 供应商采购明细导出:按「供应商×市场」拆分工作表,模板列序=品名/汇总/市场/各门店明细 */
public function test_export_suppliers_multi_sheet(): void
{
[$purchase, $veg, $meat, , , $supplierA, $supplierB] = $this->buildPurchaseWithSuppliers();
[$purchase, $veg, $meat, $storeA, $storeB, $supplierA, $supplierB] = $this->buildPurchaseWithSuppliers();
Excel::fake();
$this->actingAsSysUser();
@@ -288,37 +288,103 @@ class ExportTest extends ProcurementTestCase
Excel::assertDownloaded(
$purchase->purchase_no . '_供应商采购明细.xlsx',
static function (PurchaseSupplierExport $export) use ($supplierA, $supplierB, $veg, $meat): bool {
static function (PurchaseSupplierExport $export) use ($supplierA, $supplierB, $veg, $meat, $storeA, $storeB): bool {
$sheets = $export->sheets();
if (count($sheets) !== 2) {
return false;
}
$titles = array_map(static fn ($sheet) => $sheet->title(), $sheets);
if ($titles !== [$supplierA->name, $supplierB->name]) {
if ($titles !== [$supplierA->name . '·新发地', $supplierB->name . '·岳各庄']) {
return false;
}
// 供应商甲:蔬菜 2+3=5 件、金额 5×5=25(市场列在品名后)
// 供应商甲·新发地:蔬菜 2+3=5 件;门店列=有该供应商明细的两家门店
$rowsA = $sheets[0]->collection()->values();
if ($rowsA[2] !== ['序号', '品名', '市场', '包规', '单位', '成本价', '数量', '重量(斤)', '金额']) {
if ($rowsA[2] !== ['品名', '汇总', '市场', $storeA->name, $storeB->name]) {
return false;
}
$vegRow = $rowsA->firstWhere(1, $veg->name);
if ($vegRow === null || $vegRow[2] !== '新发地' || (int) $vegRow[6] !== 5 || (float) $vegRow[8] !== 25.0) {
$vegRow = $rowsA->firstWhere(0, $veg->name);
if ($vegRow === null || (int) $vegRow[1] !== 5 || $vegRow[2] !== '新发地'
|| (int) $vegRow[3] !== 2 || (int) $vegRow[4] !== 3) {
return false;
}
$totalA = $rowsA->last();
if ($totalA[1] !== '合计' || (int) $totalA[6] !== 5 || (float) $totalA[8] !== 25.0) {
if ($totalA[0] !== '合计' || (int) $totalA[1] !== 5 || (int) $totalA[3] !== 2 || (int) $totalA[4] !== 3) {
return false;
}
// 供应商乙:肉 1 件、金额 20
// 供应商乙·岳各庄:肉 1 件;门店列仅门店A
$rowsB = $sheets[1]->collection()->values();
$meatRow = $rowsB->firstWhere(1, $meat->name);
return $meatRow !== null && $meatRow[2] === '岳各庄' && (int) $meatRow[6] === 1 && (float) $meatRow[8] === 20.0;
if ($rowsB[2] !== ['品名', '汇总', '市场', $storeA->name]) {
return false;
}
$meatRow = $rowsB->firstWhere(0, $meat->name);
return $meatRow !== null && (int) $meatRow[1] === 1 && $meatRow[2] === '岳各庄' && (int) $meatRow[3] === 1;
}
);
}
/** 供应商采购明细导出:单供应商导出 + 无明细供应商拒绝 */
/** 供应商采购明细导出:同一供应商多个市场拆分为多个工作表(空市场归入「未设置」) */
public function test_export_suppliers_split_by_market(): void
{
$supplier = SupplierModel::factory()->create();
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$vegA = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON, 'cost_price' => '5.00',
'supplier_id' => $supplier->id, 'market' => '新发地',
]);
$vegB = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON, 'cost_price' => '6.00',
'supplier_id' => $supplier->id, 'market' => '岳各庄',
]);
$vegC = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON, 'cost_price' => '7.00',
'supplier_id' => $supplier->id, 'market' => '',
]);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [
['product_id' => $vegA->id, 'quantity' => 1],
['product_id' => $vegB->id, 'quantity' => 1],
['product_id' => $vegC->id, 'quantity' => 1],
]])->assertJsonPath('success', true);
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
Excel::fake();
$this->get("/purchase/order/{$purchase->id}/exportSuppliers?supplier_id={$supplier->id}")->assertOk();
Excel::assertDownloaded(
$purchase->purchase_no . '_供应商采购明细_' . $supplier->name . '.xlsx',
static function (PurchaseSupplierExport $export) use ($supplier, $vegA, $vegB, $vegC): bool {
$sheets = $export->sheets();
$titles = array_map(static fn ($sheet) => $sheet->title(), $sheets);
sort($titles);
if ($titles !== [$supplier->name . '·岳各庄', $supplier->name . '·新发地', $supplier->name . '·未设置']) {
return false;
}
// 每个工作表仅含本市场商品
foreach ($sheets as $sheet) {
$rows = $sheet->collection()->values();
$names = $rows->slice(3, -1)->map(static fn ($row) => $row[0])->values()->all();
$expected = match (true) {
str_ends_with($sheet->title(), '新发地') => [$vegA->name],
str_ends_with($sheet->title(), '岳各庄') => [$vegB->name],
default => [$vegC->name],
};
if ($names !== $expected) {
return false;
}
}
return true;
}
);
}
/** 供应商采购明细导出:单供应商导出(工作表名=供应商·市场) + 无明细供应商拒绝 */
public function test_export_suppliers_single(): void
{
[$purchase, , , , , , $supplierB] = $this->buildPurchaseWithSuppliers();
@@ -331,7 +397,7 @@ class ExportTest extends ProcurementTestCase
$purchase->purchase_no . '_供应商采购明细_' . $supplierB->name . '.xlsx',
static function (PurchaseSupplierExport $export) use ($supplierB): bool {
$sheets = $export->sheets();
return count($sheets) === 1 && $sheets[0]->title() === $supplierB->name;
return count($sheets) === 1 && $sheets[0]->title() === $supplierB->name . '·岳各庄';
}
);
}
+41
View File
@@ -108,6 +108,47 @@ class ProductCostPriceTest extends ProcurementTestCase
$this->assertSame('10.00', (string) $product->cost_price);
}
/** 单价单位:不传默认「元/斤」,创建/编辑可修改 */
public function test_product_price_unit_default_and_editable(): void
{
$this->actingAsSysUser();
$category = $this->makeCategory();
// 不传单价单位 → 默认「元/斤」
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '默认单价单位商品',
'content' => '测试图文详情',
])->assertOk()->assertJsonPath('success', true);
$product = ProductModel::where('name', '默认单价单位商品')->first();
$this->assertSame('元/斤', $product->price_unit);
// 创建时指定 + 编辑修改
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '箱装商品',
'content' => '测试图文详情',
'price_unit' => '元/箱',
])->assertOk()->assertJsonPath('success', true);
$product2 = ProductModel::where('name', '箱装商品')->first();
$this->assertSame('元/箱', $product2->price_unit);
$this->putJson("/product/goods/{$product2->id}", [
'category_id' => $category->id,
'name' => '箱装商品',
'price_unit' => '元/件',
])->assertOk()->assertJsonPath('success', true);
$this->assertSame('元/件', $product2->refresh()->price_unit);
// 超长拒绝
$this->postJson('/product/goods', [
'category_id' => $category->id,
'name' => '异常单价单位商品',
'content' => '测试图文详情',
'price_unit' => str_repeat('元', 21),
])->assertOk()->assertJsonPath('success', false);
}
/** 批量调价-成本价行:更新成本并通知门店;等级售价随上浮比例联动 */
public function test_batch_price_cost_row_update(): void
{
+48
View File
@@ -271,4 +271,52 @@ class StoreOrderItemTest extends ProcurementTestCase
$this->getJson('/order/store?product_name=' . urlencode('不存在的商品'))
->assertOk()->assertJsonPath('data.total', 0);
}
/** 后台订单列表按订货日期区间筛选(快捷按钮下发 Y-m-d 起止日期) */
public function test_admin_order_list_filters_by_order_date_range(): void
{
[$store, $product] = $this->makeStoreWithProduct();
$this->placeOrder($product, $store, 1);
$this->placeOrder($product, $store, 1);
$this->placeOrder($product, $store, 1);
$orders = StoreOrderModel::where('store_id', $store->id)->orderBy('id')->get();
$orders[0]->update(['order_date' => '2026-08-01']);
$orders[1]->update(['order_date' => '2026-08-05']);
$orders[2]->update(['order_date' => '2026-08-10']);
$this->actingAsSysUser();
$this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-10')
->assertOk()->assertJsonPath('data.total', 2);
$this->getJson('/order/store?order_date[]=2026-08-05&order_date[]=2026-08-05')
->assertOk()->assertJsonPath('data.total', 1, '今天/昨天等单日快捷按钮起止同日');
$this->getJson('/order/store?order_date[]=2026-08-11&order_date[]=2026-08-12')
->assertOk()->assertJsonPath('data.total', 0);
}
/** 后台订单列表按采购单号模糊搜索 */
public function test_admin_order_list_search_by_purchase_no(): void
{
[$store, $product] = $this->makeStoreWithProduct();
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
// 生成采购单外的另一笔订单(不应被命中)
$this->placeOrder($product, $store, 1);
$this->actingAsSysUser();
$response = $this->getJson('/order/store?purchase_no=' . $purchase->purchase_no);
$response->assertOk()->assertJsonPath('success', true);
$this->assertSame(1, $response->json('data.total'));
$this->assertSame($purchase->purchase_no, $response->json('data.data.0.purchase.purchase_no'));
// 单号片段模糊命中
$fragment = substr($purchase->purchase_no, -6);
$this->getJson('/order/store?purchase_no=' . $fragment)
->assertOk()->assertJsonPath('data.total', 1);
// 无匹配单号 → 空列表
$this->getJson('/order/store?purchase_no=NOT-EXIST-NO')
->assertOk()->assertJsonPath('data.total', 0);
}
}