235 lines
9.8 KiB
PHP
235 lines
9.8 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\CustomerLevelModel;
|
||
use App\Models\ProductModel;
|
||
use App\Models\ProductPriceModel;
|
||
use App\Models\PurchaseOrderModel;
|
||
use App\Models\StoreModel;
|
||
use App\Models\StoreOrderItemModel;
|
||
use App\Models\StoreOrderModel;
|
||
use App\Models\SupplierModel;
|
||
use App\Models\UserModel;
|
||
|
||
/**
|
||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格数量、行级成本/称重,
|
||
* 修改同步订货明细并重算订单/采购单汇总
|
||
*/
|
||
class PurchaseEditTest extends ProcurementTestCase
|
||
{
|
||
/**
|
||
* 造采购链路:门店A 2 件 + 门店B 3 件(同一商品,等级价 10.00,成本 20,包规 10斤/箱 → 单价 2.00),
|
||
* 接单后生成采购单
|
||
*
|
||
* @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' => 20,
|
||
'spec' => '10斤/箱',
|
||
'unit' => '斤',
|
||
]);
|
||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 10.00]);
|
||
|
||
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
|
||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||
$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('20.00', (string) $row['cost_price']);
|
||
$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])
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('data.amount', 50);
|
||
|
||
$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->product_amount);
|
||
$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, '50+30');
|
||
}
|
||
|
||
/** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×单价 重算 */
|
||
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}", ['cost_price' => 30])
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('data.count', 2);
|
||
|
||
$this->assertSame(2, StoreOrderItemModel::where('cost_price', '30.00')->count());
|
||
|
||
$purchase = $purchase->fresh();
|
||
// 单价 = 30 ÷ 10 = 3.00,实际金额 = 5 × 3.00
|
||
$this->assertSame('15.00', (string) $purchase->actual_amount);
|
||
$this->assertSame('50.00', (string) $purchase->estimate_amount, '订货金额不受成本修改影响');
|
||
}
|
||
|
||
/** 行级称重修改 → 按各店数量比例分摊写入明细(尾差修正守恒),实际金额按 称重×单价 计 */
|
||
public function test_update_row_weight_distributed_by_quantity(): void
|
||
{
|
||
[$purchase, $product, $stores] = $this->buildPurchase();
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['weight' => 10])
|
||
->assertJsonPath('success', true);
|
||
|
||
$items = StoreOrderItemModel::query()->orderBy('store_id')->get()->keyBy('store_id');
|
||
$this->assertSame('4.000', (string) $items[$stores[0]->id]->weight, '10 × 2/5');
|
||
$this->assertSame('6.000', (string) $items[$stores[1]->id]->weight, '10 × 3/5');
|
||
$this->assertSame(
|
||
'10.000',
|
||
bcadd((string) $items[$stores[0]->id]->weight, (string) $items[$stores[1]->id]->weight, 3),
|
||
'分摊守恒'
|
||
);
|
||
|
||
$purchase = $purchase->fresh();
|
||
$this->assertSame('10.000', (string) $purchase->total_weight);
|
||
// 称重>0 → 金额按 称重×单价:10 × 2.00
|
||
$this->assertSame('20.00', (string) $purchase->actual_amount);
|
||
}
|
||
|
||
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细 */
|
||
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);
|
||
}
|
||
|
||
// 未传 sync_product:商品档案保持原值
|
||
$product = $product->fresh();
|
||
$this->assertNotSame('优选土豆', $product->name);
|
||
|
||
// 新单价 = 25 ÷ 5 = 5.00,实际金额 = 5 件 × 5.00
|
||
$this->assertSame('25.00', (string) $purchase->fresh()->actual_amount);
|
||
}
|
||
|
||
/** sync_product=true:订货明细与商品档案同步更新;软删除商品拒绝 */
|
||
public function test_update_row_syncs_to_product_catalog(): 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,
|
||
'sync_product' => true,
|
||
])->assertJsonPath('success', true);
|
||
|
||
$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);
|
||
|
||
// 软删除商品 → 拒绝并回滚(订货明细不同步)
|
||
$product->delete();
|
||
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
|
||
'product_name' => '再次改名',
|
||
'sync_product' => true,
|
||
])->assertJsonPath('success', false);
|
||
$this->assertSame('优选土豆', StoreOrderItemModel::first()->product_name, '回滚后明细保持原值');
|
||
}
|
||
|
||
/** 行级修改:属性与称重至少一项 */
|
||
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])
|
||
->assertJsonPath('success', false);
|
||
}
|
||
}
|