158 lines
6.2 KiB
PHP
158 lines
6.2 KiB
PHP
<?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;
|
||
});
|
||
}
|
||
}
|