Additional Changes
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled

This commit is contained in:
2026-06-02 22:04:03 +08:00
parent e6a9a80c32
commit 64d4b331a8
151 changed files with 15070 additions and 945 deletions

View File

@@ -15,37 +15,65 @@ use Modules\MaterialLogistics\Models\PurchaseOrder;
use Modules\MaterialLogistics\Models\Warehouse;
use Modules\MaterialLogistics\Services\DocumentNumberService;
use Modules\ProjectManagement\Models\Project;
use Modules\ProjectManagement\Services\ProjectWorkflowService;
class PurchaseOrderController extends Controller
{
public function index(Request $request)
{
$purchaseOrders = PurchaseOrder::with([
$query = PurchaseOrder::with([
'requester:id,ulid,name',
'approver:id,ulid,name',
'project:id,ulid,name,code',
'requisitions:id,ulid,document_number',
'items:id,purchase_order_id,quantity,unit_cost'
])
->latest()
->paginate(20);
]);
if ($request->filled('project_ulid')) {
$project = Project::where('ulid', $request->project_ulid)->first();
if ($project) {
$query->where('project_id', $project->id);
}
}
$purchaseOrders = $query->latest()->paginate(20);
return Inertia::render('MaterialLogistics::PurchaseOrders/Index', [
'purchaseOrders' => $purchaseOrders,
'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(),
'warehouses' => Warehouse::active()->select('id', 'ulid', 'name', 'code')->get(),
]);
}
public function create()
public function create(Request $request)
{
$selectedProject = null;
$budgetAnalysis = null;
if ($request->filled('project_ulid')) {
$selectedProject = Project::where('ulid', $request->project_ulid)->firstOrFail();
$workflowService = new ProjectWorkflowService();
if (!$workflowService->canCreatePurchaseOrder($selectedProject)) {
return redirect()->back()->with('error', 'Cannot create Purchase Order: Project does not have approved Material Requisitions.');
}
$budgetAnalysis = $workflowService->getBudgetAnalysis($selectedProject);
}
$materials = Material::select('id', 'ulid', 'name', 'unit', 'unit_cost')
->where('status', 'active')
->orderBy('name')
->get();
$approvedRequisitions = MaterialRequisition::with(['items.material:id,ulid,name,unit,sku', 'requester:id,name'])
// Filter approved requisitions by project if selected
$requisitionsQuery = MaterialRequisition::with(['items.material:id,ulid,name,unit,sku', 'requester:id,name'])
->where('status', 'approved')
->doesntHave('purchaseOrders')
->orderByDesc('created_at')
->get();
->doesntHave('purchaseOrders');
if ($selectedProject) {
$requisitionsQuery->where('project_id', $selectedProject->id);
}
$approvedRequisitions = $requisitionsQuery->orderByDesc('created_at')->get();
// Find personnel with approve_po permission across the whole system
$approverIds = \App\Models\User::permission('approve_po')
@@ -57,22 +85,33 @@ class PurchaseOrderController extends Controller
return back()->with('error', 'No approvers found in the system for Purchase Orders.');
}
$warehousesQuery = Warehouse::active()
->whereDoesntHave('project', function ($q) {
$q->whereIn('status', ['closed', 'completed']);
});
if ($selectedProject) {
$warehousesQuery->where('project_id', $selectedProject->id);
}
$warehouses = $warehousesQuery->with('project:id,ulid,name')
->select('id', 'ulid', 'name', 'code', 'project_id')
->get();
return Inertia::render('MaterialLogistics::PurchaseOrders/Form', [
'materials' => $materials,
'approvedRequisitions' => $approvedRequisitions,
'warehouses' => Inertia::lazy(fn () => Warehouse::active()
->whereDoesntHave('project', function ($q) {
$q->whereIn('status', ['closed', 'completed']);
})
->with('project:id,ulid,name')
->select('id', 'ulid', 'name', 'code', 'project_id')
->get()),
'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(),
'selectedProject' => $selectedProject,
'budgetAnalysis' => $budgetAnalysis,
'warehouses' => $warehouses,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'project_ulid' => 'required|string|exists:projects,ulid',
'target_warehouse_ulid' => 'required|string',
'supplier' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:1000',
@@ -85,11 +124,23 @@ class PurchaseOrderController extends Controller
'items.*.requisition_item_id' => 'nullable|integer',
]);
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
$targetWarehouse = Warehouse::where('ulid', $validated['target_warehouse_ulid'])->firstOrFail();
if ($targetWarehouse->project_id !== $project->id) {
return redirect()->back()->with('error', 'Target warehouse does not belong to the selected project.');
}
$workflowService = new ProjectWorkflowService();
if (!$workflowService->canCreatePurchaseOrder($project)) {
return redirect()->back()->with('error', 'Cannot create Purchase Order: Project workflow state does not permit PO creation.');
}
$docService = new DocumentNumberService();
$po = DB::transaction(function () use ($validated, $targetWarehouse, $docService) {
$po = DB::transaction(function () use ($validated, $project, $targetWarehouse, $docService) {
$po = PurchaseOrder::create([
'project_id' => $project->id,
'target_warehouse_id' => $targetWarehouse->id,
'document_number' => $docService->nextPoNumber(),
'supplier' => $validated['supplier'] ?? null,
@@ -127,7 +178,7 @@ class PurchaseOrderController extends Controller
return redirect()->back()->with('error', 'Only draft POs can be edited.');
}
$purchaseOrder->load('items.material:id,ulid,name,unit,unit_cost', 'requisitions:id,ulid,document_number', 'targetWarehouse:id,ulid,name,code');
$purchaseOrder->load('items.material:id,ulid,name,unit,unit_cost', 'requisitions:id,ulid,document_number', 'targetWarehouse:id,ulid,name,code', 'project:id,ulid,name,code');
$materials = Material::select('id', 'ulid', 'name', 'unit', 'unit_cost')
->where('status', 'active')
@@ -144,17 +195,20 @@ class PurchaseOrderController extends Controller
->orderByDesc('created_at')
->get();
$budgetAnalysis = null;
if ($purchaseOrder->project) {
$workflowService = new ProjectWorkflowService();
$budgetAnalysis = $workflowService->getBudgetAnalysis($purchaseOrder->project);
}
return Inertia::render('MaterialLogistics::PurchaseOrders/Form', [
'purchaseOrder' => $purchaseOrder,
'materials' => $materials,
'approvedRequisitions' => $approvedRequisitions,
'warehouses' => Inertia::lazy(fn () => Warehouse::active()
->whereDoesntHave('project', function ($q) {
$q->whereIn('status', ['closed', 'completed']);
})
->with('project:id,ulid,name')
->select('id', 'ulid', 'name', 'code', 'project_id')
->get()),
'projects' => Project::active()->select('id', 'ulid', 'name', 'code')->get(),
'selectedProject' => $purchaseOrder->project,
'budgetAnalysis' => $budgetAnalysis,
'warehouses' => Warehouse::active()->with('project:id,ulid,name')->select('id', 'ulid', 'name', 'code', 'project_id')->get(),
]);
}
@@ -165,6 +219,7 @@ class PurchaseOrderController extends Controller
}
$validated = $request->validate([
'project_ulid' => 'required|string|exists:projects,ulid',
'target_warehouse_ulid' => 'required|string',
'supplier' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:1000',
@@ -177,10 +232,21 @@ class PurchaseOrderController extends Controller
'items.*.requisition_item_id' => 'nullable|integer',
]);
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
$targetWarehouse = Warehouse::where('ulid', $validated['target_warehouse_ulid'])->firstOrFail();
DB::transaction(function () use ($validated, $targetWarehouse, $purchaseOrder) {
if ($targetWarehouse->project_id !== $project->id) {
return redirect()->back()->with('error', 'Target warehouse does not belong to the selected project.');
}
$workflowService = new ProjectWorkflowService();
if (!$workflowService->canCreatePurchaseOrder($project)) {
return redirect()->back()->with('error', 'Cannot update Purchase Order: Project workflow state does not permit PO updates.');
}
DB::transaction(function () use ($validated, $project, $targetWarehouse, $purchaseOrder) {
$purchaseOrder->update([
'project_id' => $project->id,
'target_warehouse_id' => $targetWarehouse->id,
'supplier' => $validated['supplier'] ?? null,
'notes' => $validated['notes'] ?? null,
@@ -215,6 +281,7 @@ class PurchaseOrderController extends Controller
'targetWarehouse:id,ulid,name',
'requester:id,ulid,name',
'approver:id,ulid,name',
'project:id,ulid,name,code',
'items.material' => function ($query) {
$query->select('id', 'ulid', 'name', 'unit', 'unit_cost', 'type')
->with('components.component:id,name,unit');
@@ -225,9 +292,16 @@ class PurchaseOrderController extends Controller
$warehouses = Warehouse::active()->select('id', 'ulid', 'name', 'code')->get();
$budgetAnalysis = null;
if ($purchaseOrder->project) {
$workflowService = new ProjectWorkflowService();
$budgetAnalysis = $workflowService->getBudgetAnalysis($purchaseOrder->project);
}
return Inertia::render('MaterialLogistics::PurchaseOrders/Show', [
'purchaseOrder' => $purchaseOrder,
'warehouses' => $warehouses,
'budgetAnalysis' => $budgetAnalysis,
]);
}
@@ -307,15 +381,31 @@ class PurchaseOrderController extends Controller
{
$request->validate([
'q' => 'nullable|string|max:100',
'warehouse_ulid' => 'nullable|string',
'project_ulid' => 'nullable|string',
]);
$query = PurchaseOrder::where('status', 'approved')
$query = PurchaseOrder::whereIn('status', ['approved', 'delivered'])
->with('items.material:id,ulid,name,unit,unit_cost');
if ($request->filled('q')) {
$query->where('document_number', 'like', "%{$request->q}%");
}
if ($request->filled('warehouse_ulid')) {
$warehouse = Warehouse::where('ulid', $request->warehouse_ulid)->first();
if ($warehouse) {
$query->where('target_warehouse_id', $warehouse->id);
}
}
if ($request->filled('project_ulid')) {
$project = Project::where('ulid', $request->project_ulid)->first();
if ($project) {
$query->where('project_id', $project->id);
}
}
$results = $query->select('id', 'ulid', 'document_number', 'supplier')
->latest()
->limit(20)