生成采购单基础
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
@@ -74,7 +75,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 待汇总预览:聚合所有待汇总订单明细(按商品分组),
|
||||
* 已接单预览:聚合所有已接单订单明细(按商品分组),
|
||||
* 供生成采购单前确认(C1 前置)
|
||||
*/
|
||||
#[GetRoute('/summary', 'query')]
|
||||
@@ -87,7 +88,7 @@ class StoreOrderController extends BaseController
|
||||
->selectRaw('SUM(quantity) as total_quantity')
|
||||
->selectRaw('COUNT(DISTINCT store_id) as store_count')
|
||||
->whereHas('order', function ($query) {
|
||||
$query->where('status', StoreOrderModel::STATUS_PENDING);
|
||||
$query->where('status', StoreOrderModel::STATUS_SUMMARIZED);
|
||||
})
|
||||
->groupBy('product_id')
|
||||
->orderBy('product_id')
|
||||
@@ -117,13 +118,13 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态流转(待汇总→配送中→完成;待汇总可取消;已汇总可转配送中)
|
||||
* 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function status(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'status' => 'required|integer|in:2,3,9',
|
||||
'status' => 'required|integer|in:1,3,4,9',
|
||||
], [
|
||||
'status.required' => '目标状态不能为空',
|
||||
'status.in' => '目标状态值不正确',
|
||||
@@ -135,20 +136,7 @@ class StoreOrderController extends BaseController
|
||||
}
|
||||
|
||||
$target = (int) $data['status'];
|
||||
$allowed = match ($order->status) {
|
||||
StoreOrderModel::STATUS_PENDING => [
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
],
|
||||
StoreOrderModel::STATUS_SUMMARIZED => [
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
],
|
||||
StoreOrderModel::STATUS_DELIVERING => [
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
if (! in_array($target, $allowed, true)) {
|
||||
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
|
||||
);
|
||||
@@ -162,6 +150,75 @@ class StoreOrderController extends BaseController
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量状态流转:先全量校验流转路径,任一订单不允许则整批中止;
|
||||
* 全部合法时在事务内流转并逐单通知门店
|
||||
*/
|
||||
#[PutRoute(route: '/batchStatus', authorize: 'update')]
|
||||
public function batchStatus(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'ids' => 'required|array|min:1',
|
||||
'ids.*' => 'integer|exists:store_order,id',
|
||||
'status' => 'required|integer|in:1,3,4,9',
|
||||
], [
|
||||
'ids.required' => '请选择要流转的订单',
|
||||
'ids.min' => '请选择要流转的订单',
|
||||
'ids.*.exists' => '订单不存在',
|
||||
'status.required' => '目标状态不能为空',
|
||||
'status.in' => '目标状态值不正确',
|
||||
]);
|
||||
|
||||
$target = (int) $data['status'];
|
||||
$orders = StoreOrderModel::query()->whereIn('id', $data['ids'])->get();
|
||||
|
||||
// 全量预校验:任一订单不满足流转条件,整批中止,不做任何修改
|
||||
$blocked = [];
|
||||
foreach ($orders as $order) {
|
||||
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
|
||||
$blocked[] = $order->order_no . '(' . (self::STATUS_NAMES[$order->status] ?? $order->status) . ')';
|
||||
}
|
||||
}
|
||||
if (! empty($blocked)) {
|
||||
throw new RepositoryException(
|
||||
'以下订单不允许流转为「' . (self::STATUS_NAMES[$target] ?? $target) . '」,批量操作已中止:' . implode('、', $blocked)
|
||||
);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($orders, $target) {
|
||||
foreach ($orders as $order) {
|
||||
$order->status = $target;
|
||||
$order->save();
|
||||
$this->notifyStore($order);
|
||||
}
|
||||
});
|
||||
|
||||
return $this->success(['success' => $orders->count()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态流转合法路径(与迁移状态定义一致):
|
||||
* 待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
* (已接单→采购中 只能经「生成采购单」完成,不在本流转内)
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function allowedTransitions(int $status): array
|
||||
{
|
||||
return match ($status) {
|
||||
StoreOrderModel::STATUS_PENDING => [
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DELIVERING => [
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
],
|
||||
StoreOrderModel::STATUS_DISTRIBUTION => [StoreOrderModel::STATUS_COMPLETED],
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改周转框/周转托盘数量(仅已接单、采购中、配送中可改),
|
||||
* 自动重算附加金额与订单总金额
|
||||
|
||||
@@ -78,20 +78,28 @@ class PurchaseOrderController extends BaseController
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/** C1 按门店订单汇总生成采购单 */
|
||||
/**
|
||||
* C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids),
|
||||
* 生成后源订单转为「采购中」并回写 purchase_id
|
||||
*/
|
||||
#[PostRoute('/generate', 'generate')]
|
||||
public function generate(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'purchase_date' => 'required|date_format:Y-m-d',
|
||||
'order_ids' => 'sometimes|array|min:1',
|
||||
'order_ids.*' => 'integer|exists:store_order,id',
|
||||
], [
|
||||
'purchase_date.required' => '请选择采购日期',
|
||||
'purchase_date.date_format' => '采购日期格式为 Y-m-d',
|
||||
'order_ids.min' => '请选择要合并的订单',
|
||||
'order_ids.*.exists' => '订单不存在',
|
||||
]);
|
||||
|
||||
$purchase = app(PurchaseGenerateService::class)->generate(
|
||||
$data['purchase_date'],
|
||||
(int) $request->user()->id,
|
||||
array_map('intval', $data['order_ids'] ?? []),
|
||||
);
|
||||
|
||||
return $this->success(
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user