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

158 lines
6.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Exceptions\RepositoryException;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\PurchaseOrderItemModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreOrderModel;
use Illuminate\Support\Facades\DB;
/**
* C1 订单汇总生成采购单
*
* 流程(事务内):
* 1. 行锁查询当日全部「待汇总」订单(无则报错;状态条件天然排除已汇总订单,幂等)
* 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商)
* 3. 估算单价 = 该商品最低等级价(product_price MIN),amount = quantity × 估算单价
* 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount
* 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号
* 6. 源订单批量回写 status = 已汇总
*/
class PurchaseGenerateService
{
public function __construct(private readonly BillNumberService $billNumberService)
{
}
/**
* @param string $date 订货/采购日期(Y-m-d
* @param int $operatorId 制单人(后台系统用户ID)
*/
public function generate(string $date, int $operatorId): PurchaseOrderModel
{
return DB::transaction(function () use ($date, $operatorId) {
// 1. 行锁当日待汇总订单(并发防护)
$orders = StoreOrderModel::query()
->whereDate('order_date', $date)
->where('status', StoreOrderModel::STATUS_PENDING)
->lockForUpdate()
->get();
if ($orders->isEmpty()) {
throw new RepositoryException('当日无待汇总订单');
}
// 2. 展开明细按商品聚合
$aggregated = [];
$orderIds = [];
foreach ($orders as $order) {
$orderIds[] = $order->id;
foreach ($order->items as $item) {
$productId = (int) $item->product_id;
if (! isset($aggregated[$productId])) {
$aggregated[$productId] = [
'product_id' => $productId,
'product_name' => $item->product_name,
'product_spec' => $item->product_spec,
'quantity' => '0',
];
}
$aggregated[$productId]['quantity'] = bcadd(
$aggregated[$productId]['quantity'],
(string) $item->quantity,
2
);
}
}
if ($aggregated === []) {
throw new RepositoryException('当日待汇总订单均无明细,无法生成采购单');
}
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', array_keys($aggregated))
->get()
->keyBy('id');
// 3. 估算单价 = 最低等级价
$minPrices = ProductPriceModel::query()
->whereIn('product_id', array_keys($aggregated))
->groupBy('product_id')
->selectRaw('product_id, MIN(price) as min_price')
->pluck('min_price', 'product_id');
// 组装明细行并按「分类 sort → 商品 sort」排序
$rows = [];
foreach ($aggregated as $productId => $item) {
$product = $products->get($productId);
$price = (string) ($minPrices[$productId] ?? '0');
$rows[] = [
'product_id' => $productId,
'supplier_id' => (int) ($product->supplier_id ?? 0),
'product_name' => $item['product_name'],
'product_spec' => $item['product_spec'],
'price' => $price,
'quantity' => $item['quantity'],
'amount' => bcmul($item['quantity'], $price, 2),
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
$estimateAmount = array_reduce(
$rows,
static fn (string $carry, array $row): string => bcadd($carry, $row['amount'], 2),
'0'
);
$totalQuantity = array_reduce(
$rows,
static fn (string $carry, array $row): string => bcadd($carry, $row['quantity'], 2),
'0'
);
// 4. 采购单头
$purchase = PurchaseOrderModel::create([
'purchase_no' => $this->billNumberService->make('PO'),
'purchase_date' => $date,
'status' => PurchaseOrderModel::STATUS_PENDING,
'total_quantity' => $totalQuantity,
'total_weight' => 0,
'estimate_amount' => $estimateAmount,
'actual_amount' => 0,
'operator_id' => $operatorId,
]);
// 5. 采购明细(sort 行号)
$sort = 1;
foreach ($rows as $row) {
PurchaseOrderItemModel::create([
'purchase_id' => $purchase->id,
'product_id' => $row['product_id'],
'supplier_id' => $row['supplier_id'],
'product_name' => $row['product_name'],
'product_spec' => $row['product_spec'],
'price' => $row['price'],
'quantity' => $row['quantity'],
'weight' => 0,
'amount' => $row['amount'],
'sort' => $sort++,
'is_sent' => PurchaseOrderItemModel::NOT_SENT,
]);
}
// 6. 源订单回写「已汇总」
StoreOrderModel::whereIn('id', $orderIds)
->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
return $purchase;
});
}
}