Files
xin-procurement/app/Http/Controllers/Purchase/PurchaseOrderController.php
T
2026-08-12 17:10:05 +08:00

275 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Purchase;
use App\Exceptions\RepositoryException;
use App\Http\Requests\Purchase\PurchaseCellUpdateRequest;
use App\Http\Requests\Purchase\PurchaseRowUpdateRequest;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Services\ExportService;
use App\Services\PurchaseEditService;
use App\Services\PurchaseGenerateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use Modules\Common\Http\Controllers\BaseController;
use Symfony\Component\HttpFoundation\Response;
/**
* 采购单管理(C1 汇总生成 / C2-C3 导出 / C4 修改)
* 采购单无独立明细表,明细直接溯源门店订货明细(store_order_item.purchase_id
*/
#[RequestAttribute('/purchase/order', 'purchase.order')]
class PurchaseOrderController extends BaseController
{
protected array $searchField = [
'status' => '=',
'purchase_no' => 'like',
'purchase_date' => 'betweenDate',
];
/** 采购单列表 */
#[GetRoute(authorize: 'query')]
public function query(Request $request): JsonResponse
{
$params = $request->all();
$pageSize = $params['pageSize'] ?? 10;
$data = $this->buildSearch($params, PurchaseOrderModel::query()->with('operator:id,nickname'))
->orderBy('purchase_date', 'desc')
->orderBy('id', 'desc')
->paginate($pageSize)
->toArray();
return $this->success($data);
}
/**
* 采购单详情:头 + 明细矩阵(商品行 × 门店列)
* 行 = 商品(按分类 sort → 商品 sort 排序),列 = 商品信息 + 每个门店一格(数量/金额)
*/
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
{
$purchase = PurchaseOrderModel::with('operator:id,nickname')->find($id);
if (empty($purchase)) {
return $this->error('采购单不存在');
}
// 订货明细
$orderItems = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $purchase->id)
->whereNull('store_order.deleted_at')
->with('supplier:id,name')
->select('store_order_item.*')
->get()
->makeVisible('cost_price');
// 门店列(含软删除门店,保证历史单据可见)
$stores = StoreModel::withTrashed()
->whereIn('id', $orderItems->pluck('store_id')->unique())
->get(['id', 'name'])
->map(static fn (StoreModel $store) => ['id' => $store->id, 'name' => $store->name])
->values();
// 排序键:分类 sort → 商品 sort
$products = ProductModel::withTrashed()
->with('category:id,sort')
->whereIn('id', $orderItems->pluck('product_id')->unique())
->get()
->keyBy('id');
$rows = [];
foreach ($orderItems->groupBy('product_id') as $productId => $group) {
$first = $group->first();
// 商品
$product = $products->get((int) $productId);
// 采购总数
$quantity = 0;
// 采购总重量
$weight = '0';
// 门店明细
$cellsMap = [];
foreach ($group as $item) {
$storeId = $item->store_id;
// 累加总量
$quantity += (int) $item->quantity;
$weight = bcadd($weight, (string) $item->weight, 3);
// 按门店合并
if (!isset($cellsMap[$storeId])) {
$cellsMap[$storeId] = 0;
}
$cellsMap[$storeId] += (int) $item->quantity;
}
// 供应商
$supplier = $first->supplier ? ['id' => $first->supplier->id, 'name' => $first->supplier->name] : null;
$rows[] = [
'product_id' => (int) $productId,
'product_name' => $first->product_name, // 品名
'product_spec' => $first->product_spec, // 包规
'unit' => $first->unit, // 单位
'supplier_id' => (int) $first->supplier_id, // 供应商id
'supplier' => $supplier, // 供应商
'cost_price' => $first->cost_price, // 成本价
'quantity' => $quantity,
'weight' => (float) $weight,
'cells' => $cellsMap,
'category_sort' => (int) ($product->category->sort ?? 9999),
'product_sort' => (int) ($product->sort ?? 9999),
];
}
usort($rows, static fn (array $a, array $b): int =>
[$a['category_sort'], $a['product_sort'], $a['product_id']]
<=> [$b['category_sort'], $b['product_sort'], $b['product_id']]);
return $this->success([
'purchase' => $purchase->toArray(),
'stores' => $stores->toArray(),
'items' => array_map(static function (array $row): array {
unset($row['category_sort'], $row['product_sort']);
return $row;
}, $rows),
]);
}
/** C4 修改采购单头信息(采购日期、状态、备注) */
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function update(int $id, Request $request): JsonResponse
{
$data = $request->validate([
'purchase_date' => 'nullable|date_format:Y-m-d',
'status' => 'nullable|integer|in:' . PurchaseOrderModel::STATUS_PENDING . ',' . PurchaseOrderModel::STATUS_COMPLETED,
'remark' => 'nullable|string|max:255',
], [
'purchase_date.date_format' => '采购日期格式为 Y-m-d',
'status.in' => '采购单状态不正确',
]);
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$purchase->update(array_filter($data, static fn ($v) => $v !== null));
return $this->success();
}
/**
* 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(
['id' => $purchase->id, 'purchase_no' => $purchase->purchase_no],
'采购单已生成'
);
}
/** C2/C3 导出采购单:?type=all|category & format=xlsx|pdf */
#[GetRoute(route: '/{id}/export', authorize: 'export', where: ['id' => '[0-9]+'])]
public function export(int $id, Request $request): Response
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$type = (string) $request->query('type', 'all');
if (! in_array($type, ['all', 'category'], true)) {
throw new RepositoryException('导出类型参数不正确(all 全品类 / category 蔬果分类)');
}
return app(ExportService::class)->download(
'purchase',
$purchase,
(string) $request->query('format', ExportService::FORMAT_XLSX),
type: $type,
);
}
/**
* C4 门店单元格修改:数量/称重回写订货明细,金额重算并同步订单/采购单汇总
*/
#[PutRoute(route: '/cell/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
public function updateCell(int $id, PurchaseCellUpdateRequest $request): JsonResponse
{
$item = StoreOrderItemModel::find($id);
if (empty($item)) {
throw new RepositoryException('订货明细不存在');
}
$validated = $request->validated();
$item = app(PurchaseEditService::class)->updateCell(
$item,
(int) $validated['quantity'],
isset($validated['weight']) ? (float) $validated['weight'] : null,
);
return $this->success([
'quantity' => (int) $item->quantity,
'weight' => (float) $item->weight,
'amount' => (float) $item->amount,
]);
}
/**
* C4 商品行修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细;
* 实际称重按数量比例分摊(尾差修正守恒)
*/
#[PutRoute(route: '/{id}/row/{productId}', authorize: 'update', where: ['id' => '[0-9]+', 'productId' => '[0-9]+'])]
public function updateRow(int $id, int $productId, PurchaseRowUpdateRequest $request): JsonResponse
{
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$validated = $request->validated();
$attrs = [];
foreach (['product_name', 'supplier_id', 'product_spec', 'unit', 'cost_price'] as $field) {
if (isset($validated[$field])) {
$attrs[$field] = $validated[$field];
}
}
$weight = isset($validated['weight']) ? (float) $validated['weight'] : null;
if ($attrs === [] && $weight === null) {
throw new RepositoryException('品名/供应商/包规/单位/成本/实际称重至少填写一项');
}
$count = app(PurchaseEditService::class)->updateRow(
$purchase,
$productId,
$attrs,
$weight,
(bool) ($validated['sync_product'] ?? false),
);
return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细');
}
}