采购单优化

This commit is contained in:
liu
2026-08-12 23:20:05 +08:00
parent c2c56408a0
commit 0211e5e98d
22 changed files with 768 additions and 1425 deletions
@@ -9,17 +9,17 @@ 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\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 Symfony\Component\HttpFoundation\Response;
use Throwable;
/**
@@ -50,8 +50,7 @@ class PurchaseOrderController extends BaseController
}
/**
* 采购单详情:头 + 明细矩阵(商品行 × 门店列)
* 行 = 商品(按分类 sort → 商品 sort 排序),列 = 商品信息 + 每个门店一格(数量/金额)
* 采购单详情
*/
#[GetRoute(route: '/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
public function detail(int $id): JsonResponse
@@ -163,8 +162,7 @@ class PurchaseOrderController extends BaseController
}
/**
* C1 生成采购单:合并全部「已接单」门店订单(或指定的 order_ids),
* 生成后源订单转为「采购中」并回写 purchase_id(订单与订货明细同步归集)
* 生成采购单
*/
#[PostRoute('/generate', 'generate')]
public function generate(Request $request): JsonResponse
@@ -192,29 +190,77 @@ class PurchaseOrderController extends BaseController
);
}
/** 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
/**
* 单元格下钻:采购单中某门店某商品的全部订货明细
*/
#[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('采购单不存在');
}
$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,
);
$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]+'])]
@@ -224,25 +270,77 @@ class PurchaseOrderController extends BaseController
if (empty($purchase)) {
throw new RepositoryException('采购单不存在');
}
if ($purchase->status !== PurchaseOrderModel::STATUS_PENDING) {
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];
$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('采购单已完成,不允许修改明细');
}
}
if ($attrs === []) {
throw new RepositoryException('品名/供应商/包规/单位/成本至少填写一项');
}
$count = app(PurchaseEditService::class)->updateRow(
$purchase,
$productId,
$attrs,
(bool) ($validated['sync_product'] ?? false),
);
$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();
return $this->success(['count' => $count], '已同步 ' . $count . ' 条订货明细');
// 重算订单金额
$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();
});
}
}