2026-02-13 10:39:10 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Modules\Inventory\Services;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Modules\Inventory\Models\StoreRequisition;
|
|
|
|
|
|
use App\Modules\Inventory\Models\StoreRequisitionItem;
|
|
|
|
|
|
use App\Modules\Inventory\Models\InventoryTransferOrder;
|
|
|
|
|
|
use App\Modules\Inventory\Models\InventoryTransferItem;
|
|
|
|
|
|
use App\Modules\Inventory\Notifications\StoreRequisitionNotification;
|
|
|
|
|
|
use App\Modules\Core\Models\User;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
|
|
|
|
|
|
|
|
class StoreRequisitionService
|
|
|
|
|
|
{
|
|
|
|
|
|
protected TransferService $transferService;
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct(TransferService $transferService)
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->transferService = $transferService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 建立叫貨單(含明細)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function create(array $data, array $items, int $userId): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
return DB::transaction(function () use ($data, $items, $userId) {
|
|
|
|
|
|
$requisition = StoreRequisition::create([
|
|
|
|
|
|
'store_warehouse_id' => $data['store_warehouse_id'],
|
|
|
|
|
|
'status' => 'draft',
|
|
|
|
|
|
'remark' => $data['remark'] ?? null,
|
|
|
|
|
|
'created_by' => $userId,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$requisition->items()->create([
|
|
|
|
|
|
'product_id' => $item['product_id'],
|
|
|
|
|
|
'requested_qty' => $item['requested_qty'],
|
|
|
|
|
|
'remark' => $item['remark'] ?? null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition->load('items');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新叫貨單(僅限 draft / rejected 狀態)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function update(StoreRequisition $requisition, array $data, array $items): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!in_array($requisition->status, ['draft', 'rejected'])) {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'status' => '僅能編輯草稿或被駁回的叫貨單',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($requisition, $data, $items) {
|
|
|
|
|
|
$requisition->update([
|
|
|
|
|
|
'store_warehouse_id' => $data['store_warehouse_id'],
|
|
|
|
|
|
'remark' => $data['remark'] ?? null,
|
|
|
|
|
|
'reject_reason' => null, // 清除駁回原因
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 重建明細
|
|
|
|
|
|
$requisition->items()->delete();
|
|
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$requisition->items()->create([
|
|
|
|
|
|
'product_id' => $item['product_id'],
|
|
|
|
|
|
'requested_qty' => $item['requested_qty'],
|
|
|
|
|
|
'remark' => $item['remark'] ?? null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition->load('items');
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 提交審核(draft → pending)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function submit(StoreRequisition $requisition, int $userId): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($requisition->status !== 'draft' && $requisition->status !== 'rejected') {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'status' => '僅能提交草稿或被駁回的叫貨單',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($requisition->items()->count() === 0) {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'items' => '叫貨單必須至少有一項商品',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$requisition->update([
|
|
|
|
|
|
'status' => 'pending',
|
|
|
|
|
|
'submitted_at' => now(),
|
|
|
|
|
|
'reject_reason' => null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 通知有審核權限的使用者
|
|
|
|
|
|
$this->notifyApprovers($requisition, 'submitted', $userId);
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 核准叫貨單(pending → approved),選擇供貨倉庫並自動產生調撥單
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function approve(StoreRequisition $requisition, array $data, int $userId): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($requisition->status !== 'pending') {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'status' => '僅能核准待審核的叫貨單',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return DB::transaction(function () use ($requisition, $data, $userId) {
|
|
|
|
|
|
// 更新核准數量
|
|
|
|
|
|
if (isset($data['items'])) {
|
|
|
|
|
|
foreach ($data['items'] as $itemData) {
|
|
|
|
|
|
StoreRequisitionItem::where('id', $itemData['id'])
|
|
|
|
|
|
->where('store_requisition_id', $requisition->id)
|
|
|
|
|
|
->update(['approved_qty' => $itemData['approved_qty']]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-13 13:16:05 +08:00
|
|
|
|
// 查詢供貨倉庫是否有預設在途倉
|
|
|
|
|
|
$supplyWarehouse = \App\Modules\Inventory\Models\Warehouse::find($data['supply_warehouse_id']);
|
|
|
|
|
|
$defaultTransitId = $supplyWarehouse?->default_transit_warehouse_id;
|
|
|
|
|
|
|
2026-02-13 10:39:10 +08:00
|
|
|
|
// 產生調撥單(供貨倉庫 → 門市倉庫)
|
|
|
|
|
|
$transferOrder = $this->transferService->createOrder(
|
|
|
|
|
|
fromWarehouseId: $data['supply_warehouse_id'],
|
|
|
|
|
|
toWarehouseId: $requisition->store_warehouse_id,
|
|
|
|
|
|
remarks: "由叫貨單 {$requisition->doc_no} 自動產生",
|
|
|
|
|
|
userId: $userId,
|
2026-02-13 13:16:05 +08:00
|
|
|
|
transitWarehouseId: $defaultTransitId,
|
2026-02-13 10:39:10 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 將核准的明細寫入調撥單
|
|
|
|
|
|
$requisition->load('items');
|
|
|
|
|
|
$transferItems = [];
|
|
|
|
|
|
foreach ($requisition->items as $item) {
|
|
|
|
|
|
$qty = $item->approved_qty ?? $item->requested_qty;
|
|
|
|
|
|
if ($qty > 0) {
|
|
|
|
|
|
$transferItems[] = [
|
|
|
|
|
|
'product_id' => $item->product_id,
|
|
|
|
|
|
'quantity' => $qty,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!empty($transferItems)) {
|
|
|
|
|
|
$this->transferService->updateItems($transferOrder, $transferItems);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新叫貨單狀態
|
|
|
|
|
|
$requisition->update([
|
|
|
|
|
|
'status' => 'approved',
|
|
|
|
|
|
'supply_warehouse_id' => $data['supply_warehouse_id'],
|
|
|
|
|
|
'approved_by' => $userId,
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
'transfer_order_id' => $transferOrder->id,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 通知申請人
|
|
|
|
|
|
$this->notifyCreator($requisition, 'approved', $userId);
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition->load(['items', 'transferOrder']);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 駁回叫貨單(pending → rejected)
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function reject(StoreRequisition $requisition, string $reason, int $userId): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
if ($requisition->status !== 'pending') {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'status' => '僅能駁回待審核的叫貨單',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$requisition->update([
|
|
|
|
|
|
'status' => 'rejected',
|
|
|
|
|
|
'reject_reason' => $reason,
|
|
|
|
|
|
'approved_by' => $userId,
|
|
|
|
|
|
'approved_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 通知申請人
|
|
|
|
|
|
$this->notifyCreator($requisition, 'rejected', $userId);
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 取消叫貨單
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function cancel(StoreRequisition $requisition): StoreRequisition
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!in_array($requisition->status, ['draft', 'pending'])) {
|
|
|
|
|
|
throw ValidationException::withMessages([
|
|
|
|
|
|
'status' => '僅能取消草稿或待審核的叫貨單',
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$requisition->update(['status' => 'cancelled']);
|
|
|
|
|
|
|
|
|
|
|
|
return $requisition;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通知有審核權限的使用者
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected function notifyApprovers(StoreRequisition $requisition, string $action, int $actorId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$actor = User::find($actorId);
|
|
|
|
|
|
$actorName = $actor?->name ?? 'System';
|
|
|
|
|
|
|
|
|
|
|
|
// 找出有 store_requisitions.approve 權限的使用者
|
|
|
|
|
|
$approvers = User::permission('store_requisitions.approve')->get();
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($approvers as $approver) {
|
|
|
|
|
|
if ($approver->id !== $actorId) {
|
|
|
|
|
|
$approver->notify(new StoreRequisitionNotification($requisition, $action, $actorName));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 通知叫貨單申請人
|
|
|
|
|
|
*/
|
|
|
|
|
|
protected function notifyCreator(StoreRequisition $requisition, string $action, int $actorId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$actor = User::find($actorId);
|
|
|
|
|
|
$actorName = $actor?->name ?? 'System';
|
|
|
|
|
|
|
|
|
|
|
|
$creator = User::find($requisition->created_by);
|
|
|
|
|
|
if ($creator && $creator->id !== $actorId) {
|
|
|
|
|
|
$creator->notify(new StoreRequisitionNotification($requisition, $action, $actorName));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|