采购单优化

This commit is contained in:
liu
2026-08-12 23:20:05 +08:00
parent c2c56408a0
commit 0211e5e98d
22 changed files with 768 additions and 1425 deletions
+152 -24
View File
@@ -13,8 +13,8 @@ use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格数量、行级成本/称重
* 修改同步订货明细并重算订单/采购单汇总
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本,
* 修改同步订货明细并重算订单/采购单汇总(金额口径:数量(包) × 每包价格,单价/包规不参与金额计算)
*/
class PurchaseEditTest extends ProcurementTestCase
{
@@ -101,9 +101,10 @@ class PurchaseEditTest extends ProcurementTestCase
$purchase = $purchase->fresh();
$this->assertSame('8.00', (string) $purchase->total_quantity, '5+3');
$this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30');
$this->assertSame('160.00', (string) $purchase->actual_amount, '8 包 × 每包成本 20.00');
}
/** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×单价 重算 */
/** 行级成本修改 → 同步该商品全部订货明细,采购单实际金额按 数量×每包成本 重算 */
public function test_update_row_cost_syncs_all_order_items(): void
{
[$purchase, $product] = $this->buildPurchase();
@@ -116,33 +117,20 @@ class PurchaseEditTest extends ProcurementTestCase
$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('150.00', (string) $purchase->actual_amount, '5 包 × 每包成本 30.00');
$this->assertSame('50.00', (string) $purchase->estimate_amount, '订货金额不受成本修改影响');
}
/** 行级称重修改 → 按各店数量比例分摊写入明细(尾差修正守恒),实际金额按 称重×单价 计 */
public function test_update_row_weight_distributed_by_quantity(): void
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细(重量为手动录入,不受行修改影响) */
public function test_update_row_does_not_touch_manual_weight(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
[$purchase, $product] = $this->buildPurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['weight' => 10])
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30])
->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);
$this->assertSame(0, StoreOrderItemModel::where('weight', '<>', 0)->count(), '重量保持手动录入值,不自动计算');
}
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细 */
@@ -175,8 +163,7 @@ class PurchaseEditTest extends ProcurementTestCase
$product = $product->fresh();
$this->assertNotSame('优选土豆', $product->name);
// 新单价 = 25 ÷ 5 = 5.00,实际金额 = 5 × 5.00
$this->assertSame('25.00', (string) $purchase->fresh()->actual_amount);
$this->assertSame('125.00', (string) $purchase->fresh()->actual_amount, '5 × 每包成本 25.00');
}
/** sync_product=true:订货明细与商品档案同步更新;软删除商品拒绝 */
@@ -231,4 +218,145 @@ class PurchaseEditTest extends ProcurementTestCase
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1])
->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->assertTrue($item['editable']);
// 其他门店 → 另一条明细
$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, 'weight' => 4.5])
->assertJsonPath('success', true)
->assertJsonPath('data.amount', 50);
$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->product_amount);
$purchase = $purchase->fresh();
$this->assertSame('4.500', (string) $purchase->total_weight);
$this->assertSame('80.00', (string) $purchase->estimate_amount, '50+30 订货金额');
$this->assertSame('160.00', (string) $purchase->actual_amount, '8 包 × 每包成本 20.00(称重仅参考,不参与金额)');
}
/** 单元格一键同步:按商品ID同步档案 + 等级价重算,订货单与采购单汇总级联 */
public function test_cell_sync_refreshes_snapshot_and_cascades(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => '10.00',
'spec' => '1斤/袋',
'unit' => '斤',
]);
ProductPriceModel::factory()->create([
'product_id' => $product->id,
'level_id' => $level->id,
'price_type' => ProductPriceModel::PRICE_TYPE_PERCENT,
'percent' => 30,
]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 2]]])
->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();
$item = StoreOrderItemModel::first();
$this->assertSame('13.00', (string) $item->price, '下单时 10 元上浮 30%');
$this->assertSame('26.00', (string) $purchase->estimate_amount);
// 成本上调后同步:单价按最新成本重算,金额与两级汇总级联
$product->update(['cost_price' => '20.00']);
$this->putJson("/purchase/order/cell/{$item->id}/sync")
->assertJsonPath('success', true)
->assertJsonPath('data.amount', 52);
$item->refresh();
$this->assertSame('20.00', (string) $item->cost_price);
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
$order = StoreOrderModel::first();
$this->assertSame('52.00', (string) $order->product_amount);
$this->assertSame('52.00', (string) $order->total_amount);
$purchase = $purchase->fresh();
$this->assertSame('52.00', (string) $purchase->estimate_amount);
$this->assertSame('40.00', (string) $purchase->actual_amount, '2 包 × 每包成本 20.00');
}
/** 采购单已完成:单元格编辑/同步、行修改均拒绝,明细不变 */
public function test_edits_rejected_when_purchase_completed(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5])
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
$this->putJson("/purchase/order/cell/{$item->id}/sync")
->assertJsonPath('success', false);
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", ['cost_price' => 30])
->assertJsonPath('success', false);
$this->assertSame(2, $item->fresh()->quantity, '被拒绝后明细不变');
$this->assertSame('20.00', (string) $item->fresh()->cost_price);
// 下钻明细同步标记不可编辑
$this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}")
->assertOk()
->assertJsonPath('data.items.0.editable', false);
}
}