采购单优化

This commit is contained in:
liu
2026-08-17 18:13:33 +08:00
parent a06c45dc49
commit 3dbb9205a4
16 changed files with 2215 additions and 437 deletions
+82 -112
View File
@@ -4,14 +4,16 @@ 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;
use App\Models\UserModel;
/**
* 门店订单明细(商品快照):
* 下单快照完整商品档案、成本价防泄漏、后台明细修改重算总价、一键同步商品档案、按商品名称搜索订单
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
*/
class StoreOrderItemTest extends ProcurementTestCase
{
@@ -50,6 +52,17 @@ class StoreOrderItemTest extends ProcurementTestCase
return $order;
}
/** 下单 → 接单 → 生成采购单,返回采购单 */
private function generatePurchase(): PurchaseOrderModel
{
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
return PurchaseOrderModel::first();
}
/** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */
public function test_place_order_snapshots_full_product_info(): void
{
@@ -114,33 +127,23 @@ class StoreOrderItemTest extends ProcurementTestCase
->assertJsonPath('data.items.0.supplier.name', $supplier->name);
}
/** 修改明细:重算单品金额与订单总量/总额(总额 = 商品金额,附加金额已迁入账单) */
public function test_update_item_recalculates_order_totals(): void
/** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */
public function test_update_store_item_recalculates_totals(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$supplier = SupplierModel::factory()->create();
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}", [
'supplier_id' => $supplier->id,
'product_name' => '改名后的商品',
'product_spec' => '新规格',
'unit' => '箱',
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'price' => '6.00',
'cost_price' => '3.50',
'quantity' => 5,
'weight' => '2.5',
])->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame($supplier->id, $item->supplier_id);
$this->assertSame('改名后的商品', $item->product_name);
$this->assertSame('新规格', $item->product_spec);
$this->assertSame('箱', $item->unit);
$this->assertSame('6.00', (string) $item->price);
$this->assertSame('3.50', (string) $item->cost_price);
$this->assertSame(5, $item->quantity);
$this->assertSame('2.500', (string) $item->weight);
$this->assertSame('30.00', (string) $item->amount, '6.00 × 5');
@@ -148,139 +151,106 @@ class StoreOrderItemTest extends ProcurementTestCase
$order->refresh();
$this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计');
$this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计');
$this->assertSame('30.00', (string) $order->product_amount, '商品总金额 = 明细金额合计');
$this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 商品金额');
$this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 明细金额合计');
$purchase->refresh();
$this->assertSame('5.00', (string) $purchase->total_quantity);
$this->assertSame('2.500', (string) $purchase->total_weight);
$this->assertSame('20.00', (string) $purchase->estimate_amount, '5 包 × 成本 4.00');
}
/** 已完成/已取消订单不允许修改明细 */
public function test_update_item_rejected_when_completed_or_cancelled(): void
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
public function test_store_item_edits_rejected_when_purchase_completed(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1);
$item = $order->items->first();
$payload = [
'supplier_id' => 0,
'product_name' => '不应生效',
'product_spec' => '',
'unit' => '斤',
'price' => '1.00',
'cost_price' => '1.00',
'quantity' => 1,
'weight' => 0,
];
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$store = StoreModel::find($order->store_id);
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
foreach ([StoreOrderModel::STATUS_COMPLETED, StoreOrderModel::STATUS_CANCELLED] as $status) {
$order->update(['status' => $status]);
$this->putJson("/order/store/item/{$item->id}", $payload)
->assertOk()->assertJsonPath('success', false);
}
$this->assertNotSame('不应生效', $item->fresh()->product_name, '被拒绝后明细不变');
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => 9,
])->assertOk()->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $product->id,
'quantity' => 1,
])->assertJsonPath('success', false);
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}")
->assertJsonPath('success', false);
$item = $order->items->first();
$this->assertSame(1, $item->fresh()->quantity, '被拒绝后明细不变');
$this->assertSame('5.00', (string) $order->fresh()->total_amount, '被拒绝后总金额不变');
}
/** 修改明细参数校验:负单价被拦截 */
public function test_update_item_validates_negative_price(): void
/** 修改门店单品参数校验:负单价/负数量被拦截 */
public function test_update_store_item_validates_negative_values(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}", [
'supplier_id' => 0,
'product_name' => $item->product_name,
'unit' => '斤',
'price' => -1,
'cost_price' => 0,
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => 1,
'weight' => 0,
'price' => -1,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '单价不能小于 0');
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => -1,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购数量不能小于 0');
}
/** 一键同步:按商品ID同步最新档案(固定价等级保留原单价 */
public function test_sync_item_updates_snapshot_from_product(): void
/** 新增单品:单价按门店等级上浮比例换算(成本 20 上浮 30% = 26.00 */
public function test_add_store_item_uses_level_percent_price(): void
{
[$store, $product, $user] = $this->makeStoreWithProduct('5.50', '4.00');
$order = $this->placeOrder($product, $user, 3, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$this->assertSame('16.50', (string) $item->amount);
// 下单后商品档案变更:改名/改规格/改单位/换供应商/调成本价
$supplier = SupplierModel::factory()->create();
$product->update([
'name' => '同步后的品名',
'spec' => '同步后的规格',
'unit' => '箱',
'supplier_id' => $supplier->id,
'cost_price' => '9.99',
]);
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame('同步后的品名', $item->product_name);
$this->assertSame('同步后的规格', $item->product_spec);
$this->assertSame('箱', $item->unit);
$this->assertSame($supplier->id, $item->supplier_id);
$this->assertSame('9.99', (string) $item->cost_price);
$this->assertSame('5.50', (string) $item->price, '固定价等级单价不随档案变化');
$this->assertSame('16.50', (string) $item->amount, '单价未变,金额不变');
}
/** 一键同步:百分比计价等级按最新成本价重算单价与订单总价 */
public function test_sync_item_recalculates_percent_price_with_latest_cost(): void
{
// 等级上浮 30%:下单时成本 10.00 → 售价 13.00
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => '10.00',
]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']);
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']);
$user = UserModel::factory()->forStore($store->id)->create();
$order = $this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
$this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%');
$this->assertSame('26.00', (string) $order->total_amount);
// 成本价上调后同步:单价应按最新成本重算
$product->update(['cost_price' => '20.00']);
$this->placeOrder($product, $user, 2, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()->assertJsonPath('success', true);
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $extra->id,
'quantity' => 2,
])->assertOk()->assertJsonPath('success', true);
$item->refresh();
$this->assertSame('20.00', (string) $item->cost_price);
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
$this->assertNotNull($item);
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
$order->refresh();
$this->assertSame('52.00', (string) $order->product_amount);
$this->assertSame('52.00', (string) $order->total_amount);
$this->assertSame('20.00', (string) $item->cost_price, '快照成本价');
$this->assertSame($extra->name, $item->product_name, '快照品名取自商品档案');
}
/** 一键同步:商品已删除时拒绝同步 */
public function test_sync_item_rejected_when_product_deleted(): void
/** 新增单品:商品已删除时拒绝 */
public function test_add_store_item_rejected_when_product_deleted(): void
{
[, $product, $user] = $this->makeStoreWithProduct('5.00');
$order = $this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$item = $order->items->first();
[$store, $product, $user] = $this->makeStoreWithProduct('5.00');
$this->placeOrder($product, $user, 1, StoreOrderModel::STATUS_SUMMARIZED);
$purchase = $this->generatePurchase();
$product->delete(); // 软删除
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']);
$extra->delete(); // 软删除
$this->actingAsSysUser();
$this->putJson("/order/store/item/{$item->id}/sync")
->assertOk()
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
'product_id' => $extra->id,
'quantity' => 1,
])->assertOk()
->assertJsonPath('success', false)
->assertJsonPath('msg', '商品不存在或已被删除,无法同步');
->assertJsonPath('msg', '商品不存在或已被删除,无法添加');
}
/** 订单列表按包含的商品名称搜索 */