115 lines
4.7 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|