修复BUG
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 . '·岳各庄';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user