Files
xin-procurement/app/Services/PurchaseAllocateService.php
T
2026-07-23 20:41:25 +08:00

115 lines
4.7 KiB
PHP

<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use App\Models\PurchaseAllocationModel;
use App\Models\PurchaseOrderItemModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use Illuminate\Support\Facades\DB;
/**
* D3 采购金额分摊
*
* 流程(事务内):
* 1. 采购单须已录入实际金额(存在 amount>0 的明细),否则拒绝
* 2. 每个采购明细溯源「采购日当天、已汇总订单」中该商品的订货明细
* 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2)
* 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒)
* 4. 重复分摊先删旧记录再重建(幂等)
*/
class PurchaseAllocateService
{
/**
* @return int 生成的分摊记录数
*/
public function allocate(PurchaseOrderModel $purchase): int
{
return DB::transaction(function () use ($purchase) {
$items = PurchaseOrderItemModel::query()
->where('purchase_id', $purchase->id)
->lockForUpdate()
->get();
// 1. 须已录入实际金额
if (! $items->contains(static fn ($item) => (float) $item->amount > 0)) {
throw new RepositoryException('采购单尚未录入实际金额,无法分摊');
}
// 2. 溯源采购日当天「已汇总」订单的订货明细,按商品分组
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->whereDate('store_order.order_date', $purchase->purchase_date)
->where('store_order.status', StoreOrderModel::STATUS_SUMMARIZED)
->select('store_order_item.*')
->get()
->groupBy('product_id');
// 4. 幂等:删除旧分摊记录
PurchaseAllocationModel::query()
->whereIn('purchase_item_id', $items->pluck('id'))
->delete();
$count = 0;
$now = now();
foreach ($items as $item) {
$sources = $orderItems->get((int) $item->product_id);
if ($sources === null || $sources->isEmpty()) {
continue;
}
$totalQty = $sources->reduce(
static fn (string $carry, $orderItem): string => bcadd($carry, (string) $orderItem->quantity, 2),
'0'
);
if ((float) $totalQty <= 0) {
continue;
}
$sourceValues = $sources->values();
$lastIndex = $sourceValues->count() - 1;
$allocatedAmount = '0';
$allocatedQuantity = '0';
$allocatedWeight = '0';
$records = [];
foreach ($sourceValues as $index => $orderItem) {
if ($index === $lastIndex) {
// 3. 尾差修正:最后一行 = 总额 − 已分摊,保证守恒
$amount = bcsub((string) $item->amount, $allocatedAmount, 2);
$quantity = bcsub((string) $item->quantity, $allocatedQuantity, 2);
$weight = bcsub((string) $item->weight, $allocatedWeight, 3);
} else {
$ratio = bcdiv((string) $orderItem->quantity, $totalQty, 6);
$amount = bcmul((string) $item->amount, $ratio, 2);
$quantity = bcmul((string) $item->quantity, $ratio, 2);
$weight = bcmul((string) $item->weight, $ratio, 3);
$allocatedAmount = bcadd($allocatedAmount, $amount, 2);
$allocatedQuantity = bcadd($allocatedQuantity, $quantity, 2);
$allocatedWeight = bcadd($allocatedWeight, $weight, 3);
}
$records[] = [
'purchase_item_id' => $item->id,
'order_item_id' => $orderItem->id,
'store_id' => $orderItem->store_id,
'product_id' => $item->product_id,
'quantity' => $quantity,
'weight' => $weight,
'amount' => $amount,
'created_at' => $now,
'updated_at' => $now,
];
}
PurchaseAllocationModel::insert($records);
$count += count($records);
}
return $count;
});
}
}