-
-
-
-
- Product Code
- {{ $product->product_code }}
-
-
- Barcode Symbology
- {{ $product->product_barcode_symbology }}
-
-
- Name
- {{ $product->product_name }}
-
-
- Category
- {{ $product->category->category_name }}
-
-
- Cost
- {{ format_currency($product->product_cost) }}
-
-
- Price
- {{ format_currency($product->product_price) }}
-
-
- Quantity
- {{ $product->product_quantity . ' ' . $product->product_unit }}
-
-
- Stock Worth
-
- COST:: {{ format_currency($product->product_cost * $product->product_quantity) }} /
- PRICE:: {{ format_currency($product->product_price * $product->product_quantity) }}
-
-
-
- Alert Quantity
- {{ $product->product_stock_alert }}
-
-
- Tax (%)
- {{ $product->product_order_tax ?? 'N/A' }}
-
-
- Tax Type
-
- @if($product->product_tax_type == 1)
- Exclusive
- @elseif($product->product_tax_type == 2)
- Inclusive
- @else
- N/A
- @endif
-
-
-
- Note
- {{ $product->product_note ?? 'N/A' }}
-
-
-
+
+
+
+ Product Code
+ {{ $product->product_code }}
+
+
+ Name
+ {{ $product->product_name }}
+
+
+ Category
+ {{ $product->category->category_name }}
+
+
+ Unit
+ {{ $product->product_unit }}
+
+
+ Stock Alert
+ {{ $product->product_stock_alert }}
+
+
+ Note
+ {{ $product->product_note }}
+
+
@@ -91,11 +49,14 @@
- @forelse($product->getMedia('images') as $media)
-
- @empty
-
- @endforelse
+
Product Images
+
+ @foreach($product->getMedia('images') as $media)
+
+
+
+ @endforeach
+
diff --git a/Modules/Purchase/DataTables/PurchaseDataTable.php b/Modules/Purchase/DataTables/PurchaseDataTable.php
index 9eb3d19b..0f1568b4 100644
--- a/Modules/Purchase/DataTables/PurchaseDataTable.php
+++ b/Modules/Purchase/DataTables/PurchaseDataTable.php
@@ -11,35 +11,43 @@ use Yajra\DataTables\Services\DataTable;
class PurchaseDataTable extends DataTable
{
-
- public function dataTable($query) {
+ public function dataTable($query)
+ {
return datatables()
->eloquent($query)
- ->addColumn('total_amount', function ($data) {
+ ->addColumn('action', function ($data) {
+ return view('purchase::partials.actions', [
+ 'id' => $data->id
+ ]);
+ })
+ ->editColumn('total_amount', function ($data) {
return format_currency($data->total_amount);
})
- ->addColumn('paid_amount', function ($data) {
+ ->editColumn('paid_amount', function ($data) {
return format_currency($data->paid_amount);
})
- ->addColumn('due_amount', function ($data) {
+ ->editColumn('due_amount', function ($data) {
return format_currency($data->due_amount);
})
- ->addColumn('status', function ($data) {
- return view('purchase::partials.status', compact('data'));
+ ->editColumn('status', function ($data) {
+ return view('purchase::partials.status', [
+ 'status' => $data->status
+ ]);
})
- ->addColumn('payment_status', function ($data) {
- return view('purchase::partials.payment-status', compact('data'));
- })
- ->addColumn('action', function ($data) {
- return view('purchase::partials.actions', compact('data'));
+ ->editColumn('payment_status', function ($data) {
+ return view('purchase::partials.payment-status', [
+ 'payment_status' => $data->payment_status
+ ]);
});
}
- public function query(Purchase $model) {
+ public function query(Purchase $model)
+ {
return $model->newQuery();
}
- public function html() {
+ public function html()
+ {
return $this->builder()
->setTableId('purchases-table')
->columns($this->getColumns())
@@ -47,50 +55,64 @@ class PurchaseDataTable extends DataTable
->dom("<'row'<'col-md-3'l><'col-md-5 mb-2'B><'col-md-4'f>> .
'tr' .
<'row'<'col-md-5'i><'col-md-7 mt-2'p>>")
- ->orderBy(8)
+ ->orderBy(0)
->buttons(
Button::make('excel')
- ->text('
Excel'),
+ ->text('
Excel')
+ ->className('btn btn-success btn-sm no-corner'),
Button::make('print')
- ->text('
Print'),
+ ->text('
Print')
+ ->className('btn btn-primary btn-sm no-corner'),
Button::make('reset')
- ->text('
Reset'),
+ ->text('
Reset')
+ ->className('btn btn-warning btn-sm no-corner'),
Button::make('reload')
->text('
Reload')
+ ->className('btn btn-info btn-sm no-corner')
);
}
- protected function getColumns() {
+ protected function getColumns()
+ {
return [
+ Column::make('date')
+ ->title('Date')
+ ->className('text-center align-middle'),
+
Column::make('reference')
+ ->title('Reference')
->className('text-center align-middle'),
Column::make('supplier_name')
->title('Supplier')
->className('text-center align-middle'),
- Column::computed('status')
+ Column::make('status')
+ ->title('Status')
->className('text-center align-middle'),
- Column::computed('total_amount')
+ Column::make('payment_status')
+ ->title('Payment Status')
->className('text-center align-middle'),
- Column::computed('paid_amount')
+ Column::make('total_amount')
+ ->title('Total')
->className('text-center align-middle'),
- Column::computed('due_amount')
+ Column::make('paid_amount')
+ ->title('Paid')
->className('text-center align-middle'),
- Column::computed('payment_status')
+ Column::make('due_amount')
+ ->title('Due')
->className('text-center align-middle'),
Column::computed('action')
+ ->title('Action')
->exportable(false)
->printable(false)
- ->className('text-center align-middle'),
-
- Column::make('created_at')
- ->visible(false)
+ ->className('text-center align-middle')
+ ->addClass('text-center'),
];
}
diff --git a/Modules/Purchase/Database/Migrations/2024_03_21_000001_modify_purchases_table_structure.php b/Modules/Purchase/Database/Migrations/2024_03_21_000001_modify_purchases_table_structure.php
new file mode 100644
index 00000000..a9a21b54
--- /dev/null
+++ b/Modules/Purchase/Database/Migrations/2024_03_21_000001_modify_purchases_table_structure.php
@@ -0,0 +1,71 @@
+dropColumn($column);
+ }
+ }
+ });
+
+ // Add user_id column if not exists
+ if (!Schema::hasColumn('purchases', 'user_id')) {
+ Schema::table('purchases', function (Blueprint $table) {
+ $table->foreignId('user_id')->nullable()->after('branch_id')
+ ->constrained('users')->nullOnDelete();
+ });
+ }
+
+ // Modify columns to be nullable
+ DB::statement('ALTER TABLE purchases MODIFY discount_amount DECIMAL(10,2) NULL');
+ DB::statement('ALTER TABLE purchases MODIFY due_amount DECIMAL(10,2) NULL');
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ // Make columns non-nullable again
+ DB::statement('ALTER TABLE purchases MODIFY discount_amount DECIMAL(10,2) NOT NULL');
+ DB::statement('ALTER TABLE purchases MODIFY due_amount DECIMAL(10,2) NOT NULL');
+
+ // Drop user_id column if exists
+ if (Schema::hasColumn('purchases', 'user_id')) {
+ Schema::table('purchases', function (Blueprint $table) {
+ $table->dropForeign(['user_id']);
+ $table->dropColumn('user_id');
+ });
+ }
+
+ // Add back dropped columns
+ Schema::table('purchases', function (Blueprint $table) {
+ if (!Schema::hasColumn('purchases', 'tax_percentage')) {
+ $table->decimal('tax_percentage', 10, 2)->default(0);
+ }
+ if (!Schema::hasColumn('purchases', 'tax_amount')) {
+ $table->decimal('tax_amount', 10, 2)->default(0);
+ }
+ if (!Schema::hasColumn('purchases', 'shipping_amount')) {
+ $table->decimal('shipping_amount', 10, 2)->default(0);
+ }
+ if (!Schema::hasColumn('purchases', 'status')) {
+ $table->string('status')->default('Pending');
+ }
+ });
+ }
+};
\ No newline at end of file
diff --git a/Modules/Purchase/Database/Migrations/2024_03_21_000002_add_purchase_id_to_product_batches.php b/Modules/Purchase/Database/Migrations/2024_03_21_000002_add_purchase_id_to_product_batches.php
new file mode 100644
index 00000000..6cb88fda
--- /dev/null
+++ b/Modules/Purchase/Database/Migrations/2024_03_21_000002_add_purchase_id_to_product_batches.php
@@ -0,0 +1,36 @@
+foreignId('purchase_id')->nullable()->after('expired_date')
+ ->constrained('purchases')->nullOnDelete();
+ }
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('product_batches', function (Blueprint $table) {
+ // Drop purchase_id column if exists
+ if (Schema::hasColumn('product_batches', 'purchase_id')) {
+ $table->dropForeign(['purchase_id']);
+ $table->dropColumn('purchase_id');
+ }
+ });
+ }
+};
\ No newline at end of file
diff --git a/Modules/Purchase/Database/Migrations/2024_03_21_update_purchases_table.php b/Modules/Purchase/Database/Migrations/2024_03_21_update_purchases_table.php
new file mode 100644
index 00000000..55121055
--- /dev/null
+++ b/Modules/Purchase/Database/Migrations/2024_03_21_update_purchases_table.php
@@ -0,0 +1,48 @@
+dropColumn([
+ 'tax_percentage',
+ 'tax_amount',
+ 'shipping_amount',
+ 'status'
+ ]);
+
+ // Make columns nullable
+ $table->decimal('discount_amount', 10, 2)->nullable()->change();
+ $table->decimal('due_amount', 10, 2)->nullable()->change();
+
+ // Add user_id column
+ $table->foreignId('user_id')->nullable()->after('branch_id')
+ ->constrained('users')->nullOnDelete();
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('purchases', function (Blueprint $table) {
+ // Revert nullable columns
+ $table->decimal('discount_amount', 10, 2)->nullable(false)->change();
+ $table->decimal('due_amount', 10, 2)->nullable(false)->change();
+
+ // Drop user_id column
+ $table->dropForeign(['user_id']);
+ $table->dropColumn('user_id');
+
+ // Add back dropped columns
+ $table->decimal('tax_percentage', 10, 2)->default(0);
+ $table->decimal('tax_amount', 10, 2)->default(0);
+ $table->decimal('shipping_amount', 10, 2)->default(0);
+ $table->string('status')->default('Pending');
+ });
+ }
+};
\ No newline at end of file
diff --git a/Modules/Purchase/Entities/ProductBatch.php b/Modules/Purchase/Entities/ProductBatch.php
new file mode 100644
index 00000000..84c177a8
--- /dev/null
+++ b/Modules/Purchase/Entities/ProductBatch.php
@@ -0,0 +1,88 @@
+ 'integer',
+ 'purchase_price' => 'decimal:2',
+ 'expired_date' => 'date'
+ ];
+
+ public function product(): BelongsTo
+ {
+ return $this->belongsTo('Modules\Product\Entities\Product');
+ }
+
+ public function branch(): BelongsTo
+ {
+ return $this->belongsTo('App\Models\Branch');
+ }
+
+ public function purchase(): BelongsTo
+ {
+ return $this->belongsTo(Purchase::class);
+ }
+
+ public static function addStock(array $data)
+ {
+ return DB::transaction(function () use ($data) {
+ // Check if batch with same product, branch, and expired_date exists
+ $batch = self::where('product_id', $data['product_id'])
+ ->where('branch_id', $data['branch_id'])
+ ->where('expired_date', $data['expired_date'])
+ ->first();
+
+ if ($batch) {
+ // Update existing batch
+ $batch->update([
+ 'quantity' => $batch->quantity + $data['quantity'],
+ 'purchase_price' => $data['purchase_price'], // Update price to latest
+ 'updated_by' => auth()->user()->name
+ ]);
+ return $batch;
+ }
+
+ // Create new batch
+ return self::create([
+ 'product_id' => $data['product_id'],
+ 'branch_id' => $data['branch_id'],
+ 'batch_code' => $data['batch_code'] ?? null,
+ 'quantity' => $data['quantity'],
+ 'purchase_price' => $data['purchase_price'],
+ 'expired_date' => $data['expired_date'],
+ 'purchase_id' => $data['purchase_id'],
+ 'created_by' => auth()->user()->name,
+ 'updated_by' => auth()->user()->name
+ ]);
+ });
+ }
+
+ public static function getAvailableStock($productId, $branchId)
+ {
+ return self::where('product_id', $productId)
+ ->where('branch_id', $branchId)
+ ->where(function ($query) {
+ $query->whereNull('expired_date')
+ ->orWhere('expired_date', '>', now());
+ })
+ ->sum('quantity');
+ }
+}
\ No newline at end of file
diff --git a/Modules/Purchase/Entities/Purchase.php b/Modules/Purchase/Entities/Purchase.php
index c7af85f6..f92192ed 100644
--- a/Modules/Purchase/Entities/Purchase.php
+++ b/Modules/Purchase/Entities/Purchase.php
@@ -4,15 +4,55 @@ namespace Modules\Purchase\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Purchase extends Model
{
use HasFactory;
- protected $guarded = [];
+ protected $fillable = [
+ 'reference_no',
+ 'supplier_id',
+ 'date',
+ 'discount_amount',
+ 'payment_method',
+ 'paid_amount',
+ 'total_amount',
+ 'due_amount',
+ 'payment_status',
+ 'user_id',
+ 'branch_id',
+ 'created_by',
+ 'updated_by'
+ ];
- public function purchaseDetails() {
- return $this->hasMany(PurchaseDetail::class, 'purchase_id', 'id');
+ protected $casts = [
+ 'date' => 'date',
+ 'paid_amount' => 'decimal:2',
+ 'total_amount' => 'decimal:2',
+ 'due_amount' => 'decimal:2',
+ 'discount_amount' => 'decimal:2'
+ ];
+
+ public function purchaseDetails(): HasMany
+ {
+ return $this->hasMany(PurchaseDetail::class);
+ }
+
+ public function supplier(): BelongsTo
+ {
+ return $this->belongsTo('Modules\People\Entities\Supplier');
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo('App\Models\User');
+ }
+
+ public function branch(): BelongsTo
+ {
+ return $this->belongsTo('App\Models\Branch');
}
public function purchasePayments() {
@@ -24,16 +64,12 @@ class Purchase extends Model
static::creating(function ($model) {
$number = Purchase::max('id') + 1;
- $model->reference = make_reference_id('PR', $number);
+ $model->reference_no = make_reference_id('PR', $number);
});
}
public function scopeCompleted($query) {
- return $query->where('status', 'Completed');
- }
-
- public function getShippingAmountAttribute($value) {
- return $value / 100;
+ return $query->where('payment_status', 'Paid');
}
public function getPaidAmountAttribute($value) {
@@ -48,10 +84,6 @@ class Purchase extends Model
return $value / 100;
}
- public function getTaxAmountAttribute($value) {
- return $value / 100;
- }
-
public function getDiscountAmountAttribute($value) {
return $value / 100;
}
diff --git a/Modules/Purchase/Entities/PurchaseDetail.php b/Modules/Purchase/Entities/PurchaseDetail.php
index 7beadeda..e6c0756a 100644
--- a/Modules/Purchase/Entities/PurchaseDetail.php
+++ b/Modules/Purchase/Entities/PurchaseDetail.php
@@ -5,20 +5,38 @@ namespace Modules\Purchase\Entities;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Modules\Product\Entities\Product;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PurchaseDetail extends Model
{
use HasFactory;
- protected $guarded = [];
+ protected $fillable = [
+ 'purchase_id',
+ 'product_id',
+ 'product_name',
+ 'quantity',
+ 'price',
+ 'sub_total',
+ 'created_by',
+ 'updated_by'
+ ];
+
+ protected $casts = [
+ 'quantity' => 'integer',
+ 'price' => 'decimal:2',
+ 'sub_total' => 'decimal:2'
+ ];
protected $with = ['product'];
- public function product() {
+ public function product(): BelongsTo
+ {
return $this->belongsTo(Product::class, 'product_id', 'id');
}
- public function purchase() {
+ public function purchase(): BelongsTo
+ {
return $this->belongsTo(Purchase::class, 'purchase_id', 'id');
}
diff --git a/Modules/Purchase/Http/Controllers/PurchaseController.php b/Modules/Purchase/Http/Controllers/PurchaseController.php
index 287b2a39..68ca07de 100644
--- a/Modules/Purchase/Http/Controllers/PurchaseController.php
+++ b/Modules/Purchase/Http/Controllers/PurchaseController.php
@@ -14,6 +14,10 @@ use Modules\Purchase\Entities\PurchaseDetail;
use Modules\Purchase\Entities\PurchasePayment;
use Modules\Purchase\Http\Requests\StorePurchaseRequest;
use Modules\Purchase\Http\Requests\UpdatePurchaseRequest;
+use App\Models\ProductBatch;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Validator;
+use Barryvdh\DomPDF\Facade\Pdf;
class PurchaseController extends Controller
{
@@ -25,83 +29,143 @@ class PurchaseController extends Controller
}
- public function create() {
- abort_if(Gate::denies('create_purchases'), 403);
-
- Cart::instance('purchase')->destroy();
-
+ public function create()
+ {
return view('purchase::create');
}
- public function store(StorePurchaseRequest $request) {
- DB::transaction(function () use ($request) {
- $due_amount = $request->total_amount - $request->paid_amount;
- if ($due_amount == $request->total_amount) {
- $payment_status = 'Unpaid';
- } elseif ($due_amount > 0) {
- $payment_status = 'Partial';
- } else {
- $payment_status = 'Paid';
- }
+ public function store(Request $request)
+ {
+ $validator = Validator::make($request->all(), [
+ 'reference_no' => 'required|string',
+ 'supplier_id' => 'required|exists:suppliers,id',
+ 'date' => 'required|date',
+ 'payment_method' => 'required|string',
+ 'paid_amount' => 'required|numeric|min:0',
+ 'products' => 'required|array|min:1',
+ 'products.*.product_id' => 'required|exists:products,id',
+ 'products.*.quantity' => 'required|integer|min:1',
+ 'products.*.purchase_price' => 'required|numeric|min:0',
+ 'products.*.expired_date' => 'nullable|date|after:today',
+ 'discount_percentage' => 'nullable|numeric|min:0',
+ 'discount_amount' => 'nullable|numeric|min:0',
+ 'note' => 'nullable|string'
+ ]);
+ if ($validator->fails()) {
+ return response()->json([
+ 'message' => 'Validation failed',
+ 'errors' => $validator->errors()
+ ], 422);
+ }
+
+ DB::beginTransaction();
+
+ try {
+ // Calculate total amount
+ $totalAmount = collect($request->products)->sum(function ($product) {
+ return $product['quantity'] * $product['purchase_price'];
+ });
+
+ // Calculate due amount
+ $dueAmount = $totalAmount - $request->paid_amount;
+
+ // Determine payment status
+ $paymentStatus = $request->paid_amount == 0 ? 'Unpaid' :
+ ($dueAmount == 0 ? 'Paid' : 'Partial');
+
+ // 1. Simpan ke purchases
$purchase = Purchase::create([
- 'date' => $request->date,
+ 'reference_no' => $request->reference_no,
'supplier_id' => $request->supplier_id,
- 'supplier_name' => Supplier::findOrFail($request->supplier_id)->supplier_name,
- 'tax_percentage' => $request->tax_percentage,
- 'discount_percentage' => $request->discount_percentage,
- 'shipping_amount' => $request->shipping_amount * 100,
- 'paid_amount' => $request->paid_amount * 100,
- 'total_amount' => $request->total_amount * 100,
- 'due_amount' => $due_amount * 100,
- 'status' => $request->status,
- 'payment_status' => $payment_status,
+ 'supplier_name' => Supplier::findOrFail($request->supplier_id)->name,
+ 'date' => $request->date,
+ 'discount_percentage' => $request->discount_percentage ?? 0,
+ //'discount_amount' => $request->discount_amount ?? 0,
'payment_method' => $request->payment_method,
+ 'paid_amount' => round($request->paid_amount * 100),
+ 'total_amount' => round($totalAmount * 100),
+ 'discount_amount' => round($request->discount_amount * 100),
+ 'due_amount' => $dueAmount,
+ 'payment_status' => $paymentStatus,
'note' => $request->note,
- 'tax_amount' => Cart::instance('purchase')->tax() * 100,
- 'discount_amount' => Cart::instance('purchase')->discount() * 100,
+ 'user_id' => auth()->id(),
+ 'branch_id' => session(['active_branch' => 1]),// contoh ID cabang default
+ 'created_by' => auth()->user()->name,
+ 'updated_by' => auth()->user()->name
]);
- foreach (Cart::instance('purchase')->content() as $cart_item) {
+ // 2. Simpan detail produk & batch
+ foreach ($request->products as $product) {
+ // Detail
PurchaseDetail::create([
'purchase_id' => $purchase->id,
- 'product_id' => $cart_item->id,
- 'product_name' => $cart_item->name,
- 'product_code' => $cart_item->options->code,
- 'quantity' => $cart_item->qty,
- 'price' => $cart_item->price * 100,
- 'unit_price' => $cart_item->options->unit_price * 100,
- 'sub_total' => $cart_item->options->sub_total * 100,
- 'product_discount_amount' => $cart_item->options->product_discount * 100,
- 'product_discount_type' => $cart_item->options->product_discount_type,
- 'product_tax_amount' => $cart_item->options->product_tax * 100,
+ 'product_id' => $product['product_id'],
+ 'product_name' => Product::findOrFail($product['product_id'])->product_name,
+ 'product_code' => Product::findOrFail($product['product_id'])->product_code,
+ 'quantity' => $product['quantity'],
+ 'price' => $product['purchase_price'],
+ 'unit_price' => $product['purchase_price'],
+ 'sub_total' => $product['quantity'] * $product['purchase_price'],
+ 'product_discount_amount' => 0,
+ 'product_discount_type' => 'fixed',
+ 'product_tax_amount' => 0,
+ 'created_by' => auth()->user()->name,
+ 'updated_by' => auth()->user()->name
]);
- if ($request->status == 'Completed') {
- $product = Product::findOrFail($cart_item->id);
- $product->update([
- 'product_quantity' => $product->product_quantity + $cart_item->qty
- ]);
- }
+ // Batch
+ ProductBatch::addStock([
+ 'product_id' => $product['product_id'],
+ 'branch_id' => session(['active_branch' => 1]), // contoh ID cabang default,
+ 'quantity' => $product['quantity'],
+ 'purchase_price' => $product['purchase_price'],
+ 'expired_date' => $product['expired_date'],
+ 'purchase_id' => $purchase->id,
+ 'batch_code' => $purchase->reference_no . '-' . $product['product_id'],
+ 'created_by' => auth()->user()->name,
+ 'updated_by' => auth()->user()->name
+ ]);
}
- Cart::instance('purchase')->destroy();
-
+ // 3. Simpan pembayaran (jika ada)
if ($purchase->paid_amount > 0) {
PurchasePayment::create([
- 'date' => $request->date,
- 'reference' => 'INV/'.$purchase->reference,
- 'amount' => $purchase->paid_amount,
'purchase_id' => $purchase->id,
- 'payment_method' => $request->payment_method
+ 'branch_id' => session(['active_branch' => 1]), // contoh ID cabang default,
+ 'amount' => $purchase->paid_amount,
+ 'date' => $purchase->date,
+ 'reference' => 'PAY-' . $purchase->reference_no,
+ 'payment_method' => $purchase->payment_method,
+ 'note' => 'Initial payment for purchase ' . $purchase->reference_no,
+ 'created_by' => auth()->user()->name,
+ 'updated_by' => auth()->user()->name
]);
}
- });
- toast('Purchase Created!', 'success');
+ DB::commit();
- return redirect()->route('purchases.index');
+ return response()->json([
+ 'message' => 'Purchase created successfully',
+ 'data' => $purchase
+ ], 201);
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ return response()->json([
+ 'message' => 'Failed to create purchase',
+ 'error' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ public function getStock(Request $request, $productId, $branchId)
+ {
+ $stock = ProductBatch::getAvailableStock($productId, $branchId);
+ return response()->json([
+ 'data' => $stock
+ ]);
}
@@ -227,4 +291,17 @@ class PurchaseController extends Controller
return redirect()->route('purchases.index');
}
+
+ public function pdf($id)
+ {
+ $purchase = Purchase::findOrFail($id);
+ $supplier = Supplier::findOrFail($purchase->supplier_id);
+
+ $pdf = PDF::loadView('purchase::print', [
+ 'purchase' => $purchase,
+ 'supplier' => $supplier,
+ ])->setPaper('a4');
+
+ return $pdf->stream('purchase-'. $purchase->reference .'.pdf');
+ }
}
diff --git a/Modules/Purchase/Http/Livewire/CreatePurchase.php b/Modules/Purchase/Http/Livewire/CreatePurchase.php
new file mode 100644
index 00000000..6b0d4e87
--- /dev/null
+++ b/Modules/Purchase/Http/Livewire/CreatePurchase.php
@@ -0,0 +1,230 @@
+ 'required|exists:suppliers,id',
+ 'reference_no' => 'required|unique:purchases,reference_no',
+ 'date' => 'required|date',
+ 'payment_method' => 'required|string',
+ 'items' => 'required|array|min:1',
+ 'items.*.product_id' => 'required|exists:products,id',
+ 'items.*.qty' => 'required|integer|min:1',
+ 'items.*.purchase_price' => 'required|numeric|min:0',
+ 'items.*.unit_price' => 'required|numeric|min:0',
+ 'discount' => 'nullable|numeric|min:0',
+ 'discount_percentage' => 'nullable|numeric|min:0|max:100',
+ 'paid_amount' => 'nullable|numeric|min:0',
+ 'note' => 'nullable|string',
+ ];
+
+ protected $messages = [
+ 'supplier_id.required' => 'Please select a supplier',
+ 'supplier_id.exists' => 'The selected supplier is invalid',
+ 'reference_no.required' => 'Reference number is required',
+ 'reference_no.unique' => 'This reference number has already been used',
+ 'date.required' => 'Date is required',
+ 'date.date' => 'Please enter a valid date',
+ 'payment_method.required' => 'Please select a payment method',
+ 'items.required' => 'At least one product is required',
+ 'items.min' => 'At least one product is required',
+ 'items.*.product_id.required' => 'Product is required',
+ 'items.*.product_id.exists' => 'Selected product is invalid',
+ 'items.*.qty.required' => 'Quantity is required',
+ 'items.*.qty.min' => 'Quantity must be at least 1',
+ 'items.*.purchase_price.required' => 'Purchase price is required',
+ 'items.*.purchase_price.min' => 'Purchase price must be greater than 0',
+ 'items.*.unit_price.required' => 'Unit price is required',
+ 'items.*.unit_price.min' => 'Unit price must be greater than 0',
+ 'discount.numeric' => 'Discount must be a number',
+ 'discount.min' => 'Discount cannot be negative',
+ 'discount_percentage.numeric' => 'Discount percentage must be a number',
+ 'discount_percentage.min' => 'Discount percentage cannot be negative',
+ 'discount_percentage.max' => 'Discount percentage cannot be more than 100',
+ 'paid_amount.numeric' => 'Paid amount must be a number',
+ 'paid_amount.min' => 'Paid amount cannot be negative',
+ ];
+
+ public function mount()
+ {
+ $this->date = date('Y-m-d');
+ $this->reference_no = 'PR-' . date('Ymd') . '-' . rand(1000, 9999);
+ }
+
+ public function addItem()
+ {
+ $this->items[] = [
+ 'product_id' => '',
+ 'qty' => 1,
+ 'purchase_price' => 0,
+ 'unit_price' => 0,
+ 'discount' => 0,
+ 'discount_type' => null,
+ 'tax' => 0
+ ];
+ }
+
+ public function removeItem($index)
+ {
+ unset($this->items[$index]);
+ $this->items = array_values($this->items);
+ $this->calculateTotal();
+ }
+
+ public function calculateTotal()
+ {
+ $total = 0;
+ foreach ($this->items as $item) {
+ $subtotal = $item['qty'] * $item['unit_price'];
+ $total += $subtotal;
+ }
+ $this->total_amount = $total;
+ $this->calculateTotalAfterDiscount();
+ }
+
+ public function calculateTotalAfterDiscount()
+ {
+ $this->total_after_discount = $this->total_amount - $this->discount;
+ $this->due_amount = $this->total_after_discount - $this->paid_amount;
+ }
+
+ public function updated($propertyName)
+ {
+ $this->validateOnly($propertyName);
+
+ if (str_contains($propertyName, 'items')) {
+ $this->calculateTotal();
+ }
+
+ if (in_array($propertyName, ['discount', 'discount_percentage', 'paid_amount'])) {
+ $this->calculateTotalAfterDiscount();
+ }
+ }
+
+ public function save()
+ {
+ $this->validate();
+
+ try {
+ DB::beginTransaction();
+
+ $supplier = Supplier::findOrFail($this->supplier_id);
+
+ $purchase = Purchase::create([
+ 'branch_id' => session('branch_id'),
+ 'user_id' => auth()->id(),
+ 'date' => $this->date,
+ 'reference_no' => $this->reference_no,
+ 'supplier_id' => $this->supplier_id,
+ 'supplier_name' => $supplier->supplier_name,
+ 'discount_percentage' => $this->discount_percentage,
+ 'discount' => $this->discount,
+ 'total' => $this->total_amount,
+ 'paid_amount' => $this->paid_amount,
+ 'due_amount' => $this->due_amount,
+ 'payment_status' => ($this->paid_amount >= $this->total_after_discount) ? 'paid' : 'due',
+ 'payment_method' => $this->payment_method,
+ 'note' => $this->note,
+ ]);
+
+ // Save purchase items and update product quantities
+ foreach ($this->items as $item) {
+ $product = Product::findOrFail($item['product_id']);
+
+ // Create purchase detail
+ PurchaseDetail::create([
+ 'purchase_id' => $purchase->id,
+ 'product_id' => $item['product_id'],
+ 'product_name' => $product->product_name,
+ 'product_code' => $product->product_code,
+ 'qty' => $item['qty'],
+ 'purchase_price' => $item['purchase_price'],
+ 'unit_price' => $item['unit_price'],
+ 'subtotal' => $item['qty'] * $item['unit_price'],
+ 'product_discount_amount' => $item['discount'] ?? 0,
+ 'product_discount_type' => $item['discount_type'] ?? null,
+ 'product_tax_amount' => $item['tax'] ?? 0,
+ ]);
+
+ // Update product quantity
+ $product->update([
+ 'product_quantity' => $product->product_quantity + $item['qty']
+ ]);
+
+ // Create or update product batch
+ DB::table('product_batches')->updateOrInsert(
+ [
+ 'product_id' => $item['product_id'],
+ 'batch_number' => $this->reference_no,
+ ],
+ [
+ 'product_id' => $item['product_id'],
+ 'batch_number' => $this->reference_no,
+ 'qty' => $item['qty'],
+ 'expiry_date' => null,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]
+ );
+ }
+
+ // Create purchase payment if there's a paid amount
+ if ($this->paid_amount > 0) {
+ PurchasePayment::create([
+ 'branch_id' => session('branch_id'),
+ 'purchase_id' => $purchase->id,
+ 'amount' => $this->paid_amount,
+ 'date' => now(),
+ 'reference' => 'PAY-' . strtoupper(Str::random(6)),
+ 'payment_method' => $this->payment_method,
+ 'note' => $this->note,
+ ]);
+ }
+
+ DB::commit();
+
+ $this->dispatch('showSuccessMessage', [
+ 'message' => 'Purchase created successfully!',
+ 'redirect' => route('purchases.index')
+ ]);
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ $this->dispatch('showErrorMessage', [
+ 'message' => 'Error creating purchase: ' . $e->getMessage()
+ ]);
+ }
+ }
+
+ public function render()
+ {
+ return view('purchase::livewire.create-purchase', [
+ 'suppliers' => Supplier::all(),
+ 'products' => Product::all()
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/Modules/Purchase/Http/Livewire/PurchaseList.php b/Modules/Purchase/Http/Livewire/PurchaseList.php
new file mode 100644
index 00000000..9efbe75e
--- /dev/null
+++ b/Modules/Purchase/Http/Livewire/PurchaseList.php
@@ -0,0 +1,92 @@
+ ['except' => ''],
+ 'dateFrom' => ['except' => ''],
+ 'dateTo' => ['except' => ''],
+ 'supplier_id' => ['except' => ''],
+ 'payment_status' => ['except' => ''],
+ ];
+
+ public function updatingSearch()
+ {
+ $this->resetPage();
+ }
+
+ public function deletePurchase($id)
+ {
+ $purchase = Purchase::findOrFail($id);
+
+ if ($purchase->payment_status === 'Paid') {
+ session()->flash('error', 'Cannot delete a paid purchase.');
+ return;
+ }
+
+ try {
+ DB::beginTransaction();
+
+ // Delete related records
+ $purchase->purchaseDetails()->delete();
+ $purchase->purchasePayments()->delete();
+
+ // Delete the purchase
+ $purchase->delete();
+
+ DB::commit();
+ session()->flash('message', 'Purchase deleted successfully.');
+ } catch (\Exception $e) {
+ DB::rollBack();
+ session()->flash('error', 'Error deleting purchase: ' . $e->getMessage());
+ }
+ }
+
+ public function render()
+ {
+ $purchases = Purchase::query()
+ ->when($this->search, function ($query) {
+ $query->where('reference_no', 'like', '%' . $this->search . '%')
+ ->orWhereHas('supplier', function ($q) {
+ $q->where('name', 'like', '%' . $this->search . '%');
+ });
+ })
+ ->when($this->dateFrom, function ($query) {
+ $query->whereDate('date', '>=', $this->dateFrom);
+ })
+ ->when($this->dateTo, function ($query) {
+ $query->whereDate('date', '<=', $this->dateTo);
+ })
+ ->when($this->supplier_id, function ($query) {
+ $query->where('supplier_id', $this->supplier_id);
+ })
+ ->when($this->payment_status, function ($query) {
+ $query->where('payment_status', $this->payment_status);
+ })
+ ->where('branch_id', session('active_branch'))
+ ->latest()
+ ->paginate($this->perPage);
+
+ return view('purchase::livewire.purchase-list', [
+ 'purchases' => $purchases,
+ 'suppliers' => \Modules\People\Entities\Supplier::all()
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/Modules/Purchase/Http/Livewire/PurchaseShow.php b/Modules/Purchase/Http/Livewire/PurchaseShow.php
new file mode 100644
index 00000000..26ec32b0
--- /dev/null
+++ b/Modules/Purchase/Http/Livewire/PurchaseShow.php
@@ -0,0 +1,75 @@
+ 'required|numeric|min:0',
+ 'payment_method' => 'required',
+ 'payment_note' => 'nullable|string|max:255'
+ ];
+
+ public function mount($id)
+ {
+ $this->purchase_id = $id;
+ $this->loadPurchase();
+ }
+
+ public function loadPurchase()
+ {
+ $this->purchase = Purchase::with(['supplier', 'purchaseDetails.product', 'purchasePayments'])
+ ->findOrFail($this->purchase_id);
+ }
+
+ public function addPayment()
+ {
+ $this->validate();
+
+ if ($this->payment_amount > $this->purchase->due_amount) {
+ session()->flash('error', 'Payment amount cannot be greater than due amount.');
+ return;
+ }
+
+ try {
+ $this->purchase->purchasePayments()->create([
+ 'amount' => $this->payment_amount,
+ 'payment_method' => $this->payment_method,
+ 'note' => $this->payment_note,
+ 'user_id' => Auth::id(),
+ 'created_by' => Auth::user()->name,
+ 'updated_by' => Auth::user()->name
+ ]);
+
+ // Update purchase payment status
+ $total_paid = $this->purchase->purchasePayments()->sum('amount');
+ $this->purchase->update([
+ 'paid_amount' => $total_paid,
+ 'due_amount' => $this->purchase->total_amount - $total_paid,
+ 'payment_status' => $total_paid >= $this->purchase->total_amount ? 'Paid' : 'Partial',
+ 'updated_by' => Auth::user()->name
+ ]);
+
+ $this->reset(['payment_amount', 'payment_method', 'payment_note']);
+ $this->loadPurchase();
+ session()->flash('message', 'Payment added successfully.');
+ } catch (\Exception $e) {
+ session()->flash('error', 'Error adding payment: ' . $e->getMessage());
+ }
+ }
+
+ public function render()
+ {
+ return view('purchase::livewire.purchase-show');
+ }
+}
\ No newline at end of file
diff --git a/Modules/Purchase/Providers/PurchaseServiceProvider.php b/Modules/Purchase/Providers/PurchaseServiceProvider.php
index 397fdb9f..d2ad72db 100644
--- a/Modules/Purchase/Providers/PurchaseServiceProvider.php
+++ b/Modules/Purchase/Providers/PurchaseServiceProvider.php
@@ -4,6 +4,8 @@ namespace Modules\Purchase\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Factory;
+use Livewire\Livewire;
+use Modules\Purchase\Http\Livewire\CreatePurchase;
class PurchaseServiceProvider extends ServiceProvider
{
@@ -24,10 +26,13 @@ class PurchaseServiceProvider extends ServiceProvider
*/
public function boot()
{
- $this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
+ $this->loadRoutesFrom(module_path($this->moduleName, 'Routes/web.php'));
+
+ // Register Livewire Component
+ Livewire::component('purchase::create-purchase', CreatePurchase::class);
}
/**
@@ -63,7 +68,6 @@ class PurchaseServiceProvider extends ServiceProvider
public function registerViews()
{
$viewPath = resource_path('views/modules/' . $this->moduleNameLower);
-
$sourcePath = module_path($this->moduleName, 'Resources/views');
$this->publishes([
diff --git a/Modules/Purchase/Providers/RouteServiceProvider.php b/Modules/Purchase/Providers/RouteServiceProvider.php
index 55cea7a2..f7844f17 100644
--- a/Modules/Purchase/Providers/RouteServiceProvider.php
+++ b/Modules/Purchase/Providers/RouteServiceProvider.php
@@ -33,8 +33,6 @@ class RouteServiceProvider extends ServiceProvider
*/
public function map()
{
- $this->mapApiRoutes();
-
$this->mapWebRoutes();
}
@@ -49,7 +47,7 @@ class RouteServiceProvider extends ServiceProvider
{
Route::middleware('web')
->namespace($this->moduleNamespace)
- ->group(module_path('Purchase', '/Routes/web.php'));
+ ->group(module_path('Purchase', 'Routes/web.php'));
}
/**
diff --git a/Modules/Purchase/Resources/views/create.blade.php b/Modules/Purchase/Resources/views/create.blade.php
index 73b924b4..ee4a13a7 100644
--- a/Modules/Purchase/Resources/views/create.blade.php
+++ b/Modules/Purchase/Resources/views/create.blade.php
@@ -2,138 +2,6 @@
@section('title', 'Create Purchase')
-@section('breadcrumb')
-
- Home
- Purchases
- Add
-
-@endsection
-
@section('content')
-
-
-
-
-
-
-
- @include('utils.alerts')
-
-
-
-
-
-
+
@endsection
-
-@push('page_scripts')
-
-
-@endpush
diff --git a/Modules/Purchase/Resources/views/index.blade.php b/Modules/Purchase/Resources/views/index.blade.php
index 5f9f3478..84c37368 100644
--- a/Modules/Purchase/Resources/views/index.blade.php
+++ b/Modules/Purchase/Resources/views/index.blade.php
@@ -14,27 +14,21 @@
@endsection
@section('content')
-
-
-
-
+
+
+
+ {{ $dataTable->table() }}
+
@endsection
-@push('page_scripts')
- {!! $dataTable->scripts() !!}
+@push('scripts')
+ {{ $dataTable->scripts() }}
@endpush
diff --git a/Modules/Purchase/Resources/views/livewire/create-purchase.blade.php b/Modules/Purchase/Resources/views/livewire/create-purchase.blade.php
new file mode 100644
index 00000000..29f852f8
--- /dev/null
+++ b/Modules/Purchase/Resources/views/livewire/create-purchase.blade.php
@@ -0,0 +1,252 @@
+@push('scripts')
+
+@endpush
+
+
+
+
+
+
+
+
+
+ Reference No
+
+ @error('reference_no') {{ $message }} @enderror
+
+
+
+
+ Supplier
+
+ Select Supplier
+ @foreach($suppliers as $supplier)
+ {{ $supplier->supplier_name }}
+ @endforeach
+
+ @error('supplier_id') {{ $message }} @enderror
+
+
+
+
+ Date
+
+ @error('date') {{ $message }} @enderror
+
+
+
+
+
+
+
+ Payment Method
+
+ Select Payment Method
+ Cash
+ Transfer
+ Credit
+
+ @error('payment_method') {{ $message }} @enderror
+
+
+
+
+ Discount Percentage
+
+ @error('discount_percentage') {{ $message }} @enderror
+
+
+
+
+ Discount Amount
+
+ @error('discount') {{ $message }} @enderror
+
+
+
+
+
+
+
+
Products
+
+ Add Product
+
+
+
+ @error('items')
{{ $message }} @enderror
+
+
+
+
+
+
+
+
+ Paid Amount
+
+ @error('paid_amount') {{ $message }} @enderror
+
+
+
+
+
+
+
+
+
+ Note
+
+ @error('note') {{ $message }} @enderror
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Modules/Purchase/Resources/views/livewire/purchase-list.blade.php b/Modules/Purchase/Resources/views/livewire/purchase-list.blade.php
new file mode 100644
index 00000000..4daab923
--- /dev/null
+++ b/Modules/Purchase/Resources/views/livewire/purchase-list.blade.php
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All Suppliers
+ @foreach($suppliers as $supplier)
+ {{ $supplier->name }}
+ @endforeach
+
+
+
+
+ All Status
+ Paid
+ Partial
+ Unpaid
+
+
+
+
+ 10 per page
+ 25 per page
+ 50 per page
+ 100 per page
+
+
+
+
+
+
+
+
+
+ Reference
+ Date
+ Supplier
+ Total Amount
+ Paid Amount
+ Due Amount
+ Payment Status
+ Created By
+ Actions
+
+
+
+ @forelse($purchases as $purchase)
+
+ {{ $purchase->reference_no }}
+ {{ $purchase->date->format('d/m/Y') }}
+ {{ $purchase->supplier->name }}
+ {{ number_format($purchase->total_amount, 2) }}
+ {{ number_format($purchase->paid_amount, 2) }}
+ {{ $purchase->due_amount ? number_format($purchase->due_amount, 2) : '0.00' }}
+
+
+ {{ $purchase->payment_status }}
+
+
+ {{ $purchase->created_by }}
+
+
+
+
+
+ @if($purchase->payment_status !== 'Paid')
+
+
+
+
+
+
+ @endif
+
+
+
+ @empty
+
+ No purchases found.
+
+ @endforelse
+
+
+
+
+
+
+ {{ $purchases->links() }}
+
+
+
+
+ @if (session()->has('message'))
+
+ {{ session('message') }}
+
+ ×
+
+
+ @endif
+
+ @if (session()->has('error'))
+
+ {{ session('error') }}
+
+ ×
+
+
+ @endif
+
\ No newline at end of file
diff --git a/Modules/Purchase/Resources/views/livewire/purchase-show.blade.php b/Modules/Purchase/Resources/views/livewire/purchase-show.blade.php
new file mode 100644
index 00000000..20370155
--- /dev/null
+++ b/Modules/Purchase/Resources/views/livewire/purchase-show.blade.php
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
Purchase Information
+
+
+ Reference No
+ {{ $purchase->reference_no }}
+
+
+ Date
+ {{ $purchase->date->format('d/m/Y') }}
+
+
+ Supplier
+ {{ $purchase->supplier->name }}
+
+
+ Created By
+ {{ $purchase->created_by }}
+
+
+
+
+
Payment Information
+
+
+ Total Amount
+ {{ number_format($purchase->total_amount, 2) }}
+
+
+ Paid Amount
+ {{ number_format($purchase->paid_amount, 2) }}
+
+
+ Due Amount
+ {{ $purchase->due_amount ? number_format($purchase->due_amount, 2) : '0.00' }}
+
+
+ Payment Status
+
+
+ {{ $purchase->payment_status }}
+
+
+
+
+
+
+
+
+
Purchase Details
+
+
+
+
+ Product
+ Quantity
+ Price
+ Subtotal
+
+
+
+ @foreach($purchase->purchaseDetails as $detail)
+
+ {{ $detail->product_name }}
+ {{ $detail->quantity }}
+ {{ number_format($detail->price, 2) }}
+ {{ number_format($detail->sub_total, 2) }}
+
+ @endforeach
+
+
+
+
+
+
Payment History
+
+
+
+
+ Date
+ Amount
+ Method
+ Note
+ Created By
+
+
+
+ @forelse($purchase->purchasePayments as $payment)
+
+ {{ $payment->created_at->format('d/m/Y H:i') }}
+ {{ number_format($payment->amount, 2) }}
+ {{ $payment->payment_method }}
+ {{ $payment->note }}
+ {{ $payment->created_by }}
+
+ @empty
+
+ No payments found.
+
+ @endforelse
+
+
+
+
+
+ @if($purchase->payment_status !== 'Paid')
+
+
+
+
+
+
+
+ Amount
+
+ @error('payment_amount') {{ $message }} @enderror
+
+
+
+
+ Payment Method
+
+ Cash
+ Bank Transfer
+ Credit Card
+
+ @error('payment_method') {{ $message }} @enderror
+
+
+
+
+ Note
+
+ @error('payment_note') {{ $message }} @enderror
+
+
+
+
+
+
+ Add Payment
+
+
+
+
+
+
+
+ @endif
+
+
+
+ @if (session()->has('message'))
+
+ {{ session('message') }}
+
+ ×
+
+
+ @endif
+
+ @if (session()->has('error'))
+
+ {{ session('error') }}
+
+ ×
+
+
+ @endif
+
\ No newline at end of file
diff --git a/Modules/Purchase/Resources/views/partials/actions.blade.php b/Modules/Purchase/Resources/views/partials/actions.blade.php
index e01e8a7f..3e92bfc7 100644
--- a/Modules/Purchase/Resources/views/partials/actions.blade.php
+++ b/Modules/Purchase/Resources/views/partials/actions.blade.php
@@ -1,42 +1,41 @@
-
-
-
+
+
+@push('scripts')
+
+@endpush
diff --git a/Modules/Purchase/Routes/web.php b/Modules/Purchase/Routes/web.php
index 079b2300..2a2eff7a 100644
--- a/Modules/Purchase/Routes/web.php
+++ b/Modules/Purchase/Routes/web.php
@@ -11,30 +11,36 @@
|
*/
-Route::group(['middleware' => 'auth'], function () {
+use Illuminate\Support\Facades\Route;
+use Modules\Purchase\Http\Controllers\PurchaseController;
+use Modules\Purchase\Http\Livewire\CreatePurchase;
- //Generate PDF
- Route::get('/purchases/pdf/{id}', function ($id) {
- $purchase = \Modules\Purchase\Entities\Purchase::findOrFail($id);
- $supplier = \Modules\People\Entities\Supplier::findOrFail($purchase->supplier_id);
+Route::middleware(['web', 'auth'])->group(function () {
+ // Purchases
+ Route::prefix('purchases')->group(function () {
+ Route::get('/', [PurchaseController::class, 'index'])->name('purchases.index');
+ Route::get('/create', [PurchaseController::class, 'create'])->name('purchases.create');
+ Route::post('/', [PurchaseController::class, 'store'])->name('purchases.store');
+ Route::get('/{purchase}', [PurchaseController::class, 'show'])->name('purchases.show');
+ Route::get('/{purchase}/edit', [PurchaseController::class, 'edit'])->name('purchases.edit');
+ Route::put('/{purchase}', [PurchaseController::class, 'update'])->name('purchases.update');
+ Route::delete('/{purchase}', [PurchaseController::class, 'destroy'])->name('purchases.destroy');
+ Route::get('/pdf/{id}', [PurchaseController::class, 'pdf'])->name('purchases.pdf');
+ Route::get('/stock/{productId}/{branchId}', [PurchaseController::class, 'getStock'])->name('purchases.stock');
+ });
- $pdf = \PDF::loadView('purchase::print', [
- 'purchase' => $purchase,
- 'supplier' => $supplier,
- ])->setPaper('a4');
-
- return $pdf->stream('purchase-'. $purchase->reference .'.pdf');
- })->name('purchases.pdf');
-
- //Sales
- Route::resource('purchases', 'PurchaseController');
-
- //Payments
- Route::get('/purchase-payments/{purchase_id}', 'PurchasePaymentsController@index')->name('purchase-payments.index');
- Route::get('/purchase-payments/{purchase_id}/create', 'PurchasePaymentsController@create')->name('purchase-payments.create');
- Route::post('/purchase-payments/store', 'PurchasePaymentsController@store')->name('purchase-payments.store');
- Route::get('/purchase-payments/{purchase_id}/edit/{purchasePayment}', 'PurchasePaymentsController@edit')->name('purchase-payments.edit');
- Route::patch('/purchase-payments/update/{purchasePayment}', 'PurchasePaymentsController@update')->name('purchase-payments.update');
- Route::delete('/purchase-payments/destroy/{purchasePayment}', 'PurchasePaymentsController@destroy')->name('purchase-payments.destroy');
+ // Purchase Payments
+ Route::prefix('purchase-payments')->group(function () {
+ Route::get('/{purchase_id}', 'PurchasePaymentsController@index')->name('purchase-payments.index');
+ Route::get('/{purchase_id}/create', 'PurchasePaymentsController@create')->name('purchase-payments.create');
+ Route::post('/store', 'PurchasePaymentsController@store')->name('purchase-payments.store');
+ Route::get('/{purchase_id}/edit/{purchasePayment}', 'PurchasePaymentsController@edit')->name('purchase-payments.edit');
+ Route::patch('/update/{purchasePayment}', 'PurchasePaymentsController@update')->name('purchase-payments.update');
+ Route::delete('/destroy/{purchasePayment}', 'PurchasePaymentsController@destroy')->name('purchase-payments.destroy');
+ });
+ // Livewire Routes
+ Route::get('/purchases/create', function () {
+ return view('purchase::create');
+ })->name('purchases.create');
});
diff --git a/Modules/Purchase/composer.json b/Modules/Purchase/composer.json
index 8de2a200..14aa56bb 100644
--- a/Modules/Purchase/composer.json
+++ b/Modules/Purchase/composer.json
@@ -1,5 +1,5 @@
{
- "name": "nwidart/purchase",
+ "name": "nwidart/purchase-module",
"description": "",
"authors": [
{
@@ -9,10 +9,10 @@
],
"extra": {
"laravel": {
- "providers": [],
- "aliases": {
-
- }
+ "providers": [
+ "Modules\\Purchase\\Providers\\PurchaseServiceProvider",
+ "Modules\\Purchase\\Providers\\RouteServiceProvider"
+ ]
}
},
"autoload": {
diff --git a/app/Http/Controllers/ProductBatchController.php b/app/Http/Controllers/ProductBatchController.php
new file mode 100644
index 00000000..e638158e
--- /dev/null
+++ b/app/Http/Controllers/ProductBatchController.php
@@ -0,0 +1,60 @@
+validated());
+ return response()->json([
+ 'message' => 'Stock added successfully',
+ 'data' => $batch
+ ], 201);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'message' => 'Failed to add stock',
+ 'error' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ public function getStock(Request $request, $productId, $branchId)
+ {
+ $stock = ProductBatch::getAvailableStock($productId, $branchId);
+ return response()->json([
+ 'data' => $stock
+ ]);
+ }
+
+ public function deductStock(Request $request)
+ {
+ $request->validate([
+ 'product_id' => 'required|exists:products,id',
+ 'branch_id' => 'required|exists:branches,id',
+ 'quantity' => 'required|integer|min:1'
+ ]);
+
+ try {
+ ProductBatch::deductStock(
+ $request->product_id,
+ $request->branch_id,
+ $request->quantity
+ );
+
+ return response()->json([
+ 'message' => 'Stock deducted successfully'
+ ]);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'message' => 'Failed to deduct stock',
+ 'error' => $e->getMessage()
+ ], 500);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Requests/ProductBatchRequest.php b/app/Http/Requests/ProductBatchRequest.php
new file mode 100644
index 00000000..29ba26e4
--- /dev/null
+++ b/app/Http/Requests/ProductBatchRequest.php
@@ -0,0 +1,47 @@
+ 'required|exists:products,id',
+ 'branch_id' => 'required|exists:branches,id',
+ 'quantity' => 'required|integer|min:1',
+ 'purchase_price' => 'required|numeric|min:0',
+ ];
+
+ // Get product category
+ $product = Product::find($this->input('product_id'));
+
+ // If product is in make-up category, require batch_code and expired_date
+ if ($product && $product->category === 'make up') {
+ $rules['batch_code'] = 'required|string';
+ $rules['expired_date'] = 'required|date|after:today';
+ } else {
+ $rules['batch_code'] = 'nullable|string';
+ $rules['expired_date'] = 'nullable|date|after:today';
+ }
+
+ return $rules;
+ }
+
+ public function messages()
+ {
+ return [
+ 'batch_code.required' => 'Batch code is required for make-up products',
+ 'expired_date.required' => 'Expiry date is required for make-up products',
+ 'expired_date.after' => 'Expiry date must be a future date',
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Livewire/Purchase/CreatePurchase.php b/app/Livewire/Purchase/CreatePurchase.php
new file mode 100644
index 00000000..f3308fa8
--- /dev/null
+++ b/app/Livewire/Purchase/CreatePurchase.php
@@ -0,0 +1,13 @@
+ 'date',
+ 'purchase_price' => 'decimal:2',
+ ];
+
+ public function product(): BelongsTo
+ {
+ return $this->belongsTo(Product::class);
+ }
+
+ public function branch(): BelongsTo
+ {
+ return $this->belongsTo(Branch::class);
+ }
+
+ /**
+ * Generate a unique batch code
+ */
+ public static function generateBatchCode(): string
+ {
+ return 'BATCH-' . strtoupper(Str::random(8));
+ }
+
+ /**
+ * Get available stock for a product in a specific branch using FEFO/FIFO
+ */
+ public static function getAvailableStock(int $productId, int $branchId)
+ {
+ return self::where('product_id', $productId)
+ ->where('branch_id', $branchId)
+ ->where('quantity', '>', 0)
+ ->orderBy('expired_date', 'asc')
+ ->orderBy('created_at', 'asc')
+ ->get();
+ }
+
+ /**
+ * Deduct stock from batches using FEFO/FIFO
+ */
+ public static function deductStock(int $productId, int $branchId, int $quantity)
+ {
+ $remainingQuantity = $quantity;
+ $batches = self::getAvailableStock($productId, $branchId);
+ $usedBatches = [];
+
+ foreach ($batches as $batch) {
+ if ($remainingQuantity <= 0) break;
+
+ $deductAmount = min($remainingQuantity, $batch->quantity);
+ $batch->quantity -= $deductAmount;
+ $batch->save();
+
+ $usedBatches[] = [
+ 'batch_id' => $batch->id,
+ 'quantity' => $deductAmount,
+ 'purchase_price' => $batch->purchase_price,
+ 'expired_date' => $batch->expired_date
+ ];
+
+ $remainingQuantity -= $deductAmount;
+ }
+
+ if ($remainingQuantity > 0) {
+ throw new \Exception('Insufficient stock available');
+ }
+
+ return $usedBatches;
+ }
+
+ /**
+ * Add new stock to product batches
+ */
+ public static function addStock(array $data)
+ {
+ // Generate batch code if not provided
+ if (!isset($data['batch_code'])) {
+ $data['batch_code'] = self::generateBatchCode();
+ }
+
+ return self::create($data);
+ }
+}
\ No newline at end of file
diff --git a/composer.lock b/composer.lock
index a092e73c..7444b255 100644
--- a/composer.lock
+++ b/composer.lock
@@ -10290,5 +10290,5 @@
"php": "^8.1"
},
"platform-dev": [],
- "plugin-api-version": "2.3.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/database/migrations/2024_03_21_create_product_batches_table.php b/database/migrations/2024_03_21_create_product_batches_table.php
new file mode 100644
index 00000000..d70989ec
--- /dev/null
+++ b/database/migrations/2024_03_21_create_product_batches_table.php
@@ -0,0 +1,30 @@
+id();
+ $table->foreignId('product_id')->constrained('products')->onDelete('cascade');
+ $table->foreignId('branch_id')->constrained('branches')->onDelete('cascade');
+ $table->string('batch_code')->nullable();
+ $table->integer('quantity');
+ $table->decimal('purchase_price', 10, 2);
+ $table->date('expired_date')->nullable();
+ $table->timestamps();
+
+ // Add index for faster queries
+ $table->index(['product_id', 'branch_id', 'expired_date']);
+ });
+ }
+
+ public function down()
+ {
+ Schema::dropIfExists('product_batches');
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2024_03_21_remove_columns_from_products_table.php b/database/migrations/2024_03_21_remove_columns_from_products_table.php
new file mode 100644
index 00000000..1b3d9cb7
--- /dev/null
+++ b/database/migrations/2024_03_21_remove_columns_from_products_table.php
@@ -0,0 +1,36 @@
+dropColumn([
+ 'product_quantity',
+ 'product_cost',
+ 'product_price',
+ 'min_quantity_for_wholesale',
+ 'wholesale_discount_percentage',
+ 'product_order_tax',
+ 'product_tax_type'
+ ]);
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('products', function (Blueprint $table) {
+ $table->integer('product_quantity')->default(0);
+ $table->integer('product_cost')->default(0);
+ $table->integer('product_price')->default(0);
+ $table->integer('min_quantity_for_wholesale')->nullable();
+ $table->integer('wholesale_discount_percentage')->nullable();
+ $table->integer('product_order_tax')->default(0);
+ $table->tinyInteger('product_tax_type')->default(0);
+ });
+ }
+};
\ No newline at end of file
diff --git a/resources/views/layouts/header.blade.php b/resources/views/layouts/header.blade.php
index 7aa04376..81f583f6 100644
--- a/resources/views/layouts/header.blade.php
+++ b/resources/views/layouts/header.blade.php
@@ -24,7 +24,10 @@
@php
- $low_quantity_products = \Modules\Product\Entities\Product::select('id', 'product_quantity', 'product_stock_alert', 'product_code')->whereColumn('product_quantity', '<=', 'product_stock_alert')->get();
+ $low_quantity_products = \Modules\Product\Entities\Product::select('id', 'product_code', 'product_stock_alert')
+ ->withSum('batches', 'qty', 'batches_sum_qty')
+ ->having('batches_sum_qty', '<=', DB::raw('product_stock_alert'))
+ ->get();
echo $low_quantity_products->count();
@endphp
diff --git a/resources/views/livewire/purchase/create-purchase.blade.php b/resources/views/livewire/purchase/create-purchase.blade.php
new file mode 100644
index 00000000..15e56605
--- /dev/null
+++ b/resources/views/livewire/purchase/create-purchase.blade.php
@@ -0,0 +1,3 @@
+
+ {{-- The whole world belongs to you. --}}
+
diff --git a/routes/api.php b/routes/api.php
index bcb8b189..ecc567b5 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -2,6 +2,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\ProductBatchController;
/*
|--------------------------------------------------------------------------
@@ -17,3 +18,10 @@ use Illuminate\Support\Facades\Route;
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
+
+// Product Batch Routes
+Route::prefix('product-batches')->group(function () {
+ Route::post('/', [ProductBatchController::class, 'store']);
+ Route::get('/stock/{productId}/{branchId}', [ProductBatchController::class, 'getStock']);
+ Route::post('/deduct', [ProductBatchController::class, 'deductStock']);
+});