partial update transaction work with stock product

This commit is contained in:
2025-06-24 19:42:19 +07:00
parent 33502e905d
commit c3233ea6b2
20 changed files with 3432 additions and 239 deletions

View File

@@ -9,17 +9,30 @@ use App\Models\Stock;
use App\Models\Transaction;
use App\Models\User;
use App\Models\Work;
use App\Services\StockService;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Exception;
class TransactionController extends Controller
{
protected $stockService;
public function __construct(StockService $stockService)
{
$this->stockService = $stockService;
}
public function index()
{
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%kerja%')->get();
$work_works = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')
->select('c.name as category_name', 'works.*')
->where('c.name', 'LIKE', '%kerja%')
->orderBy('works.name', 'asc')
->get();
$wash_work = Work::leftJoin('categories as c', 'c.id', '=', 'works.category_id')->select('c.name as category_name', 'works.*')->where('c.name', 'LIKE', '%cuci%')->first();
$user_sas = User::where('role_id', 4)->where('dealer_id', Auth::user()->dealer_id)->get();
$count_transaction_users = Transaction::where("user_id", Auth::user()->id)->count();
@@ -41,7 +54,9 @@ class TransactionController extends Controller
public function workcategory($category_id)
{
$works = Work::where('category_id', $category_id)->get();
$works = Work::where('category_id', $category_id)
->orderBy('name', 'asc')
->get();
$response = [
"message" => "get work category successfully",
"data" => $works,
@@ -629,14 +644,28 @@ class TransactionController extends Controller
public function destroy($id)
{
Transaction::find($id)->delete();
$response = [
'message' => 'Data deleted successfully',
'status' => 200
];
return redirect()->back();
DB::beginTransaction();
try {
$transaction = Transaction::find($id);
if (!$transaction) {
return redirect()->back()->withErrors(['error' => 'Transaksi tidak ditemukan']);
}
// Restore stock before deleting transaction
$this->stockService->restoreStockForTransaction($transaction);
// Delete the transaction
$transaction->delete();
DB::commit();
return redirect()->back()->with('success', 'Transaksi berhasil dihapus dan stock telah dikembalikan');
} catch (Exception $e) {
DB::rollback();
return redirect()->back()->withErrors(['error' => 'Gagal menghapus transaksi: ' . $e->getMessage()]);
}
}
public function store(Request $request)
@@ -645,9 +674,19 @@ class TransactionController extends Controller
$request->validate([
'work_id.*' => ['required', 'integer'],
'quantity.*' => ['required', 'integer'],
'spk_no' => ['required', function($attribute, $value, $fail) use($request) {
$date = explode('/', $request->date);
$date = $date[2].'-'.$date[0].'-'.$date[1];
'spk_no' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
// Handle date format conversion safely for validation
if (strpos($request->date, '/') !== false) {
$dateParts = explode('/', $request->date);
if (count($dateParts) === 3) {
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
} else {
$fail('Format tanggal tidak valid');
return;
}
} else {
$date = $request->date;
}
if(!$request->work_id) {
$fail('Pekerjaan harus diisi');
@@ -665,9 +704,19 @@ class TransactionController extends Controller
}
}
}],
'police_number' => ['required', function($attribute, $value, $fail) use($request) {
$date = explode('/', $request->date);
$date = $date[2].'-'.$date[0].'-'.$date[1];
'police_number' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
// Handle date format conversion safely for validation
if (strpos($request->date, '/') !== false) {
$dateParts = explode('/', $request->date);
if (count($dateParts) === 3) {
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
} else {
$fail('Format tanggal tidak valid');
return;
}
} else {
$date = $request->date;
}
if(!$request->work_id) {
$fail('Pekerjaan harus diisi');
@@ -686,9 +735,19 @@ class TransactionController extends Controller
}
}],
'warranty' => ['required'],
'date' => ['required', function($attribute, $value, $fail) use($request) {
$date = explode('/', $value);
$date = $date[2].'-'.$date[0].'-'.$date[1];
'date' => ['required', 'string', 'min:1', function($attribute, $value, $fail) use($request) {
// Handle date format conversion safely for validation
if (strpos($value, '/') !== false) {
$dateParts = explode('/', $value);
if (count($dateParts) === 3) {
$date = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
} else {
$fail('Format tanggal tidak valid. Gunakan format MM/DD/YYYY atau YYYY-MM-DD');
return;
}
} else {
$date = $value;
}
if(!$request->work_id) {
$fail('Pekerjaan harus diisi');
@@ -707,31 +766,117 @@ class TransactionController extends Controller
}
}],
'category' => ['required'],
'user_sa_id' => ['required', 'integer'],
'user_sa_id' => ['required', 'integer', 'exists:users,id'],
], [
'spk_no.required' => 'No. SPK harus diisi',
'spk_no.min' => 'No. SPK tidak boleh kosong',
'police_number.required' => 'No. Polisi harus diisi',
'police_number.min' => 'No. Polisi tidak boleh kosong',
'date.required' => 'Tanggal Pekerjaan harus diisi',
'date.min' => 'Tanggal Pekerjaan tidak boleh kosong',
'user_sa_id.required' => 'Service Advisor harus dipilih',
'user_sa_id.exists' => 'Service Advisor yang dipilih tidak valid',
'work_id.*.required' => 'Pekerjaan harus dipilih',
'quantity.*.required' => 'Quantity harus diisi',
]);
$request['date'] = explode('/', $request->date);
$request['date'] = $request['date'][2].'-'.$request['date'][0].'-'.$request['date'][1];
$data = [];
for($i = 0; $i < count($request->work_id); $i++) {
$data[] = [
"user_id" => $request->mechanic_id,
"dealer_id" => $request->dealer_id,
"form" => $request->form,
"work_id" => $request->work_id[$i],
"qty" => $request->quantity[$i],
"spk" => $request->spk_no,
"police_number" => $request->police_number,
"warranty" => $request->warranty,
"user_sa_id" => $request->user_sa_id,
"date" => $request->date,
"created_at" => date('Y-m-d H:i:s')
];
// Handle date format conversion safely
$dateValue = $request->date;
if (strpos($dateValue, '/') !== false) {
// If date is in MM/DD/YYYY format, convert to Y-m-d
$dateParts = explode('/', $dateValue);
if (count($dateParts) === 3) {
$request['date'] = $dateParts[2].'-'.$dateParts[0].'-'.$dateParts[1];
} else {
// Invalid date format, use as is
$request['date'] = $dateValue;
}
} else {
// Date is already in Y-m-d format or other format, use as is
$request['date'] = $dateValue;
}
Transaction::insert($data);
return redirect()->back()->with('success', 'Berhasil input pekerjaan');
// Check stock availability for all works before creating transactions
$stockErrors = [];
for($i = 0; $i < count($request->work_id); $i++) {
$stockCheck = $this->stockService->checkStockAvailability(
$request->work_id[$i],
$request->dealer_id,
$request->quantity[$i]
);
if (!$stockCheck['available']) {
$work = Work::find($request->work_id[$i]);
$stockErrors[] = "Pekerjaan '{$work->name}': {$stockCheck['message']}";
// Add detailed stock information
if (!empty($stockCheck['details'])) {
foreach ($stockCheck['details'] as $detail) {
if (!$detail['is_available']) {
$stockErrors[] = "- {$detail['product_name']}: Dibutuhkan {$detail['required_quantity']}, Tersedia {$detail['available_stock']}";
}
}
}
}
}
// If there are stock errors, return with error messages
if (!empty($stockErrors)) {
return redirect()->back()
->withErrors(['stock' => implode('<br>', $stockErrors)])
->withInput();
}
DB::beginTransaction();
try {
$transactions = [];
$data = [];
// Create transaction records
for($i = 0; $i < count($request->work_id); $i++) {
$transactionData = [
"user_id" => $request->mechanic_id,
"dealer_id" => $request->dealer_id,
"form" => $request->form,
"work_id" => $request->work_id[$i],
"qty" => $request->quantity[$i],
"spk" => $request->spk_no,
"police_number" => $request->police_number,
"warranty" => $request->warranty,
"user_sa_id" => $request->user_sa_id,
"date" => $request->date,
"status" => 'completed', // Mark as completed to trigger stock reduction
"created_at" => date('Y-m-d H:i:s'),
"updated_at" => date('Y-m-d H:i:s')
];
$data[] = $transactionData;
}
// Insert all transactions
Transaction::insert($data);
// Get the created transactions for stock reduction
$createdTransactions = Transaction::where('spk', $request->spk_no)
->where('police_number', $request->police_number)
->where('date', $request->date)
->where('dealer_id', $request->dealer_id)
->get();
// Reduce stock for each transaction
foreach ($createdTransactions as $transaction) {
$this->stockService->reduceStockForTransaction($transaction);
}
DB::commit();
return redirect()->back()->with('success', 'Berhasil input pekerjaan dan stock telah dikurangi otomatis');
} catch (Exception $e) {
DB::rollback();
return redirect()->back()
->withErrors(['error' => 'Gagal menyimpan transaksi: ' . $e->getMessage()])
->withInput();
}
}
public function edit($id)
@@ -764,4 +909,62 @@ class TransactionController extends Controller
return response()->json($response);
}
/**
* Check stock availability for work at dealer
*/
public function checkStockAvailability(Request $request)
{
$request->validate([
'work_id' => 'required|exists:works,id',
'dealer_id' => 'required|exists:dealers,id',
'quantity' => 'required|integer|min:1'
]);
try {
$availability = $this->stockService->checkStockAvailability(
$request->work_id,
$request->dealer_id,
$request->quantity
);
return response()->json([
'status' => 200,
'data' => $availability
]);
} catch (Exception $e) {
return response()->json([
'status' => 500,
'message' => 'Error checking stock: ' . $e->getMessage()
], 500);
}
}
/**
* Get stock prediction for work
*/
public function getStockPrediction(Request $request)
{
$request->validate([
'work_id' => 'required|exists:works,id',
'quantity' => 'required|integer|min:1'
]);
try {
$prediction = $this->stockService->getStockUsagePrediction(
$request->work_id,
$request->quantity
);
return response()->json([
'status' => 200,
'data' => $prediction
]);
} catch (Exception $e) {
return response()->json([
'status' => 500,
'message' => 'Error getting prediction: ' . $e->getMessage()
], 500);
}
}
}

View File

@@ -26,16 +26,28 @@ class WorkController extends Controller
$data = DB::table('works as w')->leftJoin('categories as c', 'c.id', '=', 'w.category_id')->select('w.shortname as shortname', 'w.id as work_id', 'w.name as name', 'w.desc as desc', 'c.name as category_name', 'w.category_id as category_id');
return DataTables::of($data)->addIndexColumn()
->addColumn('action', function($row) use ($menu) {
$btn = '';
$btn = '<div class="d-flex flex-row gap-1">';
if(Auth::user()->can('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-bold" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')"> Hapus </button>';
// Products Management Button
if(Gate::allows('view', $menu)) {
$btn .= '<a href="'. route('work.products.index', ['work' => $row->work_id]) .'" class="btn btn-info btn-sm" title="Kelola Produk">
Produk
</a>';
}
if(Auth::user()->can('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-bold" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')"> Edit </button>';
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm" id="editWork'. $row->work_id .'" data-url="'. route('work.edit', $row->work_id) .'" data-action="'. route('work.update', $row->work_id) .'" onclick="editWork('. $row->work_id .')">
Edit
</button>';
}
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm" data-action="'. route('work.destroy', $row->work_id) .'" id="destroyWork'. $row->work_id .'" onclick="destroyWork('. $row->work_id .')">
Hapus
</button>';
}
$btn .= '</div>';
return $btn;
})
->rawColumns(['action'])

View File

@@ -0,0 +1,247 @@
<?php
namespace App\Http\Controllers;
use App\Models\Work;
use App\Models\Product;
use App\Models\WorkProduct;
use App\Models\Menu;
use App\Services\StockService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Yajra\DataTables\DataTables;
class WorkProductController extends Controller
{
protected $stockService;
public function __construct(StockService $stockService)
{
$this->stockService = $stockService;
}
/**
* Display work products for a specific work
*/
public function index(Request $request, $workId)
{
$menu = Menu::where('link', 'work.index')->first();
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
$work = Work::with('category')->findOrFail($workId);
if ($request->ajax()) {
Log::info('Work products index AJAX request for work ID: ' . $workId);
$workProducts = WorkProduct::with(['product', 'product.category'])
->where('work_id', $workId)
->get();
Log::info('Found ' . $workProducts->count() . ' work products');
return DataTables::of($workProducts)
->addIndexColumn()
->addColumn('product_name', function($row) {
return $row->product->name;
})
->addColumn('product_code', function($row) {
return $row->product->code;
})
->addColumn('product_category', function($row) {
return $row->product->category ? $row->product->category->name : '-';
})
->addColumn('unit', function($row) {
return $row->product->unit;
})
->addColumn('quantity_required', function($row) {
return number_format($row->quantity_required, 2);
})
->addColumn('action', function($row) use ($menu) {
$btn = '<div class="d-flex flex-row gap-1">';
if(Gate::allows('update', $menu)) {
$btn .= '<button class="btn btn-warning btn-sm btn-edit-work-product" data-id="'.$row->id.'">
Edit
</button>';
}
if(Gate::allows('delete', $menu)) {
$btn .= '<button class="btn btn-danger btn-sm btn-delete-work-product" data-id="'.$row->id.'">
Hapus
</button>';
}
$btn .= '</div>';
return $btn;
})
->rawColumns(['action'])
->make(true);
}
$products = Product::where('active', true)->with('category')->get();
return view('back.master.work-products', compact('work', 'products'));
}
/**
* Store work product relationship
*/
public function store(Request $request)
{
$menu = Menu::where('link', 'work.index')->first();
abort_if(Gate::denies('create', $menu), 403, 'Unauthorized User');
$request->validate([
'work_id' => 'required|exists:works,id',
'product_id' => 'required|exists:products,id',
'quantity_required' => 'required|numeric|min:0.01',
'notes' => 'nullable|string'
]);
// Check if combination already exists
$exists = WorkProduct::where('work_id', $request->work_id)
->where('product_id', $request->product_id)
->exists();
if ($exists) {
return response()->json([
'status' => 422,
'message' => 'Produk sudah ditambahkan ke pekerjaan ini'
], 422);
}
WorkProduct::create($request->all());
return response()->json([
'status' => 200,
'message' => 'Produk berhasil ditambahkan ke pekerjaan'
]);
}
/**
* Show work product for editing
*/
public function show($workId, $workProductId)
{
$menu = Menu::where('link', 'work.index')->first();
abort_if(Gate::denies('view', $menu), 403, 'Unauthorized User');
try {
$workProduct = WorkProduct::with(['work', 'product', 'product.category'])
->where('work_id', $workId)
->where('id', $workProductId)
->firstOrFail();
return response()->json([
'status' => 200,
'data' => $workProduct
]);
} catch (\Exception $e) {
Log::error('Error fetching work product: ' . $e->getMessage());
return response()->json([
'status' => 404,
'message' => 'Work product tidak ditemukan'
], 404);
}
}
/**
* Update work product relationship
*/
public function update(Request $request, $workId, $workProductId)
{
$menu = Menu::where('link', 'work.index')->first();
abort_if(Gate::denies('update', $menu), 403, 'Unauthorized User');
$request->validate([
'quantity_required' => 'required|numeric|min:0.01',
'notes' => 'nullable|string'
]);
try {
$workProduct = WorkProduct::where('work_id', $workId)
->where('id', $workProductId)
->firstOrFail();
$workProduct->update($request->only(['quantity_required', 'notes']));
return response()->json([
'status' => 200,
'message' => 'Data produk pekerjaan berhasil diupdate'
]);
} catch (\Exception $e) {
Log::error('Error updating work product: ' . $e->getMessage());
return response()->json([
'status' => 404,
'message' => 'Work product tidak ditemukan'
], 404);
}
}
/**
* Remove work product relationship
*/
public function destroy($workId, $workProductId)
{
$menu = Menu::where('link', 'work.index')->first();
abort_if(Gate::denies('delete', $menu), 403, 'Unauthorized User');
try {
$workProduct = WorkProduct::where('work_id', $workId)
->where('id', $workProductId)
->firstOrFail();
$workProduct->delete();
return response()->json([
'status' => 200,
'message' => 'Produk berhasil dihapus dari pekerjaan'
]);
} catch (\Exception $e) {
Log::error('Error deleting work product: ' . $e->getMessage());
return response()->json([
'status' => 404,
'message' => 'Work product tidak ditemukan'
], 404);
}
}
/**
* Get stock prediction for work
*/
public function stockPrediction(Request $request, $workId)
{
$quantity = $request->get('quantity', 1);
$prediction = $this->stockService->getStockUsagePrediction($workId, $quantity);
return response()->json([
'status' => 200,
'data' => $prediction
]);
}
/**
* Check stock availability for work at specific dealer
*/
public function checkStock(Request $request)
{
$request->validate([
'work_id' => 'required|exists:works,id',
'dealer_id' => 'required|exists:dealers,id',
'quantity' => 'required|integer|min:1'
]);
$availability = $this->stockService->checkStockAvailability(
$request->work_id,
$request->dealer_id,
$request->quantity
);
return response()->json([
'status' => 200,
'data' => $availability
]);
}
}

View File

@@ -47,4 +47,26 @@ class Product extends Model
{
return $this->stocks()->where('dealer_id', $dealerId)->first()?->quantity ?? 0;
}
/**
* Get all works that require this product
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function works()
{
return $this->belongsToMany(Work::class, 'work_products')
->withPivot('quantity_required', 'notes')
->withTimestamps();
}
/**
* Get work products pivot records
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function workProducts()
{
return $this->hasMany(WorkProduct::class);
}
}

View File

@@ -16,10 +16,40 @@ class Transaction extends Model
/**
* Get the work associated with the Transaction
*
* @return \Illuminate\Database\Eloquent\Relations\HasOne
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function work()
{
return $this->hasOne(Work::class, 'id', 'work_id');
return $this->belongsTo(Work::class, 'work_id', 'id');
}
/**
* Get the dealer associated with the Transaction
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function dealer()
{
return $this->belongsTo(Dealer::class);
}
/**
* Get the user who created the transaction
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Get the SA user associated with the transaction
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function userSa()
{
return $this->belongsTo(User::class, 'user_sa_id');
}
}

View File

@@ -22,4 +22,36 @@ class Work extends Model
{
return $this->hasMany(Transaction::class, 'work_id', 'id');
}
/**
* Get all products required for this work
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function products()
{
return $this->belongsToMany(Product::class, 'work_products')
->withPivot('quantity_required', 'notes')
->withTimestamps();
}
/**
* Get work products pivot records
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function workProducts()
{
return $this->hasMany(WorkProduct::class);
}
/**
* Get the category associated with the Work
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function category()
{
return $this->belongsTo(Category::class);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class WorkProduct extends Model
{
use HasFactory;
protected $fillable = [
'work_id',
'product_id',
'quantity_required',
'notes'
];
protected $casts = [
'quantity_required' => 'decimal:2'
];
public function work()
{
return $this->belongsTo(Work::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
}

View File

@@ -17,7 +17,9 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(\App\Services\StockService::class, function ($app) {
return new \App\Services\StockService();
});
}
/**

View File

@@ -0,0 +1,197 @@
<?php
namespace App\Services;
use App\Models\Stock;
use App\Models\Work;
use App\Models\Transaction;
use App\Models\StockLog;
use Illuminate\Support\Facades\DB;
use Exception;
class StockService
{
/**
* Check if dealer has sufficient stock for work
*
* @param int $workId
* @param int $dealerId
* @param int $workQuantity
* @return array
*/
public function checkStockAvailability($workId, $dealerId, $workQuantity = 1)
{
$work = Work::with('products')->find($workId);
if (!$work) {
return [
'available' => false,
'message' => 'Pekerjaan tidak ditemukan',
'details' => []
];
}
$stockDetails = [];
$allAvailable = true;
foreach ($work->products as $product) {
$requiredQuantity = $product->pivot->quantity_required * $workQuantity;
$availableStock = $product->getStockByDealer($dealerId);
$isAvailable = $availableStock >= $requiredQuantity;
if (!$isAvailable) {
$allAvailable = false;
}
$stockDetails[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'required_quantity' => $requiredQuantity,
'available_stock' => $availableStock,
'is_available' => $isAvailable
];
}
return [
'available' => $allAvailable,
'message' => $allAvailable ? 'Stock tersedia' : 'Stock tidak mencukupi',
'details' => $stockDetails
];
}
/**
* Reduce stock when work transaction is completed
*
* @param Transaction $transaction
* @return bool
* @throws Exception
*/
public function reduceStockForTransaction(Transaction $transaction)
{
return DB::transaction(function () use ($transaction) {
$work = $transaction->work;
if (!$work) {
throw new Exception('Work not found for transaction');
}
$work->load('products');
if ($work->products->isEmpty()) {
// No products required for this work, return true
return true;
}
foreach ($work->products as $product) {
$requiredQuantity = $product->pivot->quantity_required * $transaction->qty;
$stock = Stock::where('product_id', $product->id)
->where('dealer_id', $transaction->dealer_id)
->first();
if (!$stock) {
throw new Exception("Stock not found for product {$product->name} at dealer");
}
if ($stock->quantity < $requiredQuantity) {
throw new Exception("Insufficient stock for product {$product->name}. Required: {$requiredQuantity}, Available: {$stock->quantity}");
}
// Reduce stock
$newQuantity = $stock->quantity - $requiredQuantity;
$stock->updateStock(
$newQuantity,
$transaction,
"Stock reduced for work: {$work->name} (Transaction #{$transaction->id})"
);
}
return true;
});
}
/**
* Restore stock when work transaction is cancelled/reversed
*
* @param Transaction $transaction
* @return bool
* @throws Exception
*/
public function restoreStockForTransaction(Transaction $transaction)
{
return DB::transaction(function () use ($transaction) {
$work = $transaction->work;
if (!$work) {
throw new Exception('Work not found for transaction');
}
$work->load('products');
if ($work->products->isEmpty()) {
return true;
}
foreach ($work->products as $product) {
$restoreQuantity = $product->pivot->quantity_required * $transaction->qty;
$stock = Stock::where('product_id', $product->id)
->where('dealer_id', $transaction->dealer_id)
->first();
if (!$stock) {
// Create new stock record if doesn't exist
$stock = Stock::create([
'product_id' => $product->id,
'dealer_id' => $transaction->dealer_id,
'quantity' => 0
]);
}
// Restore stock
$newQuantity = $stock->quantity + $restoreQuantity;
$stock->updateStock(
$newQuantity,
$transaction,
"Stock restored from cancelled work: {$work->name} (Transaction #{$transaction->id})"
);
}
return true;
});
}
/**
* Get stock usage prediction for a work
*
* @param int $workId
* @param int $quantity
* @return array
*/
public function getStockUsagePrediction($workId, $quantity = 1)
{
$work = Work::with('products')->find($workId);
if (!$work) {
return [];
}
$predictions = [];
foreach ($work->products as $product) {
$totalRequired = $product->pivot->quantity_required * $quantity;
$predictions[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'product_code' => $product->code,
'unit' => $product->unit,
'quantity_per_work' => $product->pivot->quantity_required,
'total_quantity_needed' => $totalRequired,
'notes' => $product->pivot->notes
];
}
return $predictions;
}
}