优化采购单
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -207,60 +207,6 @@ class StoreOrderController extends BaseController
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改周转框/周转托盘数量
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/container', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function container(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'box_num' => 'required|integer|min:0',
|
||||
'tray_num' => 'required|integer|min:0',
|
||||
], [
|
||||
'box_num.required' => '周转框数量不能为空',
|
||||
'box_num.integer' => '周转框数量必须为整数',
|
||||
'box_num.min' => '周转框数量不能小于 0',
|
||||
'tray_num.required' => '周转托盘数量不能为空',
|
||||
'tray_num.integer' => '周转托盘数量必须为整数',
|
||||
'tray_num.min' => '周转托盘数量不能小于 0',
|
||||
]);
|
||||
|
||||
$order = StoreOrderModel::find($id);
|
||||
if (empty($order)) {
|
||||
throw new RepositoryException('订单不存在');
|
||||
}
|
||||
|
||||
$editable = [
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
];
|
||||
if (! in_array($order->status, $editable, true)) {
|
||||
throw new RepositoryException(
|
||||
'订单当前状态为「' . (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status) . '」,不允许修改周转框/托盘数量'
|
||||
);
|
||||
}
|
||||
|
||||
$boxPrice = (float) site_config('services.box_amount', 0);
|
||||
$trayPrice = (float) site_config('services.tray_amount', 0);
|
||||
$addedAmount = round($data['box_num'] * $boxPrice + $data['tray_num'] * $trayPrice, 2);
|
||||
|
||||
// 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立
|
||||
$productAmount = (float) $order->product_amount;
|
||||
if ($productAmount <= 0 && (float) $order->total_amount > 0) {
|
||||
$productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2);
|
||||
}
|
||||
|
||||
$order->box_num = (int) $data['box_num'];
|
||||
$order->tray_num = (int) $data['tray_num'];
|
||||
$order->product_amount = $productAmount;
|
||||
$order->added_amount = $addedAmount;
|
||||
$order->total_amount = round($productAmount + $addedAmount, 2);
|
||||
$order->save();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态流转合法路径
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Purchase;
|
||||
use App\Exceptions\RepositoryException;
|
||||
use App\Exports\PurchaseOrderExport;
|
||||
use App\Http\Requests\Purchase\PurchaseCellUpdateRequest;
|
||||
use App\Http\Requests\Purchase\PurchaseContainerUpdateRequest;
|
||||
use App\Http\Requests\Purchase\PurchaseRowUpdateRequest;
|
||||
use App\Models\ProductModel;
|
||||
use App\Models\PurchaseOrderModel;
|
||||
@@ -13,6 +14,7 @@ use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Services\ItemImageResolver;
|
||||
use App\Services\PurchaseGenerateService;
|
||||
use App\Services\StoreOrderContainerService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -133,6 +135,45 @@ class PurchaseOrderController extends BaseController
|
||||
[$a['category_sort'], $a['product_sort'], $a['product_id']]
|
||||
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
|
||||
|
||||
// 周转框/托盘合并记录:按门店聚合全部订单(附底层订单明细,供采购单完成前修改)
|
||||
$storeOrders = StoreOrderModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->whereNull('deleted_at')
|
||||
->orderBy('id')
|
||||
->get(['id', 'order_no', 'store_id', 'box_num', 'tray_num']);
|
||||
|
||||
$boxPrice = (float) site_config('services.box_amount', 0);
|
||||
$trayPrice = (float) site_config('services.tray_amount', 0);
|
||||
$storeSort = $stores->pluck('id')->flip();
|
||||
$containers = [];
|
||||
foreach ($storeOrders->groupBy('store_id') as $storeId => $orders) {
|
||||
$boxNum = (int) $orders->sum('box_num');
|
||||
$trayNum = (int) $orders->sum('tray_num');
|
||||
$containers[] = [
|
||||
'store_id' => (int) $storeId,
|
||||
'store_name' => $stores->firstWhere('id', (int) $storeId)['name'] ?? '门店#' . $storeId,
|
||||
'box_num' => $boxNum,
|
||||
'tray_num' => $trayNum,
|
||||
'box_price' => number_format($boxPrice, 2),
|
||||
'tray_price' => number_format($trayPrice, 2),
|
||||
'added_amount' => number_format($boxNum * $boxPrice + $trayNum * $trayPrice, 2),
|
||||
'order_count' => $orders->count(),
|
||||
'orders' => $orders->map(static fn (StoreOrderModel $order) => [
|
||||
'order_id' => $order->id,
|
||||
'order_no' => $order->order_no,
|
||||
'box_num' => (int) $order->box_num,
|
||||
'tray_num' => (int) $order->tray_num,
|
||||
])->values()->toArray(),
|
||||
'store_sort' => (int) ($storeSort[$storeId] ?? 9999),
|
||||
];
|
||||
}
|
||||
usort($containers, static fn (array $a, array $b): int =>
|
||||
[$a['store_sort'], $a['store_id']] <=> [$b['store_sort'], $b['store_id']]);
|
||||
$containers = array_map(static function (array $row): array {
|
||||
unset($row['store_sort']);
|
||||
return $row;
|
||||
}, $containers);
|
||||
|
||||
return $this->success([
|
||||
'purchase' => $purchase->toArray(),
|
||||
'stores' => $stores->toArray(),
|
||||
@@ -140,6 +181,7 @@ class PurchaseOrderController extends BaseController
|
||||
unset($row['category_sort'], $row['product_sort']);
|
||||
return $row;
|
||||
}, $rows),
|
||||
'containers' => $containers,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -395,6 +437,74 @@ class PurchaseOrderController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 周转框/托盘合并记录修改:按门店覆盖全部订单逐笔更新(仅采购单进行中可改)
|
||||
* @throws Throwable
|
||||
*/
|
||||
#[PutRoute(route: '/{id}/container/{storeId}', authorize: 'update', where: ['id' => '[0-9]+', 'storeId' => '[0-9]+'])]
|
||||
public function updateContainer(int $id, int $storeId, PurchaseContainerUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$purchase = PurchaseOrderModel::find($id);
|
||||
if (empty($purchase)) {
|
||||
throw new RepositoryException('采购单不存在');
|
||||
}
|
||||
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
|
||||
throw new RepositoryException('采购单已完成,不允许修改周转框/托盘');
|
||||
}
|
||||
|
||||
$submitted = $request->validated()['orders'];
|
||||
|
||||
return DB::transaction(function () use ($purchase, $storeId, $submitted) {
|
||||
$orders = StoreOrderModel::query()
|
||||
->where('purchase_id', $purchase->id)
|
||||
->where('store_id', $storeId)
|
||||
->whereNull('deleted_at')
|
||||
->lockForUpdate()
|
||||
->get();
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
throw new RepositoryException('该采购单下无此门店的订单');
|
||||
}
|
||||
|
||||
// 合并记录修改必须覆盖该门店全部订单,避免只改部分造成汇总偏差
|
||||
$actualIds = $orders->pluck('id')->map(static fn ($orderId) => (int) $orderId)->all();
|
||||
$submittedIds = array_map(static fn (array $row) => (int) $row['order_id'], $submitted);
|
||||
$missing = array_diff($actualIds, $submittedIds);
|
||||
if ($missing !== []) {
|
||||
throw new RepositoryException('提交不完整,缺少订单:' . implode('、', $missing));
|
||||
}
|
||||
if (array_diff($submittedIds, $actualIds) !== []) {
|
||||
throw new RepositoryException('包含不属于该采购单该门店的订单');
|
||||
}
|
||||
|
||||
foreach ($orders as $order) {
|
||||
if (! in_array($order->status, [
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
], true)) {
|
||||
throw new RepositoryException(
|
||||
'订单 ' . $order->order_no . ' 当前状态为「'
|
||||
. (StoreOrderModel::STATUS_NAMES[$order->status] ?? $order->status)
|
||||
. '」,不允许修改周转框/托盘数量'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$ordersById = $orders->keyBy('id');
|
||||
$containerService = app(StoreOrderContainerService::class);
|
||||
foreach ($submitted as $row) {
|
||||
$containerService->update(
|
||||
$ordersById->get($row['order_id']),
|
||||
(int) $row['box_num'],
|
||||
(int) $row['tray_num'],
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品行修改:品名/供应商/包规/单位/成本
|
||||
* @throws Throwable
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Purchase;
|
||||
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
/**
|
||||
* 采购单周转框/托盘合并记录修改 验证(按门店覆盖全部订单,逐笔重算附加金额)
|
||||
*/
|
||||
class PurchaseContainerUpdateRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'orders' => 'required|array|min:1',
|
||||
'orders.*.order_id' => 'required|integer|distinct',
|
||||
'orders.*.box_num' => 'required|integer|min:0',
|
||||
'orders.*.tray_num' => 'required|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'orders.required' => '请提交门店订单周转框/托盘数量',
|
||||
'orders.array' => '订单数据格式错误',
|
||||
'orders.min' => '请提交门店订单周转框/托盘数量',
|
||||
'orders.*.order_id.required' => '订单ID不能为空',
|
||||
'orders.*.order_id.integer' => '订单ID格式错误',
|
||||
'orders.*.order_id.distinct' => '存在重复订单',
|
||||
'orders.*.box_num.required' => '周转框数量不能为空',
|
||||
'orders.*.box_num.integer' => '周转框数量必须为整数',
|
||||
'orders.*.box_num.min' => '周转框数量不能小于 0',
|
||||
'orders.*.tray_num.required' => '周转托盘数量不能为空',
|
||||
'orders.*.tray_num.integer' => '周转托盘数量必须为整数',
|
||||
'orders.*.tray_num.min' => '周转托盘数量不能小于 0',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -58,9 +58,9 @@ readonly class PurchaseGenerateService
|
||||
static fn (string $carry, $item): string => bcadd($carry, (string) $item->quantity, 2),
|
||||
'0'
|
||||
);
|
||||
// 预估金额 = Σ订货金额(明细金额 price×quantity),与采购单修改重算口径一致
|
||||
// 预估成本
|
||||
$estimateAmount = $items->reduce(
|
||||
static fn (string $carry, $item): string => bcadd($carry, (string) $item->amount, 2),
|
||||
static fn (string $carry, $item): string => bcadd($carry, bcmul($item->quantity, $item->cost_price, 2), 2),
|
||||
'0'
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\StoreOrderModel;
|
||||
|
||||
/**
|
||||
* 周转框/周转托盘数量修改(订单附加金额重算)
|
||||
*/
|
||||
readonly class StoreOrderContainerService
|
||||
{
|
||||
/**
|
||||
* 更新订单周转框/托盘数量并重算附加金额与订单总金额。
|
||||
* 历史订单未写商品金额:按「总额 - 附加」反推并回写,保证 总额 = 商品 + 附加 恒成立。
|
||||
*/
|
||||
public function update(StoreOrderModel $order, int $boxNum, int $trayNum): void
|
||||
{
|
||||
$boxPrice = (float) site_config('services.box_amount', 0);
|
||||
$trayPrice = (float) site_config('services.tray_amount', 0);
|
||||
$addedAmount = round($boxNum * $boxPrice + $trayNum * $trayPrice, 2);
|
||||
|
||||
$productAmount = (float) $order->product_amount;
|
||||
if ($productAmount <= 0 && (float) $order->total_amount > 0) {
|
||||
$productAmount = round((float) $order->total_amount - (float) $order->added_amount, 2);
|
||||
}
|
||||
|
||||
$order->box_num = $boxNum;
|
||||
$order->tray_num = $trayNum;
|
||||
$order->product_amount = $productAmount;
|
||||
$order->added_amount = $addedAmount;
|
||||
$order->total_amount = round($productAmount + $addedAmount, 2);
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ use App\Models\StoreOrderItemModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\SupplierModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
|
||||
/**
|
||||
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑/同步、行级成本,
|
||||
@@ -52,6 +55,23 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]];
|
||||
}
|
||||
|
||||
/** 播种站点配置:周转框单价 2.00、周转托盘单价 10.00 */
|
||||
private function seedContainerConfig(): void
|
||||
{
|
||||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']);
|
||||
foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) {
|
||||
SysSiteConfigItemsModel::create([
|
||||
'group_id' => $group->id,
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
'type' => 'InputNumber',
|
||||
'values' => $value,
|
||||
'sort' => 0,
|
||||
]);
|
||||
}
|
||||
Cache::forget('site_config');
|
||||
}
|
||||
|
||||
/** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */
|
||||
public function test_detail_returns_store_matrix(): void
|
||||
{
|
||||
@@ -440,4 +460,148 @@ class PurchaseEditTest extends ProcurementTestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('data.items.0.editable', false);
|
||||
}
|
||||
|
||||
/** 详情返回周转框/托盘合并记录:按门店聚合全部订单(含底层订单明细与单价) */
|
||||
public function test_detail_returns_container_summary(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
|
||||
StoreOrderModel::where('store_id', $stores[0]->id)->update(['box_num' => 2, 'tray_num' => 1]);
|
||||
StoreOrderModel::where('store_id', $stores[1]->id)->update(['box_num' => 3, 'tray_num' => 0]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$containers = $this->getJson("/purchase/order/{$purchase->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('success', true)
|
||||
->json('data.containers');
|
||||
$this->assertCount(2, $containers);
|
||||
$this->assertSame($stores[0]->id, $containers[0]['store_id'], '与门店列同序');
|
||||
|
||||
$row = $containers[0];
|
||||
$this->assertSame($stores[0]->name, $row['store_name']);
|
||||
$this->assertSame(2, $row['box_num']);
|
||||
$this->assertSame(1, $row['tray_num']);
|
||||
$this->assertSame('2.00', $row['box_price']);
|
||||
$this->assertSame('10.00', $row['tray_price']);
|
||||
$this->assertSame('14.00', $row['added_amount'], '2×2.00 + 1×10.00');
|
||||
$this->assertSame(1, $row['order_count']);
|
||||
$this->assertCount(1, $row['orders']);
|
||||
|
||||
$order = $row['orders'][0];
|
||||
$this->assertStringStartsWith('SO', $order['order_no']);
|
||||
$this->assertSame(2, $order['box_num']);
|
||||
$this->assertSame(1, $order['tray_num']);
|
||||
|
||||
$this->assertSame(3, $containers[1]['box_num']);
|
||||
$this->assertSame('6.00', $containers[1]['added_amount']);
|
||||
}
|
||||
|
||||
/** 合并记录修改:逐笔更新订单数量并重算附加金额与订单总金额,其他门店不受影响 */
|
||||
public function test_update_container_syncs_order_amounts(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
$this->assertSame('20.00', (string) $order->product_amount, '下单时应写入商品金额');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => 3, 'tray_num' => 1]],
|
||||
])->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame(3, $order->box_num);
|
||||
$this->assertSame(1, $order->tray_num);
|
||||
$this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00');
|
||||
$this->assertSame('20.00', (string) $order->product_amount);
|
||||
$this->assertSame('36.00', (string) $order->total_amount, '20.00 + 16.00');
|
||||
|
||||
$other = StoreOrderModel::where('store_id', $stores[1]->id)->first();
|
||||
$this->assertSame(0, $other->box_num, '其他门店订单不受影响');
|
||||
$this->assertSame('30.00', (string) $other->total_amount);
|
||||
}
|
||||
|
||||
/** 采购单已完成:合并记录修改拒绝,数量与金额不变 */
|
||||
public function test_update_container_rejected_when_purchase_completed(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => 5, 'tray_num' => 5]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '采购单已完成,不允许修改周转框/托盘');
|
||||
|
||||
$this->assertSame(0, $order->fresh()->box_num, '被拒绝后数量不变');
|
||||
}
|
||||
|
||||
/** 提交必须覆盖该门店全部订单;跨门店/跨采购单订单拒绝 */
|
||||
public function test_update_container_rejects_incomplete_or_foreign_orders(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$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' => 10.00]);
|
||||
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
|
||||
foreach ([1, 2] as $qty) {
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
|
||||
->assertJsonPath('success', true);
|
||||
}
|
||||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||||
->assertJsonPath('success', true);
|
||||
$purchase = PurchaseOrderModel::first();
|
||||
$orders = StoreOrderModel::where('purchase_id', $purchase->id)->orderBy('id')->get();
|
||||
$this->assertCount(2, $orders, '同一门店两笔订单');
|
||||
|
||||
// 只提交一笔 → 缺少另一笔,整批拒绝
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [
|
||||
'orders' => [['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '提交不完整,缺少订单:' . $orders[1]->id);
|
||||
|
||||
// 混入其他门店订单(同一采购单另一门店)→ 拒绝
|
||||
$otherStore = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$this->actingAsMiniUser(UserModel::factory()->forStore($otherStore->id)->create());
|
||||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]])
|
||||
->assertJsonPath('success', true);
|
||||
$foreign = StoreOrderModel::where('store_id', $otherStore->id)->first();
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$store->id}", [
|
||||
'orders' => [
|
||||
['order_id' => $orders[0]->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $orders[1]->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $foreign->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '包含不属于该采购单该门店的订单');
|
||||
|
||||
$this->assertSame(0, $orders[0]->fresh()->box_num, '被拒绝后数量不变');
|
||||
}
|
||||
|
||||
/** 数量为负/重复订单时校验失败 */
|
||||
public function test_update_container_validates_negative_and_duplicate_orders(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
[$purchase, , $stores] = $this->buildPurchase();
|
||||
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [['order_id' => $order->id, 'box_num' => -1, 'tray_num' => 0]],
|
||||
])->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '周转框数量不能小于 0');
|
||||
|
||||
$this->putJson("/purchase/order/{$purchase->id}/container/{$stores[0]->id}", [
|
||||
'orders' => [
|
||||
['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
['order_id' => $order->id, 'box_num' => 1, 'tray_num' => 0],
|
||||
],
|
||||
])->assertJsonPath('success', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
{
|
||||
/**
|
||||
* 造当日已接单订单:门店A 两单各 3 件 + 门店B 一单 4 件(同一商品),下单后统一接单
|
||||
* 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价)
|
||||
* 门店等级价 5.00(另设低等级价 4.00 不影响本店下单价),每包成本 6.00
|
||||
*/
|
||||
private function seedAcceptedOrders(): ProductModel
|
||||
{
|
||||
@@ -27,7 +27,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$levelLow = CustomerLevelModel::factory()->create();
|
||||
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
|
||||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '6.00']);
|
||||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => 5.00]);
|
||||
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $levelLow->id, 'price' => 4.00]);
|
||||
|
||||
@@ -43,7 +43,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
return $product;
|
||||
}
|
||||
|
||||
/** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ订货金额 */
|
||||
/** 头部汇总取订货明细合计:total_quantity = Σ数量,estimate_amount = Σ数量×成本价(预估成本) */
|
||||
public function test_generate_aggregates_order_items_into_header(): void
|
||||
{
|
||||
$this->seedAcceptedOrders();
|
||||
@@ -61,7 +61,7 @@ class PurchaseGenerateTest extends ProcurementTestCase
|
||||
$this->assertSame(PurchaseOrderModel::STATUS_PENDING, $purchase->status);
|
||||
|
||||
$this->assertSame('10.00', (string) $purchase->total_quantity, '3+3+4');
|
||||
$this->assertSame('50.00', (string) $purchase->estimate_amount, '10 件 × 等级价 5.00');
|
||||
$this->assertSame('60.00', (string) $purchase->estimate_amount, '10 件 × 每包成本 6.00');
|
||||
$this->assertSame('0.00', (string) $purchase->actual_amount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@ use App\Models\ProductPriceModel;
|
||||
use App\Models\StoreModel;
|
||||
use App\Models\StoreOrderModel;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
|
||||
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
|
||||
|
||||
/**
|
||||
* 小程序下单:等级价快照、服务端重算总价、取消限制、门店数据隔离
|
||||
@@ -133,23 +130,6 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
$this->getJson('/mini/order')->assertJsonPath('data.total', 0);
|
||||
}
|
||||
|
||||
/** 种子化周转框/托盘单价配置(框 2.00、托盘 10.00) */
|
||||
private function seedContainerConfig(): void
|
||||
{
|
||||
$group = SysSiteConfigGroupModel::create(['title' => '业务配置', 'key' => 'services']);
|
||||
foreach ([['box_amount', '周转框单价', '2.00'], ['tray_amount', '周转托盘单价', '10.00']] as [$key, $title, $value]) {
|
||||
SysSiteConfigItemsModel::create([
|
||||
'group_id' => $group->id,
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
'type' => 'InputNumber',
|
||||
'values' => $value,
|
||||
'sort' => 0,
|
||||
]);
|
||||
}
|
||||
Cache::forget('site_config');
|
||||
}
|
||||
|
||||
/** 造一笔指定状态的订单(商品 5.00 × 2 = 10.00) */
|
||||
private function makeOrderWithStatus(int $status): StoreOrderModel
|
||||
{
|
||||
@@ -163,94 +143,6 @@ class StoreOrderTest extends ProcurementTestCase
|
||||
return $order;
|
||||
}
|
||||
|
||||
/** 修改周转框/托盘:自动重算附加金额与订单总金额 */
|
||||
public function test_container_update_recalculates_amounts(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$this->assertSame('10.00', (string) $order->product_amount, '下单时应写入商品金额');
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 3, 'tray_num' => 1])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame(3, $order->box_num);
|
||||
$this->assertSame(1, $order->tray_num);
|
||||
$this->assertSame('16.00', (string) $order->added_amount, '3×2.00 + 1×10.00');
|
||||
$this->assertSame('10.00', (string) $order->product_amount);
|
||||
$this->assertSame('26.00', (string) $order->total_amount, '10.00 + 16.00');
|
||||
}
|
||||
|
||||
/** 已接单、采购中、配送中均可修改 */
|
||||
public function test_container_update_allowed_in_editable_statuses(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$this->actingAsSysUser();
|
||||
|
||||
foreach ([
|
||||
StoreOrderModel::STATUS_SUMMARIZED,
|
||||
StoreOrderModel::STATUS_DELIVERING,
|
||||
StoreOrderModel::STATUS_DISTRIBUTION,
|
||||
] as $status) {
|
||||
$order->update(['status' => $status]);
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 1, 'tray_num' => 0])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
$this->assertSame('2.00', (string) $order->fresh()->added_amount);
|
||||
}
|
||||
}
|
||||
|
||||
/** 待接单、已完成、已取消不允许修改 */
|
||||
public function test_container_update_rejected_in_other_statuses(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_PENDING);
|
||||
$this->actingAsSysUser();
|
||||
|
||||
foreach ([
|
||||
StoreOrderModel::STATUS_PENDING,
|
||||
StoreOrderModel::STATUS_COMPLETED,
|
||||
StoreOrderModel::STATUS_CANCELLED,
|
||||
] as $status) {
|
||||
$order->update(['status' => $status]);
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 5, 'tray_num' => 5])
|
||||
->assertOk()->assertJsonPath('success', false);
|
||||
}
|
||||
$order->refresh();
|
||||
$this->assertSame(0, $order->box_num, '被拒绝后数量不变');
|
||||
$this->assertSame('10.00', (string) $order->total_amount, '被拒绝后总金额不变');
|
||||
}
|
||||
|
||||
/** 历史订单未写商品金额时按「总额 - 附加」反推回写 */
|
||||
public function test_container_update_heals_legacy_product_amount(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
$order->update(['product_amount' => 0]); // 模拟历史数据
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => 2, 'tray_num' => 0])
|
||||
->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$order->refresh();
|
||||
$this->assertSame('10.00', (string) $order->product_amount, '反推回写商品金额');
|
||||
$this->assertSame('4.00', (string) $order->added_amount, '2×2.00');
|
||||
$this->assertSame('14.00', (string) $order->total_amount);
|
||||
}
|
||||
|
||||
/** 数量为负时校验失败 */
|
||||
public function test_container_update_validates_negative_numbers(): void
|
||||
{
|
||||
$this->seedContainerConfig();
|
||||
$order = $this->makeOrderWithStatus(StoreOrderModel::STATUS_SUMMARIZED);
|
||||
|
||||
$this->actingAsSysUser();
|
||||
$this->putJson("/order/store/{$order->id}/container", ['box_num' => -1, 'tray_num' => 0])
|
||||
->assertJsonPath('success', false)
|
||||
->assertJsonPath('msg', '周转框数量不能小于 0');
|
||||
}
|
||||
|
||||
/** 软删除:仅已取消订单可由后台删除,删除后后台/小程序端均不可见 */
|
||||
public function test_soft_delete_only_cancelled_orders(): void
|
||||
{
|
||||
|
||||
@@ -27,15 +27,6 @@ export async function batchUpdateOrderStatus(ids: number[], status: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改周转框/周转托盘数量(自动重算附加金额与订单总金额,仅已接单/采购中/配送中可改) */
|
||||
export async function updateOrderContainer(id: number, data: { box_num: number; tray_num: number }) {
|
||||
return createAxios({
|
||||
url: `/order/store/${id}/container`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除订单 */
|
||||
export async function deleteStoreOrder(id: number) {
|
||||
return createAxios({
|
||||
|
||||
@@ -23,6 +23,13 @@ export interface PurchaseCellUpdateParams {
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
/** 采购单周转框/托盘合并记录修改:单笔订单的框/托盘数量 */
|
||||
export interface PurchaseContainerOrderParams {
|
||||
order_id: number;
|
||||
box_num: number;
|
||||
tray_num: number;
|
||||
}
|
||||
|
||||
|
||||
/** 生成采购单 */
|
||||
export async function generatePurchase(purchase_date: string, order_ids?: number[]) {
|
||||
@@ -77,6 +84,19 @@ export async function getPurchaseStoreSummary(purchaseId: number, storeId: numbe
|
||||
});
|
||||
}
|
||||
|
||||
/** 采购单周转框/托盘合并记录修改:按门店覆盖全部订单逐笔更新(自动重算附加金额与订单总金额) */
|
||||
export async function updatePurchaseContainer(
|
||||
purchaseId: number,
|
||||
storeId: number,
|
||||
orders: PurchaseContainerOrderParams[],
|
||||
) {
|
||||
return createAxios({
|
||||
url: `/purchase/order/${purchaseId}/container/${storeId}`,
|
||||
method: 'put',
|
||||
data: { orders },
|
||||
});
|
||||
}
|
||||
|
||||
/** C2/C3 导出采购单(Excel 表格):type=all 全品类 / category 仅蔬果分类 */
|
||||
export async function exportPurchase(id: number, type: PurchaseExportType = 'all') {
|
||||
return downloadBlob(
|
||||
|
||||
@@ -48,6 +48,34 @@ export interface IPurchaseDetail {
|
||||
purchase: IPurchaseOrder;
|
||||
stores: { id: number; name: string }[];
|
||||
items: IPurchaseDetailRow[];
|
||||
/** 周转框/托盘合并记录(按门店聚合) */
|
||||
containers: IPurchaseContainerStore[];
|
||||
}
|
||||
|
||||
/** 合并记录中的底层门店订单(订单号 + 框/托盘数量) */
|
||||
export interface IPurchaseContainerOrder {
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
box_num: number;
|
||||
tray_num: number;
|
||||
}
|
||||
|
||||
/** 门店周转框/托盘合并记录(该门店在本采购单下全部订单的合计) */
|
||||
export interface IPurchaseContainerStore {
|
||||
store_id: number;
|
||||
store_name: string;
|
||||
/** 周转框合计 */
|
||||
box_num: number;
|
||||
/** 周转托盘合计 */
|
||||
tray_num: number;
|
||||
/** 周转框单价(站点配置) */
|
||||
box_price: string;
|
||||
/** 周转托盘单价(站点配置) */
|
||||
tray_price: string;
|
||||
/** 附加金额合计 = 框合计×框单价 + 托盘合计×托盘单价 */
|
||||
added_amount: string;
|
||||
order_count: number;
|
||||
orders: IPurchaseContainerOrder[];
|
||||
}
|
||||
|
||||
/** 单元格下钻明细行(溯源订货单明细,附订单号/状态/可编辑标记) */
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
@@ -27,7 +26,6 @@ import type IStoreOrder from '@/domain/iStoreOrder.ts';
|
||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||
import {
|
||||
getStoreOrder,
|
||||
updateOrderContainer,
|
||||
updateOrderStatus,
|
||||
batchUpdateOrderStatus,
|
||||
deleteStoreOrder,
|
||||
@@ -36,14 +34,11 @@ import { generatePurchase } from '@/api/purchase/order.ts';
|
||||
import { getStoreOptions } from '@/api/customer/store.ts';
|
||||
import type IStore from '@/domain/iStore.ts';
|
||||
import AuthButton from '@/components/AuthButton';
|
||||
import { DeleteOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { DeleteOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import {PURCHASE_STATUS_MAP} from "@/domain/iPurchaseOrder.ts";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
|
||||
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
|
||||
|
||||
|
||||
/**
|
||||
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
@@ -74,11 +69,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
const [detail, setDetail] = useState<IStoreOrder | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
const [containerOpen, setContainerOpen] = useState(false);
|
||||
const [containerOrder, setContainerOrder] = useState<IStoreOrder | null>(null);
|
||||
const [containerSaving, setContainerSaving] = useState(false);
|
||||
const [containerForm] = Form.useForm<{ box_num: number; tray_num: number }>();
|
||||
|
||||
/** 勾选行(批量流转/生成采购单用) */
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [batchOpen, setBatchOpen] = useState(false);
|
||||
@@ -147,25 +137,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openContainer = (record: IStoreOrder) => {
|
||||
setContainerOrder(record);
|
||||
containerForm.setFieldsValue({ box_num: record.box_num, tray_num: record.tray_num });
|
||||
setContainerOpen(true);
|
||||
};
|
||||
|
||||
const handleContainerSave = async (values: { box_num: number; tray_num: number }) => {
|
||||
if (!containerOrder?.id) return;
|
||||
setContainerSaving(true);
|
||||
try {
|
||||
await updateOrderContainer(containerOrder.id, values);
|
||||
message.success('周转框/托盘数量已更新');
|
||||
setContainerOpen(false);
|
||||
await tableRef.current?.reload();
|
||||
} finally {
|
||||
setContainerSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteStoreOrder(id);
|
||||
@@ -174,13 +145,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
await tableRef.current?.reload();
|
||||
};
|
||||
|
||||
// 弹窗内实时预览:附加金额 = 框×单价 + 托盘×单价;订单总金额 = 商品金额 + 附加金额
|
||||
const watchBoxNum = Number(Form.useWatch('box_num', containerForm) ?? 0);
|
||||
const watchTrayNum = Number(Form.useWatch('tray_num', containerForm) ?? 0);
|
||||
const previewAdded = watchBoxNum * Number(containerOrder?.box_price ?? 0)
|
||||
+ watchTrayNum * Number(containerOrder?.tray_price ?? 0);
|
||||
const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded;
|
||||
|
||||
const columns: XinTableColumn<IStoreOrder>[] = [
|
||||
{
|
||||
title: '订单号',
|
||||
@@ -387,15 +351,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
icon={<UnorderedListOutlined />}
|
||||
onClick={() => openDetail(record.id!)}
|
||||
/>
|
||||
{ CONTAINER_EDITABLE_STATUS.includes(record.status ?? -1) && (
|
||||
<AuthButton key="container" auth="order.store.update">
|
||||
<Button
|
||||
icon={<SettingOutlined />}
|
||||
type={'primary'}
|
||||
onClick={() => openContainer(record)}
|
||||
/>
|
||||
</AuthButton>
|
||||
)}
|
||||
{ NEXT_STATUS[record.status!] && (
|
||||
<AuthButton auth="order.store.update">
|
||||
<Popconfirm
|
||||
@@ -487,13 +442,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
footer={
|
||||
detail ? (
|
||||
<Space className="flex justify-end" wrap>
|
||||
{CONTAINER_EDITABLE_STATUS.includes(detail.status ?? -1) && (
|
||||
<AuthButton auth="order.store.update">
|
||||
<Button icon={<SettingOutlined />} onClick={() => openContainer(detail)}>
|
||||
设置附加信息
|
||||
</Button>
|
||||
</AuthButton>
|
||||
)}
|
||||
{ NEXT_STATUS[detail.status!] && (
|
||||
<AuthButton auth="order.store.update">
|
||||
<Popconfirm
|
||||
@@ -715,50 +663,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
</Space>
|
||||
</Radio.Group>
|
||||
</Modal>
|
||||
|
||||
{/* 修改周转框/托盘数量 */}
|
||||
<Modal
|
||||
title="修改周转框/托盘"
|
||||
open={containerOpen}
|
||||
onCancel={() => setContainerOpen(false)}
|
||||
onOk={() => containerForm.submit()}
|
||||
confirmLoading={containerSaving}
|
||||
okText="保存"
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
订单 {containerOrder?.order_no},保存后将按单价自动重算附加金额与订单总金额。
|
||||
</div>
|
||||
<Form form={containerForm} layout="vertical" onFinish={handleContainerSave}>
|
||||
<Form.Item
|
||||
label={`周转框数量(单价 ¥${containerOrder?.box_price ?? '0.00'})`}
|
||||
name="box_num"
|
||||
rules={[{ required: true, message: '请输入周转框数量' }]}
|
||||
>
|
||||
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入周转框数量" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={`周转托盘数量(单价 ¥${containerOrder?.tray_price ?? '0.00'})`}
|
||||
name="tray_num"
|
||||
rules={[{ required: true, message: '请输入周转托盘数量' }]}
|
||||
>
|
||||
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入周转托盘数量" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Space orientation="vertical" className="w-full rounded bg-gray-50 p-3">
|
||||
<div>
|
||||
<Text type="secondary">商品总金额:</Text>¥{containerOrder?.product_amount ?? '0.00'}
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">附加总金额:</Text>
|
||||
<Text strong>¥{previewAdded.toFixed(2)}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">订单总金额:</Text>
|
||||
<Text strong type="danger">¥{previewTotal.toFixed(2)}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
|
||||
import type {
|
||||
IPurchaseCell,
|
||||
IPurchaseCellItem,
|
||||
IPurchaseContainerStore,
|
||||
IPurchaseDetail,
|
||||
IPurchaseDetailRow,
|
||||
IPurchaseStoreItem,
|
||||
@@ -44,8 +45,9 @@ import {
|
||||
getPurchaseCell,
|
||||
getPurchaseDetail,
|
||||
getPurchaseStoreSummary,
|
||||
type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
||||
type PurchaseCellUpdateParams, type PurchaseContainerOrderParams, type PurchaseRowUpdateParams,
|
||||
updatePurchaseCellItem,
|
||||
updatePurchaseContainer,
|
||||
updatePurchaseRow,
|
||||
} from '@/api/purchase/order.ts';
|
||||
import { Update } from '@/api/common/table.ts';
|
||||
@@ -98,6 +100,26 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const [storeSummary, setStoreSummary] = useState<IPurchaseStoreSummary | null>(null);
|
||||
const [storeLoading, setStoreLoading] = useState(false);
|
||||
|
||||
// 周转框/托盘合并记录修改(按门店覆盖全部订单逐笔修改)
|
||||
const [containerOpen, setContainerOpen] = useState(false);
|
||||
const [containerStore, setContainerStore] = useState<IPurchaseContainerStore | null>(null);
|
||||
const [containerSaving, setContainerSaving] = useState(false);
|
||||
const [containerForm] = Form.useForm<{ orders: PurchaseContainerOrderParams[] }>();
|
||||
|
||||
// 弹窗内实时预览:合并合计 = Σ 各订单框/托盘数量,附加金额 = 合计 × 单价
|
||||
const watchContainerOrders = Form.useWatch('orders', containerForm) ?? [];
|
||||
const previewContainerBox = watchContainerOrders.reduce(
|
||||
(sum, order) => sum + Number(order?.box_num ?? 0),
|
||||
0,
|
||||
);
|
||||
const previewContainerTray = watchContainerOrders.reduce(
|
||||
(sum, order) => sum + Number(order?.tray_num ?? 0),
|
||||
0,
|
||||
);
|
||||
const previewContainerAdded =
|
||||
previewContainerBox * Number(containerStore?.box_price ?? 0) +
|
||||
previewContainerTray * Number(containerStore?.tray_price ?? 0);
|
||||
|
||||
useEffect(() => {
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
}, []);
|
||||
@@ -242,6 +264,37 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开周转框/托盘合并记录修改(初始化该门店全部订单的当前数量) */
|
||||
const openContainerEdit = (store: IPurchaseContainerStore) => {
|
||||
setContainerStore(store);
|
||||
containerForm.setFieldsValue({
|
||||
orders: store.orders.map((order) => ({
|
||||
order_id: order.order_id,
|
||||
box_num: order.box_num,
|
||||
tray_num: order.tray_num,
|
||||
})),
|
||||
});
|
||||
setContainerOpen(true);
|
||||
};
|
||||
|
||||
/** 提交合并记录修改:逐笔更新订单,自动重算附加金额与订单总金额 */
|
||||
const handleContainerSave = async (values: { orders: PurchaseContainerOrderParams[] }) => {
|
||||
if (!detail || !containerStore) {
|
||||
return;
|
||||
}
|
||||
setContainerSaving(true);
|
||||
try {
|
||||
await updatePurchaseContainer(detail.purchase.id!, containerStore.store_id, values.orders);
|
||||
message.success('周转框/托盘已更新,订单金额已重算');
|
||||
setContainerOpen(false);
|
||||
setContainerStore(null);
|
||||
await loadDetail(detail.purchase.id!);
|
||||
await tableRef.current?.reload();
|
||||
} finally {
|
||||
setContainerSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async (id: number) => {
|
||||
setCompleting(true);
|
||||
try {
|
||||
@@ -420,6 +473,61 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/** 周转框/托盘合并记录列:门店/订单数/框合计/托盘合计/附加金额(合计可点击下钻逐订单修改) */
|
||||
const containerColumns: TableProps<IPurchaseContainerStore>['columns'] = [
|
||||
{ title: '门店', dataIndex: 'store_name', width: 180, align: 'center' },
|
||||
{ title: '订单数', dataIndex: 'order_count', width: 90, align: 'center' },
|
||||
{
|
||||
title: '周转框合计',
|
||||
dataIndex: 'box_num',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (_, row) => (
|
||||
<Typography.Link onClick={() => openContainerEdit(row)}>{row.box_num}</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '周转托盘合计',
|
||||
dataIndex: 'tray_num',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
render: (_, row) => (
|
||||
<Typography.Link onClick={() => openContainerEdit(row)}>{row.tray_num}</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '附加金额',
|
||||
dataIndex: 'added_amount',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (_, row) => <Text strong type="danger">¥{row.added_amount}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
/** 周转框/托盘合并记录合计行 */
|
||||
const renderContainerSummary = () => {
|
||||
const containers = detail?.containers ?? [];
|
||||
const totalBox = containers.reduce((sum, row) => sum + row.box_num, 0);
|
||||
const totalTray = containers.reduce((sum, row) => sum + row.tray_num, 0);
|
||||
const totalAdded = containers.reduce((sum, row) => sum + Number(row.added_amount), 0);
|
||||
return (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0} colSpan={2} align="center">
|
||||
<Text strong>合计</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2} align="center">
|
||||
<Text strong>{totalBox}</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={3} align="center">
|
||||
<Text strong>{totalTray}</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={4} align="center">
|
||||
<Text strong type="danger">¥{totalAdded.toFixed(2)}</Text>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: XinTableColumn<IPurchaseOrder>[] = [
|
||||
{
|
||||
title: '采购单号',
|
||||
@@ -588,6 +696,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
items={[
|
||||
{ key: 'items', label: '商品明细' },
|
||||
{ key: 'stores', label: '门店购买详情' },
|
||||
{ key: 'containers', label: '周转框/托盘' },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -627,6 +736,24 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
)}
|
||||
</Spin>
|
||||
</>
|
||||
) : detailTab === 'containers' ? (
|
||||
detail.containers.length > 0 ? (
|
||||
<Table<IPurchaseContainerStore>
|
||||
rowKey="store_id"
|
||||
size="small"
|
||||
bordered
|
||||
columns={containerColumns}
|
||||
dataSource={detail.containers}
|
||||
pagination={false}
|
||||
summary={renderContainerSummary}
|
||||
/>
|
||||
) : (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="该采购单暂无门店订单"
|
||||
className="py-8!"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Space style={{ marginBottom: 20 }}>
|
||||
@@ -877,6 +1004,101 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 周转框/托盘合并记录下钻:按门店展示全部订单逐笔修改(采购单已完成则只读) */}
|
||||
<Modal
|
||||
title={
|
||||
containerStore
|
||||
? `${containerStore.store_name} · 周转框/托盘`
|
||||
: '周转框/托盘'
|
||||
}
|
||||
open={containerOpen}
|
||||
onCancel={() => {
|
||||
setContainerOpen(false);
|
||||
setContainerStore(null);
|
||||
}}
|
||||
onOk={() => containerForm.submit()}
|
||||
confirmLoading={containerSaving}
|
||||
okText="保存"
|
||||
okButtonProps={{ disabled: detail?.purchase.status !== 0 }}
|
||||
width={760}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
该门店在本采购单下的全部订单({containerStore?.order_count ?? 0} 笔):
|
||||
{detail?.purchase.status === 0
|
||||
? '逐笔修改后保存,系统将自动重算每笔订单的附加金额与订单总金额。'
|
||||
: '采购单已完成,仅可查看。'}
|
||||
</div>
|
||||
<Form form={containerForm} layout="vertical" onFinish={handleContainerSave}>
|
||||
<Form.List name="orders">
|
||||
{(fields) => (
|
||||
<div className="overflow-hidden rounded border border-gray-200">
|
||||
<div className="flex bg-gray-50 px-4 py-2 text-sm text-gray-500">
|
||||
<div className="flex-1">订单号</div>
|
||||
<div className="w-36 shrink-0 text-center">
|
||||
周转框(单价 ¥{containerStore?.box_price ?? '0.00'})
|
||||
</div>
|
||||
<div className="w-36 shrink-0 text-center">
|
||||
周转托盘(单价 ¥{containerStore?.tray_price ?? '0.00'})
|
||||
</div>
|
||||
</div>
|
||||
{fields.map((field) => {
|
||||
const order = containerStore?.orders[field.name];
|
||||
return (
|
||||
<div key={field.key} className="flex items-center border-t border-gray-100 px-4 py-2">
|
||||
<div className="min-w-0 flex-1 pr-2 text-sm">
|
||||
<Text copyable={{ text: order?.order_no ?? '' }}>{order?.order_no ?? '-'}</Text>
|
||||
</div>
|
||||
<Form.Item name={[field.name, 'order_id']} hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-36 shrink-0"
|
||||
name={[field.name, 'box_num']}
|
||||
rules={[{ required: true, message: '请输入周转框数量' }]}
|
||||
>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={0}
|
||||
precision={0}
|
||||
disabled={detail?.purchase.status !== 0}
|
||||
placeholder="周转框数量"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="m-0! w-36 shrink-0"
|
||||
name={[field.name, 'tray_num']}
|
||||
rules={[{ required: true, message: '请输入周转托盘数量' }]}
|
||||
>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={0}
|
||||
precision={0}
|
||||
disabled={detail?.purchase.status !== 0}
|
||||
placeholder="周转托盘数量"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
<Space orientation="vertical" className="mt-3! w-full rounded bg-gray-50 p-3">
|
||||
<div>
|
||||
<Text type="secondary">周转框合计:</Text>
|
||||
<Text strong>{previewContainerBox}</Text>
|
||||
<Text type="secondary" className="ml-6!">周转托盘合计:</Text>
|
||||
<Text strong>{previewContainerTray}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="secondary">附加金额合计:</Text>
|
||||
<Text strong type="danger">¥{previewContainerAdded.toFixed(2)}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user