vat_wms/app/Http/Controllers/Api/StockEntryController.php

470 lines
16 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\Api;
use App\Http\Controllers\Controller;
use App\Models\OriginCountry;
use App\Models\StockEntry;
use App\Models\StockEntrySection;
use App\Models\StockEntryStatus;
use App\Models\StockEntryStatusHistory;
use App\Models\StockPosition;
use App\Models\PhysicalItem;
use App\Models\StockSection;
use App\Models\Supplier;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use OwenIt\Auditing\Models\Audit;
class StockEntryController extends Controller
{
/**
* Display a paginated listing of stock entries.
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
$query = StockEntry::query()
->with(['physicalItem', 'supplier', 'sections', 'stockBatch', 'physicalItem.manufacturer', 'countryOfOrigin']);
// Apply filters if provided
if ($request->has('search')) {
$search = $request->search;
$query->whereHas('physicalItem', function($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
}
// Sort
$sortField = $request->input('sort_field', 'updated_at');
$sortDirection = $request->input('sort_direction', 'desc');
$query->orderBy($sortField, $sortDirection);
// Paginate
$perPage = $request->input('per_page', 10);
$page = $request->input('page', 1);
$entries = $query->paginate($perPage, ['*'], 'page', $page);
return response()->json([
'data' => $entries->items(),
'meta' => [
'total' => $entries->total(),
'per_page' => $entries->perPage(),
'current_page' => $entries->currentPage(),
'last_page' => $entries->lastPage(),
],
]);
}
public function fetchOnTheWay(Request $request)
{
$query = StockEntry::query()
->with(['physicalItem', 'supplier', 'sections', 'stockBatch', 'physicalItem.manufacturer', 'countryOfOrigin'])->where('on_the_way', 1);
// Paginate
$perPage = $request->input('per_page', 10);
$page = $request->input('page', 1);
$entries = $query->paginate($perPage, ['*'], 'page', $page);
return response()->json([
'data' => $entries->items(),
'meta' => [
'total' => $entries->total(),
'per_page' => $entries->perPage(),
'current_page' => $entries->currentPage(),
'last_page' => $entries->lastPage(),
],
]);
}
public function addData(Request $request)
{
// build base rules
$rules = [
'physical_item_id' => 'required|integer|exists:vat_warehouse.physical_item,id',
'supplier_id' => 'required|integer|exists:vat_warehouse.supplier,id',
'count' => 'required|integer|min:0',
'price' => 'nullable|numeric|min:0',
'bought' => 'nullable|date',
'description' => 'nullable|string',
'note' => 'nullable|string',
'stock_batch_id' => 'nullable|integer|exists:stock_batch,id',
'country_of_origin_id' => 'required|integer|exists:vat_warehouse.country_of_origin,id',
'on_the_way' => 'boolean',
];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
// 1) create the main stock entry
$entry = StockEntry::create($request->only([
'physical_item_id',
'supplier_id',
'count',
'price',
'bought',
'description',
'note',
'country_of_origin_id',
'on_the_way',
'stock_batch_id',
]) + [
'created_by' => auth()->id() ?? 1,
]);
// 3) eager-load relations (including the full address hierarchy)
$entry->load([
'physicalItem',
'supplier',
'stockBatch',
'sections.position.shelf.rack.line.room',
]);
return response()->json([
'message' => 'Stock entry created successfully',
'data' => $entry,
], 201);
}
/**
* Display the specified stock entry.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function show($id)
{
$entry = StockEntry::with(['physicalItem', 'supplier', 'stockPosition', 'stockBatch'])
->findOrFail($id);
return response()->json(['data' => $entry]);
}
/**
* Update the specified stock entry.
*
* @param Request $request
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function updateData(Request $request, $id)
{
// 1) pull apart
$entryForm = $request->input('entryForm', []);
$sections = $request->input('sections', []);
// 2) validation rules as before…
$rules = [
'entryForm.physical_item_id' => ['sometimes','integer','exists:vat_warehouse.physical_item,id'],
'entryForm.supplier_id' => ['sometimes','integer','exists:vat_warehouse.supplier,id'],
'entryForm.count' => ['sometimes','integer','min:0'],
'entryForm.price' => ['nullable','numeric','min:0'],
'entryForm.bought' => ['nullable','date'],
'entryForm.description' => ['nullable','string'],
'entryForm.note' => ['nullable','string'],
'entryForm.stock_batch_id' => ['nullable','integer','exists:stock_batch,id'],
'entryForm.country_of_origin_id' => ['sometimes','integer','exists:vat_warehouse.country_of_origin,id'],
'entryForm.on_the_way' => ['sometimes','boolean'],
];
// determine if sections are required
$needsSection = function() use ($request) {
return ! $request->input('entryForm.on_the_way', false)
&& ! is_null($request->input('entryForm.stock_batch_id'));
};
// validate sections array if needed
$rules['sections'] = [Rule::requiredIf($needsSection), 'array'];
$rules['sections.*.stock_position_id'] = [Rule::requiredIf($needsSection), 'integer', 'exists:stock_section,section_id'];
$rules['sections.*.count'] = [Rule::requiredIf($needsSection), 'integer', 'min:1'];
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
return response()->json(['errors'=>$validator->errors()], 422);
}
/** @var StockEntry $entry */
$entry = StockEntry::with('sections')->findOrFail($id);
// 3) Update the main row (this is autoaudited by OwenIt)
$entry->update(array_merge(
array_filter($entryForm, fn($v) => !is_null($v)),
['updated_by'=> auth()->id() ?? 1]
));
// 4) Manual pivotaudit:
// a) snapshot what was there
$oldPivots = $entry->sections
->pluck('pivot.count','pivot.section_id')
->toArray(); // [ section_id => oldCount, … ]
// b) perform the sync
$syncData = [];
foreach ($sections as $sec) {
if (!empty($sec['stock_position_id']) && !empty($sec['count'])) {
$syncData[(int)$sec['stock_position_id']] = ['count'=>(int)$sec['count']];
}
}
// $entry->sections()->sync($syncData);
// b) perform the sync, but dont let the AuditableObserver fire here…
StockEntrySection::withoutAuditing(function() use ($entry, $syncData) {
$entry->sections()->sync($syncData);
});
// c) now compare old↔new and write audit rows
$userId = auth()->id() ?? null;
foreach ($syncData as $sectionId => $pivotData) {
$newCount = $pivotData['count'];
if (! array_key_exists($sectionId, $oldPivots)) {
// *** attached ***
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id, // pivot has no single PK; we use entry ID
'event' => 'created',
'old_values' => [],
'new_values' => ['section_id'=>$sectionId,'count'=>$newCount],
]);
} elseif ($oldPivots[$sectionId] !== $newCount) {
// *** updated ***
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id,
'event' => 'updated',
'old_values' => ['count'=>$oldPivots[$sectionId]],
'new_values' => ['count'=>$newCount],
]);
}
}
// d) any removed?
foreach (array_diff_key($oldPivots, $syncData) as $sectionId => $oldCount) {
Audit::create([
'user_id' => $userId,
'auditable_type'=> StockEntrySection::class,
'auditable_id' => $entry->id,
'event' => 'deleted',
'old_values' => ['section_id'=>$sectionId,'count'=>$oldCount],
'new_values' => [],
]);
}
// 5) reload and return
$entry->load([
'physicalItem',
'supplier',
'stockBatch',
'sections.position.shelf.rack.line.room',
]);
return response()->json([
'message'=>'Stock entry updated successfully',
'data' =>$entry,
]);
}
/**
* Remove the specified stock entry.
*
* @param int $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy($id)
{
$entry = StockEntry::findOrFail($id);
$entry->delete();
return response()->json([
'message' => 'Stock entry deleted successfully'
]);
}
/**
* Get options for dropdown lists.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getOptions()
{
$stockPositions = StockSection::with('position.shelf.rack.line.room')
->get()
->map(function (StockSection $section) {
$pos = $section->position;
$shelf = $pos->shelf;
$rack = $shelf->rack;
$line = $rack->line;
$room = $line->room;
return [
'id' => $section->section_id,
'name' => sprintf(
'%s-%s-%s-%s-%s-%s',
$room->room_symbol,
$line->line_symbol,
$rack->rack_symbol,
$shelf->shelf_symbol,
$pos->position_symbol,
$section->section_symbol
),
'capacity' => $section->capacity,
'occupied' => $section->occupied()
];
});
// Get suppliers from warehouse DB
$suppliers = Supplier::select('id', 'name')->get();
// Get physical items from warehouse DB
$countriesOrigin = OriginCountry::select('id', 'code as name')->get();
return response()->json([
'stockPositions' => $stockPositions,
'suppliers' => $suppliers,
'countriesOrigin' => $countriesOrigin,
]);
}
/**
* Get options for dropdown lists.
*
* @return \Illuminate\Http\JsonResponse
*/
public function getItems(Request $request)
{
// Get physical items from warehouse DB
$physicalItems = PhysicalItem::with(['type', 'manufacturer'])
->where('name', 'like', '%' . $request->input('item_name', '') . '%')
->limit(100)
->get();
return response()->json([
'physicalItems' => $physicalItems,
]);
}
public function audit(Request $request, $id)
{
// 1) Load the entry (so we can get its audits)
$entry = StockEntry::findOrFail($id);
// 2) Get audits for the entry model itself
$entryAudits = $entry->audits()->orderBy('created_at')->get();
// 3) Get audits for all pivot changes you logged under the pivot model
$pivotAudits = Audit::query()
->where('auditable_type', StockEntrySection::class)
->where('auditable_id', $id) // we used entry->id when creating these
->orderBy('created_at')
->get();
return response()->json([
'entry_audits' => $entryAudits,
'pivot_audits' => $pivotAudits,
]);
}
public function getStatusList(Request $request)
{
$entry = StockEntryStatus::all();
return response()->json([
'statuses' => $entry,
]);
}
public function countStock(Request $request)
{
// 1) Validate input
try {
$payload = $request->validate([
'entryId' => 'required|integer|exists:stock_entries,id',
'quantity' => 'required|integer|min:0',
]);
} catch (ValidationException $e) {
// Return 422 + a JSON body with errors
return response()->json([
'success' => false,
'error' => 'validation_failed',
'message' => $e->errors(),
], 422);
}
try {
// 2) Fetch the StockEntry (will throw ModelNotFoundException if not exists)
$entry = StockEntry::findOrFail($payload['entryId']);
// 3) If original_count is already set → 409 conflict
if ($entry->original_count !== 0) {
return response()->json([
'success' => false,
'error' => 'already_counted',
'message' => 'This stock entry has already been counted.',
], 409);
}
$quantity = $payload['quantity'];
// Determine which status to record
if ($entry->original_count_invoice === $quantity) {
StockEntryStatusHistory::create([
'stock_entries_id' => $entry->id,
'stock_entries_status_id' => StockEntryStatus::NEW_GOODS_COUNTED,
]);
} else {
$statusId = $entry->original_count_invoice > $quantity
? StockEntryStatus::NEW_GOODS_MISSING
: StockEntryStatus::NEW_GOODS_SURPLUS;
StockEntryStatusHistory::create([
'stock_entries_id' => $entry->id,
'stock_entries_status_id' => $statusId,
'status_note' => 'Count: ' . ($quantity - $entry->original_count_invoice)
]);
}
$entry->update([
'original_count' => $quantity,
'count' => $quantity,
]);
return response()->json([
'success' => true,
]); // HTTP 200
} catch (ModelNotFoundException $e) {
return response()->json([
'success' => false,
'error' => 'not_found',
'message' => 'Stock entry not found.',
], 404);
} catch (\Exception $e) {
// Log the exception for debugging
Log::error('countStock error', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'success' => false,
'error' => 'server_error',
'message' => 'An unexpected error occurred.',
], 500);
}
}
}