589 lines
27 KiB
PHP
589 lines
27 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\CustomerLevelModel;
|
||
use App\Models\ProductModel;
|
||
use App\Models\PurchaseOrderModel;
|
||
use App\Models\StoreModel;
|
||
use App\Models\StoreOrderItemModel;
|
||
use App\Models\StoreOrderModel;
|
||
use App\Models\SupplierModel;
|
||
|
||
/**
|
||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑、行级修改同步商品档案、
|
||
* 门店购买详情单品增删改,修改后重算订单/采购单汇总
|
||
* (金额口径:订单=数量×单价;采购单预估成本=Σ数量×每包成本价)
|
||
*/
|
||
class PurchaseEditTest extends ProcurementTestCase
|
||
{
|
||
/**
|
||
* 造采购链路:门店A 2 件 + 门店B 3 件(同一商品,等级上浮 0% → 售价=成本价 10.00,包规 10斤/箱),
|
||
* 接单后生成采购单
|
||
*
|
||
* @return array{0: PurchaseOrderModel, 1: ProductModel, 2: array<int, StoreModel>}
|
||
*/
|
||
private function buildPurchase(): array
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'cost_price' => 10,
|
||
'spec' => '10斤/箱',
|
||
'unit' => '斤',
|
||
]);
|
||
|
||
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||
->assertJsonPath('success', true);
|
||
}
|
||
|
||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||
|
||
$this->actingAsSysUser();
|
||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||
->assertJsonPath('success', true);
|
||
|
||
return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]];
|
||
}
|
||
|
||
/** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */
|
||
public function test_detail_returns_store_matrix(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$response = $this->getJson("/purchase/order/{$purchase->id}")
|
||
->assertOk()
|
||
->assertJsonPath('success', true);
|
||
|
||
$data = $response->json('data');
|
||
$this->assertCount(2, $data['stores']);
|
||
$this->assertCount(1, $data['items']);
|
||
|
||
$row = $data['items'][0];
|
||
$this->assertSame($product->id, $row['product_id']);
|
||
$this->assertSame('10斤/箱', $row['product_spec']);
|
||
$this->assertSame('斤', $row['unit']);
|
||
$this->assertSame('10.00', (string) $row['cost_price']);
|
||
$this->assertSame('50.00', (string) $row['amount'], '订货金额 = Σ明细 amount(5 × 10.00)');
|
||
$this->assertSame(5, $row['quantity'], '2+3');
|
||
|
||
$cells = $row['cells'];
|
||
$this->assertSame(2, $cells[$stores[0]->id]);
|
||
$this->assertSame(3, $cells[$stores[1]->id]);
|
||
}
|
||
|
||
/** 门店单元格数量/单价修改 → 明细金额重算(数量×单价),订单与采购单汇总同步 */
|
||
public function test_update_cell_syncs_order_item_and_totals(): void
|
||
{
|
||
[$purchase, , $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
|
||
->assertJsonPath('success', true);
|
||
|
||
$item = $item->fresh();
|
||
$this->assertSame(5, $item->quantity);
|
||
$this->assertSame('50.00', (string) $item->amount, '5 × 订货单价 10.00');
|
||
|
||
$order = StoreOrderModel::find($item->order_id);
|
||
$this->assertSame(5, $order->total_quantity);
|
||
$this->assertSame('50.00', (string) $order->total_amount);
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('8.00', (string) $purchase->total_quantity, '5+3');
|
||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00');
|
||
}
|
||
|
||
/** 行级成本修改 → 同步该商品全部订货明细与商品档案,采购单预估成本重算 */
|
||
public function test_update_row_cost_syncs_all_order_items(): void
|
||
{
|
||
[$purchase, $product] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => $product->name,
|
||
'supplier_id' => SupplierModel::factory()->create()->id,
|
||
'product_spec' => $product->spec,
|
||
'unit' => $product->unit,
|
||
'cost_price' => 30,
|
||
])->assertJsonPath('success', true)
|
||
->assertJsonPath('data.count', 2);
|
||
|
||
$this->assertSame(2, StoreOrderItemModel::where('cost_price', '30.00')->count());
|
||
$this->assertSame('30.00', (string) $product->fresh()->cost_price, '商品档案成本价应同步更新');
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('150.00', (string) $purchase->estimate_amount, '5 包 × 每包成本 30.00');
|
||
}
|
||
|
||
/** 行级属性修改不影响称重(重量为手动录入,仅作参考) */
|
||
public function test_update_row_does_not_touch_manual_weight(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$supplier = SupplierModel::factory()->create();
|
||
$this->actingAsSysUser();
|
||
|
||
// 先录入一笔手动称重
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 2, 'price' => 10, 'weight' => 4.5])
|
||
->assertJsonPath('success', true);
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => $product->name,
|
||
'supplier_id' => $supplier->id,
|
||
'product_spec' => $product->spec,
|
||
'unit' => $product->unit,
|
||
'cost_price' => 30,
|
||
])->assertJsonPath('success', true);
|
||
|
||
$this->assertSame('4.500', (string) $item->fresh()->weight, '重量保持手动录入值,不受行修改影响');
|
||
}
|
||
|
||
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细与商品档案 */
|
||
public function test_update_row_syncs_product_attributes(): void
|
||
{
|
||
[$purchase, $product] = $this->buildPurchase();
|
||
$supplier = SupplierModel::factory()->create();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => '优选土豆',
|
||
'supplier_id' => $supplier->id,
|
||
'product_spec' => '5斤/袋',
|
||
'unit' => '袋',
|
||
'cost_price' => 25,
|
||
])->assertJsonPath('success', true)
|
||
->assertJsonPath('data.count', 2);
|
||
|
||
$items = StoreOrderItemModel::all();
|
||
$this->assertCount(2, $items);
|
||
foreach ($items as $item) {
|
||
$this->assertSame('优选土豆', $item->product_name);
|
||
$this->assertSame($supplier->id, $item->supplier_id);
|
||
$this->assertSame('5斤/袋', $item->product_spec);
|
||
$this->assertSame('袋', $item->unit);
|
||
$this->assertSame('25.00', (string) $item->cost_price);
|
||
}
|
||
|
||
// 商品档案同步保存(商品列表可见)
|
||
$product = $product->fresh();
|
||
$this->assertSame('优选土豆', $product->name);
|
||
$this->assertSame($supplier->id, $product->supplier_id);
|
||
$this->assertSame('5斤/袋', $product->spec);
|
||
$this->assertSame('袋', $product->unit);
|
||
$this->assertSame('25.00', (string) $product->cost_price);
|
||
|
||
$this->assertSame('125.00', (string) $purchase->fresh()->estimate_amount, '5 包 × 每包成本 25.00');
|
||
}
|
||
|
||
/** 行级修改:商品已软删除时明细照常更新、跳过档案同步 */
|
||
public function test_update_row_skips_catalog_sync_when_product_deleted(): void
|
||
{
|
||
[$purchase, $product] = $this->buildPurchase();
|
||
$supplier = SupplierModel::factory()->create();
|
||
$this->actingAsSysUser();
|
||
|
||
$product->delete(); // 软删除
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => '已删商品新名',
|
||
'supplier_id' => $supplier->id,
|
||
'product_spec' => '5斤/袋',
|
||
'unit' => '袋',
|
||
'cost_price' => 25,
|
||
])->assertOk()
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('msg', '已同步 2 条订货明细;商品已删除,档案未同步;门店单价已按等级上浮比例重算');
|
||
|
||
$this->assertSame('已删商品新名', StoreOrderItemModel::first()->product_name, '订货明细照常更新');
|
||
$this->assertNotSame('已删商品新名', $product->fresh()->name, '商品档案不同步');
|
||
}
|
||
|
||
/** 行级修改:必填字段校验(空 payload 拒绝) */
|
||
public function test_update_row_requires_at_least_one_field(): void
|
||
{
|
||
[$purchase, $product] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [])
|
||
->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 单元格数量校验:负数拒绝 */
|
||
public function test_update_cell_rejects_negative_quantity(): void
|
||
{
|
||
[$purchase, , $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1, 'price' => 10])
|
||
->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 单元格下钻:返回该门店该商品的全部订货明细(订单号/状态/快照),其他门店无明细 */
|
||
public function test_cell_detail_returns_items_with_order_info(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$response = $this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}")
|
||
->assertOk()
|
||
->assertJsonPath('success', true);
|
||
|
||
$data = $response->json('data');
|
||
$this->assertSame($stores[0]->name, $data['store']['name']);
|
||
$this->assertSame($product->name, $data['product']['name']);
|
||
$this->assertCount(1, $data['items']);
|
||
|
||
$item = $data['items'][0];
|
||
$this->assertArrayHasKey('order_no', $item);
|
||
$this->assertSame(2, $item['quantity']);
|
||
$this->assertSame('20.00', (string) $item['amount']);
|
||
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $item['order_status'], '生成采购单后源订单为采购中');
|
||
|
||
// 其他门店 → 另一条明细
|
||
$this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[1]->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.items.0.quantity', 3);
|
||
}
|
||
|
||
/** 单元格下钻参数校验:缺少商品/门店参数拒绝 */
|
||
public function test_cell_detail_rejects_missing_params(): void
|
||
{
|
||
[$purchase] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->getJson("/purchase/order/{$purchase->id}/cell")
|
||
->assertJsonPath('success', false);
|
||
}
|
||
|
||
/** 单元格称重修改 → 明细/订货单/采购单重量与金额汇总级联重算 */
|
||
public function test_update_cell_weight_cascades_totals(): void
|
||
{
|
||
[$purchase, , $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10, 'weight' => 4.5])
|
||
->assertJsonPath('success', true);
|
||
|
||
$item = $item->fresh();
|
||
$this->assertSame('4.500', (string) $item->weight);
|
||
$this->assertSame('50.00', (string) $item->amount, '5 × 订货单价 10.00');
|
||
|
||
$order = StoreOrderModel::find($item->order_id);
|
||
$this->assertSame('4.500', (string) $order->total_weight);
|
||
$this->assertSame('50.00', (string) $order->total_amount);
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('4.500', (string) $purchase->total_weight);
|
||
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
|
||
}
|
||
|
||
/** 行级成本修改 → 明细单价按各门店等级上浮比例重算,订单与采购单汇总级联 */
|
||
public function test_update_row_cost_change_reprices_items_by_store_level(): void
|
||
{
|
||
// 门店A 上浮 0%(售价=成本),门店B 上浮 50%
|
||
$levelA = CustomerLevelModel::factory()->create(['percent' => 0]);
|
||
$levelB = CustomerLevelModel::factory()->create(['percent' => 50]);
|
||
$storeA = StoreModel::factory()->create(['level_id' => $levelA->id]);
|
||
$storeB = StoreModel::factory()->create(['level_id' => $levelB->id]);
|
||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
|
||
|
||
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||
->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();
|
||
|
||
// 下单时快照:A 店 10.00(10 上浮 0%),B 店 15.00(10 上浮 50%)
|
||
$itemA = StoreOrderItemModel::where('store_id', $storeA->id)->first();
|
||
$itemB = StoreOrderItemModel::where('store_id', $storeB->id)->first();
|
||
$this->assertSame('10.00', (string) $itemA->price);
|
||
$this->assertSame('15.00', (string) $itemB->price);
|
||
|
||
// 行修改成本 10 → 20:单价按各店等级上浮比例重算
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => $product->name,
|
||
'supplier_id' => SupplierModel::factory()->create()->id,
|
||
'product_spec' => $product->spec,
|
||
'unit' => $product->unit,
|
||
'cost_price' => 20,
|
||
])->assertOk()
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('msg', '已同步 2 条订货明细与商品档案;门店单价已按等级上浮比例重算');
|
||
|
||
$itemA->refresh();
|
||
$this->assertSame('20.00', (string) $itemA->cost_price);
|
||
$this->assertSame('20.00', (string) $itemA->price, 'A 店:20 上浮 0% = 20.00');
|
||
$this->assertSame('40.00', (string) $itemA->amount, '20.00 × 2');
|
||
|
||
$itemB->refresh();
|
||
$this->assertSame('30.00', (string) $itemB->price, 'B 店:20 上浮 50% = 30.00');
|
||
$this->assertSame('90.00', (string) $itemB->amount, '30.00 × 3');
|
||
|
||
$this->assertSame('40.00', (string) StoreOrderModel::where('store_id', $storeA->id)->first()->total_amount);
|
||
$this->assertSame('90.00', (string) StoreOrderModel::where('store_id', $storeB->id)->first()->total_amount);
|
||
$this->assertSame('100.00', (string) $purchase->fresh()->estimate_amount, '5 包 × 新成本 20.00');
|
||
}
|
||
|
||
/** 行修改未改成本 → 明细单价保持不变(不覆盖单元格的手工改价) */
|
||
public function test_update_row_without_cost_change_keeps_item_price(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
// 先通过单元格手工改价 10 → 12
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 2, 'price' => 12])
|
||
->assertJsonPath('success', true);
|
||
|
||
// 行修改只改品名(成本不变)
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => '只改品名',
|
||
'supplier_id' => SupplierModel::factory()->create()->id,
|
||
'product_spec' => $product->spec,
|
||
'unit' => $product->unit,
|
||
'cost_price' => 10,
|
||
])->assertOk()
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('msg', '已同步 2 条订货明细与商品档案');
|
||
|
||
$this->assertSame('12.00', (string) $item->fresh()->price, '成本未变时保留手工改价');
|
||
}
|
||
|
||
/** 门店购买详情-新增单品:挂靠最新一笔订单,汇总级联重算;重复添加拒绝 */
|
||
public function test_store_item_add_creates_item_and_recalculates(): void
|
||
{
|
||
[$purchase, , $stores] = $this->buildPurchase();
|
||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 8]);
|
||
$this->actingAsSysUser();
|
||
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||
'product_id' => $extra->id,
|
||
'quantity' => 4,
|
||
'weight' => 1.5,
|
||
])->assertOk()->assertJsonPath('success', true);
|
||
|
||
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
|
||
$this->assertNotNull($item);
|
||
$this->assertSame($stores[0]->id, $item->store_id);
|
||
$this->assertSame($purchase->id, $item->purchase_id);
|
||
$this->assertSame('8.00', (string) $item->price, '等级上浮 0% → 单价=成本价');
|
||
$this->assertSame('32.00', (string) $item->amount, '8.00 × 4');
|
||
$this->assertSame('1.500', (string) $item->weight);
|
||
|
||
$order = StoreOrderModel::find($item->order_id);
|
||
$this->assertSame(6, $order->total_quantity, '2+4');
|
||
$this->assertSame('52.00', (string) $order->total_amount, '20+32');
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('9.00', (string) $purchase->total_quantity, '2+3+4');
|
||
$this->assertSame('82.00', (string) $purchase->estimate_amount, '50+32(4 包 × 成本 8.00)');
|
||
$this->assertSame('1.500', (string) $purchase->total_weight);
|
||
|
||
// 重复添加同一商品 → 拒绝
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||
'product_id' => $extra->id,
|
||
'quantity' => 1,
|
||
])->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '该商品已在此门店采购明细中,请直接修改数量');
|
||
}
|
||
|
||
/** 门店购买详情-修改单品:同商品多笔订单明细合并到最早一条,涉及订单逐一重算 */
|
||
public function test_store_item_update_merges_multi_order_rows(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
|
||
|
||
// 同一门店两笔订单各订 2 件(同商品)→ 同一采购单
|
||
$this->actingAsMiniStore($store);
|
||
foreach ([2, 2] as $qty) {
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||
->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();
|
||
|
||
$this->assertSame(2, StoreOrderItemModel::count(), '两笔订单各一条明细');
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||
'quantity' => 10,
|
||
'price' => 12,
|
||
'weight' => 5,
|
||
])->assertJsonPath('success', true);
|
||
|
||
$this->assertSame(1, StoreOrderItemModel::count(), '多笔明细合并为一条');
|
||
$item = StoreOrderItemModel::first();
|
||
$this->assertSame(10, $item->quantity);
|
||
$this->assertSame('12.00', (string) $item->price);
|
||
$this->assertSame('120.00', (string) $item->amount);
|
||
$this->assertSame('5.000', (string) $item->weight);
|
||
|
||
// 保留行所在订单重算;被合并订单清零
|
||
$survivedOrder = StoreOrderModel::find($item->order_id);
|
||
$this->assertSame(10, $survivedOrder->total_quantity);
|
||
$this->assertSame('120.00', (string) $survivedOrder->total_amount);
|
||
$mergedOrder = StoreOrderModel::where('id', '<>', $item->order_id)->first();
|
||
$this->assertSame(0, $mergedOrder->total_quantity);
|
||
$this->assertSame('0.00', (string) $mergedOrder->total_amount);
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('10.00', (string) $purchase->total_quantity);
|
||
$this->assertSame('100.00', (string) $purchase->estimate_amount, '10 包 × 成本 10.00');
|
||
$this->assertSame('5.000', (string) $purchase->total_weight);
|
||
}
|
||
|
||
/** 门店购买详情-移除单品:明细删除,订单与采购单汇总级联重算 */
|
||
public function test_store_item_remove_deletes_and_recalculates(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
|
||
->assertOk()->assertJsonPath('success', true);
|
||
|
||
$this->assertSame(0, StoreOrderItemModel::where('store_id', $stores[0]->id)->count());
|
||
$this->assertSame(1, StoreOrderItemModel::count(), '门店B 明细不受影响');
|
||
|
||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||
$this->assertSame(0, $order->total_quantity);
|
||
$this->assertSame('0.00', (string) $order->total_amount);
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('3.00', (string) $purchase->total_quantity, '仅剩门店B 3 件');
|
||
$this->assertSame('30.00', (string) $purchase->estimate_amount);
|
||
}
|
||
|
||
/** 门店购买详情:按商品聚合该门店采购汇总(数量/包规/单位/单价/预计金额),仅含该门店明细 */
|
||
public function test_store_summary_returns_aggregated_items(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$response = $this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[0]->id}")
|
||
->assertOk()
|
||
->assertJsonPath('success', true);
|
||
|
||
$data = $response->json('data');
|
||
$this->assertSame($stores[0]->name, $data['store']['name']);
|
||
$this->assertCount(1, $data['items']);
|
||
|
||
$row = $data['items'][0];
|
||
$this->assertSame($product->id, $row['product_id']);
|
||
$this->assertSame('10斤/箱', $row['product_spec']);
|
||
$this->assertSame('斤', $row['unit']);
|
||
$this->assertSame('10.00', (string) $row['price']);
|
||
$this->assertSame(2, $row['quantity']);
|
||
$this->assertSame('20.00', (string) $row['amount']);
|
||
|
||
// 另一门店只见自身明细
|
||
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[1]->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.items.0.quantity', 3)
|
||
->assertJsonPath('data.items.0.amount', '30.00');
|
||
}
|
||
|
||
/** 门店购买详情:同一商品多笔订单不同单价 → 数量合计、金额求和、单价为加权平均 */
|
||
public function test_store_summary_aggregates_multiple_orders_with_weighted_price(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'cost_price' => 10,
|
||
'spec' => '10斤/箱',
|
||
'unit' => '斤',
|
||
]);
|
||
|
||
$this->actingAsMiniStore($store);
|
||
foreach ([2, 3] as $qty) {
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||
->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();
|
||
// 第二笔明细单价改为 20.00,构造同商品双单价:2×10 + 3×20
|
||
$secondItem = StoreOrderItemModel::orderBy('id')->skip(1)->first();
|
||
$this->putJson("/purchase/order/cell/{$secondItem->id}", ['quantity' => 3, 'price' => 20])
|
||
->assertJsonPath('success', true);
|
||
|
||
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$store->id}")
|
||
->assertOk()
|
||
->assertJsonPath('data.items.0.quantity', 5)
|
||
->assertJsonPath('data.items.0.price', '16.00', '(20+60) ÷ 5 加权平均')
|
||
->assertJsonPath('data.items.0.amount', '80.00');
|
||
}
|
||
|
||
/** 门店购买详情参数校验:缺少门店参数 / 采购单不存在均拒绝 */
|
||
public function test_store_summary_rejects_missing_or_invalid_params(): void
|
||
{
|
||
[$purchase] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->getJson("/purchase/order/{$purchase->id}/store")
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '缺少门店参数');
|
||
|
||
$this->getJson('/purchase/order/99999/store?store_id=1')
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '采购单不存在');
|
||
}
|
||
|
||
/** 采购单已完成:单元格编辑、行修改、门店单品增删改均拒绝,明细不变 */
|
||
public function test_edits_rejected_when_purchase_completed(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
|
||
$supplier = SupplierModel::factory()->create();
|
||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => '改名',
|
||
'supplier_id' => $supplier->id,
|
||
'product_spec' => '5斤/袋',
|
||
'unit' => '袋',
|
||
'cost_price' => 30,
|
||
])->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
|
||
'product_id' => $product->id,
|
||
'quantity' => 1,
|
||
])->assertJsonPath('success', false);
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}", [
|
||
'quantity' => 5,
|
||
])->assertJsonPath('success', false);
|
||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
|
||
->assertJsonPath('success', false);
|
||
|
||
$this->assertSame(2, $item->fresh()->quantity, '被拒绝后明细不变');
|
||
$this->assertSame('10.00', (string) $item->fresh()->cost_price);
|
||
}
|
||
}
|