采购单优化

This commit is contained in:
liu
2026-08-12 14:38:20 +08:00
parent ce1f36b5b4
commit dadfdc1511
30 changed files with 1059 additions and 1441 deletions
+47 -37
View File
@@ -5,7 +5,6 @@ namespace Tests\Feature;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\PurchaseOrderModel;
use App\Models\ReconciliationItemModel;
use App\Models\ReconciliationModel;
use App\Models\SettlementModel;
@@ -15,27 +14,31 @@ use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 财务对账:明细构建(品类/供应商筛选)、D4 修改后差额与头汇总重算、D8 标记、D9 结算
* 财务对账:明细构建(已完成订单直读,品类/供应商筛选)、D4 修改后差额与头汇总重算、D8 标记、D9 结算
* publish = 订货金额;actual = 采购成本(称重>0 ? 称重×单价 : 数量×单价,单价 = 成本/包规)
*/
class ReconciliationTest extends ProcurementTestCase
{
/**
* 构造已分摊的完整链路:2 门店下单(2/3 件)→ 生成采购单 → 录入实际金额 → 分摊
* 构造已完成订单链路:2 门店下单(2/3 件,等级价 10.00;成本 8,包规 1斤 → 单价 8.00),订单置为已完成
* 预期:publish 20/30actual 16/24diff 4/6
*
* @return array{0: PurchaseOrderModel, 1: array<int, StoreModel>, 2: SupplierModel}
* @return array{0: array<int, StoreModel>, 1: SupplierModel}
*/
private function buildAllocatedChain(): array
private function buildCompletedOrders(): array
{
$level = CustomerLevelModel::factory()->create();
$supplier = SupplierModel::factory()->create();
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'supplier_id' => $supplier->id,
'cost_price' => 8,
'spec' => '1斤',
]);
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '10.00']);
$stores = [];
foreach (['2.00', '3.00'] as $qty) {
foreach ([2, 3] as $qty) {
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$stores[] = $store;
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
@@ -43,23 +46,10 @@ class ReconciliationTest extends ProcurementTestCase
->assertJsonPath('success', true);
}
// 接单后生成采购单
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
// 订单完成(对账数据源为已完成订单的订货明细)
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
$item = $purchase->items->first();
$this->putJson("/purchase/order/item/{$item->id}", [
'price' => '10.00',
'quantity' => $item->quantity,
'weight' => 0,
])->assertJsonPath('success', true);
$this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', true);
return [$purchase->fresh(), $stores, $supplier];
return [$stores, $supplier];
}
private function createRecon(array $extra = []): int
@@ -74,34 +64,53 @@ class ReconciliationTest extends ProcurementTestCase
return (int) $response->json('data.id');
}
/** 构建明细:publish=订货金额,actual=分摊金额diff=publish-actual,头汇总回写 */
/** 构建明细:publish=订货金额,actual=采购成本(数量×单价)diff=publish-actual,头汇总回写 */
public function test_build_creates_reconciliation_items(): void
{
[, $stores] = $this->buildAllocatedChain();
[$stores] = $this->buildCompletedOrders();
$this->actingAsSysUser();
$reconId = $this->createRecon();
$this->postJson("/recon/list/{$reconId}/build")->assertJsonPath('success', true);
$items = ReconciliationItemModel::where('recon_id', $reconId)->get();
$this->assertCount(2, $items, '两门店分摊 → 两条对账明细');
$this->assertCount(2, $items, '两门店已完成订单 → 两条对账明细');
$byStore = $items->keyBy('store_id');
$this->assertSame('20.00', (string) $byStore[$stores[0]->id]->publish_amount, '订货金额 2×10');
$this->assertSame('20.00', (string) $byStore[$stores[0]->id]->actual_amount, '分摊金额');
$this->assertSame('0.00', (string) $byStore[$stores[0]->id]->diff_amount);
$this->assertSame('16.00', (string) $byStore[$stores[0]->id]->actual_amount, '采购成本 2×8');
$this->assertSame('4.00', (string) $byStore[$stores[0]->id]->diff_amount);
$this->assertSame('30.00', (string) $byStore[$stores[1]->id]->publish_amount);
$this->assertSame('24.00', (string) $byStore[$stores[1]->id]->actual_amount);
$recon = ReconciliationModel::find($reconId);
$this->assertSame('50.00', (string) $recon->publish_amount);
$this->assertSame('50.00', (string) $recon->actual_amount);
$this->assertSame('0.00', (string) $recon->diff_amount);
$this->assertSame('40.00', (string) $recon->actual_amount);
$this->assertSame('10.00', (string) $recon->diff_amount);
$this->assertSame(ReconciliationModel::STATUS_WORKING, $recon->status);
}
/** 供应商筛选:仅拉取该供应商的采购数据 */
/** 未完成订单不计入对账 */
public function test_build_excludes_unfinished_orders(): void
{
[$stores] = $this->buildCompletedOrders();
// 第二家门店订单回退为配送中 → 仅第一家进入对账
StoreOrderModel::where('store_id', $stores[1]->id)
->update(['status' => StoreOrderModel::STATUS_DISTRIBUTION]);
$this->actingAsSysUser();
$reconId = $this->createRecon();
$this->postJson("/recon/list/{$reconId}/build")->assertJsonPath('success', true);
$items = ReconciliationItemModel::where('recon_id', $reconId)->get();
$this->assertCount(1, $items);
$this->assertSame($stores[0]->id, $items->first()->store_id);
}
/** 供应商筛选:仅拉取该供应商的订单数据 */
public function test_build_filters_by_supplier(): void
{
[, , $supplier] = $this->buildAllocatedChain();
[, $supplier] = $this->buildCompletedOrders();
$this->actingAsSysUser();
// 无关供应商 → 无数据报错
@@ -118,7 +127,7 @@ class ReconciliationTest extends ProcurementTestCase
/** D4 修改明细:自动重算本行 diff 与对账单头汇总 */
public function test_update_item_recalculates_diff_and_header(): void
{
$this->buildAllocatedChain();
$this->buildCompletedOrders();
$this->actingAsSysUser();
$reconId = $this->createRecon();
@@ -132,14 +141,14 @@ class ReconciliationTest extends ProcurementTestCase
$this->assertSame('-5.00', (string) $item->diff_amount, '20.00 - 25.00');
$recon = ReconciliationModel::find($reconId);
$this->assertSame('55.00', (string) $recon->actual_amount, '25 + 30');
$this->assertSame('-5.00', (string) $recon->diff_amount, '50 - 55');
$this->assertSame('49.00', (string) $recon->actual_amount, '25 + 24');
$this->assertSame('1.00', (string) $recon->diff_amount, '50 - 49');
}
/** D8 对账状态标记翻转 */
public function test_toggle_reconciled_flag(): void
{
$this->buildAllocatedChain();
$this->buildCompletedOrders();
$this->actingAsSysUser();
$reconId = $this->createRecon();
@@ -157,7 +166,7 @@ class ReconciliationTest extends ProcurementTestCase
/** D9 结算:按门店生成结算表,对账单转为已结算且不可重复结算 */
public function test_settle_creates_settlements_per_store(): void
{
[, $stores] = $this->buildAllocatedChain();
[$stores] = $this->buildCompletedOrders();
$this->actingAsSysUser();
$reconId = $this->createRecon();
@@ -170,7 +179,8 @@ class ReconciliationTest extends ProcurementTestCase
$byStore = $settlements->keyBy('store_id');
$this->assertSame('20.00', (string) $byStore[$stores[0]->id]->total_amount);
$this->assertSame('20.00', (string) $byStore[$stores[0]->id]->actual_amount);
$this->assertSame('16.00', (string) $byStore[$stores[0]->id]->actual_amount);
$this->assertSame('4.00', (string) $byStore[$stores[0]->id]->diff_amount);
$this->assertStringStartsWith('JS', $byStore[$stores[0]->id]->settlement_no);
$this->assertSame(ReconciliationModel::STATUS_SETTLED, ReconciliationModel::find($reconId)->status);