生成采购单基础
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@ use App\Models\StoreOrderModel;
|
|||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||||
@@ -74,7 +75,7 @@ class StoreOrderController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待汇总预览:聚合所有待汇总订单明细(按商品分组),
|
* 已接单预览:聚合所有已接单订单明细(按商品分组),
|
||||||
* 供生成采购单前确认(C1 前置)
|
* 供生成采购单前确认(C1 前置)
|
||||||
*/
|
*/
|
||||||
#[GetRoute('/summary', 'query')]
|
#[GetRoute('/summary', 'query')]
|
||||||
@@ -87,7 +88,7 @@ class StoreOrderController extends BaseController
|
|||||||
->selectRaw('SUM(quantity) as total_quantity')
|
->selectRaw('SUM(quantity) as total_quantity')
|
||||||
->selectRaw('COUNT(DISTINCT store_id) as store_count')
|
->selectRaw('COUNT(DISTINCT store_id) as store_count')
|
||||||
->whereHas('order', function ($query) {
|
->whereHas('order', function ($query) {
|
||||||
$query->where('status', StoreOrderModel::STATUS_PENDING);
|
$query->where('status', StoreOrderModel::STATUS_SUMMARIZED);
|
||||||
})
|
})
|
||||||
->groupBy('product_id')
|
->groupBy('product_id')
|
||||||
->orderBy('product_id')
|
->orderBy('product_id')
|
||||||
@@ -117,13 +118,13 @@ class StoreOrderController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态流转(待汇总→配送中→完成;待汇总可取消;已汇总可转配送中)
|
* 状态流转:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||||
*/
|
*/
|
||||||
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
|
#[PutRoute(route: '/{id}/status', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||||
public function status(int $id, Request $request): JsonResponse
|
public function status(int $id, Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'status' => 'required|integer|in:2,3,9',
|
'status' => 'required|integer|in:1,3,4,9',
|
||||||
], [
|
], [
|
||||||
'status.required' => '目标状态不能为空',
|
'status.required' => '目标状态不能为空',
|
||||||
'status.in' => '目标状态值不正确',
|
'status.in' => '目标状态值不正确',
|
||||||
@@ -135,20 +136,7 @@ class StoreOrderController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$target = (int) $data['status'];
|
$target = (int) $data['status'];
|
||||||
$allowed = match ($order->status) {
|
if (! in_array($target, $this->allowedTransitions($order->status), true)) {
|
||||||
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)) {
|
|
||||||
throw new RepositoryException(
|
throw new RepositoryException(
|
||||||
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
|
'订单当前状态为「' . (self::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许该操作'
|
||||||
);
|
);
|
||||||
@@ -162,6 +150,75 @@ class StoreOrderController extends BaseController
|
|||||||
return $this->success();
|
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();
|
return $this->success();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** C1 按门店订单汇总生成采购单 */
|
/**
|
||||||
|
* C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids),
|
||||||
|
* 生成后源订单转为「采购中」并回写 purchase_id
|
||||||
|
*/
|
||||||
#[PostRoute('/generate', 'generate')]
|
#[PostRoute('/generate', 'generate')]
|
||||||
public function generate(Request $request): JsonResponse
|
public function generate(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'purchase_date' => 'required|date_format:Y-m-d',
|
'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.required' => '请选择采购日期',
|
||||||
'purchase_date.date_format' => '采购日期格式为 Y-m-d',
|
'purchase_date.date_format' => '采购日期格式为 Y-m-d',
|
||||||
|
'order_ids.min' => '请选择要合并的订单',
|
||||||
|
'order_ids.*.exists' => '订单不存在',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$purchase = app(PurchaseGenerateService::class)->generate(
|
$purchase = app(PurchaseGenerateService::class)->generate(
|
||||||
$data['purchase_date'],
|
$data['purchase_date'],
|
||||||
(int) $request->user()->id,
|
(int) $request->user()->id,
|
||||||
|
array_map('intval', $data['order_ids'] ?? []),
|
||||||
);
|
);
|
||||||
|
|
||||||
return $this->success(
|
return $this->success(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\Models\PurchaseAllocationModel;
|
|||||||
use App\Models\PurchaseOrderItemModel;
|
use App\Models\PurchaseOrderItemModel;
|
||||||
use App\Models\PurchaseOrderModel;
|
use App\Models\PurchaseOrderModel;
|
||||||
use App\Models\StoreOrderItemModel;
|
use App\Models\StoreOrderItemModel;
|
||||||
use App\Models\StoreOrderModel;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +14,7 @@ use Illuminate\Support\Facades\DB;
|
|||||||
*
|
*
|
||||||
* 流程(事务内):
|
* 流程(事务内):
|
||||||
* 1. 采购单须已录入实际金额(存在 amount>0 的明细),否则拒绝
|
* 1. 采购单须已录入实际金额(存在 amount>0 的明细),否则拒绝
|
||||||
* 2. 每个采购明细溯源「采购日当天、已汇总订单」中该商品的订货明细
|
* 2. 每个采购明细按 purchase_id 溯源该采购单合并的门店订单明细(生成采购单时回写)
|
||||||
* 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2)
|
* 3. 按订货数量比例分摊实际金额/数量/重量:bcmul(item.amount, bcdiv(item_qty, total_qty, 6), 2)
|
||||||
* 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒)
|
* 尾差修正——最后一行承担舍入差额,保证 Σallocation.amount === item.amount(金额守恒)
|
||||||
* 4. 重复分摊先删旧记录再重建(幂等)
|
* 4. 重复分摊先删旧记录再重建(幂等)
|
||||||
@@ -38,11 +37,10 @@ class PurchaseAllocateService
|
|||||||
throw new RepositoryException('采购单尚未录入实际金额,无法分摊');
|
throw new RepositoryException('采购单尚未录入实际金额,无法分摊');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 溯源采购日当天「已汇总」订单的订货明细,按商品分组
|
// 2. 按 purchase_id 溯源本采购单合并的门店订单明细,按商品分组
|
||||||
$orderItems = StoreOrderItemModel::query()
|
$orderItems = StoreOrderItemModel::query()
|
||||||
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
|
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
|
||||||
->whereDate('store_order.order_date', $purchase->purchase_date)
|
->where('store_order.purchase_id', $purchase->id)
|
||||||
->where('store_order.status', StoreOrderModel::STATUS_SUMMARIZED)
|
|
||||||
->select('store_order_item.*')
|
->select('store_order_item.*')
|
||||||
->get()
|
->get()
|
||||||
->groupBy('product_id');
|
->groupBy('product_id');
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ use Illuminate\Support\Facades\DB;
|
|||||||
* C1 订单汇总生成采购单
|
* C1 订单汇总生成采购单
|
||||||
*
|
*
|
||||||
* 流程(事务内):
|
* 流程(事务内):
|
||||||
* 1. 行锁查询当日全部「待汇总」订单(无则报错;状态条件天然排除已汇总订单,幂等)
|
* 1. 行锁「已接单」订单(可传 orderIds 只合并指定订单;状态条件天然排除已归集订单,幂等)
|
||||||
* 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商)
|
* 2. 展开明细按商品聚合(Σquantity,快照品名/规格;供应商取商品默认供应商)
|
||||||
* 3. 估算单价 = 该商品最低实际等级价(按计价类型换算后取 min,PHP 侧兼容 MySQL/SQLite),amount = quantity × 估算单价
|
* 3. 估算单价 = 该商品最低实际等级价(按计价类型换算后取 min,PHP 侧兼容 MySQL/SQLite),amount = quantity × 估算单价
|
||||||
* 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount)
|
* 4. 创建采购单头(PO 单号,estimate_amount = Σitems.amount)
|
||||||
* 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号
|
* 5. 明细按「分类 sort → 商品 sort」排序写入 sort 行号
|
||||||
* 6. 源订单批量回写 status = 已汇总
|
* 6. 源订单批量回写 status = 采购中、purchase_id = 采购单ID(分摊按 purchase_id 溯源)
|
||||||
*/
|
*/
|
||||||
class PurchaseGenerateService
|
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 $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) {
|
return DB::transaction(function () use ($date, $operatorId, $orderIds) {
|
||||||
// 1. 行锁当日待汇总订单(并发防护)
|
// 1. 行锁已接单订单(并发防护);指定订单时要求全部处于已接单,否则整批拒绝
|
||||||
$orders = StoreOrderModel::query()
|
$query = StoreOrderModel::query()->lockForUpdate();
|
||||||
->whereDate('order_date', $date)
|
if ($orderIds !== []) {
|
||||||
->where('status', StoreOrderModel::STATUS_PENDING)
|
$orders = $query->whereIn('id', $orderIds)->get();
|
||||||
->lockForUpdate()
|
$invalid = $orders->where('status', '<>', StoreOrderModel::STATUS_SUMMARIZED);
|
||||||
->get();
|
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()) {
|
if ($orders->isEmpty()) {
|
||||||
throw new RepositoryException('当日无待汇总订单');
|
throw new RepositoryException('无已接单订单,无法生成采购单');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 展开明细按商品聚合
|
// 2. 展开明细按商品聚合
|
||||||
$aggregated = [];
|
$aggregated = [];
|
||||||
$orderIds = [];
|
$sourceOrderIds = [];
|
||||||
foreach ($orders as $order) {
|
foreach ($orders as $order) {
|
||||||
$orderIds[] = $order->id;
|
$sourceOrderIds[] = $order->id;
|
||||||
foreach ($order->items as $item) {
|
foreach ($order->items as $item) {
|
||||||
$productId = (int) $item->product_id;
|
$productId = (int) $item->product_id;
|
||||||
if (! isset($aggregated[$productId])) {
|
if (! isset($aggregated[$productId])) {
|
||||||
@@ -69,7 +77,7 @@ class PurchaseGenerateService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($aggregated === []) {
|
if ($aggregated === []) {
|
||||||
throw new RepositoryException('当日待汇总订单均无明细,无法生成采购单');
|
throw new RepositoryException('已接单订单均无明细,无法生成采购单');
|
||||||
}
|
}
|
||||||
|
|
||||||
$products = ProductModel::withTrashed()
|
$products = ProductModel::withTrashed()
|
||||||
@@ -164,9 +172,12 @@ class PurchaseGenerateService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 源订单回写「已汇总」
|
// 6. 源订单回写「采购中」并关联采购单(分摊按 purchase_id 溯源)
|
||||||
StoreOrderModel::whereIn('id', $orderIds)
|
StoreOrderModel::whereIn('id', $sourceOrderIds)
|
||||||
->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
->update([
|
||||||
|
'status' => StoreOrderModel::STATUS_DELIVERING,
|
||||||
|
'purchase_id' => $purchase->id,
|
||||||
|
]);
|
||||||
|
|
||||||
return $purchase;
|
return $purchase;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\PurchaseAllocationModel;
|
|||||||
use App\Models\PurchaseOrderItemModel;
|
use App\Models\PurchaseOrderItemModel;
|
||||||
use App\Models\PurchaseOrderModel;
|
use App\Models\PurchaseOrderModel;
|
||||||
use App\Models\StoreModel;
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\StoreOrderModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +36,9 @@ class AllocationTest extends ProcurementTestCase
|
|||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 接单后生成采购单(生成来源为已接单订单)
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
@@ -131,6 +135,8 @@ class AllocationTest extends ProcurementTestCase
|
|||||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
|
||||||
|
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]);
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]);
|
||||||
$purchase = PurchaseOrderModel::first();
|
$purchase = PurchaseOrderModel::first();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\ProductModel;
|
|||||||
use App\Models\ProductPriceModel;
|
use App\Models\ProductPriceModel;
|
||||||
use App\Models\PurchaseOrderModel;
|
use App\Models\PurchaseOrderModel;
|
||||||
use App\Models\StoreModel;
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\StoreOrderModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
use Maatwebsite\Excel\Facades\Excel;
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
@@ -39,6 +40,9 @@ class ExportTest extends ProcurementTestCase
|
|||||||
['product_id' => $meat->id, 'quantity' => 1],
|
['product_id' => $meat->id, 'quantity' => 1],
|
||||||
]])->assertJsonPath('success', true);
|
]])->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
// 接单后生成采购单
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ use App\Models\UserModel;
|
|||||||
class PurchaseGenerateTest extends ProcurementTestCase
|
class PurchaseGenerateTest extends ProcurementTestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 造当日待汇总订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品)
|
* 造当日已接单订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品),下单后统一接单
|
||||||
* 商品设两个等级价 5.00 / 4.00,估算单价应取最低 4.00
|
* 商品设两个等级价 5.00 / 4.00,估算单价应取最低 4.00
|
||||||
*/
|
*/
|
||||||
private function seedPendingOrders(): ProductModel
|
private function seedAcceptedOrders(): ProductModel
|
||||||
{
|
{
|
||||||
$level = CustomerLevelModel::factory()->create();
|
$level = CustomerLevelModel::factory()->create();
|
||||||
$levelLow = CustomerLevelModel::factory()->create();
|
$levelLow = CustomerLevelModel::factory()->create();
|
||||||
@@ -35,13 +35,16 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
|||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 后台接单:待接单 → 已接单(生成采购单的来源状态)
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
return $product;
|
return $product;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 多门店多订单按商品聚合,估算单价取最低等级价 */
|
/** 多门店多订单按商品聚合,估算单价取最低等级价 */
|
||||||
public function test_generate_aggregates_orders_by_product(): void
|
public function test_generate_aggregates_orders_by_product(): void
|
||||||
{
|
{
|
||||||
$this->seedPendingOrders();
|
$this->seedAcceptedOrders();
|
||||||
$admin = $this->actingAsSysUser();
|
$admin = $this->actingAsSysUser();
|
||||||
|
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
@@ -63,33 +66,71 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
|||||||
$this->assertSame('40.00', (string) $purchase->estimate_amount);
|
$this->assertSame('40.00', (string) $purchase->estimate_amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 源订单状态回写为已汇总 */
|
/** 源订单状态回写为采购中,并关联 purchase_id */
|
||||||
public function test_generate_writes_back_order_status(): void
|
public function test_generate_writes_back_order_status(): void
|
||||||
{
|
{
|
||||||
$this->seedPendingOrders();
|
$this->seedAcceptedOrders();
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
$this->assertSame(0, StoreOrderModel::where('status', StoreOrderModel::STATUS_PENDING)->count());
|
$purchase = PurchaseOrderModel::first();
|
||||||
$this->assertSame(3, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count());
|
$this->assertSame(0, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count());
|
||||||
|
$this->assertSame(3, StoreOrderModel::where('status', StoreOrderModel::STATUS_DELIVERING)->count());
|
||||||
|
$this->assertSame(3, StoreOrderModel::where('purchase_id', $purchase->id)->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当日无待汇总订单 → 报错 */
|
/** 仅有待接单订单时不可生成(待接单不再被归集)→ 报错 */
|
||||||
public function test_generate_without_pending_orders_fails(): void
|
public function test_generate_without_accepted_orders_fails(): void
|
||||||
{
|
{
|
||||||
$this->actingAsSysUser();
|
$level = CustomerLevelModel::factory()->create();
|
||||||
|
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||||
|
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||||
|
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]);
|
||||||
|
|
||||||
|
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||||
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||||
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', false);
|
->assertJsonPath('success', false);
|
||||||
$this->assertSame(0, PurchaseOrderModel::count());
|
$this->assertSame(0, PurchaseOrderModel::count());
|
||||||
|
$this->assertSame(1, StoreOrderModel::where('status', StoreOrderModel::STATUS_PENDING)->count());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 幂等:已汇总订单不会被重复归集 */
|
/** 指定订单合并:仅归集 order_ids 内的已接单订单,混入非已接单则整批拒绝 */
|
||||||
|
public function test_generate_with_selected_order_ids(): void
|
||||||
|
{
|
||||||
|
$this->seedAcceptedOrders();
|
||||||
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
|
$selected = StoreOrderModel::query()->orderBy('id')->limit(2)->pluck('id')->all();
|
||||||
|
|
||||||
|
$this->postJson('/purchase/order/generate', [
|
||||||
|
'purchase_date' => now()->toDateString(),
|
||||||
|
'order_ids' => $selected,
|
||||||
|
])->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
$purchase = PurchaseOrderModel::first();
|
||||||
|
$this->assertSame(2, StoreOrderModel::where('purchase_id', $purchase->id)->count());
|
||||||
|
$this->assertSame(1, StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->count(), '未选中的订单保持已接单');
|
||||||
|
|
||||||
|
// 混入非已接单订单 → 整批拒绝
|
||||||
|
$remaining = StoreOrderModel::where('status', StoreOrderModel::STATUS_SUMMARIZED)->value('id');
|
||||||
|
$done = StoreOrderModel::where('status', StoreOrderModel::STATUS_DELIVERING)->value('id');
|
||||||
|
$this->postJson('/purchase/order/generate', [
|
||||||
|
'purchase_date' => now()->toDateString(),
|
||||||
|
'order_ids' => [$remaining, $done],
|
||||||
|
])->assertJsonPath('success', false);
|
||||||
|
$this->assertSame(1, PurchaseOrderModel::count(), '整批拒绝,不产生新采购单');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 幂等:已归集订单不会被重复归集 */
|
||||||
public function test_generate_is_idempotent(): void
|
public function test_generate_is_idempotent(): void
|
||||||
{
|
{
|
||||||
$this->seedPendingOrders();
|
$this->seedAcceptedOrders();
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
|
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
@@ -128,6 +169,9 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
|||||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
|
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
|
|
||||||
|
// 接单后生成
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Models\ReconciliationItemModel;
|
|||||||
use App\Models\ReconciliationModel;
|
use App\Models\ReconciliationModel;
|
||||||
use App\Models\SettlementModel;
|
use App\Models\SettlementModel;
|
||||||
use App\Models\StoreModel;
|
use App\Models\StoreModel;
|
||||||
|
use App\Models\StoreOrderModel;
|
||||||
use App\Models\SupplierModel;
|
use App\Models\SupplierModel;
|
||||||
use App\Models\UserModel;
|
use App\Models\UserModel;
|
||||||
|
|
||||||
@@ -42,6 +43,9 @@ class ReconciliationTest extends ProcurementTestCase
|
|||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 接单后生成采购单
|
||||||
|
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||||
|
|
||||||
$this->actingAsSysUser();
|
$this->actingAsSysUser();
|
||||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||||
->assertJsonPath('success', true);
|
->assertJsonPath('success', true);
|
||||||
|
|||||||
+11
-2
@@ -10,7 +10,7 @@ export async function getStoreOrder(id: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 订单状态流转(2配送中 3已完成 9取消) */
|
/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成) */
|
||||||
export async function updateOrderStatus(id: number, status: number) {
|
export async function updateOrderStatus(id: number, status: number) {
|
||||||
return createAxios({
|
return createAxios({
|
||||||
url: `/order/store/${id}/status`,
|
url: `/order/store/${id}/status`,
|
||||||
@@ -19,6 +19,15 @@ export async function updateOrderStatus(id: number, status: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */
|
||||||
|
export async function batchUpdateOrderStatus(ids: number[], status: number) {
|
||||||
|
return createAxios<{ success: number }>({
|
||||||
|
url: '/order/store/batchStatus',
|
||||||
|
method: 'put',
|
||||||
|
data: { ids, status },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */
|
/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */
|
||||||
export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) {
|
export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) {
|
||||||
return createAxios({
|
return createAxios({
|
||||||
@@ -28,7 +37,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 待汇总预览(按商品聚合) */
|
/** 已接单预览(按商品聚合,生成采购单前确认) */
|
||||||
export async function getOrderSummary() {
|
export async function getOrderSummary() {
|
||||||
return createAxios<IOrderSummaryRow[]>({
|
return createAxios<IOrderSummaryRow[]>({
|
||||||
url: '/order/store/summary',
|
url: '/order/store/summary',
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ export interface PurchaseItemUpdateParams {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** C1 按门店订单汇总生成采购单 */
|
/** C1 合并「已接单」门店订单生成采购单(order_ids 为空 = 全部已接单;生成后源订单转采购中) */
|
||||||
export async function generatePurchase(purchase_date: string) {
|
export async function generatePurchase(purchase_date: string, order_ids?: number[]) {
|
||||||
return createAxios<{ id: number; purchase_no: string }>({
|
return createAxios<{ id: number; purchase_no: string }>({
|
||||||
url: '/purchase/order/generate',
|
url: '/purchase/order/generate',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { purchase_date },
|
data: { purchase_date, order_ids },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+159
-6
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DatePicker,
|
||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
@@ -8,12 +9,14 @@ import {
|
|||||||
message,
|
message,
|
||||||
Modal,
|
Modal,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
|
Radio,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
Table,
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
Image,
|
Image,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import XinTable from '@/components/XinTable';
|
import XinTable from '@/components/XinTable';
|
||||||
import type {
|
import type {
|
||||||
@@ -24,7 +27,8 @@ import type {
|
|||||||
import type IStoreOrder from '@/domain/iStoreOrder.ts';
|
import type IStoreOrder from '@/domain/iStoreOrder.ts';
|
||||||
import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts';
|
import type { IStoreOrderItem } from '@/domain/iStoreOrder.ts';
|
||||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||||
import { getStoreOrder, updateOrderContainer, updateOrderStatus } from '@/api/order/store.ts';
|
import { getStoreOrder, updateOrderContainer, updateOrderStatus, batchUpdateOrderStatus } from '@/api/order/store.ts';
|
||||||
|
import { generatePurchase } from '@/api/purchase/order.ts';
|
||||||
import { getStoreOptions } from '@/api/customer/store.ts';
|
import { getStoreOptions } from '@/api/customer/store.ts';
|
||||||
import type IStore from '@/domain/iStore.ts';
|
import type IStore from '@/domain/iStore.ts';
|
||||||
import AuthButton from '@/components/AuthButton';
|
import AuthButton from '@/components/AuthButton';
|
||||||
@@ -36,17 +40,29 @@ const { Title, Text } = Typography;
|
|||||||
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
|
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态流转合法路径:待汇总→配送中/取消;已汇总→配送中;配送中→已完成
|
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||||
|
* (已接单→采购中 通过「生成采购单」完成,不在流转按钮内)
|
||||||
*/
|
*/
|
||||||
const NEXT_STATUS: Record<number, { status: number; label: string; danger?: boolean }[]> = {
|
const NEXT_STATUS: Record<number, { status: number; label: string; danger?: boolean }[]> = {
|
||||||
0: [
|
0: [
|
||||||
{ status: 2, label: '开始配送' },
|
{ status: 1, label: '接单' },
|
||||||
{ status: 9, label: '取消订单', danger: true },
|
{ status: 9, label: '取消订单', danger: true },
|
||||||
],
|
],
|
||||||
1: [{ status: 2, label: '开始配送' }],
|
2: [
|
||||||
2: [{ status: 3, label: '完成订单' }],
|
{ status: 3, label: '开始配送' },
|
||||||
|
{ status: 4, label: '完成订单' },
|
||||||
|
],
|
||||||
|
3: [{ status: 4, label: '完成订单' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 批量流转可选目标状态(弹窗单选,后端全量校验,任一不允许则整批中止) */
|
||||||
|
const BATCH_TARGET_OPTIONS = [
|
||||||
|
{ value: 1, label: '接单(待接单 → 已接单)' },
|
||||||
|
{ value: 3, label: '开始配送(采购中 → 配送中)' },
|
||||||
|
{ value: 4, label: '完成订单(采购中/配送中 → 已完成)' },
|
||||||
|
{ value: 9, label: '取消订单(待接单 → 已取消)' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 门店订单管理(只读 + 状态流转,订单由小程序端创建)
|
* 门店订单管理(只读 + 状态流转,订单由小程序端创建)
|
||||||
*/
|
*/
|
||||||
@@ -63,6 +79,17 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
const [containerSaving, setContainerSaving] = useState(false);
|
const [containerSaving, setContainerSaving] = useState(false);
|
||||||
const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>();
|
const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>();
|
||||||
|
|
||||||
|
/** 勾选行(批量流转/生成采购单用) */
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [batchOpen, setBatchOpen] = useState(false);
|
||||||
|
const [batchTarget, setBatchTarget] = useState<number | null>(null);
|
||||||
|
const [batchLoading, setBatchLoading] = useState(false);
|
||||||
|
|
||||||
|
/** 生成采购单(合并已接单订单,生成后源订单转采购中) */
|
||||||
|
const [generateOpen, setGenerateOpen] = useState(false);
|
||||||
|
const [generateLoading, setGenerateLoading] = useState(false);
|
||||||
|
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -85,6 +112,40 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
await tableRef.current?.reload();
|
await tableRef.current?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 批量状态流转:后端全量校验,任一订单不允许流转则整批中止 */
|
||||||
|
const handleBatchStatus = async () => {
|
||||||
|
if (batchTarget === null) {
|
||||||
|
message.warning('请选择要流转的目标状态');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBatchLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await batchUpdateOrderStatus(selectedRowKeys as number[], batchTarget);
|
||||||
|
message.success(`已批量流转 ${res.data.data?.success ?? 0} 单`);
|
||||||
|
setBatchOpen(false);
|
||||||
|
setBatchTarget(null);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
await tableRef.current?.reload();
|
||||||
|
} finally {
|
||||||
|
setBatchLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 生成采购单:勾选时合并所选订单(须均为已接单),未勾选时合并全部已接单订单 */
|
||||||
|
const handleGeneratePurchase = async (values: { purchase_date: dayjs.Dayjs }) => {
|
||||||
|
setGenerateLoading(true);
|
||||||
|
try {
|
||||||
|
const orderIds = selectedRowKeys.length > 0 ? selectedRowKeys.map(Number) : undefined;
|
||||||
|
const res = await generatePurchase(values.purchase_date.format('YYYY-MM-DD'), orderIds);
|
||||||
|
message.success(`采购单 ${res.data.data?.purchase_no} 已生成,源订单已转为采购中`);
|
||||||
|
setGenerateOpen(false);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
await tableRef.current?.reload();
|
||||||
|
} finally {
|
||||||
|
setGenerateLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openContainer = (record: IStoreOrder) => {
|
const openContainer = (record: IStoreOrder) => {
|
||||||
setContainerOrder(record);
|
setContainerOrder(record);
|
||||||
containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num });
|
containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num });
|
||||||
@@ -294,6 +355,20 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
|
<Button key="detail" type={'primary'} icon={<UnorderedListOutlined />} onClick={() => openDetail(record.id!)}>
|
||||||
详情
|
详情
|
||||||
</Button>
|
</Button>
|
||||||
|
{
|
||||||
|
(NEXT_STATUS[record.status ?? -1] ?? []).map((action) => (
|
||||||
|
<AuthButton key={action.status} auth="order.store.update">
|
||||||
|
<Popconfirm
|
||||||
|
title={`确认将订单状态更新为「${action.label}」?`}
|
||||||
|
onConfirm={() => handleStatusChange(record.id!, action.status)}
|
||||||
|
>
|
||||||
|
<Button type={action.danger ? undefined : 'primary'} danger={action.danger}>
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</AuthButton>
|
||||||
|
))
|
||||||
|
}
|
||||||
{
|
{
|
||||||
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
|
CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) ? (
|
||||||
<AuthButton key="container" auth="order.store.update">
|
<AuthButton key="container" auth="order.store.update">
|
||||||
@@ -316,6 +391,28 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
tableRef,
|
tableRef,
|
||||||
operateShow: false,
|
operateShow: false,
|
||||||
formProps: false,
|
formProps: false,
|
||||||
|
rowSelection: {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
|
},
|
||||||
|
actionBarRender: (dom) => [
|
||||||
|
dom.search,
|
||||||
|
<AuthButton key="generate" auth="purchase.order.generate">
|
||||||
|
<Button type="primary" onClick={() => setGenerateOpen(true)}>
|
||||||
|
生成采购单
|
||||||
|
</Button>
|
||||||
|
</AuthButton>,
|
||||||
|
<AuthButton key="batch" auth="order.store.update">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
onClick={() => setBatchOpen(true)}
|
||||||
|
>
|
||||||
|
批量操作{selectedRowKeys.length > 0 ? `(${selectedRowKeys.length})` : ''}
|
||||||
|
</Button>
|
||||||
|
</AuthButton>,
|
||||||
|
dom.keywordSearch,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -323,7 +420,7 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
<Title level={3}>门店订单</Title>
|
<Title level={3}>门店订单</Title>
|
||||||
<Text type="secondary">
|
<Text type="secondary">
|
||||||
小程序下单汇总;待汇总订单将进入采购单生成,订单创建与取消在小程序端完成。
|
小程序下单为待接单;接单后通过「生成采购单」合并为采购单并转采购中,采购中可批量流转为配送中或已完成。
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<XinTable<IStoreOrder> {...tableProps} />
|
<XinTable<IStoreOrder> {...tableProps} />
|
||||||
@@ -397,6 +494,62 @@ const StoreOrderPage: React.FC = () => {
|
|||||||
) : null}
|
) : null}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
{/* 生成采购单:合并已接单订单 */}
|
||||||
|
<Modal
|
||||||
|
title="生成采购单"
|
||||||
|
open={generateOpen}
|
||||||
|
onCancel={() => setGenerateOpen(false)}
|
||||||
|
onOk={() => generateForm.submit()}
|
||||||
|
confirmLoading={generateLoading}
|
||||||
|
okText="确认生成"
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<div className="py-2 text-gray-500">
|
||||||
|
{selectedRowKeys.length > 0
|
||||||
|
? `将合并选中的 ${selectedRowKeys.length} 笔订单生成一张采购单(须均为已接单状态,否则整批中止)。`
|
||||||
|
: '未勾选订单时,将合并全部「已接单」订单生成一张采购单。'}
|
||||||
|
生成后源订单自动转为「采购中」。
|
||||||
|
</div>
|
||||||
|
<Form form={generateForm} layout="vertical" onFinish={handleGeneratePurchase}>
|
||||||
|
<Form.Item
|
||||||
|
label="采购日期"
|
||||||
|
name="purchase_date"
|
||||||
|
initialValue={dayjs()}
|
||||||
|
rules={[{ required: true, message: '请选择采购日期' }]}
|
||||||
|
>
|
||||||
|
<DatePicker className="w-full" allowClear={false} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 批量状态流转 */}
|
||||||
|
<Modal
|
||||||
|
title="批量状态流转"
|
||||||
|
open={batchOpen}
|
||||||
|
onCancel={() => setBatchOpen(false)}
|
||||||
|
onOk={handleBatchStatus}
|
||||||
|
confirmLoading={batchLoading}
|
||||||
|
okText="确认流转"
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<div className="py-2 text-gray-500">
|
||||||
|
已勾选 {selectedRowKeys.length} 笔订单;任一订单不满足流转条件时,本次批量操作将整批中止,不做任何修改。
|
||||||
|
</div>
|
||||||
|
<Radio.Group
|
||||||
|
className="w-full py-2"
|
||||||
|
value={batchTarget}
|
||||||
|
onChange={(e) => setBatchTarget(e.target.value as number)}
|
||||||
|
>
|
||||||
|
<Space orientation="vertical">
|
||||||
|
{BATCH_TARGET_OPTIONS.map((opt) => (
|
||||||
|
<Radio key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
</Radio.Group>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* 修改周转框/托盘数量 */}
|
{/* 修改周转框/托盘数量 */}
|
||||||
<Modal
|
<Modal
|
||||||
title="修改周转框/托盘"
|
title="修改周转框/托盘"
|
||||||
|
|||||||
@@ -457,7 +457,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<div className="py-2 text-gray-500">
|
<div className="py-2 text-gray-500">
|
||||||
将汇总所选日期全部「待汇总」订单,按商品聚合生成采购单(估算单价取最低等级价)。
|
将合并全部「已接单」门店订单,按商品聚合生成采购单(估算单价取最低等级价),生成后源订单转为「采购中」。
|
||||||
</div>
|
</div>
|
||||||
<Form
|
<Form
|
||||||
form={generateForm}
|
form={generateForm}
|
||||||
@@ -480,7 +480,7 @@ const PurchaseOrderPage: React.FC = () => {
|
|||||||
title={detail ? `采购单 ${detail.purchase_no}` : '采购单详情'}
|
title={detail ? `采购单 ${detail.purchase_no}` : '采购单详情'}
|
||||||
open={detailOpen}
|
open={detailOpen}
|
||||||
onClose={() => setDetailOpen(false)}
|
onClose={() => setDetailOpen(false)}
|
||||||
width={1080}
|
size={1080}
|
||||||
loading={detailLoading}
|
loading={detailLoading}
|
||||||
>
|
>
|
||||||
{detail ? (
|
{detail ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user