生成采购单基础

This commit is contained in:
liu
2026-08-11 21:08:01 +08:00
parent 06c7bd9f88
commit 5d26f04ba2
13 changed files with 362 additions and 68 deletions
+3 -5
View File
@@ -7,7 +7,6 @@ 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;
/**
@@ -15,7 +14,7 @@ use Illuminate\Support\Facades\DB;
*
* 流程(事务内):
* 1. 采购单须已录入实际金额(存在 amount>0 的明细),否则拒绝
* 2. 每个采购明细溯源采购日当天、已汇总订单」中该商品的订货明细
* 2. 每个采购明细按 purchase_id 溯源采购单合并的门店订单明细(生成采购单时回写)
* 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2)
* 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒)
* 4. 重复分摊先删旧记录再重建(幂等)
@@ -38,11 +37,10 @@ class PurchaseAllocateService
throw new RepositoryException('采购单尚未录入实际金额,无法分摊');
}
// 2. 溯源采购日当天「已汇总」订单的订货明细,按商品分组
// 2. 按 purchase_id 溯源采购单合并的门店订单明细,按商品分组
$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)
->where('store_order.purchase_id', $purchase->id)
->select('store_order_item.*')
->get()
->groupBy('product_id');
+29 -18
View File
@@ -14,12 +14,12 @@ use Illuminate\Support\Facades\DB;
* C1 订单汇总生成采购单
*
* 流程(事务内):
* 1. 行锁查询当日全部「待汇总」订单(无则报错;状态条件天然排除已汇总订单,幂等)
* 1. 行锁「已接单」订单(可传 orderIds 只合并指定订单;状态条件天然排除已归集订单,幂等)
* 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商)
* 3. 估算单价 = 该商品最低实际等级价(按计价类型换算后取 min,PHP 侧兼容 MySQL/SQLite),amount = quantity × 估算单价
* 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount
* 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号
* 6. 源订单批量回写 status = 已汇总
* 6. 源订单批量回写 status = 采购中、purchase_id = 采购单ID(分摊按 purchase_id 溯源)
*/
class PurchaseGenerateService
{
@@ -28,28 +28,36 @@ class PurchaseGenerateService
}
/**
* @param string $date 订货/采购日期(Y-m-d
* @param string $date 采购日期(Y-m-d
* @param int $operatorId 制单人(后台系统用户ID)
* @param int[] $orderIds 指定合并的门店订单ID(空 = 全部已接单订单)
*/
public function generate(string $date, int $operatorId): PurchaseOrderModel
public function generate(string $date, int $operatorId, array $orderIds = []): PurchaseOrderModel
{
return DB::transaction(function () use ($date, $operatorId) {
// 1. 行锁当日待汇总订单(并发防护)
$orders = StoreOrderModel::query()
->whereDate('order_date', $date)
->where('status', StoreOrderModel::STATUS_PENDING)
->lockForUpdate()
->get();
return DB::transaction(function () use ($date, $operatorId, $orderIds) {
// 1. 行锁已接单订单(并发防护);指定订单时要求全部处于已接单,否则整批拒绝
$query = StoreOrderModel::query()->lockForUpdate();
if ($orderIds !== []) {
$orders = $query->whereIn('id', $orderIds)->get();
$invalid = $orders->where('status', '<>', StoreOrderModel::STATUS_SUMMARIZED);
if ($orders->isEmpty() || $invalid->isNotEmpty()) {
throw new RepositoryException(
'所选订单包含非「已接单」状态,无法生成采购单:' . $invalid->pluck('order_no')->implode('、')
);
}
} else {
$orders = $query->where('status', StoreOrderModel::STATUS_SUMMARIZED)->get();
}
if ($orders->isEmpty()) {
throw new RepositoryException('当日无待汇总订单');
throw new RepositoryException('无已接单订单,无法生成采购单');
}
// 2. 展开明细按商品聚合
$aggregated = [];
$orderIds = [];
$sourceOrderIds = [];
foreach ($orders as $order) {
$orderIds[] = $order->id;
$sourceOrderIds[] = $order->id;
foreach ($order->items as $item) {
$productId = (int) $item->product_id;
if (! isset($aggregated[$productId])) {
@@ -69,7 +77,7 @@ class PurchaseGenerateService
}
if ($aggregated === []) {
throw new RepositoryException('当日待汇总订单均无明细,无法生成采购单');
throw new RepositoryException('已接单订单均无明细,无法生成采购单');
}
$products = ProductModel::withTrashed()
@@ -164,9 +172,12 @@ class PurchaseGenerateService
]);
}
// 6. 源订单回写「已汇总」
StoreOrderModel::whereIn('id', $orderIds)
->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
// 6. 源订单回写「采购中」并关联采购单(分摊按 purchase_id 溯源)
StoreOrderModel::whereIn('id', $sourceOrderIds)
->update([
'status' => StoreOrderModel::STATUS_DELIVERING,
'purchase_id' => $purchase->id,
]);
return $purchase;
});