采购金额

This commit is contained in:
liu
2026-08-17 18:33:36 +08:00
parent 3dbb9205a4
commit 6d9f02d831
4 changed files with 123 additions and 11 deletions
File diff suppressed because one or more lines are too long
@@ -670,6 +670,8 @@ class PurchaseOrderController extends BaseController
/** /**
* 商品行修改:品名/供应商/包规/单位/成本(同步更新商品档案;商品已删除则跳过档案同步) * 商品行修改:品名/供应商/包规/单位/成本(同步更新商品档案;商品已删除则跳过档案同步)
* 成本价变化时,按各门店客户等级的上浮比例重算明细单价与金额(无等级按成本价兜底),
* 并级联重算涉及订单与采购单汇总
* @throws Throwable * @throws Throwable
*/ */
#[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])] #[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
@@ -692,8 +694,34 @@ class PurchaseOrderController extends BaseController
if ($items->isEmpty()) { if ($items->isEmpty()) {
throw new RepositoryException('该采购单下无此商品的订货明细'); throw new RepositoryException('该采购单下无此商品的订货明细');
} }
// 成本价是否变化(未变化时保留明细单价,避免覆盖单元格/单品的手工改价)
$newCost = bcadd((string) $validated['cost_price'], '0', 2);
$costChanged = bccomp($newCost, bcadd((string) $items->first()->cost_price, '0', 2), 2) !== 0;
// 成本变化 → 按门店等级上浮比例重算单价(一次性取回涉及门店的等级,避免逐行查询)
$levelPercents = [];
if ($costChanged) {
$levelPercents = StoreModel::withTrashed()
->with('level:id,percent')
->whereIn('id', $items->pluck('store_id')->unique())
->get(['id', 'level_id'])
->mapWithKeys(static fn (StoreModel $store) => [
$store->id => (float) ($store->level?->percent ?? 0),
])
->all();
}
foreach ($items as $item) { foreach ($items as $item) {
$item->fill($validated)->save(); $item->fill($validated);
if ($costChanged) {
$item->price = CustomerLevelModel::calcLevelPrice(
$newCost,
$levelPercents[(int) $item->store_id] ?? 0,
);
$item->amount = bcmul($item->price, (string) $item->quantity, 2);
}
$item->save();
} }
// 同步保存到商品档案(软删除商品跳过,明细照常更新) // 同步保存到商品档案(软删除商品跳过,明细照常更新)
@@ -709,14 +737,20 @@ class PurchaseOrderController extends BaseController
]); ]);
} }
app(PurchaseItemService::class)->recalcPurchase($purchase->id); $service = app(PurchaseItemService::class);
// 成本变化会引起明细金额变动,涉及订单需逐一重算
if ($costChanged) {
foreach ($items->pluck('order_id')->unique() as $orderId) {
$service->recalcOrder((int) $orderId);
}
}
$service->recalcPurchase($purchase->id);
return $this->success( $message = '已同步 ' . $items->count() . ' 条订货明细'
['count' => $items->count()], . ($productSynced ? '与商品档案' : ';商品已删除,档案未同步')
$productSynced . ($costChanged ? ';门店单价已按等级上浮比例重算' : '');
? '已同步 ' . $items->count() . ' 条订货明细与商品档案'
: '已同步 ' . $items->count() . ' 条订货明细;商品已删除,档案未同步' return $this->success(['count' => $items->count()], $message);
);
}); });
} }
+79 -1
View File
@@ -200,7 +200,7 @@ class PurchaseEditTest extends ProcurementTestCase
'cost_price' => 25, 'cost_price' => 25,
])->assertOk() ])->assertOk()
->assertJsonPath('success', true) ->assertJsonPath('success', true)
->assertJsonPath('msg', '已同步 2 条订货明细;商品已删除,档案未同步'); ->assertJsonPath('msg', '已同步 2 条订货明细;商品已删除,档案未同步;门店单价已按等级上浮比例重算');
$this->assertSame('已删商品新名', StoreOrderItemModel::first()->product_name, '订货明细照常更新'); $this->assertSame('已删商品新名', StoreOrderItemModel::first()->product_name, '订货明细照常更新');
$this->assertNotSame('已删商品新名', $product->fresh()->name, '商品档案不同步'); $this->assertNotSame('已删商品新名', $product->fresh()->name, '商品档案不同步');
@@ -287,6 +287,84 @@ class PurchaseEditTest extends ProcurementTestCase
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)'); $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->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);
$purchase = PurchaseOrderModel::first();
// 下单时快照:A 店 10.0010 上浮 0%),B 店 15.0010 上浮 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 public function test_store_item_add_creates_item_and_recalculates(): void
{ {
+1 -1
View File
@@ -1184,7 +1184,7 @@ const PurchaseOrderPage: React.FC = () => {
]} ]}
> >
<div className="py-2 text-gray-500"> <div className="py-2 text-gray-500">
</div> </div>
<Form form={editForm} layout="vertical" onFinish={handleEditSave}> <Form form={editForm} layout="vertical" onFinish={handleEditSave}>
<Form.Item <Form.Item