-
Notifications
You must be signed in to change notification settings - Fork 36
feat(materials): partial consumption, returns to stock & reclassification (#99) #238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Controllers\Api\V1; | ||
|
|
||
| use App\Http\Controllers\Controller; | ||
| use App\Http\Requests\Api\V1\RecordConsumptionRequest; | ||
| use App\Http\Requests\Api\V1\ReturnAllocationRequest; | ||
| use App\Models\MaterialAllocation; | ||
| use App\Services\Material\MaterialAllocationService; | ||
| use Illuminate\Http\JsonResponse; | ||
|
|
||
| /** | ||
| * Work-order material reconciliation (#99): declare actual (partial) consumption | ||
| * and return unused quantity to stock. Both funnel through | ||
| * MaterialAllocationService so stock, reservation and lot accounting stay in sync. | ||
| */ | ||
| class MaterialAllocationController extends Controller | ||
| { | ||
| public function __construct( | ||
| protected MaterialAllocationService $allocations, | ||
| ) {} | ||
|
|
||
| public function consume(RecordConsumptionRequest $request, MaterialAllocation $allocation): JsonResponse | ||
| { | ||
| try { | ||
| $updated = $this->allocations->recordConsumption( | ||
| $allocation, | ||
| (float) $request->validated('consumed_qty'), | ||
| (float) ($request->validated('scrap_qty') ?? 0), | ||
| $request->validated('notes'), | ||
| ); | ||
|
|
||
| return response()->json([ | ||
| 'message' => 'Consumption recorded', | ||
| 'data' => $updated, | ||
| ]); | ||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||
| return response()->json([ | ||
| 'message' => $e->getMessage(), | ||
| 'errors' => ['consumed_qty' => [$e->getMessage()]], | ||
| ], 422); | ||
| } | ||
| } | ||
|
|
||
| public function return(ReturnAllocationRequest $request, MaterialAllocation $allocation): JsonResponse | ||
| { | ||
| try { | ||
| $updated = $this->allocations->returnQuantity( | ||
| $allocation, | ||
| (float) $request->validated('qty'), | ||
| $request->user(), | ||
| $request->validated('reason'), | ||
| ); | ||
|
|
||
| return response()->json([ | ||
| 'message' => 'Material returned to stock', | ||
| 'data' => $updated, | ||
| ]); | ||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||
| return response()->json([ | ||
| 'message' => $e->getMessage(), | ||
| 'errors' => ['qty' => [$e->getMessage()]], | ||
| ], 422); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Controllers\Api\V1; | ||
|
|
||
| use App\Http\Controllers\Controller; | ||
| use App\Http\Requests\Api\V1\ReclassifyClassRequest; | ||
| use App\Http\Requests\Api\V1\ReclassifyLotStatusRequest; | ||
| use App\Models\Material; | ||
| use App\Models\MaterialLot; | ||
| use App\Services\Material\MaterialReclassificationService; | ||
| use Illuminate\Http\JsonResponse; | ||
|
|
||
| /** | ||
| * Material reclassification (#99): regrade a quantity between material classes, | ||
| * or change a lot's status. Route-gated to Supervisor|Admin. | ||
| */ | ||
| class MaterialReclassificationController extends Controller | ||
| { | ||
| public function __construct( | ||
| protected MaterialReclassificationService $reclassifications, | ||
| ) {} | ||
|
|
||
| public function class(ReclassifyClassRequest $request): JsonResponse | ||
| { | ||
| try { | ||
| $source = Material::findOrFail($request->validated('source_material_id')); | ||
| $target = Material::findOrFail($request->validated('target_material_id')); | ||
| $lot = $request->validated('source_lot_id') | ||
| ? MaterialLot::findOrFail($request->validated('source_lot_id')) | ||
| : null; | ||
|
|
||
| $record = $this->reclassifications->reclassifyClass( | ||
| $source, | ||
| $target, | ||
| (float) $request->validated('qty'), | ||
| $request->user(), | ||
| $lot, | ||
| $request->validated('reason'), | ||
| ); | ||
|
|
||
| return response()->json([ | ||
| 'message' => 'Material reclassified', | ||
| 'data' => $record, | ||
| ]); | ||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||
| return response()->json([ | ||
| 'message' => $e->getMessage(), | ||
| 'errors' => ['qty' => [$e->getMessage()]], | ||
| ], 422); | ||
| } | ||
| } | ||
|
|
||
| public function status(ReclassifyLotStatusRequest $request, MaterialLot $materialLot): JsonResponse | ||
| { | ||
| try { | ||
| $record = $this->reclassifications->reclassifyStatus( | ||
| $materialLot, | ||
| $request->validated('to_status'), | ||
| $request->user(), | ||
| $request->validated('reason'), | ||
| ); | ||
|
|
||
| return response()->json([ | ||
| 'message' => 'Lot status changed', | ||
| 'data' => $record, | ||
| ]); | ||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||
| return response()->json([ | ||
| 'message' => $e->getMessage(), | ||
| 'errors' => ['to_status' => [$e->getMessage()]], | ||
| ], 422); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,15 +3,23 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace App\Http\Controllers\Web\Admin; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Controllers\Controller; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\Api\V1\ReclassifyClassRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\Api\V1\RecordConsumptionRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\Api\V1\ReturnAllocationRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\Web\Admin\StoreWorkOrderRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\Web\Admin\UpdateWorkOrderRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Http\Requests\WorkOrder\ResumeWorkOrderRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\Customer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\Line; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\Material; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\MaterialAllocation; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\MaterialLot; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\ProcessTemplate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\ProductType; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Models\WorkOrder; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Services\CustomFieldService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Services\Material\MaterialAllocationService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Services\Material\MaterialReclassificationService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Services\WorkOrder\WorkOrderService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use App\Services\WorkOrder\WorkOrderStopService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| use Illuminate\Http\Request; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,6 +163,27 @@ public function show(WorkOrder $workOrder, CustomFieldService $customFields) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'is_blocking' => (bool) ($i->issueType?->is_blocking ?? false), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ])->values(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Materials reconciliation (#99): the allocations pulled for this order, so | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // the page can offer record-consumption / return / reclassify per material. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $workOrder->load(['allocations.material', 'allocations.lotPicks.lot']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $allocations = $workOrder->allocations->map(fn ($a) => [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'id' => $a->id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'material_id' => $a->material_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'material_code' => $a->material?->code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'material_name' => $a->material?->name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'unit_of_measure' => $a->material?->unit_of_measure, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'status' => $a->status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'allocated_qty' => (float) $a->allocated_qty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'consumed_qty' => (float) $a->consumed_qty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'scrap_qty' => (float) $a->scrap_qty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'returned_qty' => (float) $a->returned_qty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'lots' => $a->lotPicks->map(fn ($p) => [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'lot_id' => $p->material_lot_id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'lot_number' => $p->lot?->lot_number, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'picked_qty' => (float) $p->picked_qty, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ])->values(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ])->values(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Change control (#182): the stop history with durations, and every change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // request raised against this order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $stops = $workOrder->stops()->with(['stoppedBy:id,name', 'resumedBy:id,name'])->get() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -188,6 +217,8 @@ public function show(WorkOrder $workOrder, CustomFieldService $customFields) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'created_at' => $cr->created_at?->toISOString(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ])->values(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $canReclassify = (bool) request()->user()?->hasAnyRole(['Supervisor', 'Admin']); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $openStop = $workOrder->openStop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // An order held for a change may only resume once one has been applied — the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // page needs to know which, so Resume can carry it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -235,11 +266,93 @@ public function show(WorkOrder $workOrder, CustomFieldService $customFields) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'product_type_name' => $workOrder->productType?->name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'batches' => $batches, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'issues' => $issues, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'allocations' => $allocations, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'canReclassify' => $canReclassify, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'materials' => $canReclassify | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? Material::where('is_active', true)->orderBy('code')->get(['id', 'code', 'name']) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : [], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'customFields' => $customFields->clientConfig('work_order'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Materials reconciliation (#99): declare actual consumption, return unused | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * material to stock, and reclassify a quantity to another class. Each | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * allocation must belong to this work order. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function recordConsumption(RecordConsumptionRequest $request, WorkOrder $workOrder, MaterialAllocation $allocation, MaterialAllocationService $allocations) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->assertAllocationBelongs($workOrder, $allocation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $allocations->recordConsumption( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $allocation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (float) $request->validated('consumed_qty'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (float) ($request->validated('scrap_qty') ?? 0), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $request->validated('notes'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('success', __('Consumption recorded')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('error', $e->getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function returnAllocation(ReturnAllocationRequest $request, WorkOrder $workOrder, MaterialAllocation $allocation, MaterialAllocationService $allocations) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $this->assertAllocationBelongs($workOrder, $allocation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $allocations->returnQuantity( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $allocation, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (float) $request->validated('qty'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $request->user(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $request->validated('reason'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('success', __('Material returned to stock')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('error', $e->getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function reclassify(ReclassifyClassRequest $request, WorkOrder $workOrder, MaterialReclassificationService $reclassifications) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $source = Material::findOrFail($request->validated('source_material_id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The panel always reclassifies one of this order's pulled materials — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // enforce that so the nested route is a real scope, not decoration. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (! $workOrder->allocations()->where('material_id', $source->id)->exists()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| abort(404); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $target = Material::findOrFail($request->validated('target_material_id')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $lot = $request->validated('source_lot_id') ? MaterialLot::findOrFail($request->validated('source_lot_id')) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $reclassifications->reclassifyClass( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $source, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $target, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (float) $request->validated('qty'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $request->user(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $lot, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $request->validated('reason'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+320
to
+341
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Scope The action receives Add a check that the source material is allocated to this work order. That matches the panel's UI, where the source is always the allocation's material. 🔒 Proposed scoping check public function reclassify(ReclassifyClassRequest $request, WorkOrder $workOrder, MaterialReclassificationService $reclassifications)
{
try {
$source = Material::findOrFail($request->validated('source_material_id'));
+
+ // The panel always reclassifies one of this order's pulled materials —
+ // enforce that, so the nested route is a real scope and not decoration.
+ if (! $workOrder->allocations()->where('material_id', $source->id)->exists()) {
+ abort(404);
+ }
+
$target = Material::findOrFail($request->validated('target_material_id'));📝 Committable suggestion
Suggested change
🧰 Tools🪛 PHPMD (2.15.0)[warning] 320-320: Avoid unused parameters such as '$workOrder'. (undefined) (UnusedFormalParameter) 🪛 PHPStan (2.2.7)[error] 323-323: Call to an undefined static method App\Models\Material::findOrFail(). (staticMethod.notFound) [error] 324-324: Call to an undefined static method App\Models\Material::findOrFail(). (staticMethod.notFound) [error] 325-325: Call to an undefined static method App\Models\MaterialLot::findOrFail(). (staticMethod.notFound) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('success', __('Material reclassified')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (\DomainException|\InvalidArgumentException $e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return back()->with('error', $e->getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private function assertAllocationBelongs(WorkOrder $workOrder, MaterialAllocation $allocation): void | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ($allocation->batch?->work_order_id !== $workOrder->id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| abort(404); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public function edit(WorkOrder $workOrder, CustomFieldService $customFields) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Inertia::render('admin/work-orders/Edit', [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Requests\Api\V1; | ||
|
|
||
| use Illuminate\Foundation\Http\FormRequest; | ||
|
|
||
| /** | ||
| * Reclassify a quantity of material from one class (material) to another (#99). | ||
| * The route is gated to Supervisor|Admin; this request owns shape validation. | ||
| */ | ||
| class ReclassifyClassRequest extends FormRequest | ||
| { | ||
| public function authorize(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| public function rules(): array | ||
| { | ||
| return [ | ||
| 'source_material_id' => ['required', 'integer', 'exists:materials,id'], | ||
| 'target_material_id' => ['required', 'integer', 'different:source_material_id', 'exists:materials,id'], | ||
| 'qty' => ['required', 'numeric', 'gt:0'], | ||
| 'source_lot_id' => ['nullable', 'integer', 'exists:material_lots,id'], | ||
| 'reason' => ['nullable', 'string', 'max:255'], | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| namespace App\Http\Requests\Api\V1; | ||
|
|
||
| use App\Models\MaterialLot; | ||
| use Illuminate\Foundation\Http\FormRequest; | ||
| use Illuminate\Validation\Rule; | ||
|
|
||
| /** | ||
| * Change a material lot's status (#99) — released / quarantine / rejected. The | ||
| * route is gated to Supervisor|Admin. A reason is required when quarantining or | ||
| * rejecting, so the disposition is explainable in the audit trail. | ||
| */ | ||
| class ReclassifyLotStatusRequest extends FormRequest | ||
| { | ||
| public function authorize(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| public function rules(): array | ||
| { | ||
| return [ | ||
| 'to_status' => ['required', Rule::in([ | ||
| MaterialLot::STATUS_RELEASED, | ||
| MaterialLot::STATUS_QUARANTINE, | ||
| MaterialLot::STATUS_REJECTED, | ||
| ])], | ||
| 'reason' => [ | ||
| 'nullable', | ||
| 'string', | ||
| 'max:255', | ||
| Rule::requiredIf(fn () => in_array($this->input('to_status'), [ | ||
| MaterialLot::STATUS_QUARANTINE, | ||
| MaterialLot::STATUS_REJECTED, | ||
| ], true)), | ||
| ], | ||
| ]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Mes-Open/OpenMes
Length of output: 3726
🏁 Script executed:
Repository: Mes-Open/OpenMes
Length of output: 50372
🏁 Script executed:
Repository: Mes-Open/OpenMes
Length of output: 39499
🏁 Script executed:
Repository: Mes-Open/OpenMes
Length of output: 406
Guard missing batches in both authorization methods.
material_allocations.batch_idis non-nullable, butBatchusesSoftDeletesWithAudit. A soft-deleted batch is hidden from$allocation->batch, so bothRecordConsumptionRequest::authorize()andReturnAllocationRequest::authorize()dereferenceworkOrderonnulland return 500 before the controller check. Add an explicit batch-null guard and deny authorization when the batch is unavailable.🤖 Prompt for AI Agents