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

347 lines
14 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\Models\StoreOrderModel;
use App\Services\ItemImageResolver;
use App\Services\PurchaseGenerateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
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 Throwable;
/**
* 采购单管理(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);
}
/**
* 采购单详情
*/
#[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();
}
/**
* 生成采购单
*/
#[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],
'采购单已生成'
);
}
/**
* 单元格下钻:采购单中某门店某商品的全部订货明细
*/
#[GetRoute(route: '/{id}/cell', authorize: 'query', where: ['id' => '[0-9]+'])]
public function cell(int $id, Request $request): JsonResponse
{
$productId = (int) $request->query('product_id', 0);
$storeId = (int) $request->query('store_id', 0);
if ($productId <= 0 || $storeId <= 0) {
throw new RepositoryException('缺少商品或门店参数');
}
$purchase = PurchaseOrderModel::find($id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
$items = StoreOrderItemModel::query()
->join('store_order', 'store_order.id', '=', 'store_order_item.order_id')
->where('store_order_item.purchase_id', $purchase->id)
->where('store_order_item.product_id', $productId)
->where('store_order_item.store_id', $storeId)
->whereNull('store_order.deleted_at')
->with('supplier:id,name')
->select(
'store_order_item.*',
'store_order.order_no',
'store_order.order_date',
'store_order.status as order_status',
)
->orderBy('store_order.id')
->get()
->makeVisible('cost_price');
$rows = [];
foreach ($items as $item) {
$rows[] = [
'id' => $item->id,
'order_id' => $item->order_id,
'order_no' => $item->order_no,
'order_date' => (string) $item->order_date,
'order_status' => (int) $item->order_status,
'product_name' => $item->product_name,
'product_spec' => $item->product_spec,
'unit' => $item->unit,
'supplier_id' => (int) $item->supplier_id,
'supplier' => $item->supplier ? ['id' => $item->supplier->id, 'name' => $item->supplier->name] : null,
'price' => (string) $item->price,
'cost_price' => (string) $item->cost_price,
'quantity' => (int) $item->quantity,
'weight' => (string) $item->weight,
'amount' => (string) $item->amount,
'remark' => (string) $item->remark,
'image_ids' => $item->image_ids,
'editable' => $purchase->status === PurchaseOrderModel::STATUS_PENDING
&& in_array((int) $item->order_status, StoreOrderItemModel::ITEM_EDITABLE_STATUS, true),
];
}
app(ItemImageResolver::class)->resolve($rows);
$store = StoreModel::withTrashed()->find($storeId);
$product = ProductModel::withTrashed()->find($productId);
return $this->success([
'store' => $store ? ['id' => $store->id, 'name' => $store->name] : null,
'product' => $product ? ['id' => $product->id, 'name' => $product->name] : null,
'items' => $rows,
]);
}
/**
* 商品行修改:品名/供应商/包规/单位/成本
* @throws Throwable
*/
#[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('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改明细');
}
$validated = $request->validated();
$items = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->where('product_id', $productId)
->lockForUpdate()
->get();
if ($items->isEmpty()) {
throw new RepositoryException('该采购单下无此商品的订货明细');
}
foreach ($items as $item) {
$item->fill($validated)->save();
}
// 重算采购单成本
$query = StoreOrderItemModel::query()->where('purchase_id', $purchase->id);
$purchase->total_quantity = $query->sum('quantity');
$purchase->estimate_amount = $query->sum(DB::raw('quantity * cost_price'));
$purchase->save();
return $this->success(['count' => $items->count()], '已同步 ' . $items->count() . ' 条订货明细');
}
/**
* 门店订单明细修改(订货量、重量、单价),自动重算价格
* @throws Throwable
*/
#[PutRoute(route: '/cell/{itemId}', authorize: 'update', where: ['itemId' => '[0-9]+'])]
public function cellUpdate(int $itemId, PurchaseCellUpdateRequest $request): JsonResponse
{
$validated = $request->validated();
return DB::transaction(function () use ($itemId, $validated) {
$item = StoreOrderItemModel::query()->lockForUpdate()->find($itemId);
if (empty($item) || (int) $item->purchase_id === 0) {
throw new RepositoryException('采购单明细不存在');
}
$purchase = PurchaseOrderModel::find((int) $item->purchase_id);
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
throw new RepositoryException('采购单已完成,不允许修改明细');
}
$item->quantity = $validated['quantity'];
$item->price = $validated['price'];
if (array_key_exists('weight', $validated)) {
$item->weight = bcadd((string) $validated['weight'], '0', 3);
}
$item->amount = bcmul($validated['quantity'], $validated['price'], 3);
$item->save();
// 重算订单金额
$order = StoreOrderModel::query()->lockForUpdate()->find($item->order_id);
$order->product_amount = $order->items()->sum('amount');
$order->total_weight = $order->items()->sum('weight');
$order->total_amount = bcadd($order->product_amount, $order->added_amount, 2);
$order->save();
// 重算采购单重量
$purchase->total_weight = StoreOrderItemModel::query()
->where('purchase_id', $purchase->id)
->sum('weight');
$purchase->save();
return $this->success();
});
}
}